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