package installer import ( "bytes" "crypto/sha256" "encoding/hex" "errors" "os" "path/filepath" "testing" ) func TestExtractorExtractVerifiedFile(t *testing.T) { archivePath := writeTestZIP(t, []testZIPEntry{ {name: "app.json", body: validAppManifest("1.2.3", "bin/App.exe")}, {name: "payload/bin/App.exe", body: []byte("executable"), mode: 0o755}, {name: "payload/readme.txt", body: []byte("hello")}, }) destination := filepath.Join(t.TempDir(), "staging") extractor := mustExtractor(t, testLimits()) result, err := extractor.ExtractVerifiedFile( archivePath, destination, verifiedExpectation(t, archivePath, "1.2.3", "bin/App.exe"), ) if err != nil { t.Fatalf("ExtractVerifiedFile() error = %v", err) } if result.Files != 2 || len(result.PayloadFiles) != 2 { t.Fatalf("files = %d, payload files = %d, want 2", result.Files, len(result.PayloadFiles)) } if result.PayloadFiles[0].Path != "bin/App.exe" || result.PayloadFiles[0].Size != int64(len("executable")) { t.Fatalf("first payload file = %#v", result.PayloadFiles[0]) } wantHash := sha256.Sum256([]byte("executable")) if result.PayloadFiles[0].SHA256 != hex.EncodeToString(wantHash[:]) { t.Fatalf("first payload hash = %q", result.PayloadFiles[0].SHA256) } if _, err := os.Stat(result.EntrypointPath); err != nil { t.Fatalf("entrypoint stat error = %v", err) } } func TestExtractorExtractVerifiedFileWithCheckPreflightsBeforeStaging(t *testing.T) { archivePath := writeTestZIP(t, []testZIPEntry{ {name: "app.json", body: validAppManifest("1.2.3", "bin/App.exe")}, {name: "payload/bin/App.exe", body: []byte("executable"), mode: 0o755}, {name: "payload/readme.txt", body: []byte("hello")}, }) destination := filepath.Join(t.TempDir(), "staging") extractor := mustExtractor(t, testLimits()) stop := errors.New("pre-extract check stopped") _, err := extractor.ExtractVerifiedFileWithCheck( archivePath, destination, verifiedExpectation(t, archivePath, "1.2.3", "bin/App.exe"), func(verified VerifiedPackage) error { if verified.Entrypoint != "bin/App.exe" { t.Fatalf("entrypoint = %q", verified.Entrypoint) } if verified.PayloadFiles != 2 { t.Fatalf("payload files = %d, want 2", verified.PayloadFiles) } if verified.PayloadBytes != int64(len("executable")+len("hello")) { t.Fatalf("payload bytes = %d", verified.PayloadBytes) } return stop }, ) if !errors.Is(err, stop) { t.Fatalf("ExtractVerifiedFileWithCheck() error = %v, want %v", err, stop) } var packageErr *PackageError if !errors.As(err, &packageErr) || packageErr.Stage != PackageStagePreflight { t.Fatalf("package error = %#v, want preflight stage", packageErr) } if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) { t.Fatalf("preflight failure left staging, stat error = %v", statErr) } } func TestExtractorExtractVerifiedFileRejectsBeforeStaging(t *testing.T) { archivePath := writeTestZIP(t, []testZIPEntry{ {name: "app.json", body: validAppManifest("1.2.3", "App.exe")}, {name: "payload/App.exe", body: []byte("executable"), mode: 0o755}, }) tests := []struct { name string modify func(PackageExpectation) PackageExpectation wantErr error stage PackageStage }{ { name: "hash mismatch", modify: func(expectation PackageExpectation) PackageExpectation { expectation.SHA256 = "0000000000000000000000000000000000000000000000000000000000000000" return expectation }, wantErr: ErrPackageHashMismatch, stage: PackageStageVerify, }, { name: "app manifest identity mismatch", modify: func(expectation PackageExpectation) PackageExpectation { expectation.App.Version = "9.9.9" return expectation }, wantErr: ErrPackageIdentityMismatch, stage: PackageStageManifest, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { destination := filepath.Join(t.TempDir(), "staging") extractor := mustExtractor(t, testLimits()) expectation := test.modify(verifiedExpectation(t, archivePath, "1.2.3", "App.exe")) _, err := extractor.ExtractVerifiedFile(archivePath, destination, expectation) if !errors.Is(err, test.wantErr) { t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, test.wantErr) } var packageErr *PackageError if !errors.As(err, &packageErr) || packageErr.Stage != test.stage { t.Fatalf("package error = %#v, want stage %q", packageErr, test.stage) } if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) { t.Fatalf("rejected package left staging, stat error = %v", statErr) } }) } } func TestExtractorExtractVerifiedFileRejectsStrictAppManifest(t *testing.T) { manifest := validAppManifest("1.2.3", "App.exe") archivePath := writeTestZIP(t, []testZIPEntry{ {name: "app.json", body: append(manifest[:len(manifest)-1], []byte(`,"unexpected":true}`)...)}, {name: "payload/App.exe", body: []byte("executable"), mode: 0o755}, }) destination := filepath.Join(t.TempDir(), "staging") extractor := mustExtractor(t, testLimits()) _, err := extractor.ExtractVerifiedFile( archivePath, destination, verifiedExpectation(t, archivePath, "1.2.3", "App.exe"), ) if !errors.Is(err, ErrAppManifestInvalid) { t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, ErrAppManifestInvalid) } if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) { t.Fatalf("invalid manifest left staging, stat error = %v", statErr) } } func TestExtractorExtractVerifiedFileRejectsNonRegularDownload(t *testing.T) { destination := filepath.Join(t.TempDir(), "staging") extractor := mustExtractor(t, testLimits()) _, err := extractor.ExtractVerifiedFile( t.TempDir(), destination, PackageExpectation{ Size: 1, SHA256: "0000000000000000000000000000000000000000000000000000000000000000", App: AppExpectation{ ID: "test-app", Version: "1.2.3", Channel: "stable", MinOS: "windows-10", Architecture: "amd64", Entrypoint: "App.exe", }, }, ) if !errors.Is(err, ErrInvalidArchive) { t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, ErrInvalidArchive) } if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) { t.Fatalf("non-regular download left staging, stat error = %v", statErr) } } func TestExtractorExtractVerifiedFileBoundsAppManifest(t *testing.T) { manifest := append(validAppManifest("1.2.3", "App.exe"), bytes.Repeat([]byte(" "), MaxAppManifestBytes)...) archivePath := writeTestZIP(t, []testZIPEntry{ {name: "app.json", body: manifest}, {name: "payload/App.exe", body: []byte("executable"), mode: 0o755}, }) destination := filepath.Join(t.TempDir(), "staging") extractor := mustExtractor(t, Limits{ MaxEntries: 20, MaxArchiveBytes: 2 * MaxAppManifestBytes, MaxCentralDirectoryBytes: 4 * 1024, MaxUncompressedBytes: 2 * MaxAppManifestBytes, MaxCompressionRatio: 100, }) _, err := extractor.ExtractVerifiedFile( archivePath, destination, verifiedExpectation(t, archivePath, "1.2.3", "App.exe"), ) if !errors.Is(err, ErrAppManifestTooLarge) { t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, ErrAppManifestTooLarge) } var packageErr *PackageError if !errors.As(err, &packageErr) || packageErr.Stage != PackageStageManifest { t.Fatalf("package error = %#v, want manifest stage", packageErr) } if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) { t.Fatalf("oversized manifest left staging, stat error = %v", statErr) } } func verifiedExpectation(t *testing.T, archivePath, version, entrypoint string) PackageExpectation { t.Helper() document, err := os.ReadFile(archivePath) if err != nil { t.Fatalf("read archive: %v", err) } hash := sha256.Sum256(document) return PackageExpectation{ Size: int64(len(document)), SHA256: hex.EncodeToString(hash[:]), App: AppExpectation{ ID: "test-app", Version: version, Channel: "stable", MinOS: "windows-10", Architecture: "amd64", Entrypoint: entrypoint, RequiresAdmin: false, }, } } func validAppManifest(version, entrypoint string) []byte { return []byte(`{"schema_version":1,"id":"test-app","name":"Test App","vendor":"SoftBox","version":"` + version + `","channel":"stable","min_os":"windows-10","architecture":"amd64","entrypoint":"` + entrypoint + `","working_directory":".","product_id":"test-product","supports_trial":false,"requires_admin":false,"data_policy":"local-app-data","update_policy":"managed-by-softbox"}`) }