大家好,我是程序员幽灵。Zip是一种常见的存档格式,本文将解释Go如何与zip一起运行。让我们先看看zip文件是如何工作的。以一个小文件为例:(类Unix系统)$cathello.textHello!执行zip命令进行归档:$ziptest.ziphello.textadding:hello.text(stored0%)$ls-lahtest.zip-rw-r--r--1philphil177Nov2323:04test.zip一个6字节的文本文件变成一个177字节zip文件。这并不大,解析177个字节听起来并不复杂!进行zip文件的六角形:$hexdump-ctest.zip0000000000504b03040a000000000000000000000087777539ED8|PK........WS..WS..|0000001042B007007000000000000001C006.865|textUT...ts.|000000306174739d6175780b000104eb03000004|ats.aux.......|00000040eb03000048656c6c6f210a504b01021e|....你好!.PK。..|00000050030a00000000008ab877539ed842b007|.....wS..B..|00000060000000070000000a001800000000001|......|00000070000000a4810007.0060.62e000000806578745555405000374739d6175780b00|退出...TS.AUX..|0000009004EB03004EB030000504B050600|............00000000000000500000000000000|............Pk。...|000000b000|.|000000b1从中我们可以看到文件名和文件内容。01结构我们来看看这里定义的zip结构[1]。根据4.3.6节,文件元数据似乎是一个接一个地存储,然后是文件内容,最后一块是“中央目录”元数据。zip格式头图片来源:https://www.codeproject.com/Articles/8688/Extracting-files-from-a-remote-ZIP-archive本地头元数据如下:fieldsizelocalfileheadersignature4bytesversion需要提取2字节通用位标志2字节压缩方法2字节最后mod文件时间2字节最后mod文件日期2字节crc-324字节压缩大小4字节未压缩大小4字节文件名长度2字节额外2字段te长度文件名变量额外的字段是可变的。在有效的zip文件中,标头签名是一个整数(0x04034b50)。我们将忽略版本、通用标志和校验和。可以是不压缩(用0表示),或者使用DEFLATE方法解压(用8表示)。最后修改的时间和日期采用MSDOS风格的日期/时间格式。我们粗略地将其翻译成Go代码:bytefileContentsstring}02主要函数实现我们的入口点将读取一个zip文件并遍历它,直到我们无法解析zip文件条目。funcmain(){f,err:=ioutil.ReadFile(os.Args[1])iferr!=nil{panic(err)}end:=0forend0{break}iferr!=nil{panic(err)}end=nextfmt.Println(lfh.lastModified,lfh.fileName,lfh.fileContents)}}03files对于每个文件,报告如果前四个字节不是魔术zip签名(即0x04034b50),则会出错。varerrNotZip=fmt.Errorf("Notazipfile")funcparseLocalFileHeader(bs[]byte,startint)(*localFileHeader,int,error){signature,i,err:=readUint32(bs,start)ifsignature!=0x04034b50{returnnil,0,errNotZip}iferr!=nil{returnnil,0,err}基本模式是读取辅助函数将获取一个偏移量并返回一个Go值和一个新的偏移量。读取辅助函数将进行边界检查。遵循相同的模式直到结构结束:version,i,err:=readUint16(bs,i)iferr!=nil{returnil,0,err}bitFlag,i,err:=readUint16(bs,i)iferr!=nil{returnnil,0,err}compression:=noCompressioncompressionRaw,i,err:=readUint16(bs,i)iferr!=nil{returnnil,0,err}ifcompressionRaw==8{compression=deflateCompression}lmTime,i,err:=readUint16(bs,i)iferr!=nil{returnnil,0,err}lmDate,i,err:=readUint16(bs,i)iferr!=nil{returnnil,0,err}lastModified:=msdosTimeToGoTime(lmDate,lmTime)crc32,i,err:=readUint32(bs,i)iferr!=nil{returnil,0,err}compressedSize,i,err:=readUint32(bs,i)iferr!=nil{returnnil,0,err}uncompressedSize,i,err:=readUint32(bs,i)iferr!=nil{returnil,0,err}fileNameLength,i,err:=readUint16(bs,i)iferr!=nil{returnnil,0,err}extraFieldLength,i,err:=readUint16(bs,i)iferr!=nil{returnil,0,err}fileName,i,err:=readString(bs,i,int(fileNameLength))iferr!=nil{returnnil,0,err}extraField,i,err:=readBytes(bs,i,int(extraFieldLength))iferr!=nil{returnnil,0,err}现在,如果文件内容是解压的ed,我们只是复制文件头之后的字节。如果文件内容被压缩,我们将使用Go内置的DEFLATE支持来解压缩文件头后面的字节。varfileContentsstringifcompression==noCompression{fileContents,i,err=readString(bs,i,int(uncompressedSize))iferr!=nil{returnnil,0,err}}else{end:=i+int(compressedSize)ifend>len(bs){returnnil,0,errOverranBuffer}flateReader:=flate.NewReader(bytes.NewReader(bs[i:end]))deferflateReader.Close()read,err:=ioutil.ReadAll(flateReader)iferr!=nil{returnnil,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,nil04Readhelpers现在我们只定义那些带有边界检查的readhelpers,使用Go的内置库来处理二进制编码。varerrOverranBuffer=fmt.Errorf("Overranbuffer")funcreadUint32(bs[]byte,offsetint)(uint32,int,error){end:=offset+4ifend>len(bs){return0,0,errOverranBuffer}returnbinary.LittleEndian.Uint32(bs[offset:end]),end,nil}funcreadUint16(bs[]byte,offsetint)(uint16,int,error){end:=offset+2ifend>len(bs){return0,0,errOverranBuffer}返回二进制.LittleEndian.Uint16(bs[offset:end]),end,nil}并且基本上只对您获得的字节和字符串进行边界检查。funcreadBytes(bs[]byte,offsetint,nint)([]byte,int,error){end:=offset+nifend>len(bs){returnil,0,errOverranBuffer}returnbs[offset:offset+n],end,nil}funcreadString(bs[]byte,offsetint,nint)(string,int,error){read,end,err:=readBytes(bs,offset,n)returnstring(read),end,err}05MSDOS时间我猜MSDOS时间格式在创建zip时很流行。但它今天并不流行,所以花了一些时间终于找到了一些代码(模仿C)[2]的格式解释。funcmsdosTimeToGoTime(duint16,tuint16)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)+1980returntime.Date(year,month,day,hours,minutes,seconds,0,time.本地)}06测试运行:$gobuild$./goziptest.zip2021-11-2323:04:20+0000UTChello.text你好!这看起来不错!现在让我们尝试压缩多个文件。$catbye.textAurevoir!$rmtest.zip$ziptest.zip*.textadding:bye.text(stored0%)添加:hello.text(stored0%)$./goziptest.zip2021-11-2403:40:00+0000UTC再见。textAurevoir!2021-11-2323:04:20+0000UTChello.textHello!一切正常。07小结其实还有很多标准要处理(比如目录),还有很多常用的扩展,本文没有涉及。文件末尾还有一些空间,可能是“中央目录”元数据,但我还没有深入研究。有兴趣的可以查阅相关资料了解最后一部分的内容。原文链接: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