package installer import ( "context" "encoding/base64" "os" "path/filepath" "runtime" "strings" "testing" "github.com/bodgit/sevenzip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // A solid LZMA2 archive produced by 7-Zip 26.02. It contains: // // README.txt -> "fixture-config\n" // bin/mpv.exe -> "fixture-mpv\n" const sevenZipFixtureBase64 = "N3q8ryccAATCrNmCmwAAAAAAAAAhAAAAAAAAANNKzZUBABpmaXh0dXJlLWNvbmZpZwpmaXh0dXJlLW1wdgoAAACBMweuD8/88GwP6+qcvzY9/noN/jZNUuljkiFK88/5Gm3MACIiyKvpTCOK2ryJlI+PPaATo9XAkKngYn9BxPwKddtrS+sjD272thAiGU2Q+JJNI2FcJUibikCdv7DEilrvfWolKt6WG02okncPd+sXRtG5FrutS9IAABcGHwEJfAAHCwEAASMDAQEFXQAQAAAMgJoKAccf3SoAAA==" const ( sevenZipRegularAttributes = uint32(0100644) << 16 sevenZipDirectoryAttrs = uint32(0040755) << 16 sevenZipSymlinkAttributes = uint32(0120777) << 16 ) func writeSevenZipFixture(t *testing.T) string { t.Helper() data, err := base64.StdEncoding.DecodeString(sevenZipFixtureBase64) require.NoError(t, err) archivePath := filepath.Join(t.TempDir(), "fixture.7z") require.NoError(t, os.WriteFile(archivePath, data, 0600)) return archivePath } func sevenZipTestFile(name string, size uint64) *sevenzip.File { return &sevenzip.File{FileHeader: sevenzip.FileHeader{ Name: name, Attributes: sevenZipRegularAttributes, UncompressedSize: size, }} } func TestExtractSevenZip(t *testing.T) { t.Run("extracts a solid LZMA2 archive without an external command", func(t *testing.T) { archivePath := writeSevenZipFixture(t) destination := filepath.Join(t.TempDir(), "out") me := NewMockCommandExecutor() installer := NewInstaller(ReleaseInfo{}, "", WithExecutor(me)) output := make(chan string, 16) cr := NewCommandRunner(output, nil) require.NoError(t, installer.ExtractArchiveWithOutput(cr, archivePath, destination)) assert.Empty(t, me.GetCommands()) readme, err := os.ReadFile(filepath.Join(destination, "README.txt")) require.NoError(t, err) assert.Equal(t, "fixture-config\n", string(readme)) mpv, err := os.ReadFile(filepath.Join(destination, "bin", "mpv.exe")) require.NoError(t, err) assert.Equal(t, "fixture-mpv\n", string(mpv)) }) t.Run("replaces an existing regular file", func(t *testing.T) { archivePath := writeSevenZipFixture(t) destination := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(destination, "README.txt"), []byte("old"), 0600)) require.NoError(t, extractSevenZip(context.Background(), archivePath, destination)) content, err := os.ReadFile(filepath.Join(destination, "README.txt")) require.NoError(t, err) assert.Equal(t, "fixture-config\n", string(content)) }) t.Run("honors a cancelled context before writing", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() destination := filepath.Join(t.TempDir(), "out") err := extractSevenZip(ctx, writeSevenZipFixture(t), destination) require.Error(t, err) assert.ErrorIs(t, err, context.Canceled) _, statErr := os.Stat(destination) assert.ErrorIs(t, statErr, os.ErrNotExist) }) t.Run("refuses a destination symlink", func(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("creating symlinks may require elevated privileges on Windows") } archivePath := writeSevenZipFixture(t) destination := t.TempDir() outside := t.TempDir() require.NoError(t, os.Symlink(outside, filepath.Join(destination, "bin"))) err := extractSevenZip(context.Background(), archivePath, destination) require.Error(t, err) assert.Contains(t, err.Error(), "unsupported type") _, statErr := os.Stat(filepath.Join(outside, "mpv.exe")) assert.ErrorIs(t, statErr, os.ErrNotExist) }) } // TestSevenZipReleaseCompatibility can be enabled by release automation after // it downloads the current upstream assets. Keeping network access out of the // normal unit suite makes tests deterministic while preserving a direct // compatibility gate for zhongfly's compression settings. func TestSevenZipReleaseCompatibility(t *testing.T) { tests := []struct { name string environment string executable string }{ {name: "MPV", environment: "MPV_MANAGER_TEST_MPV_7Z", executable: "mpv.exe"}, {name: "FFmpeg", environment: "MPV_MANAGER_TEST_FFMPEG_7Z", executable: "ffmpeg.exe"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { archivePath := os.Getenv(test.environment) if archivePath == "" { t.Skipf("set %s to test a current upstream archive", test.environment) } destination := t.TempDir() require.NoError(t, extractSevenZip(context.Background(), archivePath, destination)) _, err := os.Stat(filepath.Join(destination, test.executable)) require.NoErrorf(t, err, "%s was not extracted from %s", test.executable, archivePath) }) } } func TestValidateSevenZipEntries(t *testing.T) { t.Run("accepts portable regular files and directories", func(t *testing.T) { files := []*sevenzip.File{ {FileHeader: sevenzip.FileHeader{Name: "bin/", Attributes: sevenZipDirectoryAttrs}}, sevenZipTestFile("bin/mpv.exe", 100), } entries, err := validateSevenZipEntries(files) require.NoError(t, err) require.Len(t, entries, 2) assert.True(t, entries[0].isDir) assert.Equal(t, filepath.Join("bin", "mpv.exe"), entries[1].localName) }) unsafeNames := []string{ "../outside.exe", "/absolute.exe", `dir\..\outside.exe`, "C:/outside.exe", "NUL.txt", "LPT9.log", "COM¹.txt", "dir/bad?.exe", "dir/trailing.", "dir//file.exe", } for _, name := range unsafeNames { t.Run("rejects "+strings.ReplaceAll(name, "/", "_"), func(t *testing.T) { _, err := validateSevenZipEntries([]*sevenzip.File{sevenZipTestFile(name, 1)}) require.Error(t, err) }) } t.Run("rejects symlinks and other special files", func(t *testing.T) { files := []*sevenzip.File{{FileHeader: sevenzip.FileHeader{ Name: "link", Attributes: sevenZipSymlinkAttributes, }}} _, err := validateSevenZipEntries(files) require.Error(t, err) assert.Contains(t, err.Error(), "unsupported special file type") }) t.Run("rejects duplicate and case-colliding names", func(t *testing.T) { files := []*sevenzip.File{ sevenZipTestFile("MPV.exe", 1), sevenZipTestFile("mpv.EXE", 1), } _, err := validateSevenZipEntries(files) require.Error(t, err) assert.Contains(t, err.Error(), "collide on Windows") }) t.Run("rejects a regular file used as a parent directory", func(t *testing.T) { files := []*sevenzip.File{ sevenZipTestFile("bin", 1), sevenZipTestFile("bin/mpv.exe", 1), } _, err := validateSevenZipEntries(files) require.Error(t, err) assert.Contains(t, err.Error(), "as a directory") }) t.Run("rejects excessive expanded size", func(t *testing.T) { _, err := validateSevenZipEntries([]*sevenzip.File{ sevenZipTestFile("huge.bin", maxSevenZipExpandedSize+1), }) require.Error(t, err) assert.Contains(t, err.Error(), "expands beyond") }) t.Run("rejects excessive entry count", func(t *testing.T) { files := make([]*sevenzip.File, maxSevenZipEntries+1) _, err := validateSevenZipEntries(files) require.Error(t, err) assert.Contains(t, err.Error(), "too many entries") }) } func TestRequireSevenZipHash(t *testing.T) { valid := "blake3:" + strings.Repeat("a", 64) assert.NoError(t, requireSevenZipHash(valid, "mpv.7z")) for _, hash := range []string{"", "blake3:HASH", "sha256:" + strings.Repeat("a", 64), "blake3:xyz"} { err := requireSevenZipHash(hash, "mpv.7z") require.Error(t, err) } }