//go:build !windows // +build !windows package file import ( "crypto/sha256" "encoding/json" "errors" "fmt" "io" "os" "path" "path/filepath" "strings" "testing" "github.com/go-test/deep" "github.com/stretchr/testify/assert" ) func equal(r1, r2 io.Reader) (bool, error) { w1 := sha256.New() w2 := sha256.New() n1, err1 := io.Copy(w1, r1) if err1 != nil { return false, err1 } n2, err2 := io.Copy(w2, r2) if err2 != nil { return false, err2 } var b1, b2 [sha256.Size]byte copy(b1[:], w1.Sum(nil)) copy(b2[:], w2.Sum(nil)) return n1 != n2 || b1 == b2, nil } func TestUnzipToDir(t *testing.T) { cwd, err := os.Getwd() if err != nil { t.Fatal(err) } goldenRootDir := filepath.Join(cwd, "test-fixtures") sourceDirPath := path.Join(goldenRootDir, "zip-source") archiveFilePath := setupZipFileTest(t, sourceDirPath, false) unzipDestinationDir := t.TempDir() t.Logf("content path: %s", unzipDestinationDir) expectedPaths := len(expectedZipArchiveEntries) observedPaths := 0 err = UnzipToDir(archiveFilePath, unzipDestinationDir) if err != nil { t.Fatalf("unable to unzip archive: %+v", err) } // compare the source dir tree and the unzipped tree err = filepath.Walk(unzipDestinationDir, func(path string, info os.FileInfo, err error) error { // We don't unzip the root archive dir, since there's no archive entry for it if path != unzipDestinationDir { t.Logf("unzipped path: %s", path) observedPaths++ } if err != nil { t.Fatalf("this should not happen") return err } goldenPath := filepath.Join(sourceDirPath, strings.TrimPrefix(path, unzipDestinationDir)) if info.IsDir() { i, err := os.Stat(goldenPath) if err != nil { t.Fatalf("unable to stat golden path: %+v", err) } if !i.IsDir() { t.Fatalf("mismatched file types: %s", goldenPath) } return nil } // this is a file, not a dir... testFile, err := os.Open(path) if err != nil { t.Fatalf("unable to open test file=%s :%+v", path, err) } goldenFile, err := os.Open(goldenPath) if err != nil { t.Fatalf("unable to open golden file=%s :%+v", goldenPath, err) } same, err := equal(testFile, goldenFile) if err != nil { t.Fatalf("could not compare files (%s, %s): %+v", goldenPath, path, err) } if !same { t.Errorf("paths are not the same (%s, %s)", goldenPath, path) } return nil }) if err != nil { t.Errorf("failed to walk dir: %+v", err) } if observedPaths != expectedPaths { t.Errorf("missed test paths: %d != %d", observedPaths, expectedPaths) } } func TestContentsFromZip(t *testing.T) { tests := []struct { name string archivePrep func(tb testing.TB) string }{ { name: "standard, non-nested zip", archivePrep: prepZipSourceFixture, }, { name: "zip with prepended bytes", archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { archivePath := test.archivePrep(t) expected := zipSourceFixtureExpectedContents() var paths []string for p := range expected { paths = append(paths, p) } actual, err := ContentsFromZip(archivePath, paths...) if err != nil { t.Fatalf("unable to extract from unzip archive: %+v", err) } assertZipSourceFixtureContents(t, actual, expected) }) } } func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string { if len(value) == 0 { tb.Fatalf("no bytes given to prefix") } return func(t testing.TB) string { archivePath := prepZipSourceFixture(t) // create a temp file tmpFile, err := os.CreateTemp(tb.TempDir(), "syft-ziputil-prependZipSourceFixtureWithString-") if err != nil { t.Fatalf("unable to create tempfile: %+v", err) } defer tmpFile.Close() // write value to the temp file if _, err := tmpFile.WriteString(value); err != nil { t.Fatalf("unable to write to tempfile: %+v", err) } // open the original archive sourceFile, err := os.Open(archivePath) if err != nil { t.Fatalf("unable to read source file: %+v", err) } // copy all contents from the archive to the temp file if _, err := io.Copy(tmpFile, sourceFile); err != nil { t.Fatalf("unable to copy source to dest: %+v", err) } sourceFile.Close() // remove the original archive and replace it with the temp file if err := os.Remove(archivePath); err != nil { t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err) } if err := os.Rename(tmpFile.Name(), archivePath); err != nil { t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err) } return archivePath } } func prepZipSourceFixture(t testing.TB) string { t.Helper() archivePrefix := path.Join(t.TempDir(), "syft-ziputil-prepZipSourceFixture-") // the zip utility will add ".zip" to the end of the given name archivePath := archivePrefix + ".zip" t.Logf("archive path: %s", archivePath) createZipArchive(t, "zip-source", archivePrefix, false) return archivePath } func zipSourceFixtureExpectedContents() map[string]string { return map[string]string{ filepath.Join("some-dir", "a-file.txt"): "A file! nice!", filepath.Join("b-file.txt"): "B file...", } } func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) { t.Helper() diffs := deep.Equal(actual, expected) if len(diffs) > 0 { for _, d := range diffs { t.Errorf("diff: %+v", d) } b, err := json.MarshalIndent(actual, "", " ") if err != nil { t.Fatalf("can't show results: %+v", err) } t.Errorf("full result: %s", string(b)) } } // looks like there isn't a helper for this yet? https://github.com/stretchr/testify/issues/497 func assertErrorAs(expectedErr interface{}) assert.ErrorAssertionFunc { return func(t assert.TestingT, actualErr error, i ...interface{}) bool { return errors.As(actualErr, &expectedErr) } } func TestSafeJoin(t *testing.T) { tests := []struct { prefix string args []string expected string errAssertion assert.ErrorAssertionFunc }{ // go cases... { prefix: "/a/place", args: []string{ "somewhere/else", }, expected: "/a/place/somewhere/else", errAssertion: assert.NoError, }, { prefix: "/a/place", args: []string{ "somewhere/../else", }, expected: "/a/place/else", errAssertion: assert.NoError, }, { prefix: "/a/../place", args: []string{ "somewhere/else", }, expected: "/place/somewhere/else", errAssertion: assert.NoError, }, // zip slip examples.... { prefix: "/a/place", args: []string{ "../../../etc/passwd", }, expected: "", errAssertion: assertErrorAs(&errZipSlipDetected{}), }, { prefix: "/a/place", args: []string{ "../", "../", }, expected: "", errAssertion: assertErrorAs(&errZipSlipDetected{}), }, { prefix: "/a/place", args: []string{ "../", }, expected: "", errAssertion: assertErrorAs(&errZipSlipDetected{}), }, } for _, test := range tests { t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) { actual, err := safeJoin(test.prefix, test.args...) test.errAssertion(t, err) assert.Equal(t, test.expected, actual) }) } }