Files
soft_quay/core/installer/zipscan.go
T

241 lines
8.0 KiB
Go

package installer
import (
"encoding/binary"
"fmt"
"io"
"os"
)
const (
endOfCentralDirectorySignature = 0x06054b50
zip64EndSignature = 0x06064b50
zip64LocatorSignature = 0x07064b50
endOfCentralDirectoryLength = 22
zip64EndLength = 56
zip64LocatorLength = 20
maxZIPCommentLength = 1<<16 - 1
maxEOCDSearchLength = endOfCentralDirectoryLength + maxZIPCommentLength
maxInt64 = 1<<63 - 1
)
type endOfCentralDirectory struct {
offset int64
diskNumber uint32
centralDirectoryDisk uint32
entriesOnThisDisk uint64
centralDirectoryEntries uint64
centralDirectorySize uint64
centralDirectoryOffset uint64
}
func (extractor Extractor) openAndScanArchive(
zipPath string,
expectedPackageSize int64,
) (*os.File, int64, error) {
if err := extractor.limits.validate(); err != nil {
return nil, 0, err
}
if expectedPackageSize <= 0 {
return nil, 0, fmt.Errorf("%w: expected size must be positive", ErrArchiveSizeMismatch)
}
pathInfo, err := os.Lstat(zipPath)
if err != nil {
return nil, 0, fmt.Errorf("%w: inspect package: %v", ErrInvalidArchive, err)
}
if pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.Mode().IsRegular() {
return nil, 0, fmt.Errorf("%w: package is not a regular file", ErrInvalidArchive)
}
file, err := os.Open(zipPath)
if err != nil {
return nil, 0, fmt.Errorf("%w: open package: %v", ErrInvalidArchive, err)
}
closeWithError := func(err error) (*os.File, int64, error) {
_ = file.Close()
return nil, 0, err
}
info, err := file.Stat()
if err != nil {
return closeWithError(fmt.Errorf("%w: stat package: %v", ErrInvalidArchive, err))
}
if !info.Mode().IsRegular() {
return closeWithError(fmt.Errorf("%w: opened package is not a regular file", ErrInvalidArchive))
}
size := info.Size()
if size != expectedPackageSize {
return closeWithError(fmt.Errorf(
"%w: got %d, expected %d",
ErrArchiveSizeMismatch,
size,
expectedPackageSize,
))
}
if size > extractor.limits.MaxArchiveBytes {
return closeWithError(fmt.Errorf(
"%w: got %d, limit %d",
ErrArchiveTooLarge,
size,
extractor.limits.MaxArchiveBytes,
))
}
if err := scanCentralDirectory(file, size, extractor.limits); err != nil {
return closeWithError(err)
}
return file, size, nil
}
func scanCentralDirectory(file *os.File, size int64, limits Limits) error {
end, err := findEndOfCentralDirectory(file, size)
if err != nil {
return err
}
if requiresZIP64(end) {
if err := readZIP64EndOfCentralDirectory(file, &end); err != nil {
return err
}
}
if end.diskNumber != 0 || end.centralDirectoryDisk != 0 ||
end.entriesOnThisDisk != end.centralDirectoryEntries {
return fmt.Errorf("%w: multi-disk archives are unsupported", ErrInvalidArchive)
}
if end.centralDirectoryEntries > uint64(limits.MaxEntries) {
return fmt.Errorf(
"%w: got %d, limit %d",
ErrTooManyEntries,
end.centralDirectoryEntries,
limits.MaxEntries,
)
}
if end.centralDirectorySize > uint64(limits.MaxCentralDirectoryBytes) {
return fmt.Errorf(
"%w: got %d, limit %d",
ErrCentralDirectoryTooLarge,
end.centralDirectorySize,
limits.MaxCentralDirectoryBytes,
)
}
if end.centralDirectorySize > uint64(maxInt64) ||
end.centralDirectoryOffset > uint64(maxInt64) {
return fmt.Errorf("%w: central directory exceeds int64", ErrInvalidArchive)
}
centralDirectorySize := int64(end.centralDirectorySize)
centralDirectoryOffset := int64(end.centralDirectoryOffset)
if centralDirectorySize > end.offset {
return fmt.Errorf("%w: central directory exceeds end record", ErrInvalidArchive)
}
centralDirectoryStart := end.offset - centralDirectorySize
if centralDirectoryOffset > centralDirectoryStart {
return fmt.Errorf("%w: central directory offset is outside archive", ErrInvalidArchive)
}
baseOffset := centralDirectoryStart - centralDirectoryOffset
if baseOffset < 0 || centralDirectoryStart > size ||
centralDirectoryStart+centralDirectorySize != end.offset {
return fmt.Errorf("%w: central directory bounds are inconsistent", ErrInvalidArchive)
}
return nil
}
func findEndOfCentralDirectory(file *os.File, size int64) (endOfCentralDirectory, error) {
if size < endOfCentralDirectoryLength {
return endOfCentralDirectory{}, fmt.Errorf("%w: archive is shorter than EOCD", ErrInvalidArchive)
}
readLength := int64(maxEOCDSearchLength)
if readLength > size {
readLength = size
}
buffer := make([]byte, int(readLength))
readOffset := size - readLength
if err := readAtExactly(file, buffer, readOffset); err != nil {
return endOfCentralDirectory{}, err
}
for offset := len(buffer) - endOfCentralDirectoryLength; offset >= 0; offset-- {
if binary.LittleEndian.Uint32(buffer[offset:]) != endOfCentralDirectorySignature {
continue
}
commentLength := int(binary.LittleEndian.Uint16(buffer[offset+20:]))
if offset+endOfCentralDirectoryLength+commentLength != len(buffer) {
continue
}
return endOfCentralDirectory{
offset: readOffset + int64(offset),
diskNumber: uint32(binary.LittleEndian.Uint16(buffer[offset+4:])),
centralDirectoryDisk: uint32(binary.LittleEndian.Uint16(buffer[offset+6:])),
entriesOnThisDisk: uint64(binary.LittleEndian.Uint16(buffer[offset+8:])),
centralDirectoryEntries: uint64(binary.LittleEndian.Uint16(buffer[offset+10:])),
centralDirectorySize: uint64(binary.LittleEndian.Uint32(buffer[offset+12:])),
centralDirectoryOffset: uint64(binary.LittleEndian.Uint32(buffer[offset+16:])),
}, nil
}
return endOfCentralDirectory{}, fmt.Errorf("%w: EOCD is missing or malformed", ErrInvalidArchive)
}
func requiresZIP64(end endOfCentralDirectory) bool {
return end.entriesOnThisDisk == 0xffff ||
end.centralDirectoryEntries == 0xffff ||
end.centralDirectorySize == 0xffffffff ||
end.centralDirectoryOffset == 0xffffffff
}
func readZIP64EndOfCentralDirectory(file *os.File, end *endOfCentralDirectory) error {
locatorOffset := end.offset - zip64LocatorLength
if locatorOffset < 0 {
return fmt.Errorf("%w: ZIP64 locator is missing", ErrInvalidArchive)
}
locator := make([]byte, zip64LocatorLength)
if err := readAtExactly(file, locator, locatorOffset); err != nil {
return err
}
if binary.LittleEndian.Uint32(locator) != zip64LocatorSignature ||
binary.LittleEndian.Uint32(locator[4:]) != 0 ||
binary.LittleEndian.Uint32(locator[16:]) != 1 {
return fmt.Errorf("%w: ZIP64 locator is invalid", ErrInvalidArchive)
}
zip64EndOffset, ok := uint64AsInt64(binary.LittleEndian.Uint64(locator[8:]))
if !ok || zip64EndOffset < 0 || zip64EndOffset > locatorOffset-zip64EndLength {
return fmt.Errorf("%w: ZIP64 end offset is invalid", ErrInvalidArchive)
}
zip64End := make([]byte, zip64EndLength)
if err := readAtExactly(file, zip64End, zip64EndOffset); err != nil {
return err
}
if binary.LittleEndian.Uint32(zip64End) != zip64EndSignature {
return fmt.Errorf("%w: ZIP64 end record is invalid", ErrInvalidArchive)
}
recordSize := binary.LittleEndian.Uint64(zip64End[4:])
if recordSize < 44 || recordSize > uint64(locatorOffset-zip64EndOffset-12) {
return fmt.Errorf("%w: ZIP64 end record length is invalid", ErrInvalidArchive)
}
end.offset = zip64EndOffset
end.diskNumber = binary.LittleEndian.Uint32(zip64End[16:])
end.centralDirectoryDisk = binary.LittleEndian.Uint32(zip64End[20:])
end.entriesOnThisDisk = binary.LittleEndian.Uint64(zip64End[24:])
end.centralDirectoryEntries = binary.LittleEndian.Uint64(zip64End[32:])
end.centralDirectorySize = binary.LittleEndian.Uint64(zip64End[40:])
end.centralDirectoryOffset = binary.LittleEndian.Uint64(zip64End[48:])
return nil
}
func readAtExactly(file *os.File, buffer []byte, offset int64) error {
count, err := file.ReadAt(buffer, offset)
if err == nil && count == len(buffer) {
return nil
}
if err == nil {
err = io.ErrUnexpectedEOF
}
return fmt.Errorf("%w: read ZIP metadata: %v", ErrInvalidArchive, err)
}
func uint64AsInt64(value uint64) (int64, bool) {
if value > uint64(maxInt64) {
return 0, false
}
return int64(value), true
}