Preflight ZIP central directory metadata (T-612)
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user