| // 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 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 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 |
| } |