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  }