Files
soft_quay/core/installer/zipscan_test.go
T

301 lines
8.6 KiB
Go

package installer
import (
"encoding/binary"
"errors"
"os"
"path/filepath"
"testing"
)
func TestExtractorRejectsArchiveSizeBeforeZIPReader(t *testing.T) {
archivePath := writeTestZIP(t, []testZIPEntry{
{name: "app.json", body: []byte(`{}`)},
{name: "payload/App.exe", body: []byte("ok")},
})
size := archiveSize(t, archivePath)
tests := []struct {
name string
limits Limits
expected int64
wantErr error
}{
{
name: "expected size mismatch",
limits: testLimits(),
expected: size + 1,
wantErr: ErrArchiveSizeMismatch,
},
{
name: "expected size is unknown",
limits: testLimits(),
expected: 0,
wantErr: ErrArchiveSizeMismatch,
},
{
name: "archive exceeds raw size limit",
limits: Limits{
MaxEntries: 20,
MaxArchiveBytes: size - 1,
MaxCentralDirectoryBytes: size - 1,
MaxUncompressedBytes: 16 * 1024,
MaxCompressionRatio: 100,
},
expected: size,
wantErr: ErrArchiveTooLarge,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
destination := filepath.Join(t.TempDir(), "staging")
extractor := mustExtractor(t, test.limits)
_, err := extractor.ExtractFile(
archivePath,
destination,
"App.exe",
test.expected,
)
if !errors.Is(err, test.wantErr) {
t.Fatalf("ExtractFile() error = %v, want %v", err, test.wantErr)
}
assertStagingAbsent(t, destination)
})
}
}
func TestExtractorRejectsCentralDirectoryMetadataBeforeZIPReader(t *testing.T) {
tests := []struct {
name string
mutate func(t *testing.T, data []byte, eocdOffset int)
want error
}{
{
name: "declared entries exceed limit",
mutate: func(t *testing.T, data []byte, eocdOffset int) {
t.Helper()
binary.LittleEndian.PutUint16(data[eocdOffset+8:], 21)
binary.LittleEndian.PutUint16(data[eocdOffset+10:], 21)
},
want: ErrTooManyEntries,
},
{
name: "central directory exceeds limit",
mutate: func(t *testing.T, data []byte, eocdOffset int) {
t.Helper()
binary.LittleEndian.PutUint32(data[eocdOffset+12:], 1025)
},
want: ErrCentralDirectoryTooLarge,
},
{
name: "multi disk archive",
mutate: func(t *testing.T, data []byte, eocdOffset int) {
t.Helper()
binary.LittleEndian.PutUint16(data[eocdOffset+4:], 1)
},
want: ErrInvalidArchive,
},
{
name: "central directory offset is outside archive",
mutate: func(t *testing.T, data []byte, eocdOffset int) {
t.Helper()
binary.LittleEndian.PutUint32(data[eocdOffset+16:], 0xfffffffe)
},
want: ErrInvalidArchive,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archivePath := writeTestZIP(t, []testZIPEntry{
{name: "app.json", body: []byte(`{}`)},
{name: "payload/App.exe", body: []byte("ok")},
})
data, err := os.ReadFile(archivePath)
if err != nil {
t.Fatalf("read archive: %v", err)
}
eocdOffset := len(data) - endOfCentralDirectoryLength
test.mutate(t, data, eocdOffset)
if err := os.WriteFile(archivePath, data, 0o600); err != nil {
t.Fatalf("write archive: %v", err)
}
destination := filepath.Join(t.TempDir(), "staging")
extractor := mustExtractor(t, testLimits())
_, err = extractor.ExtractFile(
archivePath,
destination,
"App.exe",
archiveSize(t, archivePath),
)
if !errors.Is(err, test.want) {
t.Fatalf("ExtractFile() error = %v, want %v", err, test.want)
}
assertStagingAbsent(t, destination)
})
}
}
func TestExtractorRejectsTruncatedEOCD(t *testing.T) {
archivePath := writeTestZIP(t, []testZIPEntry{
{name: "app.json", body: []byte(`{}`)},
{name: "payload/App.exe", body: []byte("ok")},
})
if err := os.Truncate(archivePath, archiveSize(t, archivePath)-1); err != nil {
t.Fatalf("truncate archive: %v", err)
}
destination := filepath.Join(t.TempDir(), "staging")
extractor := mustExtractor(t, testLimits())
_, err := extractor.ExtractFile(
archivePath,
destination,
"App.exe",
archiveSize(t, archivePath),
)
if !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("ExtractFile() error = %v, want %v", err, ErrInvalidArchive)
}
assertStagingAbsent(t, destination)
}
func TestExtractorRejectsMissingOrInvalidZIP64End(t *testing.T) {
tests := []struct {
name string
mutate func(data []byte)
}{
{
name: "missing locator",
mutate: func(data []byte) {
eocdOffset := len(data) - endOfCentralDirectoryLength
data[eocdOffset-zip64LocatorLength] ^= 0xff
},
},
{
name: "invalid record length",
mutate: func(data []byte) {
eocdOffset := len(data) - endOfCentralDirectoryLength
zip64EndOffset := int(binary.LittleEndian.Uint64(data[eocdOffset-zip64LocatorLength+8:]))
binary.LittleEndian.PutUint64(data[zip64EndOffset+4:], 43)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archivePath := writeZIP64TestZIP(t)
data, err := os.ReadFile(archivePath)
if err != nil {
t.Fatalf("read archive: %v", err)
}
test.mutate(data)
if err := os.WriteFile(archivePath, data, 0o600); err != nil {
t.Fatalf("write archive: %v", err)
}
destination := filepath.Join(t.TempDir(), "staging")
extractor := mustExtractor(t, testLimits())
_, err = extractor.ExtractFile(
archivePath,
destination,
"App.exe",
archiveSize(t, archivePath),
)
if !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("ExtractFile() error = %v, want %v", err, ErrInvalidArchive)
}
assertStagingAbsent(t, destination)
})
}
}
func TestExtractorAcceptsZIP64EndOfCentralDirectory(t *testing.T) {
archivePath := writeZIP64TestZIP(t)
destination := filepath.Join(t.TempDir(), "staging")
extractor := mustExtractor(t, testLimits())
result, err := extractor.ExtractFile(
archivePath,
destination,
"App.exe",
archiveSize(t, archivePath),
)
if err != nil {
t.Fatalf("ExtractFile() error = %v", err)
}
if result.Files != 1 {
t.Fatalf("Files = %d, want 1", result.Files)
}
}
func TestExtractorRejectsNonRegularArchive(t *testing.T) {
directory := t.TempDir()
destination := filepath.Join(t.TempDir(), "staging")
extractor := mustExtractor(t, testLimits())
_, err := extractor.ExtractFile(directory, destination, "App.exe", 1)
if !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("ExtractFile() error = %v, want %v", err, ErrInvalidArchive)
}
assertStagingAbsent(t, destination)
}
func writeZIP64TestZIP(t *testing.T) string {
t.Helper()
archivePath := writeTestZIP(t, []testZIPEntry{
{name: "app.json", body: []byte(`{}`)},
{name: "payload/App.exe", body: []byte("ok")},
})
data, err := os.ReadFile(archivePath)
if err != nil {
t.Fatalf("read ZIP: %v", err)
}
eocdOffset := len(data) - endOfCentralDirectoryLength
entries := binary.LittleEndian.Uint16(data[eocdOffset+10:])
centralDirectorySize := binary.LittleEndian.Uint32(data[eocdOffset+12:])
centralDirectoryOffset := binary.LittleEndian.Uint32(data[eocdOffset+16:])
zip64EndOffset := len(data) - endOfCentralDirectoryLength
zip64End := make([]byte, zip64EndLength)
binary.LittleEndian.PutUint32(zip64End, zip64EndSignature)
binary.LittleEndian.PutUint64(zip64End[4:], 44)
binary.LittleEndian.PutUint16(zip64End[12:], 45)
binary.LittleEndian.PutUint16(zip64End[14:], 45)
binary.LittleEndian.PutUint64(zip64End[24:], uint64(entries))
binary.LittleEndian.PutUint64(zip64End[32:], uint64(entries))
binary.LittleEndian.PutUint64(zip64End[40:], uint64(centralDirectorySize))
binary.LittleEndian.PutUint64(zip64End[48:], uint64(centralDirectoryOffset))
locator := make([]byte, zip64LocatorLength)
binary.LittleEndian.PutUint32(locator, zip64LocatorSignature)
binary.LittleEndian.PutUint64(locator[8:], uint64(zip64EndOffset))
binary.LittleEndian.PutUint32(locator[16:], 1)
classicEnd := make([]byte, endOfCentralDirectoryLength)
binary.LittleEndian.PutUint32(classicEnd, endOfCentralDirectorySignature)
binary.LittleEndian.PutUint16(classicEnd[8:], 0xffff)
binary.LittleEndian.PutUint16(classicEnd[10:], 0xffff)
binary.LittleEndian.PutUint32(classicEnd[12:], 0xffffffff)
binary.LittleEndian.PutUint32(classicEnd[16:], 0xffffffff)
zip64Data := make([]byte, 0, len(data)+zip64EndLength+zip64LocatorLength)
zip64Data = append(zip64Data, data[:eocdOffset]...)
zip64Data = append(zip64Data, zip64End...)
zip64Data = append(zip64Data, locator...)
zip64Data = append(zip64Data, classicEnd...)
if err := os.WriteFile(archivePath, zip64Data, 0o600); err != nil {
t.Fatalf("write ZIP64 archive: %v", err)
}
return archivePath
}
func assertStagingAbsent(t *testing.T, destination string) {
t.Helper()
if _, err := os.Stat(destination); !os.IsNotExist(err) {
t.Fatalf("rejected archive left staging, stat error = %v", err)
}
}