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