github.com/anchore/syft@v1.38.2/syft/internal/fileresolver/unindexed_directory.go (about) 1 package fileresolver 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "io/fs" 9 "os" 10 "path" 11 "path/filepath" 12 "slices" 13 "sort" 14 "strings" 15 "time" 16 17 "github.com/bmatcuk/doublestar/v4" 18 "github.com/spf13/afero" 19 20 "github.com/anchore/go-homedir" 21 "github.com/anchore/syft/internal/log" 22 "github.com/anchore/syft/syft/file" 23 ) 24 25 var _ file.Resolver = (*UnindexedDirectory)(nil) 26 var _ file.WritableResolver = (*UnindexedDirectory)(nil) 27 28 type UnindexedDirectory struct { 29 ls afero.Lstater 30 lr afero.LinkReader 31 base string 32 dir string 33 fs afero.Fs 34 } 35 36 func NewFromUnindexedDirectory(dir string) file.WritableResolver { 37 return NewFromUnindexedDirectoryFS(afero.NewOsFs(), dir, "") 38 } 39 40 func NewFromRootedUnindexedDirectory(dir string, base string) file.WritableResolver { 41 return NewFromUnindexedDirectoryFS(afero.NewOsFs(), dir, base) 42 } 43 44 func NewFromUnindexedDirectoryFS(fs afero.Fs, dir string, base string) file.WritableResolver { 45 ls, ok := fs.(afero.Lstater) 46 if !ok { 47 panic(fmt.Sprintf("unable to get afero.Lstater interface from: %+v", fs)) 48 } 49 lr, ok := fs.(afero.LinkReader) 50 if !ok { 51 panic(fmt.Sprintf("unable to get afero.Lstater interface from: %+v", fs)) 52 } 53 expanded, err := homedir.Expand(dir) 54 if err == nil { 55 dir = expanded 56 } 57 if base != "" { 58 expanded, err = homedir.Expand(base) 59 if err == nil { 60 base = expanded 61 } 62 } 63 wd, err := os.Getwd() 64 if err == nil { 65 if !filepath.IsAbs(dir) { 66 dir = filepath.Clean(filepath.Join(wd, dir)) 67 } 68 if base != "" && !filepath.IsAbs(base) { 69 base = filepath.Clean(filepath.Join(wd, base)) 70 } 71 } 72 return UnindexedDirectory{ 73 base: base, 74 dir: dir, 75 fs: fs, 76 ls: ls, 77 lr: lr, 78 } 79 } 80 81 func (u UnindexedDirectory) FileContentsByLocation(location file.Location) (io.ReadCloser, error) { 82 p := u.absPath(u.scrubInputPath(location.RealPath)) 83 f, err := u.fs.Open(p) 84 if err != nil { 85 return nil, err 86 } 87 fi, err := f.Stat() 88 if err != nil { 89 return nil, err 90 } 91 if fi.IsDir() { 92 return nil, fmt.Errorf("unable to get contents of directory: %s", location.RealPath) 93 } 94 return f, nil 95 } 96 97 // - full symlink resolution should be performed on all requests 98 // - returns locations for any file or directory 99 func (u UnindexedDirectory) HasPath(p string) bool { 100 locs, err := u.filesByPath(true, true, p) 101 return err == nil && len(locs) > 0 102 } 103 104 func (u UnindexedDirectory) canLstat(p string) bool { 105 _, _, err := u.ls.LstatIfPossible(u.absPath(p)) 106 return err == nil 107 } 108 109 func (u UnindexedDirectory) isRegularFile(p string) bool { 110 fi, _, err := u.ls.LstatIfPossible(u.absPath(p)) 111 return err == nil && !fi.IsDir() 112 } 113 114 func (u UnindexedDirectory) scrubInputPath(p string) string { 115 if path.IsAbs(p) { 116 p = p[1:] 117 } 118 return path.Clean(p) 119 } 120 121 func (u UnindexedDirectory) scrubResolutionPath(p string) string { 122 if u.base != "" { 123 if path.IsAbs(p) { 124 p = p[1:] 125 } 126 for strings.HasPrefix(p, "../") { 127 p = p[3:] 128 } 129 } 130 return path.Clean(p) 131 } 132 133 func (u UnindexedDirectory) absPath(p string) string { 134 if u.base != "" { 135 if path.IsAbs(p) { 136 p = p[1:] 137 } 138 for strings.HasPrefix(p, "../") { 139 p = p[3:] 140 } 141 p = path.Join(u.base, p) 142 return path.Clean(p) 143 } 144 if path.IsAbs(p) { 145 return p 146 } 147 return path.Clean(path.Join(u.dir, p)) 148 } 149 150 // - full symlink resolution should be performed on all requests 151 // - only returns locations to files (NOT directories) 152 func (u UnindexedDirectory) FilesByPath(paths ...string) (out []file.Location, _ error) { 153 return u.filesByPath(true, false, paths...) 154 } 155 156 func (u UnindexedDirectory) filesByPath(resolveLinks bool, includeDirs bool, paths ...string) (out []file.Location, _ error) { 157 // sort here for stable output 158 sort.Strings(paths) 159 nextPath: 160 for _, p := range paths { 161 p = u.scrubInputPath(p) 162 if u.canLstat(p) && (includeDirs || u.isRegularFile(p)) { 163 l := u.newLocation(p, resolveLinks) 164 if l == nil { 165 continue 166 } 167 // only include the first entry we find 168 for i := range out { 169 existing := &out[i] 170 if existing.RealPath == l.RealPath { 171 if l.AccessPath == "" { 172 existing.AccessPath = "" 173 } 174 continue nextPath 175 } 176 } 177 out = append(out, *l) 178 } 179 } 180 return 181 } 182 183 // - full symlink resolution should be performed on all requests 184 // - if multiple paths to the same file are found, the best single match should be returned 185 // - only returns locations to files (NOT directories) 186 func (u UnindexedDirectory) FilesByGlob(patterns ...string) (out []file.Location, _ error) { 187 return u.filesByGlob(true, false, patterns...) 188 } 189 190 func (u UnindexedDirectory) filesByGlob(resolveLinks bool, includeDirs bool, patterns ...string) (out []file.Location, _ error) { 191 f := unindexedDirectoryResolverFS{ 192 u: u, 193 } 194 var paths []string 195 for _, p := range patterns { 196 opts := []doublestar.GlobOption{doublestar.WithNoFollow()} 197 if !includeDirs { 198 opts = append(opts, doublestar.WithFilesOnly()) 199 } 200 found, err := doublestar.Glob(f, p, opts...) 201 if err != nil { 202 return nil, err 203 } 204 paths = append(paths, found...) 205 } 206 return u.filesByPath(resolveLinks, includeDirs, paths...) 207 } 208 209 func (u UnindexedDirectory) FilesByMIMEType(_ ...string) ([]file.Location, error) { 210 panic("FilesByMIMEType unsupported") 211 } 212 213 // RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference. 214 // This is helpful when attempting to find a file that is in the same layer or lower as another file. 215 func (u UnindexedDirectory) RelativeFileByPath(l file.Location, p string) *file.Location { 216 p = path.Clean(path.Join(l.RealPath, p)) 217 locs, err := u.filesByPath(true, false, p) 218 if err != nil || len(locs) == 0 { 219 return nil 220 } 221 l = locs[0] 222 p = l.RealPath 223 if u.isRegularFile(p) { 224 return u.newLocation(p, true) 225 } 226 return nil 227 } 228 229 // - NO symlink resolution should be performed on results 230 // - returns locations for any file or directory 231 func (u UnindexedDirectory) AllLocations(ctx context.Context) <-chan file.Location { 232 out := make(chan file.Location) 233 errWalkCanceled := fmt.Errorf("walk canceled") 234 go func() { 235 defer close(out) 236 err := afero.Walk(u.fs, u.absPath("."), func(p string, _ fs.FileInfo, _ error) error { 237 p = strings.TrimPrefix(p, u.dir) 238 if p == "" { 239 return nil 240 } 241 p = strings.TrimPrefix(p, "/") 242 select { 243 case out <- file.NewLocation(p): 244 return nil 245 case <-ctx.Done(): 246 return errWalkCanceled 247 } 248 }) 249 if err != nil && !errors.Is(err, errWalkCanceled) { 250 log.Debug(err) 251 } 252 }() 253 return out 254 } 255 256 func (u UnindexedDirectory) FileMetadataByLocation(_ file.Location) (file.Metadata, error) { 257 panic("FileMetadataByLocation unsupported") 258 } 259 260 func (u UnindexedDirectory) Write(location file.Location, reader io.Reader) error { 261 filePath := location.RealPath 262 if path.IsAbs(filePath) { 263 filePath = filePath[1:] 264 } 265 absPath := u.absPath(filePath) 266 return afero.WriteReader(u.fs, absPath, reader) 267 } 268 269 func (u UnindexedDirectory) newLocation(filePath string, resolveLinks bool) *file.Location { 270 filePath = path.Clean(filePath) 271 272 virtualPath := filePath 273 realPath := filePath 274 275 if resolveLinks { 276 paths := u.resolveLinks(filePath) 277 if len(paths) > 1 { 278 realPath = paths[len(paths)-1] 279 // TODO: this is not quite correct, as the equivalent of os.EvalSymlinks needs to be done (in the context of afero) 280 if realPath != path.Clean(filePath) { 281 virtualPath = paths[0] 282 } 283 } 284 if len(paths) == 0 { 285 // this file does not exist, don't return a location 286 return nil 287 } 288 } 289 290 l := file.NewVirtualLocation(realPath, virtualPath) 291 return &l 292 } 293 294 //nolint:gocognit 295 func (u UnindexedDirectory) resolveLinks(filePath string) []string { 296 var visited []string 297 298 out := []string{} 299 300 resolvedPath := "" 301 302 parts := strings.Split(filePath, "/") 303 for i := 0; i < len(parts); i++ { 304 part := parts[i] 305 if resolvedPath == "" { 306 resolvedPath = part 307 } else { 308 resolvedPath = path.Clean(path.Join(resolvedPath, part)) 309 } 310 resolvedPath = u.scrubResolutionPath(resolvedPath) 311 if resolvedPath == ".." { 312 resolvedPath = "" 313 continue 314 } 315 316 absPath := u.absPath(resolvedPath) 317 if slices.Contains(visited, absPath) { 318 return nil // circular links can't resolve 319 } 320 visited = append(visited, absPath) 321 322 fi, wasLstat, err := u.ls.LstatIfPossible(absPath) 323 if fi == nil || err != nil { 324 // this file does not exist 325 return nil 326 } 327 328 for wasLstat && u.isSymlink(fi) { 329 next, err := u.lr.ReadlinkIfPossible(absPath) 330 if err == nil { 331 if !path.IsAbs(next) { 332 next = path.Clean(path.Join(path.Dir(resolvedPath), next)) 333 } 334 next = u.scrubResolutionPath(next) 335 absPath = u.absPath(next) 336 if slices.Contains(visited, absPath) { 337 return nil // circular links can't resolve 338 } 339 visited = append(visited, absPath) 340 341 fi, wasLstat, err = u.ls.LstatIfPossible(absPath) 342 if fi == nil || err != nil { 343 // this file does not exist 344 return nil 345 } 346 if i < len(parts) { 347 out = append(out, path.Join(resolvedPath, path.Join(parts[i+1:]...))) 348 } 349 if u.base != "" && path.IsAbs(next) { 350 next = next[1:] 351 } 352 resolvedPath = next 353 } 354 } 355 } 356 357 out = append(out, resolvedPath) 358 359 return out 360 } 361 362 func (u UnindexedDirectory) isSymlink(fi os.FileInfo) bool { 363 return fi.Mode().Type()&fs.ModeSymlink == fs.ModeSymlink 364 } 365 366 // ------------------------- fs.FS ------------------------------ 367 368 // unindexedDirectoryResolverFS wraps the UnindexedDirectory as a fs.FS, fs.ReadDirFS, and fs.StatFS 369 type unindexedDirectoryResolverFS struct { 370 u UnindexedDirectory 371 } 372 373 // resolve takes a virtual path and returns the resolved absolute or relative path and file info 374 func (f unindexedDirectoryResolverFS) resolve(filePath string) (resolved string, fi fs.FileInfo, err error) { 375 parts := strings.Split(filePath, "/") 376 var visited []string 377 for i, part := range parts { 378 if i > 0 { 379 resolved = path.Clean(path.Join(resolved, part)) 380 } else { 381 resolved = part 382 } 383 abs := f.u.absPath(resolved) 384 fi, _, err = f.u.ls.LstatIfPossible(abs) 385 if err != nil { 386 return resolved, fi, err 387 } 388 for f.u.isSymlink(fi) { 389 if slices.Contains(visited, resolved) { 390 return resolved, fi, fmt.Errorf("link cycle detected at: %s", f.u.absPath(resolved)) 391 } 392 visited = append(visited, resolved) 393 link, err := f.u.lr.ReadlinkIfPossible(abs) 394 if err != nil { 395 return resolved, fi, err 396 } 397 if !path.IsAbs(link) { 398 link = path.Clean(path.Join(path.Dir(abs), link)) 399 link = strings.TrimPrefix(link, abs) 400 } else if f.u.base != "" { 401 link = path.Clean(path.Join(f.u.base, link[1:])) 402 } 403 resolved = link 404 abs = f.u.absPath(resolved) 405 fi, _, err = f.u.ls.LstatIfPossible(abs) 406 if err != nil { 407 return resolved, fi, err 408 } 409 } 410 } 411 return resolved, fi, err 412 } 413 414 func (f unindexedDirectoryResolverFS) ReadDir(name string) (out []fs.DirEntry, _ error) { 415 p, _, err := f.resolve(name) 416 if err != nil { 417 return nil, err 418 } 419 entries, err := afero.ReadDir(f.u.fs, f.u.absPath(p)) 420 if err != nil { 421 return nil, err 422 } 423 for _, e := range entries { 424 isDir := e.IsDir() 425 _, fi, _ := f.resolve(path.Join(name, e.Name())) 426 if fi != nil && fi.IsDir() { 427 isDir = true 428 } 429 out = append(out, unindexedDirectoryResolverDirEntry{ 430 unindexedDirectoryResolverFileInfo: newFsFileInfo(f.u, e.Name(), isDir, e), 431 }) 432 } 433 return out, nil 434 } 435 436 func (f unindexedDirectoryResolverFS) Stat(name string) (fs.FileInfo, error) { 437 fi, err := f.u.fs.Stat(f.u.absPath(name)) 438 if err != nil { 439 return nil, err 440 } 441 return newFsFileInfo(f.u, name, fi.IsDir(), fi), nil 442 } 443 444 func (f unindexedDirectoryResolverFS) Open(name string) (fs.File, error) { 445 _, err := f.u.fs.Open(f.u.absPath(name)) 446 if err != nil { 447 return nil, err 448 } 449 450 return unindexedDirectoryResolverFile{ 451 u: f.u, 452 path: name, 453 }, nil 454 } 455 456 var _ fs.FS = (*unindexedDirectoryResolverFS)(nil) 457 var _ fs.StatFS = (*unindexedDirectoryResolverFS)(nil) 458 var _ fs.ReadDirFS = (*unindexedDirectoryResolverFS)(nil) 459 460 type unindexedDirectoryResolverDirEntry struct { 461 unindexedDirectoryResolverFileInfo 462 } 463 464 func (f unindexedDirectoryResolverDirEntry) Name() string { 465 return f.name 466 } 467 468 func (f unindexedDirectoryResolverDirEntry) IsDir() bool { 469 return f.isDir 470 } 471 472 func (f unindexedDirectoryResolverDirEntry) Type() fs.FileMode { 473 return f.mode 474 } 475 476 func (f unindexedDirectoryResolverDirEntry) Info() (fs.FileInfo, error) { 477 return f, nil 478 } 479 480 var _ fs.DirEntry = (*unindexedDirectoryResolverDirEntry)(nil) 481 482 type unindexedDirectoryResolverFile struct { 483 u UnindexedDirectory 484 path string 485 } 486 487 func (f unindexedDirectoryResolverFile) Stat() (fs.FileInfo, error) { 488 fi, err := f.u.fs.Stat(f.u.absPath(f.path)) 489 if err != nil { 490 return nil, err 491 } 492 return newFsFileInfo(f.u, fi.Name(), fi.IsDir(), fi), nil 493 } 494 495 func (f unindexedDirectoryResolverFile) Read(_ []byte) (int, error) { 496 panic("Read not implemented") 497 } 498 499 func (f unindexedDirectoryResolverFile) Close() error { 500 panic("Close not implemented") 501 } 502 503 var _ fs.File = (*unindexedDirectoryResolverFile)(nil) 504 505 type unindexedDirectoryResolverFileInfo struct { 506 u UnindexedDirectory 507 name string 508 size int64 509 mode fs.FileMode 510 modTime time.Time 511 isDir bool 512 sys any 513 } 514 515 func newFsFileInfo(u UnindexedDirectory, name string, isDir bool, fi os.FileInfo) unindexedDirectoryResolverFileInfo { 516 return unindexedDirectoryResolverFileInfo{ 517 u: u, 518 name: name, 519 size: fi.Size(), 520 mode: fi.Mode() & ^fs.ModeSymlink, // pretend nothing is a symlink 521 modTime: fi.ModTime(), 522 isDir: isDir, 523 // sys: fi.Sys(), // what values does this hold? 524 } 525 } 526 527 func (f unindexedDirectoryResolverFileInfo) Name() string { 528 return f.name 529 } 530 531 func (f unindexedDirectoryResolverFileInfo) Size() int64 { 532 return f.size 533 } 534 535 func (f unindexedDirectoryResolverFileInfo) Mode() fs.FileMode { 536 return f.mode 537 } 538 539 func (f unindexedDirectoryResolverFileInfo) ModTime() time.Time { 540 return f.modTime 541 } 542 543 func (f unindexedDirectoryResolverFileInfo) IsDir() bool { 544 return f.isDir 545 } 546 547 func (f unindexedDirectoryResolverFileInfo) Sys() any { 548 return f.sys 549 } 550 551 var _ fs.FileInfo = (*unindexedDirectoryResolverFileInfo)(nil)