...

Source file src/archive/zip/struct.go

Documentation: archive/zip

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  /*
     6  Package zip provides support for reading and writing ZIP archives.
     7  
     8  See the [ZIP specification] for details.
     9  
    10  This package does not support disk spanning.
    11  
    12  A note about ZIP64:
    13  
    14  To be backwards compatible the FileHeader has both 32 and 64 bit Size
    15  fields. The 64 bit fields will always contain the correct value and
    16  for normal archives both fields will be the same. For files requiring
    17  the ZIP64 format the 32 bit fields will be 0xffffffff and the 64 bit
    18  fields must be used instead.
    19  
    20  [ZIP specification]: https://support.pkware.com/pkzip/appnote
    21  */
    22  package zip
    23  
    24  import (
    25  	"io/fs"
    26  	"path"
    27  	"time"
    28  )
    29  
    30  // Compression methods.
    31  const (
    32  	Store   uint16 = 0 // no compression
    33  	Deflate uint16 = 8 // DEFLATE compressed
    34  )
    35  
    36  const (
    37  	fileHeaderSignature      = 0x04034b50
    38  	directoryHeaderSignature = 0x02014b50
    39  	directoryEndSignature    = 0x06054b50
    40  	directory64LocSignature  = 0x07064b50
    41  	directory64EndSignature  = 0x06064b50
    42  	dataDescriptorSignature  = 0x08074b50 // de-facto standard; required by OS X Finder
    43  	fileHeaderLen            = 30         // + filename + extra
    44  	directoryHeaderLen       = 46         // + filename + extra + comment
    45  	directoryEndLen          = 22         // + comment
    46  	dataDescriptorLen        = 16         // four uint32: descriptor signature, crc32, compressed size, size
    47  	dataDescriptor64Len      = 24         // two uint32: signature, crc32 | two uint64: compressed size, size
    48  	directory64LocLen        = 20         //
    49  	directory64EndLen        = 56         // + extra
    50  
    51  	// Constants for the first byte in CreatorVersion.
    52  	creatorFAT    = 0
    53  	creatorUnix   = 3
    54  	creatorNTFS   = 11
    55  	creatorVFAT   = 14
    56  	creatorMacOSX = 19
    57  
    58  	// Version numbers.
    59  	zipVersion20 = 20 // 2.0
    60  	zipVersion45 = 45 // 4.5 (reads and writes zip64 archives)
    61  
    62  	// Limits for non zip64 files.
    63  	uint16max = (1 << 16) - 1
    64  	uint32max = (1 << 32) - 1
    65  
    66  	// Extra header IDs.
    67  	//
    68  	// IDs 0..31 are reserved for official use by PKWARE.
    69  	// IDs above that range are defined by third-party vendors.
    70  	// Since ZIP lacked high precision timestamps (nor an official specification
    71  	// of the timezone used for the date fields), many competing extra fields
    72  	// have been invented. Pervasive use effectively makes them "official".
    73  	//
    74  	// See http://mdfs.net/Docs/Comp/Archiving/Zip/ExtraField
    75  	zip64ExtraID       = 0x0001 // Zip64 extended information
    76  	ntfsExtraID        = 0x000a // NTFS
    77  	unixExtraID        = 0x000d // UNIX
    78  	extTimeExtraID     = 0x5455 // Extended timestamp
    79  	infoZipUnixExtraID = 0x5855 // Info-ZIP Unix extension
    80  )
    81  
    82  // FileHeader describes a file within a ZIP file.
    83  // See the [ZIP specification] for details.
    84  //
    85  // [ZIP specification]: https://support.pkware.com/pkzip/appnote
    86  type FileHeader struct {
    87  	// Name is the name of the file.
    88  	//
    89  	// It must be a relative path, not start with a drive letter (such as "C:"),
    90  	// and must use forward slashes instead of back slashes. A trailing slash
    91  	// indicates that this file is a directory and should have no data.
    92  	Name string
    93  
    94  	// Comment is any arbitrary user-defined string shorter than 64KiB.
    95  	Comment string
    96  
    97  	// NonUTF8 indicates that Name and Comment are not encoded in UTF-8.
    98  	//
    99  	// By specification, the only other encoding permitted should be CP-437,
   100  	// but historically many ZIP readers interpret Name and Comment as whatever
   101  	// the system's local character encoding happens to be.
   102  	//
   103  	// This flag should only be set if the user intends to encode a non-portable
   104  	// ZIP file for a specific localized region. Otherwise, the Writer
   105  	// automatically sets the ZIP format's UTF-8 flag for valid UTF-8 strings.
   106  	NonUTF8 bool
   107  
   108  	CreatorVersion uint16
   109  	ReaderVersion  uint16
   110  	Flags          uint16
   111  
   112  	// Method is the compression method. If zero, Store is used.
   113  	Method uint16
   114  
   115  	// Modified is the modified time of the file.
   116  	//
   117  	// When reading, an extended timestamp is preferred over the legacy MS-DOS
   118  	// date field, and the offset between the times is used as the timezone.
   119  	// If only the MS-DOS date is present, the timezone is assumed to be UTC.
   120  	//
   121  	// When writing, an extended timestamp (which is timezone-agnostic) is
   122  	// always emitted. The legacy MS-DOS date field is encoded according to the
   123  	// location of the Modified time.
   124  	Modified time.Time
   125  
   126  	// ModifiedTime is an MS-DOS-encoded time.
   127  	//
   128  	// Deprecated: Use Modified instead.
   129  	ModifiedTime uint16
   130  
   131  	// ModifiedDate is an MS-DOS-encoded date.
   132  	//
   133  	// Deprecated: Use Modified instead.
   134  	ModifiedDate uint16
   135  
   136  	// CRC32 is the CRC32 checksum of the file content.
   137  	CRC32 uint32
   138  
   139  	// CompressedSize is the compressed size of the file in bytes.
   140  	// If either the uncompressed or compressed size of the file
   141  	// does not fit in 32 bits, CompressedSize is set to ^uint32(0).
   142  	//
   143  	// Deprecated: Use CompressedSize64 instead.
   144  	CompressedSize uint32
   145  
   146  	// UncompressedSize is the uncompressed size of the file in bytes.
   147  	// If either the uncompressed or compressed size of the file
   148  	// does not fit in 32 bits, UncompressedSize is set to ^uint32(0).
   149  	//
   150  	// Deprecated: Use UncompressedSize64 instead.
   151  	UncompressedSize uint32
   152  
   153  	// CompressedSize64 is the compressed size of the file in bytes.
   154  	CompressedSize64 uint64
   155  
   156  	// UncompressedSize64 is the uncompressed size of the file in bytes.
   157  	UncompressedSize64 uint64
   158  
   159  	// Extra are the extensible data fields. The writer automatically includes
   160  	// the appropriate Zip64 field if necessary, and [Writer.Close] appends the
   161  	// Central Directory version of the Zip64 field to Extra.
   162  	Extra []byte
   163  
   164  	ExternalAttrs uint32 // Meaning depends on CreatorVersion
   165  }
   166  
   167  // FileInfo returns an fs.FileInfo for the [FileHeader].
   168  func (h *FileHeader) FileInfo() fs.FileInfo {
   169  	return headerFileInfo{h}
   170  }
   171  
   172  // headerFileInfo implements [fs.FileInfo].
   173  type headerFileInfo struct {
   174  	fh *FileHeader
   175  }
   176  
   177  func (fi headerFileInfo) Name() string { return path.Base(fi.fh.Name) }
   178  func (fi headerFileInfo) Size() int64 {
   179  	if fi.fh.UncompressedSize64 > 0 {
   180  		return int64(fi.fh.UncompressedSize64)
   181  	}
   182  	return int64(fi.fh.UncompressedSize)
   183  }
   184  func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
   185  func (fi headerFileInfo) ModTime() time.Time {
   186  	if fi.fh.Modified.IsZero() {
   187  		return fi.fh.ModTime()
   188  	}
   189  	return fi.fh.Modified.UTC()
   190  }
   191  func (fi headerFileInfo) Mode() fs.FileMode { return fi.fh.Mode() }
   192  func (fi headerFileInfo) Type() fs.FileMode { return fi.fh.Mode().Type() }
   193  func (fi headerFileInfo) Sys() any          { return fi.fh }
   194  
   195  func (fi headerFileInfo) Info() (fs.FileInfo, error) { return fi, nil }
   196  
   197  func (fi headerFileInfo) String() string {
   198  	return fs.FormatFileInfo(fi)
   199  }
   200  
   201  // FileInfoHeader creates a partially-populated [FileHeader] from an
   202  // fs.FileInfo.
   203  // Because fs.FileInfo's Name method returns only the base name of
   204  // the file it describes, it may be necessary to modify the Name field
   205  // of the returned header to provide the full path name of the file.
   206  // If compression is desired, callers should set the FileHeader.Method
   207  // field; it is unset by default.
   208  func FileInfoHeader(fi fs.FileInfo) (*FileHeader, error) {
   209  	size := fi.Size()
   210  	fh := &FileHeader{
   211  		Name:               fi.Name(),
   212  		UncompressedSize64: uint64(size),
   213  	}
   214  	fh.SetModTime(fi.ModTime())
   215  	fh.SetMode(fi.Mode())
   216  	if fh.UncompressedSize64 > uint32max {
   217  		fh.UncompressedSize = uint32max
   218  	} else {
   219  		fh.UncompressedSize = uint32(fh.UncompressedSize64)
   220  	}
   221  	return fh, nil
   222  }
   223  
   224  type directoryEnd struct {
   225  	diskNbr            uint32 // unused
   226  	dirDiskNbr         uint32 // unused
   227  	dirRecordsThisDisk uint64 // unused
   228  	directoryRecords   uint64
   229  	directorySize      uint64
   230  	directoryOffset    uint64 // relative to file
   231  	commentLen         uint16
   232  	comment            string
   233  }
   234  
   235  // timeZone returns a *time.Location based on the provided offset.
   236  // If the offset is non-sensible, then this uses an offset of zero.
   237  func timeZone(offset time.Duration) *time.Location {
   238  	const (
   239  		minOffset   = -12 * time.Hour  // E.g., Baker island at -12:00
   240  		maxOffset   = +14 * time.Hour  // E.g., Line island at +14:00
   241  		offsetAlias = 15 * time.Minute // E.g., Nepal at +5:45
   242  	)
   243  	offset = offset.Round(offsetAlias)
   244  	if offset < minOffset || maxOffset < offset {
   245  		offset = 0
   246  	}
   247  	return time.FixedZone("", int(offset/time.Second))
   248  }
   249  
   250  // msDosTimeToTime converts an MS-DOS date and time into a time.Time.
   251  // The resolution is 2s.
   252  // See: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime
   253  func msDosTimeToTime(dosDate, dosTime uint16) time.Time {
   254  	return time.Date(
   255  		// date bits 0-4: day of month; 5-8: month; 9-15: years since 1980
   256  		int(dosDate>>9+1980),
   257  		time.Month(dosDate>>5&0xf),
   258  		int(dosDate&0x1f),
   259  
   260  		// time bits 0-4: second/2; 5-10: minute; 11-15: hour
   261  		int(dosTime>>11),
   262  		int(dosTime>>5&0x3f),
   263  		int(dosTime&0x1f*2),
   264  		0, // nanoseconds
   265  
   266  		time.UTC,
   267  	)
   268  }
   269  
   270  // timeToMsDosTime converts a time.Time to an MS-DOS date and time.
   271  // The resolution is 2s.
   272  // See: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime
   273  func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) {
   274  	fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9)
   275  	fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11)
   276  	return
   277  }
   278  
   279  // ModTime returns the modification time in UTC using the legacy
   280  // [ModifiedDate] and [ModifiedTime] fields.
   281  //
   282  // Deprecated: Use [Modified] instead.
   283  func (h *FileHeader) ModTime() time.Time {
   284  	return msDosTimeToTime(h.ModifiedDate, h.ModifiedTime)
   285  }
   286  
   287  // SetModTime sets the [Modified], [ModifiedTime], and [ModifiedDate] fields
   288  // to the given time in UTC.
   289  //
   290  // Deprecated: Use [Modified] instead.
   291  func (h *FileHeader) SetModTime(t time.Time) {
   292  	t = t.UTC() // Convert to UTC for compatibility
   293  	h.Modified = t
   294  	h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t)
   295  }
   296  
   297  const (
   298  	// Unix constants. The specification doesn't mention them,
   299  	// but these seem to be the values agreed on by tools.
   300  	s_IFMT   = 0xf000
   301  	s_IFSOCK = 0xc000
   302  	s_IFLNK  = 0xa000
   303  	s_IFREG  = 0x8000
   304  	s_IFBLK  = 0x6000
   305  	s_IFDIR  = 0x4000
   306  	s_IFCHR  = 0x2000
   307  	s_IFIFO  = 0x1000
   308  	s_ISUID  = 0x800
   309  	s_ISGID  = 0x400
   310  	s_ISVTX  = 0x200
   311  
   312  	msdosDir      = 0x10
   313  	msdosReadOnly = 0x01
   314  )
   315  
   316  // Mode returns the permission and mode bits for the [FileHeader].
   317  func (h *FileHeader) Mode() (mode fs.FileMode) {
   318  	switch h.CreatorVersion >> 8 {
   319  	case creatorUnix, creatorMacOSX:
   320  		mode = unixModeToFileMode(h.ExternalAttrs >> 16)
   321  	case creatorNTFS, creatorVFAT, creatorFAT:
   322  		mode = msdosModeToFileMode(h.ExternalAttrs)
   323  	}
   324  	if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' {
   325  		mode |= fs.ModeDir
   326  	}
   327  	return mode
   328  }
   329  
   330  // SetMode changes the permission and mode bits for the [FileHeader].
   331  func (h *FileHeader) SetMode(mode fs.FileMode) {
   332  	h.CreatorVersion = h.CreatorVersion&0xff | creatorUnix<<8
   333  	h.ExternalAttrs = fileModeToUnixMode(mode) << 16
   334  
   335  	// set MSDOS attributes too, as the original zip does.
   336  	if mode&fs.ModeDir != 0 {
   337  		h.ExternalAttrs |= msdosDir
   338  	}
   339  	if mode&0200 == 0 {
   340  		h.ExternalAttrs |= msdosReadOnly
   341  	}
   342  }
   343  
   344  func (h *FileHeader) hasDataDescriptor() bool {
   345  	return h.Flags&0x8 != 0
   346  }
   347  
   348  func msdosModeToFileMode(m uint32) (mode fs.FileMode) {
   349  	if m&msdosDir != 0 {
   350  		mode = fs.ModeDir | 0777
   351  	} else {
   352  		mode = 0666
   353  	}
   354  	if m&msdosReadOnly != 0 {
   355  		mode &^= 0222
   356  	}
   357  	return mode
   358  }
   359  
   360  func fileModeToUnixMode(mode fs.FileMode) uint32 {
   361  	var m uint32
   362  	switch mode & fs.ModeType {
   363  	default:
   364  		m = s_IFREG
   365  	case fs.ModeDir:
   366  		m = s_IFDIR
   367  	case fs.ModeSymlink:
   368  		m = s_IFLNK
   369  	case fs.ModeNamedPipe:
   370  		m = s_IFIFO
   371  	case fs.ModeSocket:
   372  		m = s_IFSOCK
   373  	case fs.ModeDevice:
   374  		m = s_IFBLK
   375  	case fs.ModeDevice | fs.ModeCharDevice:
   376  		m = s_IFCHR
   377  	}
   378  	if mode&fs.ModeSetuid != 0 {
   379  		m |= s_ISUID
   380  	}
   381  	if mode&fs.ModeSetgid != 0 {
   382  		m |= s_ISGID
   383  	}
   384  	if mode&fs.ModeSticky != 0 {
   385  		m |= s_ISVTX
   386  	}
   387  	return m | uint32(mode&0777)
   388  }
   389  
   390  func unixModeToFileMode(m uint32) fs.FileMode {
   391  	mode := fs.FileMode(m & 0777)
   392  	switch m & s_IFMT {
   393  	case s_IFBLK:
   394  		mode |= fs.ModeDevice
   395  	case s_IFCHR:
   396  		mode |= fs.ModeDevice | fs.ModeCharDevice
   397  	case s_IFDIR:
   398  		mode |= fs.ModeDir
   399  	case s_IFIFO:
   400  		mode |= fs.ModeNamedPipe
   401  	case s_IFLNK:
   402  		mode |= fs.ModeSymlink
   403  	case s_IFREG:
   404  		// nothing to do
   405  	case s_IFSOCK:
   406  		mode |= fs.ModeSocket
   407  	}
   408  	if m&s_ISGID != 0 {
   409  		mode |= fs.ModeSetgid
   410  	}
   411  	if m&s_ISUID != 0 {
   412  		mode |= fs.ModeSetuid
   413  	}
   414  	if m&s_ISVTX != 0 {
   415  		mode |= fs.ModeSticky
   416  	}
   417  	return mode
   418  }
   419  

View as plain text