301 lines
8.6 KiB
Go
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)
|
|
}
|
|
}
|