mirror of
https://codeberg.org/VnPower/PixivFE
synced 2024-12-06 19:16:23 +01:00
my favourite part: deleting code
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -5,20 +5,9 @@
|
||||
package zip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
_ "fmt"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,300 +17,6 @@ var (
|
||||
ErrInsecurePath = errors.New("zip: insecure file path")
|
||||
)
|
||||
|
||||
// A Reader serves content from a ZIP archive.
|
||||
type Reader struct {
|
||||
r io.ReaderAt
|
||||
File []*File
|
||||
Comment string
|
||||
decompressors map[uint16]Decompressor
|
||||
|
||||
// Some JAR files are zip files with a prefix that is a bash script.
|
||||
// The baseOffset field is the start of the zip file proper.
|
||||
baseOffset int64
|
||||
|
||||
// fileList is a list of files sorted by ename,
|
||||
// for use by the Open method.
|
||||
fileListOnce sync.Once
|
||||
fileList []fileListEntry
|
||||
}
|
||||
|
||||
// A ReadCloser is a [Reader] that must be closed when no longer needed.
|
||||
type ReadCloser struct {
|
||||
f *os.File
|
||||
Reader
|
||||
}
|
||||
|
||||
// A File is a single file in a ZIP archive.
|
||||
// The file information is in the embedded [FileHeader].
|
||||
// The file content can be accessed by calling [File.Open].
|
||||
type File struct {
|
||||
FileHeader
|
||||
zip *Reader
|
||||
zipr io.ReaderAt
|
||||
headerOffset int64 // includes overall ZIP archive baseOffset
|
||||
zip64 bool // zip64 extended information extra field presence
|
||||
}
|
||||
|
||||
// OpenReader will open the Zip file specified by name and return a ReadCloser.
|
||||
//
|
||||
// If any file inside the archive uses a non-local name
|
||||
// (as defined by [filepath.IsLocal]) or a name containing backslashes
|
||||
// and the GODEBUG environment variable contains `zipinsecurepath=0`,
|
||||
// OpenReader returns the reader with an ErrInsecurePath error.
|
||||
// A future version of Go may introduce this behavior by default.
|
||||
// Programs that want to accept non-local names can ignore
|
||||
// the ErrInsecurePath error and use the returned reader.
|
||||
func OpenReader(name string) (*ReadCloser, error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
r := new(ReadCloser)
|
||||
if err = r.init(f, fi.Size()); err != nil && err != ErrInsecurePath {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
r.f = f
|
||||
return r, err
|
||||
}
|
||||
|
||||
// NewReader returns a new [Reader] reading from r, which is assumed to
|
||||
// have the given size in bytes.
|
||||
//
|
||||
// If any file inside the archive uses a non-local name
|
||||
// (as defined by [filepath.IsLocal]) or a name containing backslashes
|
||||
// and the GODEBUG environment variable contains `zipinsecurepath=0`,
|
||||
// NewReader returns the reader with an [ErrInsecurePath] error.
|
||||
// A future version of Go may introduce this behavior by default.
|
||||
// Programs that want to accept non-local names can ignore
|
||||
// the [ErrInsecurePath] error and use the returned reader.
|
||||
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
||||
if size < 0 {
|
||||
return nil, errors.New("zip: size cannot be negative")
|
||||
}
|
||||
zr := new(Reader)
|
||||
var err error
|
||||
if err = zr.init(r, size); err != nil && err != ErrInsecurePath {
|
||||
return nil, err
|
||||
}
|
||||
return zr, err
|
||||
}
|
||||
|
||||
func (r *Reader) init(rdr io.ReaderAt, size int64) error {
|
||||
r.r = rdr
|
||||
r.baseOffset = 0
|
||||
// Since the number of directory records is not validated, it is not
|
||||
// safe to preallocate r.File without first checking that the specified
|
||||
// number of files is reasonable, since a malformed archive may
|
||||
// indicate it contains up to 1 << 128 - 1 files. Since each file has a
|
||||
// header which will be _at least_ 30 bytes we can safely preallocate
|
||||
// if (data size / 30) >= end.directoryRecords.
|
||||
r.File = []*File{}
|
||||
rs := io.NewSectionReader(rdr, 0, size)
|
||||
var err error
|
||||
if _, err = rs.Seek(r.baseOffset+0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bufio.NewReader(rs)
|
||||
|
||||
// The count of files inside a zip is truncated to fit in a uint16.
|
||||
// Gloss over this by reading headers until we encounter
|
||||
// a bad one, and then only report an ErrFormat or UnexpectedEOF if
|
||||
// the file count modulo 65536 is incorrect.
|
||||
for {
|
||||
f := &File{zip: r, zipr: rdr}
|
||||
err = readDirectoryHeader(f, buf)
|
||||
if err == ErrFormat || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.headerOffset += r.baseOffset
|
||||
r.File = append(r.File, f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterDecompressor registers or overrides a custom decompressor for a
|
||||
// specific method ID. If a decompressor for a given method is not found,
|
||||
// [Reader] will default to looking up the decompressor at the package level.
|
||||
func (r *Reader) RegisterDecompressor(method uint16, dcomp Decompressor) {
|
||||
if r.decompressors == nil {
|
||||
r.decompressors = make(map[uint16]Decompressor)
|
||||
}
|
||||
r.decompressors[method] = dcomp
|
||||
}
|
||||
|
||||
func (r *Reader) decompressor(method uint16) Decompressor {
|
||||
dcomp := r.decompressors[method]
|
||||
if dcomp == nil {
|
||||
dcomp = decompressor(method)
|
||||
}
|
||||
return dcomp
|
||||
}
|
||||
|
||||
// Close closes the Zip file, rendering it unusable for I/O.
|
||||
func (rc *ReadCloser) Close() error {
|
||||
return rc.f.Close()
|
||||
}
|
||||
|
||||
// DataOffset returns the offset of the file's possibly-compressed
|
||||
// data, relative to the beginning of the zip file.
|
||||
//
|
||||
// Most callers should instead use [File.Open], which transparently
|
||||
// decompresses data and verifies checksums.
|
||||
func (f *File) DataOffset() (offset int64, err error) {
|
||||
bodyOffset, err := f.findBodyOffset()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return f.headerOffset + bodyOffset, nil
|
||||
}
|
||||
|
||||
// Open returns a [ReadCloser] that provides access to the [File]'s contents.
|
||||
// Multiple files may be read concurrently.
|
||||
func (f *File) Open() (io.ReadCloser, error) {
|
||||
bodyOffset, err := f.findBodyOffset()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.HasSuffix(f.Name, "/") {
|
||||
// The ZIP specification (APPNOTE.TXT) specifies that directories, which
|
||||
// are technically zero-byte files, must not have any associated file
|
||||
// data. We previously tried failing here if f.CompressedSize64 != 0,
|
||||
// but it turns out that a number of implementations (namely, the Java
|
||||
// jar tool) don't properly set the storage method on directories
|
||||
// resulting in a file with compressed size > 0 but uncompressed size ==
|
||||
// 0. We still want to fail when a directory has associated uncompressed
|
||||
// data, but we are tolerant of cases where the uncompressed size is
|
||||
// zero but compressed size is not.
|
||||
if f.UncompressedSize64 != 0 {
|
||||
return &dirReader{ErrFormat}, nil
|
||||
} else {
|
||||
return &dirReader{io.EOF}, nil
|
||||
}
|
||||
}
|
||||
size := int64(f.CompressedSize64)
|
||||
r := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, size)
|
||||
dcomp := f.zip.decompressor(f.Method)
|
||||
if dcomp == nil {
|
||||
return nil, ErrAlgorithm
|
||||
}
|
||||
var rc io.ReadCloser = dcomp(r)
|
||||
var desr io.Reader
|
||||
if f.hasDataDescriptor() {
|
||||
desr = io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset+size, dataDescriptorLen)
|
||||
}
|
||||
rc = &checksumReader{
|
||||
rc: rc,
|
||||
hash: crc32.NewIEEE(),
|
||||
f: f,
|
||||
desr: desr,
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// OpenRaw returns a [Reader] that provides access to the [File]'s contents without
|
||||
// decompression.
|
||||
func (f *File) OpenRaw() (io.Reader, error) {
|
||||
bodyOffset, err := f.findBodyOffset()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := io.NewSectionReader(f.zipr, f.headerOffset+bodyOffset, int64(f.CompressedSize64))
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type dirReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *dirReader) Read([]byte) (int, error) {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
func (r *dirReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type checksumReader struct {
|
||||
rc io.ReadCloser
|
||||
hash hash.Hash32
|
||||
nread uint64 // number of bytes read so far
|
||||
f *File
|
||||
desr io.Reader // if non-nil, where to read the data descriptor
|
||||
err error // sticky error
|
||||
}
|
||||
|
||||
func (r *checksumReader) Stat() (fs.FileInfo, error) {
|
||||
return headerFileInfo{&r.f.FileHeader}, nil
|
||||
}
|
||||
|
||||
func (r *checksumReader) Read(b []byte) (n int, err error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
n, err = r.rc.Read(b)
|
||||
r.hash.Write(b[:n])
|
||||
r.nread += uint64(n)
|
||||
if r.nread > r.f.UncompressedSize64 {
|
||||
return 0, ErrFormat
|
||||
}
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == io.EOF {
|
||||
if r.nread != r.f.UncompressedSize64 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
if r.desr != nil {
|
||||
if err1 := readDataDescriptor(r.desr, r.f); err1 != nil {
|
||||
if err1 == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else {
|
||||
err = err1
|
||||
}
|
||||
} else if r.hash.Sum32() != r.f.CRC32 {
|
||||
err = ErrChecksum
|
||||
}
|
||||
} else {
|
||||
// If there's not a data descriptor, we still compare
|
||||
// the CRC32 of what we've read against the file header
|
||||
// or TOC's CRC32, if it seems like it was set.
|
||||
if r.f.CRC32 != 0 && r.hash.Sum32() != r.f.CRC32 {
|
||||
err = ErrChecksum
|
||||
}
|
||||
}
|
||||
}
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
|
||||
func (r *checksumReader) Close() error { return r.rc.Close() }
|
||||
|
||||
// findBodyOffset does the minimum work to verify the file has a header
|
||||
// and returns the file body offset.
|
||||
func (f *File) findBodyOffset() (int64, error) {
|
||||
var buf [fileHeaderLen]byte
|
||||
if _, err := f.zipr.ReadAt(buf[:], f.headerOffset); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
b := readBuf(buf[:])
|
||||
if sig := b.uint32(); sig != fileHeaderSignature {
|
||||
return 0, ErrFormat
|
||||
}
|
||||
b = b[22:] // skip over most of the header
|
||||
filenameLen := int(b.uint16())
|
||||
extraLen := int(b.uint16())
|
||||
return int64(fileHeaderLen + filenameLen + extraLen), nil
|
||||
}
|
||||
|
||||
type FileContent struct {
|
||||
Filename []byte
|
||||
Content []byte
|
||||
@@ -367,221 +62,6 @@ func ReadFile(r io.ReadSeeker) (*FileContent, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// readDirectoryHeader attempts to read a directory header from r.
|
||||
// It returns io.ErrUnexpectedEOF if it cannot read a complete header,
|
||||
// and ErrFormat if it doesn't find a valid header signature.
|
||||
func readDirectoryHeader(f *File, r io.Reader) error {
|
||||
var buf [directoryHeaderLen]byte
|
||||
if _, err := io.ReadFull(r, buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
b := readBuf(buf[:])
|
||||
if sig := b.uint32(); sig != fileHeaderSignature {
|
||||
// fmt.Printf("read dir start %x", sig)
|
||||
|
||||
return ErrFormat
|
||||
}
|
||||
f.CreatorVersion = b.uint16()
|
||||
f.ReaderVersion = b.uint16()
|
||||
f.Flags = b.uint16()
|
||||
f.Method = b.uint16()
|
||||
f.ModifiedTime = b.uint16()
|
||||
f.ModifiedDate = b.uint16()
|
||||
f.CRC32 = b.uint32()
|
||||
f.CompressedSize = b.uint32()
|
||||
f.UncompressedSize = b.uint32()
|
||||
f.CompressedSize64 = uint64(f.CompressedSize)
|
||||
f.UncompressedSize64 = uint64(f.UncompressedSize)
|
||||
filenameLen := int(b.uint16())
|
||||
extraLen := int(b.uint16())
|
||||
commentLen := int(b.uint16())
|
||||
b = b[4:] // skipped start disk number and internal attributes (2x uint16)
|
||||
f.ExternalAttrs = b.uint32()
|
||||
f.headerOffset = int64(b.uint32())
|
||||
d := make([]byte, filenameLen+extraLen+commentLen)
|
||||
if _, err := io.ReadFull(r, d); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Name = string(d[:filenameLen])
|
||||
f.Extra = d[filenameLen : filenameLen+extraLen]
|
||||
f.Comment = string(d[filenameLen+extraLen:])
|
||||
|
||||
// Determine the character encoding.
|
||||
utf8Valid1, utf8Require1 := detectUTF8(f.Name)
|
||||
utf8Valid2, utf8Require2 := detectUTF8(f.Comment)
|
||||
switch {
|
||||
case !utf8Valid1 || !utf8Valid2:
|
||||
// Name and Comment definitely not UTF-8.
|
||||
f.NonUTF8 = true
|
||||
case !utf8Require1 && !utf8Require2:
|
||||
// Name and Comment use only single-byte runes that overlap with UTF-8.
|
||||
f.NonUTF8 = false
|
||||
default:
|
||||
// Might be UTF-8, might be some other encoding; preserve existing flag.
|
||||
// Some ZIP writers use UTF-8 encoding without setting the UTF-8 flag.
|
||||
// Since it is impossible to always distinguish valid UTF-8 from some
|
||||
// other encoding (e.g., GBK or Shift-JIS), we trust the flag.
|
||||
f.NonUTF8 = f.Flags&0x800 == 0
|
||||
}
|
||||
|
||||
needUSize := f.UncompressedSize == ^uint32(0)
|
||||
needCSize := f.CompressedSize == ^uint32(0)
|
||||
needHeaderOffset := f.headerOffset == int64(^uint32(0))
|
||||
|
||||
// Best effort to find what we need.
|
||||
// Other zip authors might not even follow the basic format,
|
||||
// and we'll just ignore the Extra content in that case.
|
||||
var modified time.Time
|
||||
parseExtras:
|
||||
for extra := readBuf(f.Extra); len(extra) >= 4; { // need at least tag and size
|
||||
fieldTag := extra.uint16()
|
||||
fieldSize := int(extra.uint16())
|
||||
if len(extra) < fieldSize {
|
||||
break
|
||||
}
|
||||
fieldBuf := extra.sub(fieldSize)
|
||||
|
||||
switch fieldTag {
|
||||
case zip64ExtraID:
|
||||
f.zip64 = true
|
||||
|
||||
// update directory values from the zip64 extra block.
|
||||
// They should only be consulted if the sizes read earlier
|
||||
// are maxed out.
|
||||
// See golang.org/issue/13367.
|
||||
if needUSize {
|
||||
needUSize = false
|
||||
if len(fieldBuf) < 8 {
|
||||
return ErrFormat
|
||||
}
|
||||
f.UncompressedSize64 = fieldBuf.uint64()
|
||||
}
|
||||
if needCSize {
|
||||
needCSize = false
|
||||
if len(fieldBuf) < 8 {
|
||||
return ErrFormat
|
||||
}
|
||||
f.CompressedSize64 = fieldBuf.uint64()
|
||||
}
|
||||
if needHeaderOffset {
|
||||
needHeaderOffset = false
|
||||
if len(fieldBuf) < 8 {
|
||||
return ErrFormat
|
||||
}
|
||||
f.headerOffset = int64(fieldBuf.uint64())
|
||||
}
|
||||
case ntfsExtraID:
|
||||
if len(fieldBuf) < 4 {
|
||||
continue parseExtras
|
||||
}
|
||||
fieldBuf.uint32() // reserved (ignored)
|
||||
for len(fieldBuf) >= 4 { // need at least tag and size
|
||||
attrTag := fieldBuf.uint16()
|
||||
attrSize := int(fieldBuf.uint16())
|
||||
if len(fieldBuf) < attrSize {
|
||||
continue parseExtras
|
||||
}
|
||||
attrBuf := fieldBuf.sub(attrSize)
|
||||
if attrTag != 1 || attrSize != 24 {
|
||||
continue // Ignore irrelevant attributes
|
||||
}
|
||||
|
||||
const ticksPerSecond = 1e7 // Windows timestamp resolution
|
||||
ts := int64(attrBuf.uint64()) // ModTime since Windows epoch
|
||||
secs := ts / ticksPerSecond
|
||||
nsecs := (1e9 / ticksPerSecond) * (ts % ticksPerSecond)
|
||||
epoch := time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
modified = time.Unix(epoch.Unix()+secs, nsecs)
|
||||
}
|
||||
case unixExtraID, infoZipUnixExtraID:
|
||||
if len(fieldBuf) < 8 {
|
||||
continue parseExtras
|
||||
}
|
||||
fieldBuf.uint32() // AcTime (ignored)
|
||||
ts := int64(fieldBuf.uint32()) // ModTime since Unix epoch
|
||||
modified = time.Unix(ts, 0)
|
||||
case extTimeExtraID:
|
||||
if len(fieldBuf) < 5 || fieldBuf.uint8()&1 == 0 {
|
||||
continue parseExtras
|
||||
}
|
||||
ts := int64(fieldBuf.uint32()) // ModTime since Unix epoch
|
||||
modified = time.Unix(ts, 0)
|
||||
}
|
||||
}
|
||||
|
||||
msdosModified := msDosTimeToTime(f.ModifiedDate, f.ModifiedTime)
|
||||
f.Modified = msdosModified
|
||||
if !modified.IsZero() {
|
||||
f.Modified = modified.UTC()
|
||||
|
||||
// If legacy MS-DOS timestamps are set, we can use the delta between
|
||||
// the legacy and extended versions to estimate timezone offset.
|
||||
//
|
||||
// A non-UTC timezone is always used (even if offset is zero).
|
||||
// Thus, FileHeader.Modified.Location() == time.UTC is useful for
|
||||
// determining whether extended timestamps are present.
|
||||
// This is necessary for users that need to do additional time
|
||||
// calculations when dealing with legacy ZIP formats.
|
||||
if f.ModifiedTime != 0 || f.ModifiedDate != 0 {
|
||||
f.Modified = modified.In(timeZone(msdosModified.Sub(modified)))
|
||||
}
|
||||
}
|
||||
|
||||
// Assume that uncompressed size 2³²-1 could plausibly happen in
|
||||
// an old zip32 file that was sharding inputs into the largest chunks
|
||||
// possible (or is just malicious; search the web for 42.zip).
|
||||
// If needUSize is true still, it means we didn't see a zip64 extension.
|
||||
// As long as the compressed size is not also 2³²-1 (implausible)
|
||||
// and the header is not also 2³²-1 (equally implausible),
|
||||
// accept the uncompressed size 2³²-1 as valid.
|
||||
// If nothing else, this keeps archive/zip working with 42.zip.
|
||||
_ = needUSize
|
||||
|
||||
if needCSize || needHeaderOffset {
|
||||
return ErrFormat
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDataDescriptor(r io.Reader, f *File) error {
|
||||
var buf [dataDescriptorLen]byte
|
||||
// The spec says: "Although not originally assigned a
|
||||
// signature, the value 0x08074b50 has commonly been adopted
|
||||
// as a signature value for the data descriptor record.
|
||||
// Implementers should be aware that ZIP files may be
|
||||
// encountered with or without this signature marking data
|
||||
// descriptors and should account for either case when reading
|
||||
// ZIP files to ensure compatibility."
|
||||
//
|
||||
// dataDescriptorLen includes the size of the signature but
|
||||
// first read just those 4 bytes to see if it exists.
|
||||
if _, err := io.ReadFull(r, buf[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
off := 0
|
||||
maybeSig := readBuf(buf[:4])
|
||||
if maybeSig.uint32() != dataDescriptorSignature {
|
||||
// No data descriptor signature. Keep these four
|
||||
// bytes.
|
||||
off += 4
|
||||
}
|
||||
if _, err := io.ReadFull(r, buf[off:12]); err != nil {
|
||||
return err
|
||||
}
|
||||
b := readBuf(buf[:12])
|
||||
if b.uint32() != f.CRC32 {
|
||||
return ErrChecksum
|
||||
}
|
||||
|
||||
// The two sizes that follow here can be either 32 bits or 64 bits
|
||||
// but the spec is not very clear on this and different
|
||||
// interpretations has been made causing incompatibilities. We
|
||||
// already have the sizes from the central directory so we can
|
||||
// just ignore these.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
@@ -614,242 +94,3 @@ func (b *readBuf) sub(n int) readBuf {
|
||||
*b = (*b)[n:]
|
||||
return b2
|
||||
}
|
||||
|
||||
// A fileListEntry is a File and its ename.
|
||||
// If file == nil, the fileListEntry describes a directory without metadata.
|
||||
type fileListEntry struct {
|
||||
name string
|
||||
file *File
|
||||
isDir bool
|
||||
isDup bool
|
||||
}
|
||||
|
||||
type fileInfoDirEntry interface {
|
||||
fs.FileInfo
|
||||
fs.DirEntry
|
||||
}
|
||||
|
||||
func (f *fileListEntry) stat() (fileInfoDirEntry, error) {
|
||||
if f.isDup {
|
||||
return nil, errors.New(f.name + ": duplicate entries in zip file")
|
||||
}
|
||||
if !f.isDir {
|
||||
return headerFileInfo{&f.file.FileHeader}, nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Only used for directories.
|
||||
func (f *fileListEntry) Name() string { _, elem, _ := split(f.name); return elem }
|
||||
func (f *fileListEntry) Size() int64 { return 0 }
|
||||
func (f *fileListEntry) Mode() fs.FileMode { return fs.ModeDir | 0555 }
|
||||
func (f *fileListEntry) Type() fs.FileMode { return fs.ModeDir }
|
||||
func (f *fileListEntry) IsDir() bool { return true }
|
||||
func (f *fileListEntry) Sys() any { return nil }
|
||||
|
||||
func (f *fileListEntry) ModTime() time.Time {
|
||||
if f.file == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return f.file.FileHeader.Modified.UTC()
|
||||
}
|
||||
|
||||
func (f *fileListEntry) Info() (fs.FileInfo, error) { return f, nil }
|
||||
|
||||
func (f *fileListEntry) String() string {
|
||||
return fs.FormatDirEntry(f)
|
||||
}
|
||||
|
||||
// toValidName coerces name to be a valid name for fs.FS.Open.
|
||||
func toValidName(name string) string {
|
||||
name = strings.ReplaceAll(name, `\`, `/`)
|
||||
p := path.Clean(name)
|
||||
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
|
||||
for strings.HasPrefix(p, "../") {
|
||||
p = p[len("../"):]
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *Reader) initFileList() {
|
||||
r.fileListOnce.Do(func() {
|
||||
// files and knownDirs map from a file/directory name
|
||||
// to an index into the r.fileList entry that we are
|
||||
// building. They are used to mark duplicate entries.
|
||||
files := make(map[string]int)
|
||||
knownDirs := make(map[string]int)
|
||||
|
||||
// dirs[name] is true if name is known to be a directory,
|
||||
// because it appears as a prefix in a path.
|
||||
dirs := make(map[string]bool)
|
||||
|
||||
for _, file := range r.File {
|
||||
isDir := len(file.Name) > 0 && file.Name[len(file.Name)-1] == '/'
|
||||
name := toValidName(file.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if idx, ok := files[name]; ok {
|
||||
r.fileList[idx].isDup = true
|
||||
continue
|
||||
}
|
||||
if idx, ok := knownDirs[name]; ok {
|
||||
r.fileList[idx].isDup = true
|
||||
continue
|
||||
}
|
||||
|
||||
for dir := path.Dir(name); dir != "."; dir = path.Dir(dir) {
|
||||
dirs[dir] = true
|
||||
}
|
||||
|
||||
idx := len(r.fileList)
|
||||
entry := fileListEntry{
|
||||
name: name,
|
||||
file: file,
|
||||
isDir: isDir,
|
||||
}
|
||||
r.fileList = append(r.fileList, entry)
|
||||
if isDir {
|
||||
knownDirs[name] = idx
|
||||
} else {
|
||||
files[name] = idx
|
||||
}
|
||||
}
|
||||
for dir := range dirs {
|
||||
if _, ok := knownDirs[dir]; !ok {
|
||||
if idx, ok := files[dir]; ok {
|
||||
r.fileList[idx].isDup = true
|
||||
} else {
|
||||
entry := fileListEntry{
|
||||
name: dir,
|
||||
file: nil,
|
||||
isDir: true,
|
||||
}
|
||||
r.fileList = append(r.fileList, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(r.fileList, func(i, j int) bool { return fileEntryLess(r.fileList[i].name, r.fileList[j].name) })
|
||||
})
|
||||
}
|
||||
|
||||
func fileEntryLess(x, y string) bool {
|
||||
xdir, xelem, _ := split(x)
|
||||
ydir, yelem, _ := split(y)
|
||||
return xdir < ydir || xdir == ydir && xelem < yelem
|
||||
}
|
||||
|
||||
// Open opens the named file in the ZIP archive,
|
||||
// using the semantics of fs.FS.Open:
|
||||
// paths are always slash separated, with no
|
||||
// leading / or ../ elements.
|
||||
func (r *Reader) Open(name string) (fs.File, error) {
|
||||
r.initFileList()
|
||||
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
e := r.openLookup(name)
|
||||
if e == nil {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
if e.isDir {
|
||||
return &openDir{e, r.openReadDir(name), 0}, nil
|
||||
}
|
||||
rc, err := e.file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rc.(fs.File), nil
|
||||
}
|
||||
|
||||
func split(name string) (dir, elem string, isDir bool) {
|
||||
if len(name) > 0 && name[len(name)-1] == '/' {
|
||||
isDir = true
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
i := len(name) - 1
|
||||
for i >= 0 && name[i] != '/' {
|
||||
i--
|
||||
}
|
||||
if i < 0 {
|
||||
return ".", name, isDir
|
||||
}
|
||||
return name[:i], name[i+1:], isDir
|
||||
}
|
||||
|
||||
var dotFile = &fileListEntry{name: "./", isDir: true}
|
||||
|
||||
func (r *Reader) openLookup(name string) *fileListEntry {
|
||||
if name == "." {
|
||||
return dotFile
|
||||
}
|
||||
|
||||
dir, elem, _ := split(name)
|
||||
files := r.fileList
|
||||
i := sort.Search(len(files), func(i int) bool {
|
||||
idir, ielem, _ := split(files[i].name)
|
||||
return idir > dir || idir == dir && ielem >= elem
|
||||
})
|
||||
if i < len(files) {
|
||||
fname := files[i].name
|
||||
if fname == name || len(fname) == len(name)+1 && fname[len(name)] == '/' && fname[:len(name)] == name {
|
||||
return &files[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) openReadDir(dir string) []fileListEntry {
|
||||
files := r.fileList
|
||||
i := sort.Search(len(files), func(i int) bool {
|
||||
idir, _, _ := split(files[i].name)
|
||||
return idir >= dir
|
||||
})
|
||||
j := sort.Search(len(files), func(j int) bool {
|
||||
jdir, _, _ := split(files[j].name)
|
||||
return jdir > dir
|
||||
})
|
||||
return files[i:j]
|
||||
}
|
||||
|
||||
type openDir struct {
|
||||
e *fileListEntry
|
||||
files []fileListEntry
|
||||
offset int
|
||||
}
|
||||
|
||||
func (d *openDir) Close() error { return nil }
|
||||
func (d *openDir) Stat() (fs.FileInfo, error) { return d.e.stat() }
|
||||
|
||||
func (d *openDir) Read([]byte) (int, error) {
|
||||
return 0, &fs.PathError{Op: "read", Path: d.e.name, Err: errors.New("is a directory")}
|
||||
}
|
||||
|
||||
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
|
||||
n := len(d.files) - d.offset
|
||||
if count > 0 && n > count {
|
||||
n = count
|
||||
}
|
||||
if n == 0 {
|
||||
if count <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
list := make([]fs.DirEntry, n)
|
||||
for i := range list {
|
||||
s, err := d.files[d.offset+i].stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list[i] = s
|
||||
}
|
||||
d.offset += n
|
||||
return list, nil
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Compressor returns a new compressing writer, writing to w.
|
||||
// The WriteCloser's Close method must be used to flush pending data to w.
|
||||
// The Compressor itself must be safe to invoke from multiple goroutines
|
||||
// simultaneously, but each returned writer will be used only by
|
||||
// one goroutine at a time.
|
||||
type Compressor func(w io.Writer) (io.WriteCloser, error)
|
||||
|
||||
// A Decompressor returns a new decompressing reader, reading from r.
|
||||
// The [io.ReadCloser]'s Close method must be used to release associated resources.
|
||||
// The Decompressor itself must be safe to invoke from multiple goroutines
|
||||
// simultaneously, but each returned reader will be used only by
|
||||
// one goroutine at a time.
|
||||
type Decompressor func(r io.Reader) io.ReadCloser
|
||||
|
||||
var flateWriterPool sync.Pool
|
||||
|
||||
func newFlateWriter(w io.Writer) io.WriteCloser {
|
||||
fw, ok := flateWriterPool.Get().(*flate.Writer)
|
||||
if ok {
|
||||
fw.Reset(w)
|
||||
} else {
|
||||
fw, _ = flate.NewWriter(w, 5)
|
||||
}
|
||||
return &pooledFlateWriter{fw: fw}
|
||||
}
|
||||
|
||||
type pooledFlateWriter struct {
|
||||
mu sync.Mutex // guards Close and Write
|
||||
fw *flate.Writer
|
||||
}
|
||||
|
||||
func (w *pooledFlateWriter) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.fw == nil {
|
||||
return 0, errors.New("Write after Close")
|
||||
}
|
||||
return w.fw.Write(p)
|
||||
}
|
||||
|
||||
func (w *pooledFlateWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
var err error
|
||||
if w.fw != nil {
|
||||
err = w.fw.Close()
|
||||
flateWriterPool.Put(w.fw)
|
||||
w.fw = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var flateReaderPool sync.Pool
|
||||
|
||||
func newFlateReader(r io.Reader) io.ReadCloser {
|
||||
fr, ok := flateReaderPool.Get().(io.ReadCloser)
|
||||
if ok {
|
||||
fr.(flate.Resetter).Reset(r, nil)
|
||||
} else {
|
||||
fr = flate.NewReader(r)
|
||||
}
|
||||
return &pooledFlateReader{fr: fr}
|
||||
}
|
||||
|
||||
type pooledFlateReader struct {
|
||||
mu sync.Mutex // guards Close and Read
|
||||
fr io.ReadCloser
|
||||
}
|
||||
|
||||
func (r *pooledFlateReader) Read(p []byte) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.fr == nil {
|
||||
return 0, errors.New("Read after Close")
|
||||
}
|
||||
return r.fr.Read(p)
|
||||
}
|
||||
|
||||
func (r *pooledFlateReader) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
var err error
|
||||
if r.fr != nil {
|
||||
err = r.fr.Close()
|
||||
flateReaderPool.Put(r.fr)
|
||||
r.fr = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
compressors sync.Map // map[uint16]Compressor
|
||||
decompressors sync.Map // map[uint16]Decompressor
|
||||
)
|
||||
|
||||
func init() {
|
||||
compressors.Store(Store, Compressor(func(w io.Writer) (io.WriteCloser, error) { return &nopCloser{w}, nil }))
|
||||
compressors.Store(Deflate, Compressor(func(w io.Writer) (io.WriteCloser, error) { return newFlateWriter(w), nil }))
|
||||
|
||||
decompressors.Store(Store, Decompressor(io.NopCloser))
|
||||
decompressors.Store(Deflate, Decompressor(newFlateReader))
|
||||
}
|
||||
|
||||
// RegisterDecompressor allows custom decompressors for a specified method ID.
|
||||
// The common methods [Store] and [Deflate] are built in.
|
||||
func RegisterDecompressor(method uint16, dcomp Decompressor) {
|
||||
if _, dup := decompressors.LoadOrStore(method, dcomp); dup {
|
||||
panic("decompressor already registered")
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCompressor registers custom compressors for a specified method ID.
|
||||
// The common methods [Store] and [Deflate] are built in.
|
||||
func RegisterCompressor(method uint16, comp Compressor) {
|
||||
if _, dup := compressors.LoadOrStore(method, comp); dup {
|
||||
panic("compressor already registered")
|
||||
}
|
||||
}
|
||||
|
||||
func compressor(method uint16) Compressor {
|
||||
ci, ok := compressors.Load(method)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return ci.(Compressor)
|
||||
}
|
||||
|
||||
func decompressor(method uint16) Decompressor {
|
||||
di, ok := decompressors.Load(method)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return di.(Decompressor)
|
||||
}
|
||||
@@ -21,12 +21,6 @@ fields must be used instead.
|
||||
*/
|
||||
package zip
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Compression methods.
|
||||
const (
|
||||
Store uint16 = 0 // no compression
|
||||
@@ -47,362 +41,4 @@ const (
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
UEsDBBQACAAAAGWHaECoZTJ+BAAAAAQAAAAHABgAZm9vLnR4dFVUBQAD3lVZT3V4CwABBPUBAAAEFAAAAGZvbwqoZTJ+BAAAAAQAAABQSwMEFAAIAAAAZodoQOmzogQEAAAABAAAAAcAGABiYXIudHh0VVQFAAPgVVlPdXgLAAEE9QEAAAQUAAAAYmFyCumzogQEAAAABAAAAFBLAQIUAxQACAAAAGWHaECoZTJ+BAAAAAQAAAAHABgAAAAAAAAAAACkgQAAAABmb28udHh0VVQFAAPeVVlPdXgLAAEE9QEAAAQUAAAAUEsBAhQDFAAIAAAAZodoQOmzogQEAAAABAAAAAcAGAAAAAAAAAAAAKSBTQAAAGJhci50eHRVVAUAA+BVWU91eAsAAQT1AQAABBQAAABQSwUGAAAAAAIAAgCaAAAAmgAAAAAA
|
||||
Binary file not shown.
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 785 B |
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -1,666 +0,0 @@
|
||||
// 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 zip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
errLongName = errors.New("zip: FileHeader.Name too long")
|
||||
errLongExtra = errors.New("zip: FileHeader.Extra too long")
|
||||
)
|
||||
|
||||
// Writer implements a zip file writer.
|
||||
type Writer struct {
|
||||
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.
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{cw: &countWriter{w: bufio.NewWriter(w)}}
|
||||
}
|
||||
|
||||
// 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 (w *Writer) SetOffset(n int64) {
|
||||
if w.cw.count != 0 {
|
||||
panic("zip: SetOffset called after data was written")
|
||||
}
|
||||
w.cw.count = n
|
||||
}
|
||||
|
||||
// Flush flushes any buffered data to the underlying writer.
|
||||
// Calling Flush is not normally necessary; calling Close is sufficient.
|
||||
func (w *Writer) Flush() error {
|
||||
return w.cw.w.(*bufio.Writer).Flush()
|
||||
}
|
||||
|
||||
// SetComment sets the end-of-central-directory comment field.
|
||||
// It can only be called before [Writer.Close].
|
||||
func (w *Writer) SetComment(comment string) error {
|
||||
if len(comment) > uint16max {
|
||||
return errors.New("zip: Writer.Comment too long")
|
||||
}
|
||||
w.comment = comment
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close finishes writing the zip file by writing the central directory.
|
||||
// It does not close the underlying writer.
|
||||
func (w *Writer) Close() error {
|
||||
if w.last != nil && !w.last.closed {
|
||||
if err := w.last.close(); err != nil {
|
||||
return err
|
||||
}
|
||||
w.last = nil
|
||||
}
|
||||
if w.closed {
|
||||
return errors.New("zip: writer closed twice")
|
||||
}
|
||||
w.closed = true
|
||||
|
||||
// write central directory
|
||||
start := w.cw.count
|
||||
for _, h := range w.dir {
|
||||
var buf [directoryHeaderLen]byte
|
||||
b := writeBuf(buf[:])
|
||||
b.uint32(uint32(directoryHeaderSignature))
|
||||
b.uint16(h.CreatorVersion)
|
||||
b.uint16(h.ReaderVersion)
|
||||
b.uint16(h.Flags)
|
||||
b.uint16(h.Method)
|
||||
b.uint16(h.ModifiedTime)
|
||||
b.uint16(h.ModifiedDate)
|
||||
b.uint32(h.CRC32)
|
||||
if h.isZip64() || h.offset >= uint32max {
|
||||
// the file needs a zip64 header. store maxint in both
|
||||
// 32 bit size fields (and offset later) to signal that the
|
||||
// zip64 extra header should be used.
|
||||
b.uint32(uint32max) // compressed size
|
||||
b.uint32(uint32max) // uncompressed size
|
||||
|
||||
// append a zip64 extra block to Extra
|
||||
var buf [28]byte // 2x uint16 + 3x uint64
|
||||
eb := writeBuf(buf[:])
|
||||
eb.uint16(zip64ExtraID)
|
||||
eb.uint16(24) // size = 3x uint64
|
||||
eb.uint64(h.UncompressedSize64)
|
||||
eb.uint64(h.CompressedSize64)
|
||||
eb.uint64(h.offset)
|
||||
h.Extra = append(h.Extra, buf[:]...)
|
||||
} else {
|
||||
b.uint32(h.CompressedSize)
|
||||
b.uint32(h.UncompressedSize)
|
||||
}
|
||||
|
||||
b.uint16(uint16(len(h.Name)))
|
||||
b.uint16(uint16(len(h.Extra)))
|
||||
b.uint16(uint16(len(h.Comment)))
|
||||
b = b[4:] // skip disk number start and internal file attr (2x uint16)
|
||||
b.uint32(h.ExternalAttrs)
|
||||
if h.offset > uint32max {
|
||||
b.uint32(uint32max)
|
||||
} else {
|
||||
b.uint32(uint32(h.offset))
|
||||
}
|
||||
if _, err := w.cw.Write(buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w.cw, h.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.cw.Write(h.Extra); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w.cw, h.Comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
end := w.cw.count
|
||||
|
||||
records := uint64(len(w.dir))
|
||||
size := uint64(end - start)
|
||||
offset := uint64(start)
|
||||
|
||||
if f := w.testHookCloseSizeOffset; f != nil {
|
||||
f(size, offset)
|
||||
}
|
||||
|
||||
if records >= uint16max || size >= uint32max || offset >= uint32max {
|
||||
var buf [directory64EndLen + directory64LocLen]byte
|
||||
b := writeBuf(buf[:])
|
||||
|
||||
// zip64 end of central directory record
|
||||
b.uint32(directory64EndSignature)
|
||||
b.uint64(directory64EndLen - 12) // length minus signature (uint32) and length fields (uint64)
|
||||
b.uint16(zipVersion45) // version made by
|
||||
b.uint16(zipVersion45) // version needed to extract
|
||||
b.uint32(0) // number of this disk
|
||||
b.uint32(0) // number of the disk with the start of the central directory
|
||||
b.uint64(records) // total number of entries in the central directory on this disk
|
||||
b.uint64(records) // total number of entries in the central directory
|
||||
b.uint64(size) // size of the central directory
|
||||
b.uint64(offset) // offset of start of central directory with respect to the starting disk number
|
||||
|
||||
// zip64 end of central directory locator
|
||||
b.uint32(directory64LocSignature)
|
||||
b.uint32(0) // number of the disk with the start of the zip64 end of central directory
|
||||
b.uint64(uint64(end)) // relative offset of the zip64 end of central directory record
|
||||
b.uint32(1) // total number of disks
|
||||
|
||||
if _, err := w.cw.Write(buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// store max values in the regular end record to signal
|
||||
// that the zip64 values should be used instead
|
||||
records = uint16max
|
||||
size = uint32max
|
||||
offset = uint32max
|
||||
}
|
||||
|
||||
// write end record
|
||||
var buf [directoryEndLen]byte
|
||||
b := writeBuf(buf[:])
|
||||
b.uint32(uint32(directoryEndSignature))
|
||||
b = b[4:] // skip over disk number and first disk number (2x uint16)
|
||||
b.uint16(uint16(records)) // number of entries this disk
|
||||
b.uint16(uint16(records)) // number of entries total
|
||||
b.uint32(uint32(size)) // size of directory
|
||||
b.uint32(uint32(offset)) // start of directory
|
||||
b.uint16(uint16(len(w.comment))) // byte size of EOCD comment
|
||||
if _, err := w.cw.Write(buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w.cw, w.comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.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.
|
||||
// The file's contents must be written to the [io.Writer] before the next
|
||||
// call to [Writer.Create], [Writer.CreateHeader], or [Writer.Close].
|
||||
func (w *Writer) Create(name string) (io.Writer, error) {
|
||||
header := &FileHeader{
|
||||
Name: name,
|
||||
Method: Deflate,
|
||||
}
|
||||
return w.CreateHeader(header)
|
||||
}
|
||||
|
||||
// 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(s string) (valid, require bool) {
|
||||
for i := 0; i < len(s); {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
i += size
|
||||
// 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 r < 0x20 || r > 0x7d || r == 0x5c {
|
||||
if !utf8.ValidRune(r) || (r == utf8.RuneError && size == 1) {
|
||||
return false, false
|
||||
}
|
||||
require = true
|
||||
}
|
||||
}
|
||||
return true, require
|
||||
}
|
||||
|
||||
// prepare performs the bookkeeping operations required at the start of
|
||||
// CreateHeader and CreateRaw.
|
||||
func (w *Writer) prepare(fh *FileHeader) error {
|
||||
if w.last != nil && !w.last.closed {
|
||||
if err := w.last.close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(w.dir) > 0 && w.dir[len(w.dir)-1].FileHeader == fh {
|
||||
// See https://golang.org/issue/11144 confusion.
|
||||
return errors.New("archive/zip: invalid duplicate FileHeader")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
|
||||
if err := w.prepare(fh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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.
|
||||
utf8Valid1, utf8Require1 := detectUTF8(fh.Name)
|
||||
utf8Valid2, utf8Require2 := detectUTF8(fh.Comment)
|
||||
switch {
|
||||
case fh.NonUTF8:
|
||||
fh.Flags &^= 0x800
|
||||
case (utf8Require1 || utf8Require2) && (utf8Valid1 && utf8Valid2):
|
||||
fh.Flags |= 0x800
|
||||
}
|
||||
|
||||
fh.CreatorVersion = fh.CreatorVersion&0xff00 | zipVersion20 // preserve compatibility byte
|
||||
fh.ReaderVersion = zipVersion20
|
||||
|
||||
// If Modified is set, this takes precedence over MS-DOS timestamp fields.
|
||||
if !fh.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.
|
||||
fh.ModifiedDate, fh.ModifiedTime = timeToMsDosTime(fh.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 mbuf [9]byte // 2*SizeOf(uint16) + SizeOf(uint8) + SizeOf(uint32)
|
||||
mt := uint32(fh.Modified.Unix())
|
||||
eb := writeBuf(mbuf[:])
|
||||
eb.uint16(extTimeExtraID)
|
||||
eb.uint16(5) // Size: SizeOf(uint8) + SizeOf(uint32)
|
||||
eb.uint8(1) // Flags: ModTime
|
||||
eb.uint32(mt) // ModTime
|
||||
fh.Extra = append(fh.Extra, mbuf[:]...)
|
||||
}
|
||||
|
||||
var (
|
||||
ow io.Writer
|
||||
fw *fileWriter
|
||||
)
|
||||
h := &header{
|
||||
FileHeader: fh,
|
||||
offset: uint64(w.cw.count),
|
||||
}
|
||||
|
||||
if strings.HasSuffix(fh.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.
|
||||
fh.Method = Store
|
||||
fh.Flags &^= 0x8 // we will not write a data descriptor
|
||||
|
||||
// Explicitly clear sizes as they have no meaning for directories.
|
||||
fh.CompressedSize = 0
|
||||
fh.CompressedSize64 = 0
|
||||
fh.UncompressedSize = 0
|
||||
fh.UncompressedSize64 = 0
|
||||
|
||||
ow = dirWriter{}
|
||||
} else {
|
||||
fh.Flags |= 0x8 // we will write a data descriptor
|
||||
|
||||
fw = &fileWriter{
|
||||
zipw: w.cw,
|
||||
compCount: &countWriter{w: w.cw},
|
||||
crc32: crc32.NewIEEE(),
|
||||
}
|
||||
comp := w.compressor(fh.Method)
|
||||
if comp == nil {
|
||||
return nil, ErrAlgorithm
|
||||
}
|
||||
var err error
|
||||
fw.comp, err = comp(fw.compCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fw.rawCount = &countWriter{w: fw.comp}
|
||||
fw.header = h
|
||||
ow = fw
|
||||
}
|
||||
w.dir = append(w.dir, h)
|
||||
if err := writeHeader(w.cw, h); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If we're creating a directory, fw is nil.
|
||||
w.last = fw
|
||||
return ow, nil
|
||||
}
|
||||
|
||||
func writeHeader(w io.Writer, h *header) error {
|
||||
const maxUint16 = 1<<16 - 1
|
||||
if len(h.Name) > maxUint16 {
|
||||
return errLongName
|
||||
}
|
||||
if len(h.Extra) > maxUint16 {
|
||||
return errLongExtra
|
||||
}
|
||||
|
||||
var buf [fileHeaderLen]byte
|
||||
b := writeBuf(buf[:])
|
||||
b.uint32(uint32(fileHeaderSignature))
|
||||
b.uint16(h.ReaderVersion)
|
||||
b.uint16(h.Flags)
|
||||
b.uint16(h.Method)
|
||||
b.uint16(h.ModifiedTime)
|
||||
b.uint16(h.ModifiedDate)
|
||||
// In raw mode (caller does the compression), the values are either
|
||||
// written here or in the trailing data descriptor based on the header
|
||||
// flags.
|
||||
if h.raw && !h.hasDataDescriptor() {
|
||||
b.uint32(h.CRC32)
|
||||
b.uint32(uint32(min(h.CompressedSize64, uint32max)))
|
||||
b.uint32(uint32(min(h.UncompressedSize64, uint32max)))
|
||||
} else {
|
||||
// When this package handle the compression, these values are
|
||||
// always written to the trailing data descriptor.
|
||||
b.uint32(0) // crc32
|
||||
b.uint32(0) // compressed size
|
||||
b.uint32(0) // uncompressed size
|
||||
}
|
||||
b.uint16(uint16(len(h.Name)))
|
||||
b.uint16(uint16(len(h.Extra)))
|
||||
if _, err := w.Write(buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w, h.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(h.Extra)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (w *Writer) CreateRaw(fh *FileHeader) (io.Writer, error) {
|
||||
if err := w.prepare(fh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fh.CompressedSize = uint32(min(fh.CompressedSize64, uint32max))
|
||||
fh.UncompressedSize = uint32(min(fh.UncompressedSize64, uint32max))
|
||||
|
||||
h := &header{
|
||||
FileHeader: fh,
|
||||
offset: uint64(w.cw.count),
|
||||
raw: true,
|
||||
}
|
||||
w.dir = append(w.dir, h)
|
||||
if err := writeHeader(w.cw, h); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(fh.Name, "/") {
|
||||
w.last = nil
|
||||
return dirWriter{}, nil
|
||||
}
|
||||
|
||||
fw := &fileWriter{
|
||||
header: h,
|
||||
zipw: w.cw,
|
||||
}
|
||||
w.last = fw
|
||||
return fw, nil
|
||||
}
|
||||
|
||||
// Copy copies the file f (obtained from a [Reader]) into w. It copies the raw
|
||||
// form directly bypassing decompression, compression, and validation.
|
||||
func (w *Writer) Copy(f *File) error {
|
||||
r, err := f.OpenRaw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fw, err := w.CreateRaw(&f.FileHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(fw, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 (w *Writer) RegisterCompressor(method uint16, comp Compressor) {
|
||||
if w.compressors == nil {
|
||||
w.compressors = make(map[uint16]Compressor)
|
||||
}
|
||||
w.compressors[method] = comp
|
||||
}
|
||||
|
||||
// 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 (w *Writer) AddFS(fsys fs.FS) error {
|
||||
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return errors.New("zip: cannot add non-regular file")
|
||||
}
|
||||
h, err := FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Name = name
|
||||
h.Method = Deflate
|
||||
fw, err := w.CreateHeader(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := fsys.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(fw, f)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Writer) compressor(method uint16) Compressor {
|
||||
comp := w.compressors[method]
|
||||
if comp == nil {
|
||||
comp = compressor(method)
|
||||
}
|
||||
return comp
|
||||
}
|
||||
|
||||
type dirWriter struct{}
|
||||
|
||||
func (dirWriter) Write(b []byte) (int, error) {
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, 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 (w *fileWriter) Write(p []byte) (int, error) {
|
||||
if w.closed {
|
||||
return 0, errors.New("zip: write to closed file")
|
||||
}
|
||||
if w.raw {
|
||||
return w.zipw.Write(p)
|
||||
}
|
||||
w.crc32.Write(p)
|
||||
return w.rawCount.Write(p)
|
||||
}
|
||||
|
||||
func (w *fileWriter) close() error {
|
||||
if w.closed {
|
||||
return errors.New("zip: file closed twice")
|
||||
}
|
||||
w.closed = true
|
||||
if w.raw {
|
||||
return w.writeDataDescriptor()
|
||||
}
|
||||
if err := w.comp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update FileHeader
|
||||
fh := w.header.FileHeader
|
||||
fh.CRC32 = w.crc32.Sum32()
|
||||
fh.CompressedSize64 = uint64(w.compCount.count)
|
||||
fh.UncompressedSize64 = uint64(w.rawCount.count)
|
||||
|
||||
if fh.isZip64() {
|
||||
fh.CompressedSize = uint32max
|
||||
fh.UncompressedSize = uint32max
|
||||
fh.ReaderVersion = zipVersion45 // requires 4.5 - File uses ZIP64 format extensions
|
||||
} else {
|
||||
fh.CompressedSize = uint32(fh.CompressedSize64)
|
||||
fh.UncompressedSize = uint32(fh.UncompressedSize64)
|
||||
}
|
||||
|
||||
return w.writeDataDescriptor()
|
||||
}
|
||||
|
||||
func (w *fileWriter) writeDataDescriptor() error {
|
||||
if !w.hasDataDescriptor() {
|
||||
return nil
|
||||
}
|
||||
// Write data descriptor. This is more complicated than one would
|
||||
// think, see e.g. comments in zipfile.c:putextended() and
|
||||
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7073588.
|
||||
// The approach here is to write 8 byte sizes if needed without
|
||||
// adding a zip64 extra in the local header (too late anyway).
|
||||
var buf []byte
|
||||
if w.isZip64() {
|
||||
buf = make([]byte, dataDescriptor64Len)
|
||||
} else {
|
||||
buf = make([]byte, dataDescriptorLen)
|
||||
}
|
||||
b := writeBuf(buf)
|
||||
b.uint32(dataDescriptorSignature) // de-facto standard, required by OS X
|
||||
b.uint32(w.CRC32)
|
||||
if w.isZip64() {
|
||||
b.uint64(w.CompressedSize64)
|
||||
b.uint64(w.UncompressedSize64)
|
||||
} else {
|
||||
b.uint32(w.CompressedSize)
|
||||
b.uint32(w.UncompressedSize)
|
||||
}
|
||||
_, err := w.zipw.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
type countWriter struct {
|
||||
w io.Writer
|
||||
count int64
|
||||
}
|
||||
|
||||
func (w *countWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.w.Write(p)
|
||||
w.count += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (w nopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type writeBuf []byte
|
||||
|
||||
func (b *writeBuf) uint8(v uint8) {
|
||||
(*b)[0] = v
|
||||
*b = (*b)[1:]
|
||||
}
|
||||
|
||||
func (b *writeBuf) uint16(v uint16) {
|
||||
binary.LittleEndian.PutUint16(*b, v)
|
||||
*b = (*b)[2:]
|
||||
}
|
||||
|
||||
func (b *writeBuf) uint32(v uint32) {
|
||||
binary.LittleEndian.PutUint32(*b, v)
|
||||
*b = (*b)[4:]
|
||||
}
|
||||
|
||||
func (b *writeBuf) uint64(v uint64) {
|
||||
binary.LittleEndian.PutUint64(*b, v)
|
||||
*b = (*b)[8:]
|
||||
}
|
||||
Reference in New Issue
Block a user