mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
276 lines
7.0 KiB
Go
276 lines
7.0 KiB
Go
// Package zipstream
|
|
// A streaming zip reader that specifically supports GitHub Actions artifacts files.
|
|
// The standard library's `archive/zip` uses the central directory at the end of the file,
|
|
// so it would require downloading the entire file before reading it.
|
|
// This package reads the local file headers and supports DEFLATE streams with an unknown size,
|
|
// allowing us to read files as the response is being downloaded.
|
|
package zipstream
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"compress/flate"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
const (
|
|
headerIdentifierLen = 4
|
|
fileHeaderLen = 26
|
|
dataDescriptorLen = 16 // four uint32: descriptor signature, crc32, compressed size, size
|
|
fileHeaderSignature = 0x04034b50
|
|
directoryHeaderSignature = 0x02014b50
|
|
directoryEndSignature = 0x06054b50
|
|
dataDescriptorSignature = 0x08074b50
|
|
zip64ExtraID = 0x0001 // Zip64 extended information
|
|
)
|
|
|
|
type Reader struct {
|
|
r io.Reader
|
|
rBuf *bufio.Reader
|
|
fr io.ReadCloser
|
|
localFileEnd bool
|
|
curEntry *Entry
|
|
}
|
|
|
|
func NewReader(r io.Reader) *Reader {
|
|
return &Reader{
|
|
r: r,
|
|
fr: nil,
|
|
}
|
|
}
|
|
|
|
type Entry struct {
|
|
zip.FileHeader
|
|
r io.Reader
|
|
}
|
|
|
|
func (e *Entry) hasDataDescriptor() bool {
|
|
return e.Flags&8 != 0
|
|
}
|
|
|
|
// IsDir just simply check whether the entry name ends with "/"
|
|
func (e *Entry) IsDir() bool {
|
|
return len(e.Name) > 0 && e.Name[len(e.Name)-1] == '/'
|
|
}
|
|
|
|
func (e *Entry) Read(p []byte) (n int, err error) {
|
|
return e.r.Read(p)
|
|
}
|
|
|
|
//goland:noinspection GoDeprecation
|
|
func (z *Reader) readEntry() (*Entry, error) {
|
|
buf := make([]byte, fileHeaderLen)
|
|
if _, err := io.ReadFull(z.r, buf); err != nil {
|
|
return nil, fmt.Errorf("unable to read local file header: %w", err)
|
|
}
|
|
|
|
lr := readBuf(buf)
|
|
readerVersion := lr.uint16()
|
|
flags := lr.uint16()
|
|
method := lr.uint16()
|
|
modifiedTime := lr.uint16()
|
|
modifiedDate := lr.uint16()
|
|
crc32Sum := lr.uint32()
|
|
compressedSize := lr.uint32()
|
|
uncompressedSize := lr.uint32()
|
|
filenameLen := int(lr.uint16())
|
|
extraAreaLen := int(lr.uint16())
|
|
|
|
entry := &Entry{
|
|
FileHeader: zip.FileHeader{
|
|
ReaderVersion: readerVersion,
|
|
Flags: flags,
|
|
Method: method,
|
|
ModifiedTime: modifiedTime,
|
|
ModifiedDate: modifiedDate,
|
|
CRC32: crc32Sum,
|
|
CompressedSize: compressedSize,
|
|
UncompressedSize: uncompressedSize,
|
|
CompressedSize64: uint64(compressedSize),
|
|
UncompressedSize64: uint64(uncompressedSize),
|
|
},
|
|
r: nil,
|
|
}
|
|
|
|
nameAndExtraBuf := make([]byte, filenameLen+extraAreaLen)
|
|
if _, err := io.ReadFull(z.r, nameAndExtraBuf); err != nil {
|
|
return nil, fmt.Errorf("unable to read entry name and extra area: %w", err)
|
|
}
|
|
|
|
entry.Name = string(nameAndExtraBuf[:filenameLen])
|
|
entry.Extra = nameAndExtraBuf[filenameLen:]
|
|
|
|
entry.NonUTF8 = flags&0x800 == 0
|
|
if flags&1 == 1 {
|
|
return nil, fmt.Errorf("encrypted ZIP entry not supported")
|
|
}
|
|
if flags&8 == 8 && method != zip.Deflate {
|
|
return nil, fmt.Errorf("only DEFLATED entries can have data descriptor")
|
|
}
|
|
|
|
needCSize := entry.CompressedSize == ^uint32(0)
|
|
needUSize := entry.UncompressedSize == ^uint32(0)
|
|
|
|
ler := readBuf(entry.Extra)
|
|
for len(ler) >= 4 { // need at least tag and size
|
|
fieldTag := ler.uint16()
|
|
fieldSize := int(ler.uint16())
|
|
if len(ler) < fieldSize {
|
|
break
|
|
}
|
|
fieldBuf := ler.sub(fieldSize)
|
|
|
|
switch fieldTag {
|
|
case zip64ExtraID:
|
|
// 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 nil, zip.ErrFormat
|
|
}
|
|
entry.UncompressedSize64 = fieldBuf.uint64()
|
|
}
|
|
if needCSize {
|
|
needCSize = false
|
|
if len(fieldBuf) < 8 {
|
|
return nil, zip.ErrFormat
|
|
}
|
|
entry.CompressedSize64 = fieldBuf.uint64()
|
|
}
|
|
}
|
|
}
|
|
|
|
if needCSize {
|
|
return nil, zip.ErrFormat
|
|
}
|
|
|
|
if method == zip.Store {
|
|
entry.r = io.LimitReader(z.r, int64(entry.UncompressedSize64))
|
|
} else if method == zip.Deflate {
|
|
var reader io.Reader
|
|
if entry.CompressedSize64 > 0 {
|
|
reader = io.LimitReader(z.r, int64(entry.CompressedSize64))
|
|
} else {
|
|
// unknown size; read until deflate EOF,
|
|
// but we need z.r to be an io.ByteReader for flate to not overread
|
|
if _, ok := z.r.(io.ByteReader); !ok {
|
|
z.r = bufio.NewReader(z.r)
|
|
}
|
|
reader = z.r
|
|
}
|
|
if z.fr == nil {
|
|
z.fr = flate.NewReader(reader)
|
|
} else {
|
|
z.fr.(flate.Resetter).Reset(reader, nil)
|
|
}
|
|
entry.r = z.fr
|
|
} else {
|
|
return nil, fmt.Errorf("unknown compression method %d", method)
|
|
}
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
func (z *Reader) Next() (*Entry, error) {
|
|
if z.localFileEnd {
|
|
return nil, io.EOF
|
|
}
|
|
if z.curEntry != nil {
|
|
// Read any remaining data for the current file, if necessary.
|
|
if _, err := io.Copy(io.Discard, z.curEntry); err != nil {
|
|
return nil, fmt.Errorf("read previous file data fail: %w", err)
|
|
}
|
|
// Read the data descriptor if present.
|
|
if z.curEntry.hasDataDescriptor() {
|
|
if err := readDataDescriptor(z.r); err != nil {
|
|
return nil, fmt.Errorf("read previous entry's data descriptor fail: %w", err)
|
|
}
|
|
}
|
|
}
|
|
headerIDBuf := make([]byte, headerIdentifierLen)
|
|
if _, err := io.ReadFull(z.r, headerIDBuf); err != nil {
|
|
return nil, fmt.Errorf("unable to read header identifier: %w", err)
|
|
}
|
|
headerID := binary.LittleEndian.Uint32(headerIDBuf)
|
|
if headerID != fileHeaderSignature {
|
|
if headerID == directoryHeaderSignature || headerID == directoryEndSignature {
|
|
z.localFileEnd = true
|
|
return nil, io.EOF
|
|
}
|
|
return nil, zip.ErrFormat
|
|
}
|
|
entry, err := z.readEntry()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read zip file header: %w", err)
|
|
}
|
|
z.curEntry = entry
|
|
return entry, nil
|
|
}
|
|
|
|
func readDataDescriptor(r io.Reader) 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.
|
|
_, err := io.ReadFull(r, buf[:4])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
off := 0
|
|
maybeSig := readBuf(buf[:4])
|
|
if maybeSig.uint32() != dataDescriptorSignature {
|
|
// No data descriptor signature. Keep these four bytes.
|
|
off += 4
|
|
}
|
|
_, err = io.ReadFull(r, buf[off:12])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type readBuf []byte
|
|
|
|
func (b *readBuf) uint8() uint8 {
|
|
v := (*b)[0]
|
|
*b = (*b)[1:]
|
|
return v
|
|
}
|
|
|
|
func (b *readBuf) uint16() uint16 {
|
|
v := binary.LittleEndian.Uint16(*b)
|
|
*b = (*b)[2:]
|
|
return v
|
|
}
|
|
|
|
func (b *readBuf) uint32() uint32 {
|
|
v := binary.LittleEndian.Uint32(*b)
|
|
*b = (*b)[4:]
|
|
return v
|
|
}
|
|
|
|
func (b *readBuf) uint64() uint64 {
|
|
v := binary.LittleEndian.Uint64(*b)
|
|
*b = (*b)[8:]
|
|
return v
|
|
}
|
|
|
|
func (b *readBuf) sub(n int) readBuf {
|
|
b2 := (*b)[:n]
|
|
*b = (*b)[n:]
|
|
return b2
|
|
}
|