1. Go:不用標(biāo)準(zhǔn)庫如何解壓 zip 文件?

        共 7788字,需瀏覽 16分鐘

         ·

        2021-12-24 06:17

        zip 是一種常見的歸檔格式,本文講解 Go 如何操作 zip。

        首先看看 zip 文件是如何工作的。以一個(gè)小文件為例:(類 Unix 系統(tǒng)下)

        $?cat?hello.text
        Hello!

        執(zhí)行 zip 命令進(jìn)行歸檔:

        $?zip?test.zip?hello.text
        adding:?hello.text?(stored?0%)
        $?ls?-lah?test.zip
        -rw-r--r--?1?phil?phil?177?Nov?23?23:04?test.zip

        一個(gè) 6 字節(jié)的文本文件變成了一個(gè) 177 字節(jié)的 zip 文件。這并不大,解析 177 個(gè)字節(jié)聽起來不可能太復(fù)雜!

        對(duì) zip 文件執(zhí)行 hexdump:

        $?hexdump?-C?test.zip
        00000000??50?4b?03?04?0a?00?00?00??00?00?8a?b8?77?53?9e?d8??|PK..........wS..|
        00000010??42?b0?07?00?00?00?07?00??00?00?0a?00?1c?00?68?65??|B.............he|
        00000020??6c?6c?6f?2e?74?65?78?74??55?54?09?00?03?74?73?9d??|llo.textUT...ts.|
        00000030??61?74?73?9d?61?75?78?0b??00?01?04?eb?03?00?00?04??|ats.aux.........|
        00000040??eb?03?00?00?48?65?6c?6c??6f?21?0a?50?4b?01?02?1e??|....Hello!.PK...|
        00000050??03?0a?00?00?00?00?00?8a??b8?77?53?9e?d8?42?b0?07??|.........wS..B..|
        00000060??00?00?00?07?00?00?00?0a??00?18?00?00?00?00?00?01??|................|
        00000070??00?00?00?a4?81?00?00?00??00?68?65?6c?6c?6f?2e?74??|.........hello.t|
        00000080??65?78?74?55?54?05?00?03??74?73?9d?61?75?78?0b?00??|extUT...ts.aux..|
        00000090??01?04?eb?03?00?00?04?eb??03?00?00?50?4b?05?06?00??|...........PK...|
        000000a0??00?00?00?01?00?01?00?50??00?00?00?4b?00?00?00?00??|.......P...K....|
        000000b0??00????????????????????????????????????????????????|.|
        000000b1

        從中我們可以看到文件名和文件內(nèi)容。

        01 結(jié)構(gòu)

        我們來看看這里[1]定義的 zip 結(jié)構(gòu) 。根據(jù)第 4.3.6 節(jié),看起來文件元數(shù)據(jù)后跟文件內(nèi)容一個(gè)接一個(gè)地存儲(chǔ),最后一塊是 “central directory” 元數(shù)據(jù)。

        zip format header

        圖片來源:https://www.codeproject.com/Articles/8688/Extracting-files-from-a-remote-ZIP-archive

        本地 header 元數(shù)據(jù)如下所示:

        字段大小
        local file header signature4 bytes
        version needed to extract2 bytes
        general purpose bit flag2 bytes
        compression method2 bytes
        last mod file time2 bytes
        last mod file date2 bytes
        crc-324 bytes
        compressed size4 bytes
        uncompressed size4 bytes
        file name length2 bytes
        extra field length2 bytes
        file name可變
        extra field可變

        在一個(gè)有效 zip 文件中,header 簽名是一個(gè)整數(shù) (0x04034b50 )。我們將忽略版本、通用 flag 和校驗(yàn)和??梢允菦]有壓縮(用 0 表示),也可以是使用 DEFLATE ?方法解壓縮(用 8 表示)。

        最后修改時(shí)間和日期是 MSDOS 風(fēng)格的日期/時(shí)間格式。

        我們粗略地將其翻譯為 Go 代碼:

        package?main

        import?(
        ????"os"
        ????"bytes"
        ????"compress/flate"
        ????"io/ioutil"
        ????"encoding/binary"
        ????"time"
        ????"fmt"
        )

        type?compression?uint8
        const?(
        ????noCompression?compression?=?iota
        ????deflateCompression
        )

        type?localFileHeader?struct?{
        ????signature?uint32
        ????version?uint16
        ????bitFlag?uint16
        ????compression?compression
        ????lastModified?time.Time
        ????crc32?uint32
        ????compressedSize?uint32
        ????uncompressedSize?uint32
        ????fileName?string
        ????extraField?[]byte
        ????fileContents?string
        }

        02 main 函數(shù)實(shí)現(xiàn)

        我們的入口點(diǎn)將讀取一個(gè) zip 文件并遍歷該文件,直到我們無法解析 zip 文件條目。

        func?main()?{
        ????f,?err?:=?ioutil.ReadFile(os.Args[1])
        ????if?err?!=?nil?{
        ????????panic(err)
        ????}

        ????end?:=?0
        ????for?end?len(f)?{
        ????????var?err?error
        ????????var?lfh?*localFileHeader
        ????????var?next?int
        ????????lfh,?next,?err?=?parseLocalFileHeader(f,?end)
        ????????if?err?==?errNotZip?&&?end?>?0?{
        ????????????break
        ????????}
        ????????if?err?!=?nil?{
        ????????????panic(err)
        ????????}

        ????????end?=?next

        ????????fmt.Println(lfh.lastModified,?lfh.fileName,?lfh.fileContents)
        ????}
        }

        03 文件

        對(duì)于每個(gè)文件,如果前四個(gè)字節(jié)不是魔術(shù) zip 簽名(即 0x04034b50),則報(bào)錯(cuò)。

        var?errNotZip?=?fmt.Errorf("Not?a?zip?file")

        func?parseLocalFileHeader(bs?[]byte,?start?int)?(*localFileHeader,?int,?error)?{
        ????signature,?i,?err?:=?readUint32(bs,?start)
        ????if?signature?!=?0x04034b50?{
        ????????return?nil,?0,?errNotZip
        ????}
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        基本模式是讀取輔助函數(shù)將獲取一個(gè)偏移量并返回一個(gè) Go 值和一個(gè)新的偏移量。讀取輔助函數(shù)將進(jìn)行邊界檢查。

        遵循相同的模式直到結(jié)構(gòu)體的末尾:

        ????version,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????bitFlag,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????compression?:=?noCompression
        ????compressionRaw,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}
        ????if?compressionRaw?==?8?{
        ????????compression?=?deflateCompression
        ????}

        ????lmTime,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????lmDate,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}
        ????lastModified?:=?msdosTimeToGoTime(lmDate,?lmTime)

        ????crc32,?i,?err?:=?readUint32(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????compressedSize,?i,?err?:=?readUint32(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????uncompressedSize,?i,?err?:=?readUint32(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????fileNameLength,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????extraFieldLength,?i,?err?:=?readUint16(bs,?i)
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????fileName,?i,?err?:=?readString(bs,?i,?int(fileNameLength))
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        ????extraField,?i,?err?:=?readBytes(bs,?i,?int(extraFieldLength))
        ????if?err?!=?nil?{
        ????????return?nil,?0,?err
        ????}

        現(xiàn)在,如果文件內(nèi)容未壓縮,我們只需復(fù)制文件頭后的字節(jié)即可。如果文件內(nèi)容被壓縮,我們將使用 Go 的內(nèi)置 DEFLATE 支持來解壓縮文件頭之后的字節(jié)。

        ????var?fileContents?string
        ????if?compression?==?noCompression?{
        ????????fileContents,?i,?err?=?readString(bs,?i,?int(uncompressedSize))
        ????????if?err?!=?nil?{
        ????????????return?nil,?0,?err
        ????????}
        ????}?else?{
        ????????end?:=?i?+?int(compressedSize)
        ????????if?end?>?len(bs)?{
        ????????????return?nil,?0,?errOverranBuffer
        ????????}
        ????????flateReader?:=?flate.NewReader(bytes.NewReader(bs[i:end]))

        ????????defer?flateReader.Close()
        ????????read,?err?:=?ioutil.ReadAll(flateReader)
        ????????if?err?!=?nil?{
        ????????????return?nil,?0,?err
        ????????}

        ????????fileContents?=?string(read)

        ????????i?=?end
        ????}

        并返回填充好的結(jié)構(gòu)體實(shí)例:

        ????return?&localFileHeader{
        ????????signature:?signature,
        ????????version:?version,
        ????????bitFlag:?bitFlag,
        ????????compression:?compression,
        ????????lastModified:?lastModified,
        ????????crc32:?crc32,
        ????????compressedSize:?compressedSize,
        ????????uncompressedSize:?uncompressedSize,
        ????????fileName:?fileName,
        ????????extraField:?extraField,
        ????????fileContents:?fileContents,
        ????},?i,?nil
        }

        04 讀取輔助函數(shù)

        現(xiàn)在我們只定義那些帶有邊界檢查的讀取輔助函數(shù),使用 Go 的內(nèi)置庫來處理二進(jìn)制編碼。

        var?errOverranBuffer?=?fmt.Errorf("Overran?buffer")

        func?readUint32(bs?[]byte,?offset?int)?(uint32,?int,?error)?{
        ????end?:=?offset?+?4
        ????if?end?>?len(bs)?{
        ????????return?0,?0,?errOverranBuffer
        ????}

        ????return?binary.LittleEndian.Uint32(bs[offset:end]),?end,?nil
        }

        func?readUint16(bs?[]byte,?offset?int)?(uint16,?int,?error)?{
        ????end?:=?offset+2
        ????if?end?>?len(bs)?{
        ????????return?0,?0,?errOverranBuffer
        ????}

        ????return?binary.LittleEndian.Uint16(bs[offset:end]),?end,?nil
        }

        并且基本上只對(duì)獲取的字節(jié)和字符串進(jìn)行邊界檢查。

        func?readBytes(bs?[]byte,?offset?int,?n?int)?([]byte,?int,?error)?{
        ????end?:=?offset?+?n
        ????if?end?>?len(bs)?{
        ????????return?nil,?0,?errOverranBuffer
        ????}

        ????return?bs[offset:offset+n],?end,?nil
        }

        func?readString(bs?[]byte,?offset?int,?n?int)?(string,?int,?error)?{
        ????read,?end,?err?:=?readBytes(bs,?offset,?n)
        ????return?string(read),?end,?err
        }

        05 MSDOS 時(shí)間

        我猜在創(chuàng)建 zip 時(shí),MSDOS 時(shí)間格式很流行。但它在今天并不流行,所以花了一些時(shí)間才最終用一些代碼(模仿 C 語言)找到對(duì)該格式的解釋[2]。

        func?msdosTimeToGoTime(d?uint16,?t?uint16)?time.Time?{
        ????seconds?:=?int((t?&?0x1F)?*?2)
        ????minutes?:=?int((t?>>?5)?&?0x3F)
        ????hours?:=?int(t?>>?11)

        ????day?:=?int(d?&?0x1F)
        ????month?:=?time.Month((d?>>?5)?&?0x0F)
        ????year?:=?int((d?>>?9)?&?0x7F)?+?1980
        ????return?time.Date(year,?month,?day,?hours,?minutes,?seconds,?0,?time.Local)
        }

        06 測試

        運(yùn)行:

        $?go?build
        $?./gozip?test.zip
        2021-11-23?23:04:20?+0000?UTC?hello.text?Hello!

        這看起來不錯(cuò)!現(xiàn)在讓我們嘗試壓縮多個(gè)文件。

        $?cat?bye.text
        Au?revoir!
        $?rm?test.zip
        $?zip?test.zip?*.text
        ??adding:?bye.text?(stored?0%)
        ??adding:?hello.text?(stored?0%)
        $?./gozip?test.zip
        2021-11-24?03:40:00?+0000?UTC?bye.text?Au?revoir!

        2021-11-23?23:04:20?+0000?UTC?hello.text?Hello!

        一切正常。

        07 總結(jié)

        實(shí)際上,還有許多標(biāo)準(zhǔn)需要處理(例如目錄)和許多常見的擴(kuò)展,本文沒有涉及。

        文件末尾還有一些空間,這可能是 “central directory” 元數(shù)據(jù),但我還沒有深入研究。如果你有興趣可以查閱相關(guān)資料了解最后剩下的部分內(nèi)容。

        原文鏈接:https://notes.eatonphil.com/implementing-zip-in-go-unzipping.html

        參考資料

        [1]

        這里: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT

        [2]

        對(duì)該格式的解釋: https://groups.google.com/g/comp.os.msdos.programmer/c/ffAVUFN2NbA



        推薦閱讀


        福利

        我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù)?ebook?獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

        瀏覽 78
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 欧美精品成人a在线观看hd | 国产在线91在线电影 | 观看成人永久免费视频 | 久久超碰97 | 啊灬啊灬啊灬快灬高潮了女攻男受 |