241 lines
8.0 KiB
Go
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
|
|
}
|