github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/internal/file/zip_file_traversal_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package file 5 6 import ( 7 "crypto/sha256" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "path" 14 "path/filepath" 15 "strings" 16 "testing" 17 18 "github.com/go-test/deep" 19 "github.com/stretchr/testify/assert" 20 ) 21 22 func equal(r1, r2 io.Reader) (bool, error) { 23 w1 := sha256.New() 24 w2 := sha256.New() 25 n1, err1 := io.Copy(w1, r1) 26 if err1 != nil { 27 return false, err1 28 } 29 n2, err2 := io.Copy(w2, r2) 30 if err2 != nil { 31 return false, err2 32 } 33 34 var b1, b2 [sha256.Size]byte 35 copy(b1[:], w1.Sum(nil)) 36 copy(b2[:], w2.Sum(nil)) 37 38 return n1 != n2 || b1 == b2, nil 39 } 40 41 func TestUnzipToDir(t *testing.T) { 42 cwd, err := os.Getwd() 43 if err != nil { 44 t.Fatal(err) 45 } 46 47 goldenRootDir := filepath.Join(cwd, "test-fixtures") 48 sourceDirPath := path.Join(goldenRootDir, "zip-source") 49 archiveFilePath := setupZipFileTest(t, sourceDirPath, false) 50 51 unzipDestinationDir := t.TempDir() 52 53 t.Logf("content path: %s", unzipDestinationDir) 54 55 expectedPaths := len(expectedZipArchiveEntries) 56 observedPaths := 0 57 58 err = UnzipToDir(archiveFilePath, unzipDestinationDir) 59 if err != nil { 60 t.Fatalf("unable to unzip archive: %+v", err) 61 } 62 63 // compare the source dir tree and the unzipped tree 64 err = filepath.Walk(unzipDestinationDir, 65 func(path string, info os.FileInfo, err error) error { 66 // We don't unzip the root archive dir, since there's no archive entry for it 67 if path != unzipDestinationDir { 68 t.Logf("unzipped path: %s", path) 69 observedPaths++ 70 } 71 72 if err != nil { 73 t.Fatalf("this should not happen") 74 return err 75 } 76 77 goldenPath := filepath.Join(sourceDirPath, strings.TrimPrefix(path, unzipDestinationDir)) 78 79 if info.IsDir() { 80 i, err := os.Stat(goldenPath) 81 if err != nil { 82 t.Fatalf("unable to stat golden path: %+v", err) 83 } 84 if !i.IsDir() { 85 t.Fatalf("mismatched file types: %s", goldenPath) 86 } 87 return nil 88 } 89 90 // this is a file, not a dir... 91 92 testFile, err := os.Open(path) 93 if err != nil { 94 t.Fatalf("unable to open test file=%s :%+v", path, err) 95 } 96 97 goldenFile, err := os.Open(goldenPath) 98 if err != nil { 99 t.Fatalf("unable to open golden file=%s :%+v", goldenPath, err) 100 } 101 102 same, err := equal(testFile, goldenFile) 103 if err != nil { 104 t.Fatalf("could not compare files (%s, %s): %+v", goldenPath, path, err) 105 } 106 107 if !same { 108 t.Errorf("paths are not the same (%s, %s)", goldenPath, path) 109 } 110 111 return nil 112 }) 113 114 if err != nil { 115 t.Errorf("failed to walk dir: %+v", err) 116 } 117 118 if observedPaths != expectedPaths { 119 t.Errorf("missed test paths: %d != %d", observedPaths, expectedPaths) 120 } 121 } 122 123 func TestContentsFromZip(t *testing.T) { 124 tests := []struct { 125 name string 126 archivePrep func(tb testing.TB) string 127 }{ 128 { 129 name: "standard, non-nested zip", 130 archivePrep: prepZipSourceFixture, 131 }, 132 { 133 name: "zip with prepended bytes", 134 archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."), 135 }, 136 } 137 138 for _, test := range tests { 139 t.Run(test.name, func(t *testing.T) { 140 archivePath := test.archivePrep(t) 141 expected := zipSourceFixtureExpectedContents() 142 143 var paths []string 144 for p := range expected { 145 paths = append(paths, p) 146 } 147 148 actual, err := ContentsFromZip(archivePath, paths...) 149 if err != nil { 150 t.Fatalf("unable to extract from unzip archive: %+v", err) 151 } 152 153 assertZipSourceFixtureContents(t, actual, expected) 154 }) 155 } 156 } 157 158 func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string { 159 if len(value) == 0 { 160 tb.Fatalf("no bytes given to prefix") 161 } 162 return func(t testing.TB) string { 163 archivePath := prepZipSourceFixture(t) 164 165 // create a temp file 166 tmpFile, err := os.CreateTemp(tb.TempDir(), "syft-ziputil-prependZipSourceFixtureWithString-") 167 if err != nil { 168 t.Fatalf("unable to create tempfile: %+v", err) 169 } 170 defer tmpFile.Close() 171 172 // write value to the temp file 173 if _, err := tmpFile.WriteString(value); err != nil { 174 t.Fatalf("unable to write to tempfile: %+v", err) 175 } 176 177 // open the original archive 178 sourceFile, err := os.Open(archivePath) 179 if err != nil { 180 t.Fatalf("unable to read source file: %+v", err) 181 } 182 183 // copy all contents from the archive to the temp file 184 if _, err := io.Copy(tmpFile, sourceFile); err != nil { 185 t.Fatalf("unable to copy source to dest: %+v", err) 186 } 187 188 sourceFile.Close() 189 190 // remove the original archive and replace it with the temp file 191 if err := os.Remove(archivePath); err != nil { 192 t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err) 193 } 194 195 if err := os.Rename(tmpFile.Name(), archivePath); err != nil { 196 t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err) 197 } 198 199 return archivePath 200 } 201 } 202 203 func prepZipSourceFixture(t testing.TB) string { 204 t.Helper() 205 archivePrefix := path.Join(t.TempDir(), "syft-ziputil-prepZipSourceFixture-") 206 207 // the zip utility will add ".zip" to the end of the given name 208 archivePath := archivePrefix + ".zip" 209 210 t.Logf("archive path: %s", archivePath) 211 212 createZipArchive(t, "zip-source", archivePrefix, false) 213 214 return archivePath 215 } 216 217 func zipSourceFixtureExpectedContents() map[string]string { 218 return map[string]string{ 219 filepath.Join("some-dir", "a-file.txt"): "A file! nice!", 220 filepath.Join("b-file.txt"): "B file...", 221 } 222 } 223 224 func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) { 225 t.Helper() 226 diffs := deep.Equal(actual, expected) 227 if len(diffs) > 0 { 228 for _, d := range diffs { 229 t.Errorf("diff: %+v", d) 230 } 231 232 b, err := json.MarshalIndent(actual, "", " ") 233 if err != nil { 234 t.Fatalf("can't show results: %+v", err) 235 } 236 237 t.Errorf("full result: %s", string(b)) 238 } 239 } 240 241 // looks like there isn't a helper for this yet? https://github.com/stretchr/testify/issues/497 242 func assertErrorAs(expectedErr interface{}) assert.ErrorAssertionFunc { 243 return func(t assert.TestingT, actualErr error, i ...interface{}) bool { 244 return errors.As(actualErr, &expectedErr) 245 } 246 } 247 248 func TestSafeJoin(t *testing.T) { 249 tests := []struct { 250 prefix string 251 args []string 252 expected string 253 errAssertion assert.ErrorAssertionFunc 254 }{ 255 // go cases... 256 { 257 prefix: "/a/place", 258 args: []string{ 259 "somewhere/else", 260 }, 261 expected: "/a/place/somewhere/else", 262 errAssertion: assert.NoError, 263 }, 264 { 265 prefix: "/a/place", 266 args: []string{ 267 "somewhere/../else", 268 }, 269 expected: "/a/place/else", 270 errAssertion: assert.NoError, 271 }, 272 { 273 prefix: "/a/../place", 274 args: []string{ 275 "somewhere/else", 276 }, 277 expected: "/place/somewhere/else", 278 errAssertion: assert.NoError, 279 }, 280 // zip slip examples.... 281 { 282 prefix: "/a/place", 283 args: []string{ 284 "../../../etc/passwd", 285 }, 286 expected: "", 287 errAssertion: assertErrorAs(&errZipSlipDetected{}), 288 }, 289 { 290 prefix: "/a/place", 291 args: []string{ 292 "../", 293 "../", 294 }, 295 expected: "", 296 errAssertion: assertErrorAs(&errZipSlipDetected{}), 297 }, 298 { 299 prefix: "/a/place", 300 args: []string{ 301 "../", 302 }, 303 expected: "", 304 errAssertion: assertErrorAs(&errZipSlipDetected{}), 305 }, 306 } 307 308 for _, test := range tests { 309 t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) { 310 actual, err := safeJoin(test.prefix, test.args...) 311 test.errAssertion(t, err) 312 assert.Equal(t, test.expected, actual) 313 }) 314 } 315 }