<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 7788字,需瀏覽 16分鐘

           ·

          2021-12-24 06:17

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

          首先看看 zip 文件是如何工作的。以一個小文件為例:(類 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

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

          對 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)容一個接一個地存儲,最后一塊是 “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可變

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

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

          我們粗略地將其翻譯為 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)將讀取一個 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 文件

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

          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ù)將獲取一個偏移量并返回一個 Go 值和一個新的偏移量。讀取輔助函數(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
          }

          并且基本上只對獲取的字節(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 時間

          我猜在創(chuàng)建 zip 時,MSDOS 時間格式很流行。但它在今天并不流行,所以花了一些時間才最終用一些代碼(模仿 C 語言)找到對該格式的解釋[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!

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

          $?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]

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



          推薦閱讀


          福利

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

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲国内自拍 | 成人性交网 | 日本成人在线午夜影院 | 日韩无码黄色电影 | 在线中文字幕第一 |