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) } }