github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/embeddedfs/common/common.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package common provides common utilities for embedded filesystem extractors. 16 package common 17 18 import ( 19 "context" 20 "encoding/binary" 21 "errors" 22 "fmt" 23 "io" 24 "io/fs" 25 "os" 26 "path" 27 "path/filepath" 28 "strings" 29 "sync" 30 "time" 31 32 "archive/tar" 33 34 "github.com/diskfs/go-diskfs" 35 "github.com/diskfs/go-diskfs/disk" 36 "github.com/diskfs/go-diskfs/filesystem/fat32" 37 "github.com/diskfs/go-diskfs/partition/part" 38 "github.com/dsoprea/go-exfat" 39 "github.com/google/osv-scalibr/artifact/image/symlink" 40 scalibrfs "github.com/google/osv-scalibr/fs" 41 "github.com/masahiro331/go-ext4-filesystem/ext4" 42 "www.velocidex.com/golang/go-ntfs/parser" 43 ) 44 45 const ( 46 defaultPageSize = 1024 * 1024 47 defaultCacheSize = 100 * 1024 * 1024 48 ) 49 50 // DetectFilesystem identifies the filesystem type by magic bytes. 51 func DetectFilesystem(r io.ReaderAt, offset int64) string { 52 buf := make([]byte, 4096) 53 _, err := r.ReadAt(buf, offset) 54 if err != nil { 55 return fmt.Sprintf("read error: %v", err) 56 } 57 // https://www.kernel.org/doc/html/latest/filesystems/ext4/globals.html 58 // EXT4 magic at offset 0x438 59 if len(buf) > 0x438+2 { 60 if binary.LittleEndian.Uint16(buf[0x438:0x43A]) == 0xEF53 { 61 return "ext4" 62 } 63 } 64 // https://en.wikipedia.org/wiki/NTFS 65 // NTFS: "NTFS " at offset 0x03 66 if len(buf) > 3+8 { 67 if string(buf[3:3+8]) == "NTFS " { 68 return "NTFS" 69 } 70 } 71 // https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system 72 // FAT32: "FAT32 " at offset 0x52 73 if len(buf) > 0x52+8 { 74 if string(buf[0x52:0x52+8]) == "FAT32 " { 75 return "FAT32" 76 } 77 } 78 // https://en.wikipedia.org/wiki/ExFAT 79 // exFAT: "EXFAT " at offset 0x03 80 if len(buf) > 3+8 { 81 if string(buf[3:3+8]) == "EXFAT " { 82 return "exFAT" 83 } 84 } 85 return "unknown" 86 } 87 88 func normalizePath(p string) string { 89 p = strings.ReplaceAll(p, "\\", "/") 90 if !strings.HasPrefix(p, "/") { 91 p = "/" + p 92 } 93 return p 94 } 95 96 // filterEntriesFat32 removes ".", "..", and "lost+found" from FAT32 entries. 97 func filterEntriesFat32(entries []os.FileInfo) []os.FileInfo { 98 var filtered []os.FileInfo 99 for _, e := range entries { 100 name := e.Name() 101 if name == "." || name == ".." || name == "lost+found" { 102 continue 103 } 104 filtered = append(filtered, e) 105 } 106 return filtered 107 } 108 109 // filterEntriesExt removes ".", "..", and "lost+found" from ext4 entries. 110 func filterEntriesExt(entries []fs.DirEntry) []fs.DirEntry { 111 var filtered []fs.DirEntry 112 for _, e := range entries { 113 name := e.Name() 114 if name == "." || name == ".." || name == "lost+found" { 115 continue 116 } 117 filtered = append(filtered, e) 118 } 119 return filtered 120 } 121 122 // filterEntriesNtfs removes ".", "..", and "$"-prefixed entries from NTFS entries. 123 func filterEntriesNtfs(entries []*parser.FileInfo) []*parser.FileInfo { 124 var filtered []*parser.FileInfo 125 for _, e := range entries { 126 name := e.Name 127 if name == "" || name == "." || name == ".." || strings.HasPrefix(name, "$") { 128 continue 129 } 130 filtered = append(filtered, e) 131 } 132 return filtered 133 } 134 135 // ExtractAllRecursiveExt extracts all files from an ext4 filesystem to a temporary directory recursively. 136 func ExtractAllRecursiveExt(fs *ext4.FileSystem, srcPath, destPath string) error { 137 srcPath = normalizePath(srcPath) 138 entries, err := fs.ReadDir(srcPath) 139 if err != nil { 140 fmt.Printf("Warning: Failed to list directory %s: %v\n", srcPath, err) 141 return nil // Continue processing other entries 142 } 143 144 entries = filterEntriesExt(entries) 145 146 if err := os.MkdirAll(destPath, 0755); err != nil { 147 return fmt.Errorf("failed to create directory %s: %w", destPath, err) 148 } 149 150 for _, entry := range entries { 151 srcFullPath := path.Join(srcPath, entry.Name()) 152 destFullPath := filepath.Join(destPath, entry.Name()) 153 154 if entry.IsDir() { 155 if err := os.MkdirAll(destFullPath, 0755); err != nil { 156 fmt.Printf("Warning: Failed to create directory %s: %v\n", destFullPath, err) 157 continue 158 } 159 if err := ExtractAllRecursiveExt(fs, srcFullPath, destFullPath); err != nil { 160 fmt.Printf("Warning: Failed to extract directory %s: %v\n", srcFullPath, err) 161 continue 162 } 163 } else { 164 file, err := fs.Open(srcFullPath) 165 if err != nil { 166 fmt.Printf("Warning: Failed to open file %s: %v\n", srcFullPath, err) 167 continue 168 } 169 defer file.Close() 170 171 destFile, err := os.Create(destFullPath) 172 if err != nil { 173 fmt.Printf("Warning: Failed to create file %s: %v\n", destFullPath, err) 174 continue 175 } 176 defer destFile.Close() 177 178 if _, err := io.Copy(destFile, file); err != nil { 179 fmt.Printf("Warning: Failed to copy file %s to %s: %v\n", srcFullPath, destFullPath, err) 180 continue 181 } 182 } 183 } 184 return nil 185 } 186 187 // ExtractAllRecursiveFat32 extracts all files from a FAT32 filesystem to a temporary directory recursively. 188 func ExtractAllRecursiveFat32(fs *fat32.FileSystem, srcPath, destPath string) error { 189 if srcPath == "" || srcPath == "." { 190 srcPath = "/" 191 } 192 srcPath = normalizePath(srcPath) 193 entries, err := fs.ReadDir(srcPath) 194 if err != nil { 195 fmt.Printf("Warning: Failed to list directory %s: %v\n", srcPath, err) 196 return nil // Continue processing other entries 197 } 198 199 entries = filterEntriesFat32(entries) 200 201 if err := os.MkdirAll(destPath, 0755); err != nil { 202 return fmt.Errorf("failed to create directory %s: %w", destPath, err) 203 } 204 205 for _, entry := range entries { 206 srcFullPath := path.Join(srcPath, entry.Name()) 207 destFullPath := filepath.Join(destPath, entry.Name()) 208 209 if entry.IsDir() { 210 if err := os.MkdirAll(destFullPath, 0755); err != nil { 211 fmt.Printf("Warning: Failed to create directory %s: %v\n", destFullPath, err) 212 continue 213 } 214 if err := ExtractAllRecursiveFat32(fs, srcFullPath, destFullPath); err != nil { 215 fmt.Printf("Warning: Failed to extract directory %s: %v\n", srcFullPath, err) 216 continue 217 } 218 } else { 219 file, err := fs.OpenFile(srcFullPath, os.O_RDONLY) 220 if err != nil { 221 fmt.Printf("Warning: Failed to open file %s: %v\n", srcFullPath, err) 222 continue 223 } 224 defer file.Close() 225 226 destFile, err := os.Create(destFullPath) 227 if err != nil { 228 fmt.Printf("Warning: Failed to create file %s: %v\n", destFullPath, err) 229 continue 230 } 231 defer destFile.Close() 232 233 if _, err := io.Copy(destFile, file); err != nil { 234 fmt.Printf("Warning: Failed to copy file %s to %s: %v\n", srcFullPath, destFullPath, err) 235 continue 236 } 237 } 238 } 239 return nil 240 } 241 242 // ExtractAllRecursiveNtfs extracts all files from a NTFS filesystem to a temporary directory recursively. 243 func ExtractAllRecursiveNtfs(fs *parser.NTFSContext, srcPath, destPath string) error { 244 srcPath = normalizePath(srcPath) 245 if srcPath == "" || srcPath == "." { 246 srcPath = "/" 247 } 248 249 dir, err := fs.GetMFT(5) // Root directory MFT entry 250 if err != nil { 251 fmt.Printf("Warning: Failed to get root MFT for %s: %v\n", srcPath, err) 252 return nil // Continue processing other entries 253 } 254 entry, err := dir.Open(fs, srcPath) 255 if err != nil { 256 fmt.Printf("Warning: Failed to open directory %s: %v\n", srcPath, err) 257 return nil // Continue processing other entries 258 } 259 entries := parser.ListDir(fs, entry) 260 entries = filterEntriesNtfs(entries) 261 262 if err := os.MkdirAll(destPath, 0755); err != nil { 263 return fmt.Errorf("failed to create directory %s: %w", destPath, err) 264 } 265 266 for _, entryInfo := range entries { 267 srcFullPath := path.Join(srcPath, entryInfo.Name) 268 destFullPath := filepath.Join(destPath, entryInfo.Name) 269 270 if entryInfo.IsDir { 271 if err := os.MkdirAll(destFullPath, 0755); err != nil { 272 fmt.Printf("Warning: Failed to create directory %s: %v\n", destFullPath, err) 273 continue 274 } 275 if err := ExtractAllRecursiveNtfs(fs, srcFullPath, destFullPath); err != nil { 276 fmt.Printf("Warning: Failed to extract directory %s: %v\n", srcFullPath, err) 277 continue 278 } 279 } else { 280 fileEntry, err := dir.Open(fs, srcFullPath) 281 if err != nil { 282 fmt.Printf("Warning: Failed to open file %s: %v\n", srcFullPath, err) 283 continue 284 } 285 // Get the file's data attribute 286 attr, err := fileEntry.GetAttribute(fs, 128, -1, "") // Data attribute 287 if err != nil { 288 fmt.Printf("Warning: Failed to get data attribute for %s: %v\n", srcFullPath, err) 289 continue 290 } 291 fileReaderAt := attr.Data(fs) // io.ReaderAt for file content 292 // Convert io.ReaderAt to io.Reader using io.NewSectionReader 293 fileReader := io.NewSectionReader(fileReaderAt, 0, entryInfo.Size) 294 295 destFile, err := os.Create(destFullPath) 296 if err != nil { 297 fmt.Printf("Warning: Failed to create file %s: %v\n", destFullPath, err) 298 continue 299 } 300 defer destFile.Close() 301 302 if _, err := io.Copy(destFile, fileReader); err != nil { 303 fmt.Printf("Warning: Failed to copy file %s to %s: %v\n", srcFullPath, destFullPath, err) 304 continue 305 } 306 } 307 } 308 return nil 309 } 310 311 // ExtractAllRecursiveExFAT extracts all files from an exFAT filesystem to a temporary directory recursively. 312 func ExtractAllRecursiveExFAT(section *io.SectionReader, dst string) error { 313 er := exfat.NewExfatReader(section) 314 if err := er.Parse(); err != nil { 315 return fmt.Errorf("failed to parse exfat filesystem: %w", err) 316 } 317 318 tree := exfat.NewTree(er) 319 if err := tree.Load(); err != nil { 320 return fmt.Errorf("failed to load exfat tree: %w", err) 321 } 322 323 files, nodes, err := tree.List() 324 if err != nil { 325 return fmt.Errorf("failed to list exfat entries: %w", err) 326 } 327 328 for _, relPath := range files { 329 node := nodes[relPath] 330 resPath := strings.ReplaceAll(relPath, "\\", string(os.PathSeparator)) 331 outPath := filepath.Join(dst, resPath) 332 333 sde := node.StreamDirectoryEntry() 334 if node.IsDirectory() { 335 if err := os.MkdirAll(outPath, 0o755); err != nil { 336 return fmt.Errorf("failed to create directory %s: %w", outPath, err) 337 } 338 continue 339 } 340 341 if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { 342 return fmt.Errorf("failed to create parent directories for %s: %w", outPath, err) 343 } 344 345 outFile, err := os.Create(outPath) 346 if err != nil { 347 return fmt.Errorf("failed to create file %s: %w", outPath, err) 348 } 349 350 useFat := !sde.GeneralSecondaryFlags.NoFatChain() 351 if _, _, err := er.WriteFromClusterChain(sde.FirstCluster, sde.ValidDataLength, useFat, outFile); err != nil { 352 // Ignore this error because we're going to manually truncate the file at the end 353 if !strings.Contains(err.Error(), "written bytes do not equal data-size") { 354 return fmt.Errorf("failed to write cluster chain %s: %w", outPath, err) 355 } 356 } 357 358 err = outFile.Truncate(int64(sde.ValidDataLength)) 359 if err != nil { 360 continue 361 } 362 363 if err := outFile.Close(); err != nil { 364 return fmt.Errorf("failed to close file %s: %w", outPath, err) 365 } 366 } 367 368 return nil 369 } 370 371 // CloserWithTmpPaths is an interface for filesystems that provide temporary paths for cleanup. 372 type CloserWithTmpPaths interface { 373 scalibrfs.FS 374 Close() error 375 TempPaths() []string 376 } 377 378 // GetDiskPartitions opens a raw disk image and returns its partitions along with the disk handle. 379 func GetDiskPartitions(tmpRawPath string) ([]part.Partition, *disk.Disk, error) { 380 // Open the raw disk image with go-diskfs 381 disk, err := diskfs.Open(tmpRawPath, diskfs.WithOpenMode(diskfs.ReadOnly)) 382 if err != nil { 383 os.Remove(tmpRawPath) 384 return nil, nil, fmt.Errorf("failed to open raw disk image %s: %w", tmpRawPath, err) 385 } 386 387 partitions, err := disk.GetPartitionTable() 388 if err != nil { 389 return nil, nil, fmt.Errorf("failed to get partition table: %w", err) 390 } 391 partitionList := partitions.GetPartitions() 392 if len(partitionList) == 0 { 393 return nil, nil, errors.New("no partitions found in raw disk image") 394 } 395 return partitionList, disk, nil 396 } 397 398 // NewPartitionEmbeddedFSGetter creates a lazy getter function for an embedded filesystem from a disk partition. 399 func NewPartitionEmbeddedFSGetter(pluginName string, partitionIndex int, p part.Partition, disk *disk.Disk, tmpRawPath string, refMu *sync.Mutex, refCount *int32) func(context.Context) (scalibrfs.FS, error) { 400 return func(ctx context.Context) (scalibrfs.FS, error) { 401 // Get partition offset and size (already multiplied by sector size) 402 offset := p.GetStart() 403 size := p.GetSize() 404 405 // Open raw image for filesystem parsers 406 f, err := os.Open(tmpRawPath) 407 if err != nil { 408 return nil, fmt.Errorf("failed to open raw image %s: %w", tmpRawPath, err) 409 } 410 411 section := io.NewSectionReader(f, offset, size) 412 fsType := DetectFilesystem(section, 0) 413 414 // Create a temporary directory for extracted files 415 tempDir, err := os.MkdirTemp("", fmt.Sprintf("scalibr-%s-part-%s-%d-", pluginName, fsType, partitionIndex)) 416 if err != nil { 417 f.Close() 418 return nil, fmt.Errorf("failed to create temporary directory for %s partition %d: %w", fsType, partitionIndex, err) 419 } 420 421 params := generateFSParams{ 422 File: f, 423 Disk: disk, 424 Section: section, 425 PartitionIndex: partitionIndex, 426 TempDir: tempDir, 427 TmpRawPath: tmpRawPath, 428 RefMu: refMu, 429 RefCount: refCount, 430 } 431 432 var fsys scalibrfs.FS 433 switch fsType { 434 case "ext4": 435 fsys, err = generateEXTFS(params) 436 case "FAT32": 437 fsys, err = generateFAT32FS(params) 438 case "exFAT": 439 fsys, err = generateEXFATFS(params) 440 case "NTFS": 441 fsys, err = generateNTFSFS(params) 442 default: 443 fsys, err = nil, fmt.Errorf("unsupported filesystem type %s for partition %d", fsType, partitionIndex) 444 } 445 if err != nil { 446 if fsType != "FAT32" { 447 f.Close() 448 } 449 os.RemoveAll(tempDir) 450 return nil, err 451 } 452 return fsys, nil 453 } 454 } 455 456 // generateFSParams holds parameters for generating embedded filesystems. 457 type generateFSParams struct { 458 File *os.File 459 Disk *disk.Disk 460 Section *io.SectionReader 461 PartitionIndex int 462 TempDir string 463 TmpRawPath string 464 RefMu *sync.Mutex 465 RefCount *int32 466 } 467 468 // generateEXTFS generates an ext4 filesystem and extracts files to a temporary directory. 469 func generateEXTFS(params generateFSParams) (*EmbeddedDirFS, error) { 470 fs, err := ext4.NewFS(*params.Section, nil) 471 if err != nil { 472 return nil, fmt.Errorf("failed to create ext4 filesystem for partition %d: %w", params.PartitionIndex, err) 473 } 474 if err := ExtractAllRecursiveExt(fs, "/", params.TempDir); err != nil { 475 return nil, fmt.Errorf("failed to extract ext4 files for partition %d: %w", params.PartitionIndex, err) 476 } 477 params.RefMu.Lock() 478 *params.RefCount++ 479 params.RefMu.Unlock() 480 return &EmbeddedDirFS{ 481 FS: scalibrfs.DirFS(params.TempDir), 482 File: params.File, 483 TmpPaths: []string{params.TempDir, params.TmpRawPath}, 484 RefCount: params.RefCount, 485 RefMu: params.RefMu, 486 }, nil 487 } 488 489 // generateFAT32FS generates a FAT32 filesystem and extracts files to a temporary directory. 490 // Note that unlike in the other generator functions, the file is expected to be closed 491 // as disk.GetFilesystem() will reopen it. 492 func generateFAT32FS(params generateFSParams) (*EmbeddedDirFS, error) { 493 fs, err := params.Disk.GetFilesystem(params.PartitionIndex) 494 if err != nil { 495 return nil, fmt.Errorf("failed to get filesystem for partition %d: %w", params.PartitionIndex, err) 496 } 497 fat32fs, ok := fs.(*fat32.FileSystem) 498 if !ok { 499 return nil, fmt.Errorf("partition %d is not a FAT32 filesystem", params.PartitionIndex) 500 } 501 f, err := os.Open(params.TmpRawPath) 502 if err != nil { 503 return nil, fmt.Errorf("failed to reopen raw image %s: %w", params.TmpRawPath, err) 504 } 505 if err := ExtractAllRecursiveFat32(fat32fs, "/", params.TempDir); err != nil { 506 f.Close() 507 return nil, fmt.Errorf("failed to extract FAT32 files for partition %d: %w", params.PartitionIndex, err) 508 } 509 params.RefMu.Lock() 510 *params.RefCount++ 511 params.RefMu.Unlock() 512 return &EmbeddedDirFS{ 513 FS: scalibrfs.DirFS(params.TempDir), 514 File: f, 515 TmpPaths: []string{params.TempDir, params.TmpRawPath}, 516 RefCount: params.RefCount, 517 RefMu: params.RefMu, 518 }, nil 519 } 520 521 // generateEXFATFS generates an exFAT filesystem and extracts files to a temporary directory. 522 func generateEXFATFS(params generateFSParams) (*EmbeddedDirFS, error) { 523 if err := ExtractAllRecursiveExFAT(params.Section, params.TempDir); err != nil { 524 return nil, fmt.Errorf("failed to extract exFAT files for partition %d: %w", params.PartitionIndex, err) 525 } 526 params.RefMu.Lock() 527 *params.RefCount++ 528 params.RefMu.Unlock() 529 return &EmbeddedDirFS{ 530 FS: scalibrfs.DirFS(params.TempDir), 531 File: params.File, 532 TmpPaths: []string{params.TempDir, params.TmpRawPath}, 533 RefCount: params.RefCount, 534 RefMu: params.RefMu, 535 }, nil 536 } 537 538 // generateNTFSFS generates an NTFS filesystem and extracts files to a temporary directory. 539 func generateNTFSFS(params generateFSParams) (*EmbeddedDirFS, error) { 540 reader, err := parser.NewPagedReader(params.Section, defaultPageSize, defaultCacheSize) 541 if err != nil { 542 return nil, fmt.Errorf("failed to create paged reader for NTFS partition %d: %w", params.PartitionIndex, err) 543 } 544 fs, err := parser.GetNTFSContext(reader, 0) 545 if err != nil { 546 return nil, fmt.Errorf("failed to create NTFS filesystem for partition %d: %w", params.PartitionIndex, err) 547 } 548 if err := ExtractAllRecursiveNtfs(fs, "/", params.TempDir); err != nil { 549 return nil, fmt.Errorf("failed to extract NTFS files for partition %d: %w", params.PartitionIndex, err) 550 } 551 params.RefMu.Lock() 552 *params.RefCount++ 553 params.RefMu.Unlock() 554 return &EmbeddedDirFS{ 555 FS: scalibrfs.DirFS(params.TempDir), 556 File: params.File, 557 TmpPaths: []string{params.TempDir, params.TmpRawPath}, 558 RefCount: params.RefCount, 559 RefMu: params.RefMu, 560 }, nil 561 } 562 563 // EmbeddedDirFS wraps scalibrfs.DirFS to include reference counting and cleanup. 564 type EmbeddedDirFS struct { 565 FS scalibrfs.FS 566 File *os.File 567 TmpPaths []string 568 RefCount *int32 569 RefMu *sync.Mutex 570 } 571 572 // Open opens the specified file from the embedded filesystem. 573 func (e *EmbeddedDirFS) Open(name string) (fs.File, error) { 574 return e.FS.Open(name) 575 } 576 577 // ReadDir returns a list of directory entries for the specified path. 578 // If name is empty or "/", it reads from the root directory instead. 579 func (e *EmbeddedDirFS) ReadDir(name string) ([]fs.DirEntry, error) { 580 if name == "/" || name == "" { 581 name = "." 582 } 583 return e.FS.ReadDir(name) 584 } 585 586 // Stat returns a FileInfo describing the named file or directory. 587 // If the name refers to the root directory ("/", "", or "."), it 588 // returns a synthetic FileInfo representing a directory. 589 func (e *EmbeddedDirFS) Stat(name string) (fs.FileInfo, error) { 590 if name == "/" || name == "" || name == "." { 591 // Return synthetic FileInfo for root directory 592 return &fileInfo{ 593 name: name, 594 isDir: true, 595 modTime: time.Now(), 596 }, nil 597 } 598 return e.FS.Stat(name) 599 } 600 601 // Close closes the underlying file without removing temporary paths. 602 func (e *EmbeddedDirFS) Close() error { 603 e.RefMu.Lock() 604 defer e.RefMu.Unlock() 605 if e.File == nil { 606 return nil // Already closed 607 } 608 *e.RefCount-- 609 if *e.RefCount == 0 { 610 err := e.File.Close() 611 e.File = nil // Prevent double close 612 if err != nil { 613 return fmt.Errorf("failed to close raw file: %w", err) 614 } 615 } 616 return nil 617 } 618 619 // TempPaths returns the temporary paths associated with the filesystem for cleanup. 620 func (e *EmbeddedDirFS) TempPaths() []string { 621 return e.TmpPaths 622 } 623 624 // fileInfo is a simple implementation of fs.FileInfo for the root directory. 625 type fileInfo struct { 626 name string 627 isDir bool 628 modTime time.Time 629 } 630 631 func (fi *fileInfo) Name() string { 632 return fi.name 633 } 634 635 func (fi *fileInfo) Size() int64 { 636 return 0 637 } 638 639 func (fi *fileInfo) Mode() fs.FileMode { 640 if fi.isDir { 641 return fs.ModeDir | 0755 642 } 643 return 0644 644 } 645 646 func (fi *fileInfo) ModTime() time.Time { 647 return fi.modTime 648 } 649 650 func (fi *fileInfo) IsDir() bool { 651 return fi.isDir 652 } 653 654 func (fi *fileInfo) Sys() any { 655 return nil 656 } 657 658 // TARToTempDir extracts a tar file into a temporary directory 659 // that can be used to traverse its contents recursively. 660 func TARToTempDir(reader io.Reader) (string, error) { 661 // Create a temporary directory for extracted files 662 tempDir, err := os.MkdirTemp("", "scalibr-archive-") 663 if err != nil { 664 return "", fmt.Errorf("failed to create temporary directory: %w", err) 665 } 666 667 // Extract the tar archive 668 var extractErr error 669 tr := tar.NewReader(reader) 670 loop: 671 for { 672 hdr, err := tr.Next() 673 if err == io.EOF { 674 break 675 } 676 if err != nil { 677 extractErr = fmt.Errorf("failed to read tar header: %w", err) 678 break 679 } 680 681 if symlink.TargetOutsideRoot("/", hdr.Name) { 682 extractErr = errors.New("tar contains invalid entries") 683 break 684 } 685 686 target := filepath.Join(tempDir, hdr.Name) 687 switch hdr.Typeflag { 688 case tar.TypeDir: 689 if err := os.MkdirAll(target, 0755); err != nil { 690 extractErr = fmt.Errorf("failed to create directory %s: %w", target, err) 691 break loop 692 } 693 case tar.TypeReg: 694 dir := filepath.Dir(target) 695 if err := os.MkdirAll(dir, 0755); err != nil { 696 extractErr = fmt.Errorf("failed to create directory %s: %w", dir, err) 697 break loop 698 } 699 outFile, err := os.Create(target) 700 if err != nil { 701 extractErr = fmt.Errorf("failed to create file %s: %w", target, err) 702 break loop 703 } 704 if _, err := io.Copy(outFile, tr); err != nil { 705 outFile.Close() 706 extractErr = fmt.Errorf("failed to copy file %s: %w", target, err) 707 break loop 708 } 709 outFile.Close() 710 default: 711 // Skip other types (symlinks, etc.) for now 712 } 713 } 714 715 if extractErr != nil { 716 os.Remove(tempDir) 717 return "", extractErr 718 } 719 720 return tempDir, nil 721 }