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