// Copyright 2011 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package zipimport ()var ( errLongName = errors.New("zip: FileHeader.Name too long") errLongExtra = errors.New("zip: FileHeader.Extra too long"))// Writer implements a zip file writer.typeWriterstruct { cw *countWriter dir []*header last *fileWriter closed bool compressors map[uint16]Compressor comment string// testHookCloseSizeOffset if non-nil is called with the size // of offset of the central directory at Close. testHookCloseSizeOffset func(size, offset uint64)}type header struct { *FileHeader offset uint64 raw bool}// NewWriter returns a new [Writer] writing a zip file to w.//// Note that the exact bytes written to w are not covered by the Go 1// compatibility promise. Callers, including tests, should not depend on the// exact written bytes.func ( io.Writer) *Writer {return &Writer{cw: &countWriter{w: bufio.NewWriter()}}}// SetOffset sets the offset of the beginning of the zip data within the// underlying writer. It should be used when the zip data is appended to an// existing file, such as a binary executable.// It must be called before any data is written.func ( *Writer) ( int64) {if .cw.count != 0 {panic("zip: SetOffset called after data was written") } .cw.count = }// Flush flushes any buffered data to the underlying writer.// Calling Flush is not normally necessary; calling Close is sufficient.func ( *Writer) () error {return .cw.w.(*bufio.Writer).Flush()}// SetComment sets the end-of-central-directory comment field.// It can only be called before [Writer.Close].func ( *Writer) ( string) error {iflen() > uint16max {returnerrors.New("zip: Writer.Comment too long") } .comment = returnnil}// Close finishes writing the zip file by writing the central directory.// It does not close the underlying writer.func ( *Writer) () error {if .last != nil && !.last.closed {if := .last.close(); != nil {return } .last = nil }if .closed {returnerrors.New("zip: writer closed twice") } .closed = true// write central directory := .cw.count := falsefor , := range .dir {// For the Central Directory, we always have the correct sizes. // // Implementations disagree on what triggers the inclusion of a Zip64 // extra field: Info-ZIP only writes it if any size or offset EXCEEDS // 4GiB - 1, while libarchive writes it if any size REACHES OR EXCEEDS // 4GiB - 1, or if the offset EXCEEDS 4GiB - 1. The spec is ambiguous. // // We conservatively write Zip64 extra fields if any size or offset // REACHES OR EXCEEDS 4GiB - 1, to maximize compatibility with readers. // There is no ambiguity in parsing, so there is no downside to it. // // The spec is clear though that all and only the fields that REACH OR // EXCEED 4GiB - 1 are included in the Zip64 extra, once it's present. := .ReaderVersionif .CompressedSize64 >= uint32max || .UncompressedSize64 >= uint32max || .offset >= uint32max { = true = max(, zipVersion45)varuint16var [28]byte// 2x uint16 + up to 3x uint64 := writeBuf([:]) .uint16(zip64ExtraID) .uint16(0) // size to be filled out laterif .UncompressedSize64 >= uint32max { .uint64(.UncompressedSize64) += 8 }if .CompressedSize64 >= uint32max { .uint64(.CompressedSize64) += 8 }if .offset >= uint32max { .uint64(.offset) += 8 } := writeBuf([2:]) .uint16() .Extra = append(.Extra, [:4+]...) }var [directoryHeaderLen]byte := writeBuf([:]) .uint32(uint32(directoryHeaderSignature)) .uint16(.CreatorVersion) .uint16() .uint16(.Flags) .uint16(.Method) .uint16(.ModifiedTime) .uint16(.ModifiedDate) .uint32(.CRC32) .uint32(uint32(min(.CompressedSize64, uint32max))) .uint32(uint32(min(.UncompressedSize64, uint32max))) .uint16(uint16(len(.Name))) .uint16(uint16(len(.Extra))) .uint16(uint16(len(.Comment))) = [4:] // skip disk number start and internal file attr (2x uint16) .uint32(.ExternalAttrs) .uint32(uint32(min(.offset, uint32max)))if , := .cw.Write([:]); != nil {return }if , := io.WriteString(.cw, .Name); != nil {return }if , := .cw.Write(.Extra); != nil {return }if , := io.WriteString(.cw, .Comment); != nil {return } } := .cw.count := uint64(len(.dir)) := uint64( - ) := uint64()if := .testHookCloseSizeOffset; != nil { (, ) }// Emit the Zip64 EOCD records whenever any individual entry needed a Zip64 // extra field, even if the EOCD's own fields fit in 32 bits, matching // Info-ZIP (but not libarchive). See APPNOTE 4.3.9.2: "when Zip64 // extensions are in use, the EOCD64 record must be present."if || >= uint16max || >= uint32max || >= uint32max {var [directory64EndLen + directory64LocLen]byte := writeBuf([:])// zip64 end of central directory record .uint32(directory64EndSignature) .uint64(directory64EndLen - 12) // length minus signature (uint32) and length fields (uint64) .uint16(zipVersion45) // version made by .uint16(zipVersion45) // version needed to extract .uint32(0) // number of this disk .uint32(0) // number of the disk with the start of the central directory .uint64() // total number of entries in the central directory on this disk .uint64() // total number of entries in the central directory .uint64() // size of the central directory .uint64() // offset of start of central directory with respect to the starting disk number// zip64 end of central directory locator .uint32(directory64LocSignature) .uint32(0) // number of the disk with the start of the zip64 end of central directory .uint64(uint64()) // relative offset of the zip64 end of central directory record .uint32(1) // total number of disksif , := .cw.Write([:]); != nil {return } }// write end recordvar [directoryEndLen]byte := writeBuf([:]) .uint32(uint32(directoryEndSignature)) = [4:] // skip over disk number and first disk number (2x uint16) .uint16(uint16(min(uint16max, ))) // number of entries this disk .uint16(uint16(min(uint16max, ))) // number of entries total .uint32(uint32(min(uint32max, ))) // size of directory .uint32(uint32(min(uint32max, ))) // start of directory .uint16(uint16(len(.comment))) // byte size of EOCD commentif , := .cw.Write([:]); != nil {return }if , := io.WriteString(.cw, .comment); != nil {return }return .cw.w.(*bufio.Writer).Flush()}// Create adds a file to the zip file using the provided name.// It returns a [Writer] to which the file contents should be written.// The file contents will be compressed using the [Deflate] method.// The name must be a relative path: it must not start with a drive// letter (e.g. C:) or leading slash, and only forward slashes are// allowed. To create a directory instead of a file, add a trailing// slash to the name. Duplicate names will not overwrite previous entries// and are appended to the zip file.// The file's contents must be written to the [io.Writer] before the next// call to [Writer.Create], [Writer.CreateHeader], or [Writer.Close].func ( *Writer) ( string) (io.Writer, error) { := &FileHeader{Name: ,Method: Deflate, }return .CreateHeader()}// detectUTF8 reports whether s is a valid UTF-8 string, and whether the string// must be considered UTF-8 encoding (i.e., not compatible with CP-437, ASCII,// or any other common encoding).func detectUTF8( string) (, bool) {for := 0; < len(); { , := utf8.DecodeRuneInString([:]) += // Officially, ZIP uses CP-437, but many readers use the system's // local character encoding. Most encoding are compatible with a large // subset of CP-437, which itself is ASCII-like. // // Forbid 0x7e and 0x5c since EUC-KR and Shift-JIS replace those // characters with localized currency and overline characters.if < 0x20 || > 0x7d || == 0x5c {if !utf8.ValidRune() || ( == utf8.RuneError && == 1) {returnfalse, false } = true } }returntrue, }// prepare performs the bookkeeping operations required at the start of// CreateHeader and CreateRaw.func ( *Writer) ( *FileHeader) error {if .last != nil && !.last.closed {if := .last.close(); != nil {return } }iflen(.dir) > 0 && .dir[len(.dir)-1].FileHeader == {// See https://golang.org/issue/11144 confusion.returnerrors.New("archive/zip: invalid duplicate FileHeader") }returnnil}// CreateHeader adds a file to the zip archive using the provided [FileHeader]// for the file metadata. [Writer] takes ownership of fh and may mutate// its fields. The caller must not modify fh after calling [Writer.CreateHeader].//// This returns a [Writer] to which the file contents should be written.// The file's contents must be written to the io.Writer before the next// call to [Writer.Create], [Writer.CreateHeader], [Writer.CreateRaw], or [Writer.Close].func ( *Writer) ( *FileHeader) (io.Writer, error) {if := .prepare(); != nil {returnnil, }// The ZIP format has a sad state of affairs regarding character encoding. // Officially, the name and comment fields are supposed to be encoded // in CP-437 (which is mostly compatible with ASCII), unless the UTF-8 // flag bit is set. However, there are several problems: // // * Many ZIP readers still do not support UTF-8. // * If the UTF-8 flag is cleared, several readers simply interpret the // name and comment fields as whatever the local system encoding is. // // In order to avoid breaking readers without UTF-8 support, // we avoid setting the UTF-8 flag if the strings are CP-437 compatible. // However, if the strings require multibyte UTF-8 encoding and is a // valid UTF-8 string, then we set the UTF-8 bit. // // For the case, where the user explicitly wants to specify the encoding // as UTF-8, they will need to set the flag bit themselves. , := detectUTF8(.Name) , := detectUTF8(.Comment)switch {case .NonUTF8: .Flags &^= 0x800case ( || ) && ( && ): .Flags |= 0x800 } .CreatorVersion = .CreatorVersion&0xff00 | zipVersion20// preserve compatibility byte .ReaderVersion = zipVersion20// If Modified is set, this takes precedence over MS-DOS timestamp fields.if !.Modified.IsZero() {// Contrary to the FileHeader.SetModTime method, we intentionally // do not convert to UTC, because we assume the user intends to encode // the date using the specified timezone. A user may want this control // because many legacy ZIP readers interpret the timestamp according // to the local timezone. // // The timezone is only non-UTC if a user directly sets the Modified // field directly themselves. All other approaches sets UTC. .ModifiedDate, .ModifiedTime = timeToMsDosTime(.Modified)// Use "extended timestamp" format since this is what Info-ZIP uses. // Nearly every major ZIP implementation uses a different format, // but at least most seem to be able to understand the other formats. // // This format happens to be identical for both local and central header // if modification time is the only timestamp being encoded.var [9]byte// 2*SizeOf(uint16) + SizeOf(uint8) + SizeOf(uint32) := uint32(.Modified.Unix()) := writeBuf([:]) .uint16(extTimeExtraID) .uint16(5) // Size: SizeOf(uint8) + SizeOf(uint32) .uint8(1) // Flags: ModTime .uint32() // ModTime .Extra = append(.Extra, [:]...) }var (io.Writer *fileWriter ) := &header{FileHeader: ,offset: uint64(.cw.count), }ifstrings.HasSuffix(.Name, "/") {// Set the compression method to Store to ensure data length is truly zero, // which the writeHeader method always encodes for the size fields. // This is necessary as most compression formats have non-zero lengths // even when compressing an empty string. .Method = Store .Flags &^= 0x8// we will not write a data descriptor// Explicitly clear sizes as they have no meaning for directories. .CompressedSize = 0 .CompressedSize64 = 0 .UncompressedSize = 0 .UncompressedSize64 = 0 = dirWriter{} } else { .Flags |= 0x8// we will write a data descriptor = &fileWriter{zipw: .cw,compCount: &countWriter{w: .cw},crc32: crc32.NewIEEE(), } := .compressor(.Method)if == nil {returnnil, ErrAlgorithm }varerror .comp, = (.compCount)if != nil {returnnil, } .rawCount = &countWriter{w: .comp} .header = = } .dir = append(.dir, )if := writeHeader(.cw, ); != nil {returnnil, }// If we're creating a directory, fw is nil. .last = return , nil}func writeHeader( io.Writer, *header) error {const = 1<<16 - 1iflen(.Name) > {returnerrLongName }iflen(.Extra) > {returnerrLongExtra }// The correct behavior of a streaming writer, implemented by Info-ZIP 3.0, // would be to write 0xFFFFFFFF in the size fields and then write a Zip64 // extra field with the sizes at zero (to signal they are stored in a ZIP64 // data descriptor, in case the file is > 4GiB). // // We don't do that, and instead write zeroes directly in the size fields, // because that wastes 28 bytes for every file smaller than 4GiB, and // because it would change the encoding of nearly every zip file created by // archive/zip. (No one should rely on it being stable, but still.) // // Anyway, the Local File Header is not that important, as the Central // Directory is authoritative, and there we always write the correct sizes. // // If we do know the sizes, because [Writer.CreateRaw] is used and the data // descriptor flag is not set, then we write them to the header. If either // size reaches 4GiB, we write 0xFFFFFFFF placeholders and a Zip64 extra // field with BOTH sizes, per the spec and matching Info-ZIP. Note this is // different from the Central Directory Zip64 extra field logic, somehow. // // (One final interesting case that doesn't apply to us: if the input is // streaming but the output is seekable, Info-ZIP always writes Zip64 extra // fields, and then goes back and patches in the sizes, even for files < 4GiB.)var []byte := .ReaderVersion := .raw && !.hasDataDescriptor()if && (.CompressedSize64 > uint32max || .UncompressedSize64 > uint32max) { = max(, zipVersion45) = make([]byte, 20) // 2x uint16 + 2x uint64 := writeBuf() .uint16(zip64ExtraID) .uint16(16) // size of Zip64 extra field data .uint64(.UncompressedSize64) .uint64(.CompressedSize64) }var [fileHeaderLen]byte := writeBuf([:]) .uint32(uint32(fileHeaderSignature)) .uint16() .uint16(.Flags) .uint16(.Method) .uint16(.ModifiedTime) .uint16(.ModifiedDate)if { .uint32(.CRC32)if != nil { .uint32(uint32max) .uint32(uint32max) } else { .uint32(uint32(.CompressedSize64)) .uint32(uint32(.UncompressedSize64)) } } else { .uint32(0) // crc32 .uint32(0) // compressed size .uint32(0) // uncompressed size } .uint16(uint16(len(.Name))) .uint16(uint16(len(.Extra) + len()))if , := .Write([:]); != nil {return }if , := io.WriteString(, .Name); != nil {return }if , := .Write(.Extra); != nil {return }if , := .Write(); != nil {return }returnnil}// CreateRaw adds a file to the zip archive using the provided [FileHeader] and// returns a [Writer] to which the file contents should be written. The file's// contents must be written to the io.Writer before the next call to [Writer.Create],// [Writer.CreateHeader], [Writer.CreateRaw], or [Writer.Close].//// In contrast to [Writer.CreateHeader], the bytes passed to Writer are not compressed.//// CreateRaw's argument is stored in w. If the argument is a pointer to the embedded// [FileHeader] in a [File] obtained from a [Reader] created from in-memory data,// then w will refer to all of that memory.func ( *Writer) ( *FileHeader) (io.Writer, error) {if := .prepare(); != nil {returnnil, } .CompressedSize = uint32(min(.CompressedSize64, uint32max)) .UncompressedSize = uint32(min(.UncompressedSize64, uint32max)) := &header{FileHeader: ,offset: uint64(.cw.count),raw: true, } .dir = append(.dir, )if := writeHeader(.cw, ); != nil {returnnil, }ifstrings.HasSuffix(.Name, "/") { .last = nilreturndirWriter{}, nil } := &fileWriter{header: ,zipw: .cw, } .last = return , nil}// Copy copies the file f (obtained from a [Reader]) into w. It copies the raw// form directly bypassing decompression, compression, and validation.func ( *Writer) ( *File) error { , := .OpenRaw()if != nil {return }// Copy the FileHeader so w doesn't store a pointer to the data // of f's entire archive. See #65499. := .FileHeader , := .CreateRaw(&)if != nil {return } _, = io.Copy(, )return}// RegisterCompressor registers or overrides a custom compressor for a specific// method ID. If a compressor for a given method is not found, [Writer] will// default to looking up the compressor at the package level.func ( *Writer) ( uint16, Compressor) {if .compressors == nil { .compressors = make(map[uint16]Compressor) } .compressors[] = }// AddFS adds the files from fs.FS to the archive.// It walks the directory tree starting at the root of the filesystem// adding each file to the zip using deflate while maintaining the directory structure.func ( *Writer) ( fs.FS) error {returnfs.WalkDir(, ".", func( string, fs.DirEntry, error) error {if != nil {return }if == "." {returnnil } , := .Info()if != nil {return }if !.IsDir() && !.Mode().IsRegular() {returnerrors.New("zip: cannot add non-regular file") } , := FileInfoHeader()if != nil {return } .Name = if .IsDir() { .Name += "/" } .Method = Deflate , := .CreateHeader()if != nil {return }if .IsDir() {returnnil } , := .Open()if != nil {return }defer .Close() _, = io.Copy(, )return })}func ( *Writer) ( uint16) Compressor { := .compressors[]if == nil { = compressor() }return}type dirWriter struct{}func (dirWriter) ( []byte) (int, error) {iflen() == 0 {return0, nil }return0, errors.New("zip: write to directory")}type fileWriter struct { *header zipw io.Writer rawCount *countWriter comp io.WriteCloser compCount *countWriter crc32 hash.Hash32 closed bool}func ( *fileWriter) ( []byte) (int, error) {if .closed {return0, errors.New("zip: write to closed file") }if .raw {return .zipw.Write() } .crc32.Write()return .rawCount.Write()}func ( *fileWriter) () error {if .closed {returnerrors.New("zip: file closed twice") } .closed = trueif .raw {return .writeDataDescriptor() }if := .comp.Close(); != nil {return }// update FileHeader := .header.FileHeader .CRC32 = .crc32.Sum32() .CompressedSize64 = uint64(.compCount.count) .UncompressedSize64 = uint64(.rawCount.count)if .CompressedSize64 > uint32max || .UncompressedSize64 > uint32max { .CompressedSize = uint32max .UncompressedSize = uint32max .ReaderVersion = zipVersion45// requires 4.5 - File uses ZIP64 format extensions } else { .CompressedSize = uint32(.CompressedSize64) .UncompressedSize = uint32(.UncompressedSize64) }return .writeDataDescriptor()}func ( *fileWriter) () error {if !.hasDataDescriptor() {returnnil }// See the comment in [writeHeader] about how and why we don't signal ZIP64 // mode in the local file header. If one of the sizes turns out to exceed // 4GiB, we use the 64-bit sizes anyway, for lack of alternatives. // // See also https://bugs.openjdk.org/browse/JDK-7073588.var []byteif .CompressedSize64 > uint32max || .UncompressedSize64 > uint32max { = make([]byte, dataDescriptor64Len) } else { = make([]byte, dataDescriptorLen) } := writeBuf() .uint32(dataDescriptorSignature) // de-facto standard, required by OS X .uint32(.CRC32)if .CompressedSize64 > uint32max || .UncompressedSize64 > uint32max { .uint64(.CompressedSize64) .uint64(.UncompressedSize64) } else { .uint32(.CompressedSize) .uint32(.UncompressedSize) } , := .zipw.Write()return}type countWriter struct { w io.Writer count int64}func ( *countWriter) ( []byte) (int, error) { , := .w.Write() .count += int64()return , }type nopCloser struct {io.Writer}func ( nopCloser) () error {returnnil}type writeBuf []bytefunc ( *writeBuf) ( uint8) { (*)[0] = * = (*)[1:]}func ( *writeBuf) ( uint16) {binary.LittleEndian.PutUint16(*, ) * = (*)[2:]}func ( *writeBuf) ( uint32) {binary.LittleEndian.PutUint32(*, ) * = (*)[4:]}func ( *writeBuf) ( uint64) {binary.LittleEndian.PutUint64(*, ) * = (*)[8:]}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.