加入收藏 | 设为首页 | 会员中心 | 我要投稿 PHP编程网 - 湛江站长网 (https://www.0759zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程 > 正文

Go 不用 标准库如何解压 Zip 文件?

发布时间:2022-11-21 12:40:00 所属栏目:编程 来源:
导读:  zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。
  
  首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)
  
  $ cat hello.text
  
  Hello!
  执行 zip 命令

  zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。
  
  首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)
  
  $ cat hello.text
  
  Hello!
  执行 zip 命令进行归档:
  
  $ 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 字节的文本文件变成了一个 177 字节的 zip 文件。这并不大,解析 177 个字节听起来不可能太复杂!
  
  对 zip 文件执行 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
  从中我们可以看到文件名和文件内容。
  
  01 结构
  我们来看看这里[1]定义的 zip 结构 。根据第 4.3.6 节,看起来文件元数据后跟文件内容一个接一个地存储,最后一块是 “central directory” 元数据。
  
  
  
  zip format header
  
  图片来源:https://www.codeproject.com/Articles/8688/Extracting-files-from-a-remote-ZIP-archive
  
  本地 header 元数据如下所示:
  
  字段 大小
  local file header signature 4 bytes
  version needed to extract 2 bytes
  general purpose bit flag 2 bytes
  compression method 2 bytes
  last mod file time 2 bytes
  last mod file date 2 bytes
  crc-32 4 bytes
  compressed size 4 bytes
  uncompressed size 4 bytes
  file name length 2 bytes
  extra field length 2 bytes
  file name 可变
  extra field 可变
  在一个有效 zip 文件中,header 签名是一个整数 (0x04034b50 )。我们将忽略版本、通用 flag 和校验和。可以是没有压缩(用 0 表示),也可以是使用 DEFLATE 方法解压缩(用 8 表示)。
  
  最后修改时间和日期是 MSDOS 风格的日期/时间格式。
  
  我们粗略地将其翻译为 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 函数实现
  我们的入口点将读取一个 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 文件
  对于每个文件,如果前四个字节不是魔术 zip 签名(即 0x04034b50),则报错。
  
  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
      }
  基本模式是读取辅助函数将获取一个偏移量并返回一个 Go 值和一个新的偏移量。读取辅助函数将进行边界检查。
  
  遵循相同的模式直到结构体的末尾:
  
  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
  }
  现在,如果文件内容未压缩,我们只需复制文件头后的字节即可。如果文件内容被压缩,我们将使用 Go 的内置 DEFLATE 支持来解压缩文件头之后的字节。
  
  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
  }
  并返回填充好的结构体实例:
  
  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 读取辅助函数
  现在我们只定义那些带有边界检查的读取辅助函数,使用 Go 的内置库来处理二进制编码。
  
  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
  }
  并且基本上只对获取的字节和字符串进行边界检查。
  
  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 时间
  我猜在创建 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 测试
  运行:
  
  $ go build
  $ ./gozip test.zip
  2021-11-23 23:04:20 +0000 UTC hello.text Hello!
  这看起来不错!现在让我们尝试压缩多个文件。
  
  $ 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 总结
  实际上,还有许多标准需要处理(例如目录)和许多常见的扩展,本文没有涉及。
  
  文件末尾还有一些空间,这可能是 “central directory” 元数据,但我还没有深入研究。如果你有兴趣可以查阅相关资料了解最后剩下的部分内容。
  
  原文链接: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
  
  【编辑推荐】
  
  鸿蒙官方战略合作共建——HarmonyOS技术社区
  Go:通过 io.Writer 将 JPEG 转为 JFIF
  那些用 Go 实现的分布式事务框架
  Go 为什么不支持类和继承?
  Google 公布 2021 年热门 Chrome 扩展程序名单
  英国政府:将在2033年前逐步淘汰2G和3G移动网络zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。
  
  首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)
  
  $ cat hello.text
  
  Hello!
  执行 zip 命令进行归档:
  
  $ 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 字节的文本文件变成了一个 177 字节的 zip 文件。这并不大,解析 177 个字节听起来不可能太复杂!
  
  对 zip 文件执行 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
  从中我们可以看到文件名和文件内容。
  
  01 结构
  我们来看看这里[1]定义的 zip 结构 。根据第 4.3.6 节,看起来文件元数据后跟文件内容一个接一个地存储,最后一块是 “central directory” 元数据。
  
  
  
  zip format header
  
  图片来源:https://www.codeproject.com/Articles/8688/Extracting-files-from-a-remote-ZIP-archive
  
  本地 header 元数据如下所示:
  
  字段 大小
  local file header signature 4 bytes
  version needed to extract 2 bytes
  general purpose bit flag 2 bytes
  compression method 2 bytes
  last mod file time 2 bytes
  last mod file date 2 bytes
  crc-32 4 bytes
  compressed size 4 bytes
  uncompressed size 4 bytes
  file name length 2 bytes
  extra field length 2 bytes
  file name 可变
  extra field 可变
  在一个有效 zip 文件中,header 签名是一个整数 (0x04034b50 )。我们将忽略版本、通用 flag 和校验和。可以是没有压缩(用 0 表示),也可以是使用 DEFLATE 方法解压缩(用 8 表示)。
  
  最后修改时间和日期是 MSDOS 风格的日期/时间格式。
  
  我们粗略地将其翻译为 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 函数实现
  我们的入口点将读取一个 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 文件
  对于每个文件,如果前四个字节不是魔术 zip 签名(即 0x04034b50),则报错。
  
  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
      }
  基本模式是读取辅助函数将获取一个偏移量并返回一个 Go 值和一个新的偏移量。读取辅助函数将进行边界检查。
  
  遵循相同的模式直到结构体的末尾:
  
  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
  }
  现在,如果文件内容未压缩,我们只需复制文件头后的字节即可。如果文件内容被压缩,我们将使用 Go 的内置 DEFLATE 支持来解压缩文件头之后的字节。
  
  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
  }
  并返回填充好的结构体实例:
  
  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 读取辅助函数
  现在我们只定义那些带有边界检查的读取辅助函数,使用 Go 的内置库来处理二进制编码。
  
  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
  }
  并且基本上只对获取的字节和字符串进行边界检查。
  
  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 时间
  我猜在创建 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 测试
  运行:
  
  $ go build
  $ ./gozip test.zip
  2021-11-23 23:04:20 +0000 UTC hello.text Hello!
  这看起来不错!现在让我们尝试压缩多个文件。
  
  $ 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 总结
  实际上,还有许多标准需要处理(例如目录)和许多常见的扩展,本文没有涉及。
  
  文件末尾还有一些空间,这可能是 “central directory” 元数据,但我还没有深入研究。如果你有兴趣可以查阅相关资料了解最后剩下的部分内容。
  
  原文链接: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
  
  【编辑推荐】
  
  鸿蒙官方战略合作共建——HarmonyOS技术社区
  Go:通过 io.Writer 将 JPEG 转为 JFIF
  那些用 Go 实现的分布式事务框架
  Go 为什么不支持类和继承?
  Google 公布 2021 年热门 Chrome 扩展程序名单
  英国政府:将在2033年前逐步淘汰2G和3G移动网络
 

(编辑:PHP编程网 - 湛江站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!