| // Copyright 2010 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 zip provides support for reading and writing ZIP archives. | |
| See the [ZIP specification] for details. | |
| This package does not support disk spanning. | |
| A note about ZIP64: | |
| To be backwards compatible the FileHeader has both 32 and 64 bit Size | |
| fields. The 64 bit fields will always contain the correct value and | |
| for normal archives both fields will be the same. For files requiring | |
| the ZIP64 format the 32 bit fields will be 0xffffffff and the 64 bit | |
| fields must be used instead. | |
| [ZIP specification]: https://support.pkware.com/pkzip/appnote | |
| */ | |
| package zip | |
| import ( | |
| "io/fs" | |
| "path" | |
| "time" | |
| ) | |
| // Compression methods. | |
| const ( | |
| Store uint16 = 0 // no compression | |
| Deflate uint16 = 8 // DEFLATE compressed | |
| ) | |
| const ( | |
| fileHeaderSignature = 0x04034b50 | |
| directoryHeaderSignature = 0x02014b50 | |
| directoryEndSignature = 0x06054b50 | |
| directory64LocSignature = 0x07064b50 | |
| directory64EndSignature = 0x06064b50 | |
| dataDescriptorSignature = 0x08074b50 // de-facto standard; required by OS X Finder | |
| fileHeaderLen = 30 // + filename + extra | |
| directoryHeaderLen = 46 // + filename + extra + comment | |
| directoryEndLen = 22 // + comment | |
| dataDescriptorLen = 16 // four uint32: descriptor signature, crc32, compressed size, size | |
| dataDescriptor64Len = 24 // two uint32: signature, crc32 | two uint64: compressed size, size | |
| directory64LocLen = 20 // | |
| directory64EndLen = 56 // + extra | |
| // Constants for the first byte in CreatorVersion. | |
| creatorFAT = 0 | |
| creatorUnix = 3 | |
| creatorNTFS = 11 | |
| creatorVFAT = 14 | |
| creatorMacOSX = 19 | |
| // Version numbers. | |
| zipVersion20 = 20 // 2.0 | |
| zipVersion45 = 45 // 4.5 (reads and writes zip64 archives) | |
| // Limits for non zip64 files. | |
| uint16max = (1 << 16) - 1 | |
| uint32max = (1 << 32) - 1 | |
| // Extra header IDs. | |
| // | |
| // IDs 0..31 are reserved for official use by PKWARE. | |
| // IDs above that range are defined by third-party vendors. | |
| // Since ZIP lacked high precision timestamps (nor an official specification | |
| // of the timezone used for the date fields), many competing extra fields | |
| // have been invented. Pervasive use effectively makes them "official". | |
| // | |
| // See http://mdfs.net/Docs/Comp/Archiving/Zip/ExtraField | |
| zip64ExtraID = 0x0001 // Zip64 extended information | |
| ntfsExtraID = 0x000a // NTFS | |
| unixExtraID = 0x000d // UNIX | |
| extTimeExtraID = 0x5455 // Extended timestamp | |
| infoZipUnixExtraID = 0x5855 // Info-ZIP Unix extension | |
| ) | |
| // FileHeader describes a file within a ZIP file. | |
| // See the [ZIP specification] for details. | |
| // | |
| // [ZIP specification]: https://support.pkware.com/pkzip/appnote | |
| type FileHeader struct { | |
| // Name is the name of the file. | |
| // | |
| // It must be a relative path, not start with a drive letter (such as "C:"), | |
| // and must use forward slashes instead of back slashes. A trailing slash | |
| // indicates that this file is a directory and should have no data. | |
| Name string | |
| // Comment is any arbitrary user-defined string shorter than 64KiB. | |
| Comment string | |
| // NonUTF8 indicates that Name and Comment are not encoded in UTF-8. | |
| // | |
| // By specification, the only other encoding permitted should be CP-437, | |
| // but historically many ZIP readers interpret Name and Comment as whatever | |
| // the system's local character encoding happens to be. | |
| // | |
| // This flag should only be set if the user intends to encode a non-portable | |
| // ZIP file for a specific localized region. Otherwise, the Writer | |
| // automatically sets the ZIP format's UTF-8 flag for valid UTF-8 strings. | |
| NonUTF8 bool | |
| CreatorVersion uint16 | |
| ReaderVersion uint16 | |
| Flags uint16 | |
| // Method is the compression method. If zero, Store is used. | |
| Method uint16 | |
| // Modified is the modified time of the file. | |
| // | |
| // When reading, an extended timestamp is preferred over the legacy MS-DOS | |
| // date field, and the offset between the times is used as the timezone. | |
| // If only the MS-DOS date is present, the timezone is assumed to be UTC. | |
| // | |
| // When writing, an extended timestamp (which is timezone-agnostic) is | |
| // always emitted. The legacy MS-DOS date field is encoded according to the | |
| // location of the Modified time. | |
| Modified time.Time | |
| // ModifiedTime is an MS-DOS-encoded time. | |
| // | |
| // Deprecated: Use Modified instead. | |
| ModifiedTime uint16 | |
| // ModifiedDate is an MS-DOS-encoded date. | |
| // | |
| // Deprecated: Use Modified instead. | |
| ModifiedDate uint16 | |
| // CRC32 is the CRC32 checksum of the file content. | |
| CRC32 uint32 | |
| // CompressedSize is the compressed size of the file in bytes. | |
| // If either the uncompressed or compressed size of the file | |
| // does not fit in 32 bits, CompressedSize is set to ^uint32(0). | |
| // | |
| // Deprecated: Use CompressedSize64 instead. | |
| CompressedSize uint32 | |
| // UncompressedSize is the uncompressed size of the file in bytes. | |
| // If either the uncompressed or compressed size of the file | |
| // does not fit in 32 bits, UncompressedSize is set to ^uint32(0). | |
| // | |
| // Deprecated: Use UncompressedSize64 instead. | |
| UncompressedSize uint32 | |
| // CompressedSize64 is the compressed size of the file in bytes. | |
| CompressedSize64 uint64 | |
| // UncompressedSize64 is the uncompressed size of the file in bytes. | |
| UncompressedSize64 uint64 | |
| Extra []byte | |
| ExternalAttrs uint32 // Meaning depends on CreatorVersion | |
| } | |
| // FileInfo returns an fs.FileInfo for the [FileHeader]. | |
| func (h *FileHeader) FileInfo() fs.FileInfo { | |
| return headerFileInfo{h} | |
| } | |
| // headerFileInfo implements [fs.FileInfo]. | |
| type headerFileInfo struct { | |
| fh *FileHeader | |
| } | |
| func (fi headerFileInfo) Name() string { return path.Base(fi.fh.Name) } | |
| func (fi headerFileInfo) Size() int64 { | |
| if fi.fh.UncompressedSize64 > 0 { | |
| return int64(fi.fh.UncompressedSize64) | |
| } | |
| return int64(fi.fh.UncompressedSize) | |
| } | |
| func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() } | |
| func (fi headerFileInfo) ModTime() time.Time { | |
| if fi.fh.Modified.IsZero() { | |
| return fi.fh.ModTime() | |
| } | |
| return fi.fh.Modified.UTC() | |
| } | |
| func (fi headerFileInfo) Mode() fs.FileMode { return fi.fh.Mode() } | |
| func (fi headerFileInfo) Type() fs.FileMode { return fi.fh.Mode().Type() } | |
| func (fi headerFileInfo) Sys() any { return fi.fh } | |
| func (fi headerFileInfo) Info() (fs.FileInfo, error) { return fi, nil } | |
| func (fi headerFileInfo) String() string { | |
| return fs.FormatFileInfo(fi) | |
| } | |
| // FileInfoHeader creates a partially-populated [FileHeader] from an | |
| // fs.FileInfo. | |
| // Because fs.FileInfo's Name method returns only the base name of | |
| // the file it describes, it may be necessary to modify the Name field | |
| // of the returned header to provide the full path name of the file. | |
| // If compression is desired, callers should set the FileHeader.Method | |
| // field; it is unset by default. | |
| func FileInfoHeader(fi fs.FileInfo) (*FileHeader, error) { | |
| size := fi.Size() | |
| fh := &FileHeader{ | |
| Name: fi.Name(), | |
| UncompressedSize64: uint64(size), | |
| } | |
| fh.SetModTime(fi.ModTime()) | |
| fh.SetMode(fi.Mode()) | |
| if fh.UncompressedSize64 > uint32max { | |
| fh.UncompressedSize = uint32max | |
| } else { | |
| fh.UncompressedSize = uint32(fh.UncompressedSize64) | |
| } | |
| return fh, nil | |
| } | |
| type directoryEnd struct { | |
| diskNbr uint32 // unused | |
| dirDiskNbr uint32 // unused | |
| dirRecordsThisDisk uint64 // unused | |
| directoryRecords uint64 | |
| directorySize uint64 | |
| directoryOffset uint64 // relative to file | |
| commentLen uint16 | |
| comment string | |
| } | |
| // timeZone returns a *time.Location based on the provided offset. | |
| // If the offset is non-sensible, then this uses an offset of zero. | |
| func timeZone(offset time.Duration) *time.Location { | |
| const ( | |
| minOffset = -12 * time.Hour // E.g., Baker island at -12:00 | |
| maxOffset = +14 * time.Hour // E.g., Line island at +14:00 | |
| offsetAlias = 15 * time.Minute // E.g., Nepal at +5:45 | |
| ) | |
| offset = offset.Round(offsetAlias) | |
| if offset < minOffset || maxOffset < offset { | |
| offset = 0 | |
| } | |
| return time.FixedZone("", int(offset/time.Second)) | |
| } | |
| // msDosTimeToTime converts an MS-DOS date and time into a time.Time. | |
| // The resolution is 2s. | |
| // See: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime | |
| func msDosTimeToTime(dosDate, dosTime uint16) time.Time { | |
| return time.Date( | |
| // date bits 0-4: day of month; 5-8: month; 9-15: years since 1980 | |
| int(dosDate>>9+1980), | |
| time.Month(dosDate>>5&0xf), | |
| int(dosDate&0x1f), | |
| // time bits 0-4: second/2; 5-10: minute; 11-15: hour | |
| int(dosTime>>11), | |
| int(dosTime>>5&0x3f), | |
| int(dosTime&0x1f*2), | |
| 0, // nanoseconds | |
| time.UTC, | |
| ) | |
| } | |
| // timeToMsDosTime converts a time.Time to an MS-DOS date and time. | |
| // The resolution is 2s. | |
| // See: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime | |
| func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) { | |
| fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9) | |
| fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11) | |
| return | |
| } | |
| // ModTime returns the modification time in UTC using the legacy | |
| // [ModifiedDate] and [ModifiedTime] fields. | |
| // | |
| // Deprecated: Use [Modified] instead. | |
| func (h *FileHeader) ModTime() time.Time { | |
| return msDosTimeToTime(h.ModifiedDate, h.ModifiedTime) | |
| } | |
| // SetModTime sets the [Modified], [ModifiedTime], and [ModifiedDate] fields | |
| // to the given time in UTC. | |
| // | |
| // Deprecated: Use [Modified] instead. | |
| func (h *FileHeader) SetModTime(t time.Time) { | |
| t = t.UTC() // Convert to UTC for compatibility | |
| h.Modified = t | |
| h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t) | |
| } | |
| const ( | |
| // Unix constants. The specification doesn't mention them, | |
| // but these seem to be the values agreed on by tools. | |
| s_IFMT = 0xf000 | |
| s_IFSOCK = 0xc000 | |
| s_IFLNK = 0xa000 | |
| s_IFREG = 0x8000 | |
| s_IFBLK = 0x6000 | |
| s_IFDIR = 0x4000 | |
| s_IFCHR = 0x2000 | |
| s_IFIFO = 0x1000 | |
| s_ISUID = 0x800 | |
| s_ISGID = 0x400 | |
| s_ISVTX = 0x200 | |
| msdosDir = 0x10 | |
| msdosReadOnly = 0x01 | |
| ) | |
| // Mode returns the permission and mode bits for the [FileHeader]. | |
| func (h *FileHeader) Mode() (mode fs.FileMode) { | |
| switch h.CreatorVersion >> 8 { | |
| case creatorUnix, creatorMacOSX: | |
| mode = unixModeToFileMode(h.ExternalAttrs >> 16) | |
| case creatorNTFS, creatorVFAT, creatorFAT: | |
| mode = msdosModeToFileMode(h.ExternalAttrs) | |
| } | |
| if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' { | |
| mode |= fs.ModeDir | |
| } | |
| return mode | |
| } | |
| // SetMode changes the permission and mode bits for the [FileHeader]. | |
| func (h *FileHeader) SetMode(mode fs.FileMode) { | |
| h.CreatorVersion = h.CreatorVersion&0xff | creatorUnix<<8 | |
| h.ExternalAttrs = fileModeToUnixMode(mode) << 16 | |
| // set MSDOS attributes too, as the original zip does. | |
| if mode&fs.ModeDir != 0 { | |
| h.ExternalAttrs |= msdosDir | |
| } | |
| if mode&0200 == 0 { | |
| h.ExternalAttrs |= msdosReadOnly | |
| } | |
| } | |
| // isZip64 reports whether the file size exceeds the 32 bit limit | |
| func (h *FileHeader) isZip64() bool { | |
| return h.CompressedSize64 >= uint32max || h.UncompressedSize64 >= uint32max | |
| } | |
| func (h *FileHeader) hasDataDescriptor() bool { | |
| return h.Flags&0x8 != 0 | |
| } | |
| func msdosModeToFileMode(m uint32) (mode fs.FileMode) { | |
| if m&msdosDir != 0 { | |
| mode = fs.ModeDir | 0777 | |
| } else { | |
| mode = 0666 | |
| } | |
| if m&msdosReadOnly != 0 { | |
| mode &^= 0222 | |
| } | |
| return mode | |
| } | |
| func fileModeToUnixMode(mode fs.FileMode) uint32 { | |
| var m uint32 | |
| switch mode & fs.ModeType { | |
| default: | |
| m = s_IFREG | |
| case fs.ModeDir: | |
| m = s_IFDIR | |
| case fs.ModeSymlink: | |
| m = s_IFLNK | |
| case fs.ModeNamedPipe: | |
| m = s_IFIFO | |
| case fs.ModeSocket: | |
| m = s_IFSOCK | |
| case fs.ModeDevice: | |
| m = s_IFBLK | |
| case fs.ModeDevice | fs.ModeCharDevice: | |
| m = s_IFCHR | |
| } | |
| if mode&fs.ModeSetuid != 0 { | |
| m |= s_ISUID | |
| } | |
| if mode&fs.ModeSetgid != 0 { | |
| m |= s_ISGID | |
| } | |
| if mode&fs.ModeSticky != 0 { | |
| m |= s_ISVTX | |
| } | |
| return m | uint32(mode&0777) | |
| } | |
| func unixModeToFileMode(m uint32) fs.FileMode { | |
| mode := fs.FileMode(m & 0777) | |
| switch m & s_IFMT { | |
| case s_IFBLK: | |
| mode |= fs.ModeDevice | |
| case s_IFCHR: | |
| mode |= fs.ModeDevice | fs.ModeCharDevice | |
| case s_IFDIR: | |
| mode |= fs.ModeDir | |
| case s_IFIFO: | |
| mode |= fs.ModeNamedPipe | |
| case s_IFLNK: | |
| mode |= fs.ModeSymlink | |
| case s_IFREG: | |
| // nothing to do | |
| case s_IFSOCK: | |
| mode |= fs.ModeSocket | |
| } | |
| if m&s_ISGID != 0 { | |
| mode |= fs.ModeSetgid | |
| } | |
| if m&s_ISUID != 0 { | |
| mode |= fs.ModeSetuid | |
| } | |
| if m&s_ISVTX != 0 { | |
| mode |= fs.ModeSticky | |
| } | |
| return mode | |
| } | |