亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢(xún)客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

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

首先看看 zip 文件是如何工作的。以一個(gè)小文件為例:(類(lèi) 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é)聽(tīng)起來(lái)不可能太復(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)

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

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

 

zip format header

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

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

字段大小local file header signature4 bytesversion needed to extract2 bytesgeneral purpose bit flag2 bytescompression method2 byteslast mod file time2 byteslast mod file date2 bytescrc-324 bytescompressed size4 bytesuncompressed size4 bytesfile name length2 bytesextra field length2 bytesfile name可變extra field可變

在一個(gè)有效 zip 文件中,header 簽名是一個(gè)整數(shù) (0x04034b50 )。我們將忽略版本、通用 flag 和校驗(yàn)和。可以是沒(méi)有壓縮(用 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 文件并遍歷該文件,直到我們無(wú)法解析 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 支持來(lái)解壓縮文件頭之后的字節(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)置庫(kù)來(lá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 語(yǔ)言)找到對(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 測(cè)試

運(yùn)行:

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

這看起來(lái)不錯(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)需要處理(例如目錄)和許多常見(jiàn)的擴(kuò)展,本文沒(méi)有涉及。

文件末尾還有一些空間,這可能是 “central directory” 元數(shù)據(jù),但我還沒(méi)有深入研究。如果你有興趣可以查閱相關(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

分享到:
標(biāo)簽:語(yǔ)言
用戶(hù)無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定