github.com/google/osv-scalibr@v0.4.1/fs/fs.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 fs provides a virtual filesystem interface for SCALIBR scans and related helper functions.
    16  package fs
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"os"
    24  	"path/filepath"
    25  )
    26  
    27  // FS is a filesystem interface that allows the opening of files, reading of
    28  // directories, and performing stat on files.
    29  //
    30  // FS implementations may return ErrNotImplemented for `Open`, `ReadDir` and `Stat`.
    31  // Extractor implementations must decide whether the error is fatal or can be ignored.
    32  //
    33  // FS implementations MUST implement io.ReaderAt for opened files to enable random access.
    34  type FS interface {
    35  	fs.FS
    36  	fs.ReadDirFS
    37  	fs.StatFS
    38  }
    39  
    40  // ScanRoot defines a root directory to start a scan from.
    41  // mounted to a local dir.
    42  type ScanRoot struct {
    43  	// A virtual filesystem for file access, rooted at the scan root.
    44  	FS FS
    45  	// The path of the scan root. Empty if this is a virtual filesystem and the
    46  	// scanning environment doesn't support the DirectFS requirement.
    47  	Path string
    48  }
    49  
    50  // IsVirtual returns true if the scan root represents the root of a virtual
    51  // filesystem, i.e. one with no real location on the disk of the scanned host.
    52  func (r *ScanRoot) IsVirtual() bool {
    53  	return r.Path == ""
    54  }
    55  
    56  // WithAbsolutePath returns a copy of the ScanRoot with the Path
    57  // set an absolute path.
    58  func (r *ScanRoot) WithAbsolutePath() (*ScanRoot, error) {
    59  	if r.Path == "" {
    60  		// Virtual-only filesystem
    61  		return &ScanRoot{FS: r.FS, Path: r.Path}, nil
    62  	}
    63  	absroot, err := filepath.Abs(r.Path)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return &ScanRoot{FS: r.FS, Path: absroot}, nil
    68  }
    69  
    70  // DirFS returns an FS implementation that accesses the real filesystem at the given root.
    71  func DirFS(root string) FS {
    72  	return os.DirFS(root).(FS)
    73  }
    74  
    75  // RealFSScanRoots returns a one-element ScanRoot array representing the given
    76  // root path on the real filesystem SCALIBR is running on.
    77  func RealFSScanRoots(path string) []*ScanRoot {
    78  	return []*ScanRoot{RealFSScanRoot(path)}
    79  }
    80  
    81  // RealFSScanRoot returns a ScanRoot array the given root path on the real
    82  // filesystem SCALIBR is running on.
    83  func RealFSScanRoot(path string) *ScanRoot {
    84  	return &ScanRoot{FS: DirFS(path), Path: path}
    85  }
    86  
    87  // NewReaderAt converts an io.Reader into an io.ReaderAt.
    88  func NewReaderAt(ioReader io.Reader) (io.ReaderAt, error) {
    89  	r, ok := ioReader.(io.ReaderAt)
    90  	if ok {
    91  		return r, nil
    92  	}
    93  
    94  	// Fallback: In case ioReader does not implement ReadAt, we use a reader on byte buffer
    95  	// instead, which supports ReadAt. Note that in this case the whole file contents will be
    96  	// loaded into memory which might be expensive for large files.
    97  	buff := bytes.NewBuffer([]byte{})
    98  	_, err := io.Copy(buff, ioReader)
    99  	if err != nil {
   100  		return nil, fmt.Errorf("io.Copy(): %w", err)
   101  	}
   102  
   103  	return bytes.NewReader(buff.Bytes()), nil
   104  }
   105  
   106  // GetRealPath returns the real absolute path of the file on the scanning host's filesystem.
   107  // If the file is on a virtual filesystem (e.g. a remote container), it is first copied into a
   108  // temporary directory on the scanning host's filesystem. It's up to the caller to delete the
   109  // directory once they're done using it.
   110  //
   111  // Sample code to delete the directory:
   112  // ```
   113  // realPath := GetRealPath(...)
   114  // dir := filepath.Dir(realPath)
   115  //
   116  //	if err := os.RemoveAll(dir); err != nil {
   117  //	    log.Errorf("os.RemoveAll(%q): %w", dir, err)
   118  //	}
   119  func GetRealPath(root *ScanRoot, path string, reader io.Reader) (string, error) {
   120  	if !root.IsVirtual() {
   121  		return filepath.Join(root.Path, path), nil
   122  	}
   123  
   124  	// This is a virtual filesystem.
   125  	// Move the file to the scanning hosts's filesystem.
   126  	dir, err := os.MkdirTemp("", "scalibr-tmp")
   127  	if err != nil {
   128  		return "", err
   129  	}
   130  	realPath := filepath.Join(dir, "file")
   131  	f, err := os.Create(realPath)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  	defer f.Close()
   136  
   137  	if reader == nil {
   138  		reader, err = root.FS.Open(path)
   139  		if err != nil {
   140  			return "", err
   141  		}
   142  	}
   143  
   144  	_, err = io.Copy(f, reader)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	return realPath, nil
   150  }