github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/syft/internal/fileresolver/directory_indexer_test.go (about) 1 package fileresolver 2 3 import ( 4 "io/fs" 5 "os" 6 "path" 7 "sort" 8 "strings" 9 "testing" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/scylladb/go-set/strset" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 "github.com/wagoodman/go-progress" 16 17 "github.com/anchore/stereoscope/pkg/file" 18 ) 19 20 type indexerMock struct { 21 observedRoots []string 22 additionalRoots map[string][]string 23 } 24 25 func (m *indexerMock) indexer(s string, _ *progress.Stage) ([]string, error) { 26 m.observedRoots = append(m.observedRoots, s) 27 return m.additionalRoots[s], nil 28 } 29 30 func Test_indexAllRoots(t *testing.T) { 31 tests := []struct { 32 name string 33 root string 34 mock indexerMock 35 expectedRoots []string 36 }{ 37 { 38 name: "no additional roots", 39 root: "a/place", 40 mock: indexerMock{ 41 additionalRoots: make(map[string][]string), 42 }, 43 expectedRoots: []string{ 44 "a/place", 45 }, 46 }, 47 { 48 name: "additional roots from a single call", 49 root: "a/place", 50 mock: indexerMock{ 51 additionalRoots: map[string][]string{ 52 "a/place": { 53 "another/place", 54 "yet-another/place", 55 }, 56 }, 57 }, 58 expectedRoots: []string{ 59 "a/place", 60 "another/place", 61 "yet-another/place", 62 }, 63 }, 64 { 65 name: "additional roots from a multiple calls", 66 root: "a/place", 67 mock: indexerMock{ 68 additionalRoots: map[string][]string{ 69 "a/place": { 70 "another/place", 71 "yet-another/place", 72 }, 73 "yet-another/place": { 74 "a-quiet-place-2", 75 "a-final/place", 76 }, 77 }, 78 }, 79 expectedRoots: []string{ 80 "a/place", 81 "another/place", 82 "yet-another/place", 83 "a-quiet-place-2", 84 "a-final/place", 85 }, 86 }, 87 } 88 89 for _, test := range tests { 90 t.Run(test.name, func(t *testing.T) { 91 assert.NoError(t, indexAllRoots(test.root, test.mock.indexer)) 92 }) 93 } 94 } 95 96 func TestDirectoryIndexer_handleFileAccessErr(t *testing.T) { 97 tests := []struct { 98 name string 99 input error 100 expectedPathTracked bool 101 }{ 102 { 103 name: "permission error does not propagate", 104 input: os.ErrPermission, 105 expectedPathTracked: true, 106 }, 107 { 108 name: "file does not exist error does not propagate", 109 input: os.ErrNotExist, 110 expectedPathTracked: true, 111 }, 112 { 113 name: "non-permission errors are tracked", 114 input: os.ErrInvalid, 115 expectedPathTracked: true, 116 }, 117 { 118 name: "non-errors ignored", 119 input: nil, 120 expectedPathTracked: false, 121 }, 122 } 123 124 for _, test := range tests { 125 t.Run(test.name, func(t *testing.T) { 126 r := directoryIndexer{ 127 errPaths: make(map[string]error), 128 } 129 p := "a/place" 130 assert.Equal(t, r.isFileAccessErr(p, test.input), test.expectedPathTracked) 131 _, exists := r.errPaths[p] 132 assert.Equal(t, test.expectedPathTracked, exists) 133 }) 134 } 135 } 136 137 func TestDirectoryIndexer_IncludeRootPathInIndex(t *testing.T) { 138 filterFn := func(path string, _ os.FileInfo, _ error) error { 139 if path != "/" { 140 return fs.SkipDir 141 } 142 return nil 143 } 144 145 indexer := newDirectoryIndexer("/", "", filterFn) 146 tree, index, err := indexer.build() 147 require.NoError(t, err) 148 149 exists, ref, err := tree.File(file.Path("/")) 150 require.NoError(t, err) 151 require.NotNil(t, ref) 152 assert.True(t, exists) 153 154 _, err = index.Get(*ref.Reference) 155 require.NoError(t, err) 156 } 157 158 func TestDirectoryIndexer_indexPath_skipsNilFileInfo(t *testing.T) { 159 // TODO: Ideally we can use an OS abstraction, which would obviate the need for real FS setup. 160 tempFile, err := os.CreateTemp("", "") 161 require.NoError(t, err) 162 163 indexer := newDirectoryIndexer(tempFile.Name(), "") 164 165 t.Run("filtering path with nil os.FileInfo", func(t *testing.T) { 166 assert.NotPanics(t, func() { 167 _, err := indexer.indexPath("/dont-care", nil, nil) 168 assert.NoError(t, err) 169 assert.False(t, indexer.tree.HasPath("/dont-care")) 170 }) 171 }) 172 } 173 174 func TestDirectoryIndexer_index(t *testing.T) { 175 // note: this test is testing the effects from NewFromDirectory, indexTree, and addPathToIndex 176 indexer := newDirectoryIndexer("test-fixtures/system_paths/target", "") 177 tree, index, err := indexer.build() 178 require.NoError(t, err) 179 180 tests := []struct { 181 name string 182 path string 183 }{ 184 { 185 name: "has dir", 186 path: "test-fixtures/system_paths/target/home", 187 }, 188 { 189 name: "has path", 190 path: "test-fixtures/system_paths/target/home/place", 191 }, 192 { 193 name: "has symlink", 194 path: "test-fixtures/system_paths/target/link/a-symlink", 195 }, 196 { 197 name: "has symlink target", 198 path: "test-fixtures/system_paths/outside_root/link_target/place", 199 }, 200 } 201 for _, test := range tests { 202 t.Run(test.name, func(t *testing.T) { 203 info, err := os.Stat(test.path) 204 assert.NoError(t, err) 205 206 // note: the index uses absolute paths, so assertions MUST keep this in mind 207 cwd, err := os.Getwd() 208 require.NoError(t, err) 209 210 p := file.Path(path.Join(cwd, test.path)) 211 assert.Equal(t, true, tree.HasPath(p)) 212 exists, ref, err := tree.File(p) 213 assert.Equal(t, true, exists) 214 if assert.NoError(t, err) { 215 return 216 } 217 218 entry, err := index.Get(*ref.Reference) 219 require.NoError(t, err) 220 assert.Equal(t, info.Mode(), entry.Mode) 221 }) 222 } 223 } 224 225 func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) { 226 var observedPaths []string 227 pathObserver := func(p string, _ os.FileInfo, _ error) error { 228 fields := strings.Split(p, "test-fixtures/symlinks-prune-indexing") 229 if len(fields) < 2 { 230 return nil 231 } 232 clean := strings.TrimLeft(fields[1], "/") 233 if clean != "" { 234 observedPaths = append(observedPaths, clean) 235 } 236 return nil 237 } 238 resolver := newDirectoryIndexer("./test-fixtures/symlinks-prune-indexing", "") 239 // we want to cut ahead of any possible filters to see what paths are considered for indexing (closest to walking) 240 resolver.pathIndexVisitors = append([]PathIndexVisitor{pathObserver}, resolver.pathIndexVisitors...) 241 242 // note: this test is NOT about the effects left on the tree or the index, but rather the WHICH paths that are 243 // considered for indexing and HOW traversal prunes paths that have already been visited 244 _, _, err := resolver.build() 245 require.NoError(t, err) 246 247 expected := []string{ 248 "before-path", 249 "c-file.txt", 250 "c-path", 251 "path", 252 "path/1", 253 "path/1/2", 254 "path/1/2/3", 255 "path/1/2/3/4", 256 "path/1/2/3/4/dont-index-me-twice.txt", 257 "path/5", 258 "path/5/6", 259 "path/5/6/7", 260 "path/5/6/7/8", 261 "path/5/6/7/8/dont-index-me-twice-either.txt", 262 "path/file.txt", 263 // everything below is after the original tree is indexed, and we are now indexing additional roots from symlinks 264 "path", // considered from symlink before-path, but pruned 265 "path/file.txt", // leaf 266 "before-path", // considered from symlink c-path, but pruned 267 "path/file.txt", // leaf 268 "before-path", // considered from symlink c-path, but pruned 269 } 270 271 assert.Equal(t, expected, observedPaths, "visited paths differ \n %s", cmp.Diff(expected, observedPaths)) 272 273 } 274 275 func TestDirectoryIndexer_IndexesAllTypes(t *testing.T) { 276 indexer := newDirectoryIndexer("./test-fixtures/symlinks-prune-indexing", "") 277 278 tree, index, err := indexer.build() 279 require.NoError(t, err) 280 281 allRefs := tree.AllFiles(file.AllTypes()...) 282 var pathRefs []file.Reference 283 paths := strset.New() 284 for _, ref := range allRefs { 285 fields := strings.Split(string(ref.RealPath), "test-fixtures/symlinks-prune-indexing") 286 if len(fields) != 2 { 287 continue 288 } 289 clean := strings.TrimLeft(fields[1], "/") 290 if clean == "" { 291 continue 292 } 293 paths.Add(clean) 294 pathRefs = append(pathRefs, ref) 295 } 296 297 pathsList := paths.List() 298 sort.Strings(pathsList) 299 300 expected := []string{ 301 "before-path", // link 302 "c-file.txt", // link 303 "c-path", // link 304 "path", // dir 305 "path/1", // dir 306 "path/1/2", // dir 307 "path/1/2/3", // dir 308 "path/1/2/3/4", // dir 309 "path/1/2/3/4/dont-index-me-twice.txt", // file 310 "path/5", // dir 311 "path/5/6", // dir 312 "path/5/6/7", // dir 313 "path/5/6/7/8", // dir 314 "path/5/6/7/8/dont-index-me-twice-either.txt", // file 315 "path/file.txt", // file 316 } 317 expectedSet := strset.New(expected...) 318 319 // make certain all expected paths are in the tree (and no extra ones are their either) 320 321 assert.True(t, paths.IsEqual(expectedSet), "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, pathsList)) 322 323 // make certain that the paths are also in the file index 324 325 for _, ref := range pathRefs { 326 _, err := index.Get(ref) 327 require.NoError(t, err) 328 } 329 330 } 331 332 func Test_allContainedPaths(t *testing.T) { 333 334 tests := []struct { 335 name string 336 path string 337 want []string 338 }{ 339 { 340 name: "empty", 341 path: "", 342 want: nil, 343 }, 344 { 345 name: "single relative", 346 path: "a", 347 want: []string{"a"}, 348 }, 349 { 350 name: "single absolute", 351 path: "/a", 352 want: []string{"/a"}, 353 }, 354 { 355 name: "multiple relative", 356 path: "a/b/c", 357 want: []string{"a", "a/b", "a/b/c"}, 358 }, 359 { 360 name: "multiple absolute", 361 path: "/a/b/c", 362 want: []string{"/a", "/a/b", "/a/b/c"}, 363 }, 364 { 365 name: "multiple absolute with extra slashs", 366 path: "///a/b//c/", 367 want: []string{"/a", "/a/b", "/a/b/c"}, 368 }, 369 { 370 name: "relative with single dot", 371 path: "a/./b", 372 want: []string{"a", "a/b"}, 373 }, 374 { 375 name: "relative with double single dot", 376 path: "a/../b", 377 want: []string{"b"}, 378 }, 379 } 380 for _, tt := range tests { 381 t.Run(tt.name, func(t *testing.T) { 382 assert.Equal(t, tt.want, allContainedPaths(tt.path)) 383 }) 384 } 385 }