diff --git a/artifact/image/layerscanning/image/image.go b/artifact/image/layerscanning/image/image.go index fd2f9d80..06bbfaac 100644 --- a/artifact/image/layerscanning/image/image.go +++ b/artifact/image/layerscanning/image/image.go @@ -47,6 +47,13 @@ const ( // since that is the maximum number of symlinks the os.Root API will handle. From the os.Root API, // "8 is __POSIX_SYMLOOP_MAX (the minimum allowed value for SYMLOOP_MAX), and a common limit". DefaultMaxSymlinkDepth = 6 + + // filePermission represents the permission bits for a file, which are minimal since files in the + // layer scanning use case are read-only. + filePermission = 0600 + // dirPermission represents the permission bits for a directory, which are minimal since + // directories in the layer scanning use case are read-only. + dirPermission = 0700 ) var ( @@ -83,8 +90,7 @@ func DefaultConfig() *Config { // image. Checks include: // // (1) MaxFileBytes is positive. -// (2) Requirer is not nil. -// (3) MaxSymlinkDepth is non-negative. +// (2) MaxSymlinkDepth is non-negative. func validateConfig(config *Config) error { if config.MaxFileBytes <= 0 { return fmt.Errorf("%w: max file bytes must be positive: %d", ErrInvalidConfig, config.MaxFileBytes) @@ -101,9 +107,8 @@ type Image struct { chainLayers []*chainLayer config *Config size int64 - root *os.Root - ExtractDir string BaseImageIndex int + contentBlob *os.File } // TopFS returns the filesystem of the top-most chainlayer of the image. All available files should @@ -129,7 +134,15 @@ func (img *Image) ChainLayers() ([]scalibrImage.ChainLayer, error) { // CleanUp removes the temporary directory used to store the image files. func (img *Image) CleanUp() error { - return os.RemoveAll(img.ExtractDir) + if img.contentBlob == nil { + return nil + } + + if err := img.contentBlob.Close(); err != nil { + log.Warnf("failed to close content blob: %v", err) + } + + return os.Remove(img.contentBlob.Name()) } // Size returns the size of the underlying directory of the image in bytes. @@ -192,20 +205,10 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) { return nil, fmt.Errorf("failed to initialize chain layers: %w", err) } - imageExtractionPath, err := os.MkdirTemp("", "osv-scalibr-image-scanning-*") - if err != nil { - return nil, fmt.Errorf("failed to create temporary directory: %w", err) - } - - // OpenRoot assumes that the provided directory is trusted. In this case, we created the - // imageExtractionPath directory, so it is indeed trusted. - root, err := os.OpenRoot(imageExtractionPath) + imageContentBlob, err := os.CreateTemp("", "image-blob-*") if err != nil { - return nil, fmt.Errorf("failed to open root directory: %w", err) + return nil, fmt.Errorf("failed to create image content file: %w", err) } - // Close the root directory at the end of the function, since no more files will be unpacked - // afterward. - defer root.Close() baseImageIndex, err := findBaseImageIndex(history) if err != nil { @@ -215,14 +218,13 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) { outputImage := &Image{ chainLayers: chainLayers, config: config, - root: root, - ExtractDir: imageExtractionPath, BaseImageIndex: baseImageIndex, + contentBlob: imageContentBlob, } // Add the root directory to each chain layer. If this is not done, then the virtual paths won't // be rooted, and traversal in the virtual filesystem will be broken. - if err := addRootDirectoryToChainLayers(outputImage.chainLayers, imageExtractionPath); err != nil { + if err := addRootDirectoryToChainLayers(outputImage.chainLayers); err != nil { return nil, handleImageError(outputImage, fmt.Errorf("failed to add root directory to chain layers: %w", err)) } @@ -240,13 +242,6 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) { continue } - layerDir := layerDirectory(i) - - // Create the chain layer directory if it doesn't exist. - if err := root.Mkdir(layerDir, dirPermission); err != nil && !errors.Is(err, fs.ErrExist) { - return nil, handleImageError(outputImage, fmt.Errorf("failed to create chain layer directory: %w", err)) - } - if v1LayerIndex < 0 { return nil, handleImageError(outputImage, fmt.Errorf("mismatch between v1 layers and chain layers, on v1 layer index %d, but only %d v1 layers", v1LayerIndex, len(v1Layers))) } @@ -265,12 +260,10 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) { defer layerReader.Close() tarReader := tar.NewReader(layerReader) - layerSize, err := fillChainLayersWithFilesFromTar(outputImage, tarReader, layerDir, chainLayersToFill) - if err != nil { + if err := fillChainLayersWithFilesFromTar(outputImage, tarReader, chainLayersToFill); err != nil { return fmt.Errorf("failed to fill chain layer with v1 layer tar: %w", err) } - outputImage.size += layerSize return nil }() @@ -286,16 +279,10 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) { // Helper functions // ======================================================== -func layerDirectory(layerIndex int) string { - return fmt.Sprintf("layer-%d", layerIndex) -} - // addRootDirectoryToChainLayers adds the root ("/") directory to each chain layer. -func addRootDirectoryToChainLayers(chainLayers []*chainLayer, extractDir string) error { - for i, chainLayer := range chainLayers { +func addRootDirectoryToChainLayers(chainLayers []*chainLayer) error { + for _, chainLayer := range chainLayers { err := chainLayer.fileNodeTree.Insert("/", &virtualFile{ - extractDir: extractDir, - layerDir: layerDirectory(i), virtualPath: "/", isWhiteout: false, mode: fs.ModeDir, @@ -430,23 +417,20 @@ func initializeChainLayers(v1Layers []v1.Layer, history []v1.History, maxSymlink // fillChainLayersWithFilÄesFromTar fills the chain layers with the files found in the tar. The // chainLayersToFill are the chain layers that will be filled with the files via the virtual // filesystem. -func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir string, chainLayersToFill []*chainLayer) (int64, error) { +func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, chainLayersToFill []*chainLayer) error { if len(chainLayersToFill) == 0 { - return 0, errors.New("no chain layers provided, this should not happen") + return errors.New("no chain layers provided, this should not happen") } currentChainLayer := chainLayersToFill[0] - // layerSize is the cumulative size of all the extracted files in the tar. - var layerSize int64 - for { header, err := tarReader.Next() if errors.Is(err, io.EOF) { break } if err != nil { - return 0, fmt.Errorf("could not read tar: %w", err) + return fmt.Errorf("could not read tar: %w", err) } // Some tools prepend everything with "./", so if we don't path.Clean the name, we may have @@ -501,11 +485,11 @@ func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir var newVirtualFile *virtualFile switch header.Typeflag { case tar.TypeDir: - newVirtualFile, err = img.handleDir(virtualPath, layerDir, header, isWhiteout) + newVirtualFile = img.handleDir(virtualPath, header, isWhiteout) case tar.TypeReg: - newVirtualFile, err = img.handleFile(virtualPath, layerDir, tarReader, header, isWhiteout) + newVirtualFile, err = img.handleFile(virtualPath, tarReader, header, isWhiteout) case tar.TypeSymlink, tar.TypeLink: - newVirtualFile, err = img.handleSymlink(virtualPath, layerDir, header, isWhiteout) + newVirtualFile, err = img.handleSymlink(virtualPath, header, isWhiteout) default: log.Warnf("unsupported file type: %v, path: %s", header.Typeflag, header.Name) continue @@ -518,14 +502,12 @@ func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir log.Warnf("failed to handle tar entry with path %s: %w", virtualPath, err) continue } - return 0, fmt.Errorf("failed to handle tar entry with path %s: %w", virtualPath, err) + return fmt.Errorf("failed to handle tar entry with path %s: %w", virtualPath, err) } - layerSize += header.Size - // If the virtual path has any directories and those directories have not been populated, then // populate them with file nodes. - populateEmptyDirectoryNodes(virtualPath, layerDir, img.ExtractDir, chainLayersToFill) + populateEmptyDirectoryNodes(virtualPath, chainLayersToFill) // In each outer loop, a layer is added to each relevant output chainLayer slice. Because the // outer loop is looping backwards (latest layer first), we ignore any files that are already in @@ -536,13 +518,13 @@ func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir layer := currentChainLayer.latestLayer.(*Layer) _ = layer.fileNodeTree.Insert(virtualPath, newVirtualFile) } - return layerSize, nil + return nil } // populateEmptyDirectoryNodes populates the chain layers with file nodes for any directory paths // that do not have an associated file node. This is done by creating a file node for each directory // in the virtual path and then filling the chain layers with that file node. -func populateEmptyDirectoryNodes(virtualPath, layerDir, extractDir string, chainLayersToFill []*chainLayer) { +func populateEmptyDirectoryNodes(virtualPath string, chainLayersToFill []*chainLayer) { currentChainLayer := chainLayersToFill[0] runningDir := "/" @@ -557,8 +539,6 @@ func populateEmptyDirectoryNodes(virtualPath, layerDir, extractDir string, chain } node := &virtualFile{ - extractDir: extractDir, - layerDir: layerDir, virtualPath: runningDir, isWhiteout: false, mode: fs.ModeDir, @@ -569,7 +549,7 @@ func populateEmptyDirectoryNodes(virtualPath, layerDir, extractDir string, chain // handleSymlink returns the symlink header mode. Symlinks are handled by creating a virtual file // with the symlink mode with additional metadata. -func (img *Image) handleSymlink(virtualPath, layerDir string, header *tar.Header, isWhiteout bool) (*virtualFile, error) { +func (img *Image) handleSymlink(virtualPath string, header *tar.Header, isWhiteout bool) (*virtualFile, error) { targetPath := filepath.ToSlash(header.Linkname) if targetPath == "" { return nil, errors.New("symlink header has no target path") @@ -586,8 +566,6 @@ func (img *Image) handleSymlink(virtualPath, layerDir string, header *tar.Header } return &virtualFile{ - extractDir: img.ExtractDir, - layerDir: layerDir, virtualPath: virtualPath, targetPath: targetPath, isWhiteout: isWhiteout, @@ -596,46 +574,23 @@ func (img *Image) handleSymlink(virtualPath, layerDir string, header *tar.Header } // handleDir creates the directory specified by path, if it doesn't exist. -func (img *Image) handleDir(virtualPath, layerDir string, header *tar.Header, isWhiteout bool) (*virtualFile, error) { - realFilePath := filepath.Join(img.ExtractDir, layerDir, filepath.FromSlash(virtualPath)) - if _, err := img.root.Stat(filepath.Join(layerDir, filepath.FromSlash(virtualPath))); err != nil { - if err := os.MkdirAll(realFilePath, dirPermission); err != nil { - return nil, fmt.Errorf("failed to create directory with realFilePath %s: %w", realFilePath, err) - } - } - +func (img *Image) handleDir(virtualPath string, header *tar.Header, isWhiteout bool) *virtualFile { fileInfo := header.FileInfo() return &virtualFile{ - extractDir: img.ExtractDir, - layerDir: layerDir, virtualPath: virtualPath, isWhiteout: isWhiteout, mode: fileInfo.Mode() | fs.ModeDir, size: fileInfo.Size(), modTime: fileInfo.ModTime(), - }, nil + } } // handleFile creates the file specified by path, and then copies the contents of the tarReader into // the file. The function returns a virtual file, which is meant to represent the file in a virtual // filesystem. -func (img *Image) handleFile(virtualPath, layerDir string, tarReader *tar.Reader, header *tar.Header, isWhiteout bool) (*virtualFile, error) { - realFilePath := filepath.Join(img.ExtractDir, layerDir, filepath.FromSlash(virtualPath)) - parentDirectory := filepath.Dir(realFilePath) - if err := os.MkdirAll(parentDirectory, dirPermission); err != nil { - return nil, fmt.Errorf("failed to create parent directory %s: %w", parentDirectory, err) - } - - // Write all files as read/writable by the current user, inaccessible by anyone else. Actual - // permission bits are stored in FileNode. - f, err := img.root.OpenFile(filepath.Join(layerDir, filepath.FromSlash(virtualPath)), os.O_CREATE|os.O_RDWR, filePermission) - if err != nil { - return nil, err - } - defer f.Close() - - numBytes, err := io.Copy(f, io.LimitReader(tarReader, img.config.MaxFileBytes)) +func (img *Image) handleFile(virtualPath string, tarReader *tar.Reader, header *tar.Header, isWhiteout bool) (*virtualFile, error) { + numBytes, err := img.contentBlob.ReadFrom(io.LimitReader(tarReader, img.config.MaxFileBytes)) if numBytes >= img.config.MaxFileBytes || errors.Is(err, io.EOF) { return nil, ErrFileReadLimitExceeded } @@ -644,16 +599,20 @@ func (img *Image) handleFile(virtualPath, layerDir string, tarReader *tar.Reader return nil, fmt.Errorf("unable to copy file: %w", err) } + // Record the offset of the file in the content blob before adding the new bytes. The offset is + // the current size of the content blob. + offset := img.size + // Update the image size with the number of bytes read into the content blob. + img.size += numBytes fileInfo := header.FileInfo() return &virtualFile{ - extractDir: img.ExtractDir, - layerDir: layerDir, virtualPath: virtualPath, isWhiteout: isWhiteout, mode: fileInfo.Mode(), - size: fileInfo.Size(), modTime: fileInfo.ModTime(), + size: numBytes, + reader: io.NewSectionReader(img.contentBlob, offset, numBytes), }, nil } diff --git a/artifact/image/layerscanning/image/image_test.go b/artifact/image/layerscanning/image/image_test.go index c094a676..1959bf1f 100644 --- a/artifact/image/layerscanning/image/image_test.go +++ b/artifact/image/layerscanning/image/image_test.go @@ -1238,8 +1238,6 @@ func TestTopFS(t *testing.T) { fileNodeTree: func() *Node { root := NewNode() _ = root.Insert("/", &virtualFile{ - extractDir: "", - layerDir: "", virtualPath: "/", isWhiteout: false, mode: fs.ModeDir | dirPermission, diff --git a/artifact/image/layerscanning/image/layer_test.go b/artifact/image/layerscanning/image/layer_test.go index 60384fb4..600437ba 100644 --- a/artifact/image/layerscanning/image/layer_test.go +++ b/artifact/image/layerscanning/image/layer_test.go @@ -17,8 +17,6 @@ package image import ( "errors" "io/fs" - "os" - "path" "slices" "strings" "testing" @@ -76,22 +74,12 @@ func TestConvertV1Layer(t *testing.T) { } func TestChainLayerFS(t *testing.T) { - testDir := func() string { - dir := t.TempDir() - _ = os.WriteFile(path.Join(dir, "file1"), []byte("file1"), 0600) - return dir - }() - root := &virtualFile{ - extractDir: testDir, - layerDir: "", virtualPath: "/", isWhiteout: false, mode: fs.ModeDir | dirPermission, } file1 := &virtualFile{ - extractDir: testDir, - layerDir: "", virtualPath: "/file1", isWhiteout: false, mode: filePermission, @@ -157,7 +145,7 @@ func TestChainLayerFS(t *testing.T) { } func TestChainFSOpen(t *testing.T) { - populatedChainFS, extractDir := setUpChainFS(t, 3) + populatedChainFS := setUpChainFS(t, DefaultMaxSymlinkDepth) tests := []struct { name string @@ -185,8 +173,6 @@ func TestChainFSOpen(t *testing.T) { chainfs: populatedChainFS, path: "/", wantVirtualFile: &virtualFile{ - extractDir: extractDir, - layerDir: "layer1", virtualPath: "/", isWhiteout: false, mode: fs.ModeDir | dirPermission, @@ -197,8 +183,6 @@ func TestChainFSOpen(t *testing.T) { chainfs: populatedChainFS, path: "/dir1", wantVirtualFile: &virtualFile{ - extractDir: extractDir, - layerDir: "layer1", virtualPath: "/dir1", isWhiteout: false, mode: fs.ModeDir | dirPermission, @@ -209,8 +193,6 @@ func TestChainFSOpen(t *testing.T) { chainfs: populatedChainFS, path: "/baz", wantVirtualFile: &virtualFile{ - extractDir: extractDir, - layerDir: "layer1", virtualPath: "/baz", isWhiteout: false, mode: filePermission, @@ -221,8 +203,6 @@ func TestChainFSOpen(t *testing.T) { chainfs: populatedChainFS, path: "/dir1/foo", wantVirtualFile: &virtualFile{ - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/dir1/foo", isWhiteout: false, mode: filePermission, @@ -234,8 +214,6 @@ func TestChainFSOpen(t *testing.T) { path: "/symlink1", // The node the symlink points to is expected. wantVirtualFile: &virtualFile{ - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/dir2/bar", isWhiteout: false, mode: filePermission, @@ -247,8 +225,6 @@ func TestChainFSOpen(t *testing.T) { path: "/symlink2", // The node the symlink points to is expected. wantVirtualFile: &virtualFile{ - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/dir2/bar", isWhiteout: false, mode: filePermission, @@ -261,8 +237,11 @@ func TestChainFSOpen(t *testing.T) { wantErr: fs.ErrNotExist, }, { - name: "error opening symlink due to depth exceeded", - chainfs: populatedChainFS, + name: "error opening symlink due to depth exceeded", + chainfs: func() FS { + chainfs := setUpChainFS(t, 3) + return chainfs + }(), path: "/symlink4", wantErr: ErrSymlinkDepthExceeded, }, @@ -298,7 +277,7 @@ func TestChainFSOpen(t *testing.T) { } func TestChainFSStat(t *testing.T) { - populatedChainFS, _ := setUpChainFS(t, DefaultMaxSymlinkDepth) + populatedChainFS := setUpChainFS(t, DefaultMaxSymlinkDepth) tests := []struct { name string @@ -355,7 +334,7 @@ func TestChainFSStat(t *testing.T) { } func TestChainFSReadDir(t *testing.T) { - populatedChainFS, extractDir := setUpChainFS(t, DefaultMaxSymlinkDepth) + populatedChainFS := setUpChainFS(t, DefaultMaxSymlinkDepth) tests := []struct { name string @@ -391,93 +370,69 @@ func TestChainFSReadDir(t *testing.T) { // wh.foobar is a whiteout file and should not be returned. wantVirtualFiles: []*virtualFile{ { - extractDir: extractDir, - layerDir: "layer1", virtualPath: "/dir1", isWhiteout: false, mode: fs.ModeDir | dirPermission, }, { - extractDir: extractDir, - layerDir: "layer1", virtualPath: "/baz", isWhiteout: false, mode: filePermission, }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/dir2", isWhiteout: false, mode: fs.ModeDir | dirPermission, }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink1", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/dir2/bar", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink2", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink1", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink3", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink2", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink4", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink3", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink-cycle1", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink-cycle2", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink-cycle2", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink-cycle3", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink-cycle3", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink-cycle1", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink-to-nonexistent-file", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/nonexistent-file", }, { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/symlink-to-dir", isWhiteout: false, mode: fs.ModeSymlink, @@ -491,8 +446,6 @@ func TestChainFSReadDir(t *testing.T) { path: "/dir1", wantVirtualFiles: []*virtualFile{ { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/dir1/foo", isWhiteout: false, mode: filePermission, @@ -511,8 +464,6 @@ func TestChainFSReadDir(t *testing.T) { path: "/symlink-to-dir", wantVirtualFiles: []*virtualFile{ { - extractDir: extractDir, - layerDir: "layer2", virtualPath: "/dir2/bar", isWhiteout: false, mode: filePermission, @@ -586,9 +537,8 @@ func setUpEmptyChainFS(t *testing.T) FS { // setUpChainFS creates a chainFS with a populated tree and creates the corresponding files in a // temporary directory. It returns the chainFS and the temporary directory path. -func setUpChainFS(t *testing.T, maxSymlinkDepth int) (FS, string) { +func setUpChainFS(t *testing.T, maxSymlinkDepth int) FS { t.Helper() - tempDir := t.TempDir() chainfs := FS{ tree: NewNode(), @@ -598,122 +548,90 @@ func setUpChainFS(t *testing.T, maxSymlinkDepth int) (FS, string) { vfsMap := map[string]*virtualFile{ // Layer 1 files / directories "/": &virtualFile{ - extractDir: tempDir, - layerDir: "layer1", virtualPath: "/", isWhiteout: false, mode: fs.ModeDir | dirPermission, }, "/dir1": &virtualFile{ - extractDir: tempDir, - layerDir: "layer1", virtualPath: "/dir1", isWhiteout: false, mode: fs.ModeDir | dirPermission, }, "/baz": &virtualFile{ - extractDir: tempDir, - layerDir: "layer1", virtualPath: "/baz", isWhiteout: false, mode: filePermission, }, // Layer 2 files / directories "/dir1/foo": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "/dir1/foo", isWhiteout: false, mode: filePermission, }, "/dir2": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "/dir2", isWhiteout: false, mode: fs.ModeDir | dirPermission, }, "/dir2/bar": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "/dir2/bar", isWhiteout: false, mode: filePermission, }, "/wh.foobar": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "/wh.foobar", isWhiteout: true, mode: filePermission, }, "/symlink1": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "/symlink1", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/dir2/bar", }, "/symlink2": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink2", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink1", }, "/symlink3": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink3", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink2", }, "/symlink4": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink4", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink3", }, "/symlink-to-dir": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink-to-dir", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/dir2", }, "/symlink-to-nonexistent-file": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink-to-nonexistent-file", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/nonexistent-file", }, "/symlink-cycle1": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink-cycle1", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink-cycle2", }, "/symlink-cycle2": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink-cycle2", isWhiteout: false, mode: fs.ModeSymlink, targetPath: "/symlink-cycle3", }, "/symlink-cycle3": &virtualFile{ - extractDir: tempDir, - layerDir: "layer2", virtualPath: "symlink-cycle3", isWhiteout: false, mode: fs.ModeSymlink, @@ -723,15 +641,7 @@ func setUpChainFS(t *testing.T, maxSymlinkDepth int) (FS, string) { for path, vf := range vfsMap { _ = chainfs.tree.Insert(path, vf) - - if vf.IsDir() { - _ = os.MkdirAll(vf.RealFilePath(), dirPermission) - } else { - if vf.mode == fs.ModeSymlink { - continue - } - _ = os.WriteFile(vf.RealFilePath(), []byte(path), filePermission) - } } - return chainfs, tempDir + + return chainfs } diff --git a/artifact/image/layerscanning/image/pathtree_test.go b/artifact/image/layerscanning/image/pathtree_test.go index 15f38723..b28097d0 100644 --- a/artifact/image/layerscanning/image/pathtree_test.go +++ b/artifact/image/layerscanning/image/pathtree_test.go @@ -16,7 +16,6 @@ package image import ( "io/fs" - "path" "strings" "testing" @@ -36,16 +35,16 @@ func testTree(t *testing.T) *Node { t.Helper() tree := NewNode() - assertNoError(t, tree.Insert("/", &virtualFile{virtualPath: "/", layerDir: "/layer0", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a", &virtualFile{virtualPath: "/a", layerDir: "/layer1", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/b", &virtualFile{virtualPath: "/a/b", layerDir: "/layer1", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/b/c", &virtualFile{virtualPath: "/a/b/c", layerDir: "/layer1", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/b/d", &virtualFile{virtualPath: "/a/b/d", layerDir: "/layer1", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/b/d/f", &virtualFile{virtualPath: "/a/b/d/f", layerDir: "/layer1", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/e", &virtualFile{virtualPath: "/a/e", layerDir: "/layer2", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/e/f", &virtualFile{virtualPath: "/a/e/f", layerDir: "/layer2", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/a/g", &virtualFile{virtualPath: "a/g", layerDir: "/layer3", mode: fs.ModeDir})) - assertNoError(t, tree.Insert("/x/y/z", &virtualFile{virtualPath: "/x/y/z", layerDir: "/layer4", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/", &virtualFile{virtualPath: "/", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a", &virtualFile{virtualPath: "/a", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/b", &virtualFile{virtualPath: "/a/b", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/b/c", &virtualFile{virtualPath: "/a/b/c", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/b/d", &virtualFile{virtualPath: "/a/b/d", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/b/d/f", &virtualFile{virtualPath: "/a/b/d/f", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/e", &virtualFile{virtualPath: "/a/e", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/e/f", &virtualFile{virtualPath: "/a/e/f", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/a/g", &virtualFile{virtualPath: "/a/g", mode: fs.ModeDir})) + assertNoError(t, tree.Insert("/x/y/z", &virtualFile{virtualPath: "/x/y/z", mode: fs.ModeDir})) return tree } @@ -130,13 +129,13 @@ func TestNode_Get(t *testing.T) { name: "root node", tree: testTree(t), key: "/", - want: &virtualFile{virtualPath: "/", layerDir: "/layer0", mode: fs.ModeDir}, + want: &virtualFile{virtualPath: "/", mode: fs.ModeDir}, }, { name: "multiple nodes", tree: testTree(t), key: "/a/b/c", - want: &virtualFile{virtualPath: "/a/b/c", layerDir: "/layer1", mode: fs.ModeDir}, + want: &virtualFile{virtualPath: "/a/b/c", mode: fs.ModeDir}, }, { name: "nonexistent node", @@ -185,7 +184,7 @@ func TestNode_GetChildren(t *testing.T) { key: "/", // /x is not included since value is nil. want: []*virtualFile{ - {virtualPath: "/a", layerDir: "/layer1", mode: fs.ModeDir}, + {virtualPath: "/a", mode: fs.ModeDir}, }, }, { @@ -193,8 +192,8 @@ func TestNode_GetChildren(t *testing.T) { tree: testTree(t), key: "/a/b", want: []*virtualFile{ - {virtualPath: "/a/b/c", layerDir: "/layer1", mode: fs.ModeDir}, - {virtualPath: "/a/b/d", layerDir: "/layer1", mode: fs.ModeDir}, + {virtualPath: "/a/b/c", mode: fs.ModeDir}, + {virtualPath: "/a/b/d", mode: fs.ModeDir}, }, }, { @@ -208,7 +207,7 @@ func TestNode_GetChildren(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got := tt.tree.GetChildren(tt.key) if diff := cmp.Diff(tt.want, got, cmp.AllowUnexported(virtualFile{}), cmpopts.SortSlices(func(a, b *virtualFile) bool { - return strings.Compare(a.RealFilePath(), b.RealFilePath()) < 0 + return strings.Compare(a.virtualPath, b.virtualPath) < 0 })); diff != "" { t.Errorf("Node.GetChildren() (-want +got): %v", diff) } @@ -248,16 +247,16 @@ func TestNode_Walk(t *testing.T) { name: "multiple nodes", tree: testTree(t), want: []keyValue{ - {key: "", val: "/layer0"}, - {key: "/a", val: "/layer1/a"}, - {key: "/a/b", val: "/layer1/a/b"}, - {key: "/a/b/c", val: "/layer1/a/b/c"}, - {key: "/a/b/d", val: "/layer1/a/b/d"}, - {key: "/a/b/d/f", val: "/layer1/a/b/d/f"}, - {key: "/a/e", val: "/layer2/a/e"}, - {key: "/a/e/f", val: "/layer2/a/e/f"}, - {key: "/a/g", val: "/layer3/a/g"}, - {key: "/x/y/z", val: "/layer4/x/y/z"}, + {key: "", val: "/"}, + {key: "/a", val: "/a"}, + {key: "/a/b", val: "/a/b"}, + {key: "/a/b/c", val: "/a/b/c"}, + {key: "/a/b/d", val: "/a/b/d"}, + {key: "/a/b/d/f", val: "/a/b/d/f"}, + {key: "/a/e", val: "/a/e"}, + {key: "/a/e/f", val: "/a/e/f"}, + {key: "/a/g", val: "/a/g"}, + {key: "/x/y/z", val: "/x/y/z"}, }, }, } @@ -265,7 +264,7 @@ func TestNode_Walk(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got := []keyValue{} err := tt.tree.Walk(func(p string, vf *virtualFile) error { - got = append(got, keyValue{key: p, val: path.Join(vf.layerDir, vf.virtualPath)}) + got = append(got, keyValue{key: p, val: vf.virtualPath}) return nil }) if err != nil { diff --git a/artifact/image/layerscanning/image/virtual_file.go b/artifact/image/layerscanning/image/virtual_file.go index 679a2b48..e83c78f6 100644 --- a/artifact/image/layerscanning/image/virtual_file.go +++ b/artifact/image/layerscanning/image/virtual_file.go @@ -15,31 +15,23 @@ package image import ( + "errors" + "io" "io/fs" - "os" "path" - "path/filepath" "time" ) -const ( - // filePermission represents the permission bits for a file, which are minimal since files in the - // layer scanning use case are read-only. - filePermission = 0600 - // dirPermission represents the permission bits for a directory, which are minimal since - // directories in the layer scanning use case are read-only. - dirPermission = 0700 -) - // virtualFile represents a file in a virtual filesystem. type virtualFile struct { - // extractDir and layerDir are used to construct the real file path of the virtualFile. - extractDir string - layerDir string + // reader provides `Read()`, `Seek()`, `ReadAt()` and `Size()` operations on + // the content of this file similar to `io.SectionReader`. + // The file can still be read after closing as closing only resets the cursor. + // If the file is a directory, operations succeed with 0 byte reads. + reader *io.SectionReader // isWhiteout is true if the virtualFile represents a whiteout file isWhiteout bool - // virtualPath is the path of the virtualFile in the virtual filesystem. virtualPath string // targetPath is reserved for symlinks. It is the path that the symlink points to. @@ -49,16 +41,25 @@ type virtualFile struct { size int64 mode fs.FileMode modTime time.Time - - // file is the file object for the real file referred to by the virtualFile. - file *os.File } // ======================================================== // fs.File METHODS // ======================================================== -// Stat returns the file info of real file referred by the virtualFile. +// validateVirtualFile validates that the virtualFile is in a valid state to be read from. +func validateVirtualFile(f *virtualFile) error { + if f.isWhiteout { + return fs.ErrNotExist + } + if f.reader == nil { + return errors.New("virtual file does not reference any content") + } + + return nil +} + +// Stat returns the virtualFile itself since it implements the fs.FileInfo interface. func (f *virtualFile) Stat() (fs.FileInfo, error) { if f.isWhiteout { return nil, fs.ErrNotExist @@ -66,61 +67,49 @@ func (f *virtualFile) Stat() (fs.FileInfo, error) { return f, nil } -// Read reads the real file referred to by the virtualFile. +// Read reads the real file contents referred to by the virtualFile. func (f *virtualFile) Read(b []byte) (n int, err error) { - if f.isWhiteout { - return 0, fs.ErrNotExist - } - if f.file == nil { - f.file, err = os.Open(f.RealFilePath()) + if f.IsDir() { + return 0, nil } - if err != nil { + if err := validateVirtualFile(f); err != nil { return 0, err } - return f.file.Read(b) + return f.reader.Read(b) } -// ReadAt reads the real file referred to by the virtualFile at a specific offset. +// ReadAt reads the real file contents referred to by the virtualFile at a specific offset. func (f *virtualFile) ReadAt(b []byte, off int64) (n int, err error) { - if f.isWhiteout { - return 0, fs.ErrNotExist + if f.IsDir() { + return 0, nil } - if f.file == nil { - f.file, err = os.Open(f.RealFilePath()) - } - if err != nil { + if err := validateVirtualFile(f); err != nil { return 0, err } - return f.file.ReadAt(b, off) + return f.reader.ReadAt(b, off) } +// Seek sets the read cursor of the file contents represented by the virtualFile. func (f *virtualFile) Seek(offset int64, whence int) (n int64, err error) { - if f.isWhiteout { - return 0, fs.ErrNotExist + if f.IsDir() { + return 0, nil } - if f.file == nil { - f.file, err = os.Open(f.RealFilePath()) - } - if err != nil { + if err := validateVirtualFile(f); err != nil { return 0, err } - return f.file.Seek(offset, whence) + return f.reader.Seek(offset, whence) } -// Close closes the real file referred to by the virtualFile and resets the file field. +// Close resets the read cursor of the file contents represented by the virtualFile. func (f *virtualFile) Close() error { - if f.file != nil { - err := f.file.Close() - f.file = nil + if f.IsDir() { + return nil + } + if err := validateVirtualFile(f); err != nil { return err } - return nil -} - -// RealFilePath returns the real file path of the virtualFile. This is the concatenation of the -// root image extract directory, origin layer ID, and the virtual path. -func (f *virtualFile) RealFilePath() string { - return filepath.Join(f.extractDir, f.layerDir, filepath.FromSlash(f.virtualPath)) + _, err := f.reader.Seek(0, io.SeekStart) + return err } // ======================================================== diff --git a/artifact/image/layerscanning/image/virtual_file_test.go b/artifact/image/layerscanning/image/virtual_file_test.go index c067b34c..4ab9e82e 100644 --- a/artifact/image/layerscanning/image/virtual_file_test.go +++ b/artifact/image/layerscanning/image/virtual_file_test.go @@ -18,8 +18,7 @@ import ( "io" "io/fs" "os" - "path" - "path/filepath" + "strings" "testing" "time" @@ -29,29 +28,21 @@ import ( var ( rootDirectory = &virtualFile{ - extractDir: "/tmp/extract", - layerDir: "layer1", virtualPath: "/", isWhiteout: false, mode: fs.ModeDir | dirPermission, } rootFile = &virtualFile{ - extractDir: "/tmp/extract", - layerDir: "layer1", virtualPath: "/bar", isWhiteout: false, mode: filePermission, } nonRootDirectory = &virtualFile{ - extractDir: "/tmp/extract", - layerDir: "layer1", virtualPath: "/dir1/dir2", isWhiteout: false, mode: fs.ModeDir | dirPermission, } nonRootFile = &virtualFile{ - extractDir: "/tmp/extract", - layerDir: "layer1", virtualPath: "/dir1/foo", isWhiteout: false, mode: filePermission, @@ -62,8 +53,6 @@ var ( func TestStat(t *testing.T) { baseTime := time.Now() regularVirtualFile := &virtualFile{ - extractDir: "tempDir", - layerDir: "", virtualPath: "/bar", isWhiteout: false, mode: filePermission, @@ -71,8 +60,6 @@ func TestStat(t *testing.T) { modTime: baseTime, } symlinkVirtualFile := &virtualFile{ - extractDir: "tempDir", - layerDir: "", virtualPath: "/symlink-to-bar", targetPath: "/bar", isWhiteout: false, @@ -81,8 +68,6 @@ func TestStat(t *testing.T) { modTime: baseTime, } whiteoutVirtualFile := &virtualFile{ - extractDir: "tempDir", - layerDir: "", virtualPath: "/bar", isWhiteout: true, mode: filePermission, @@ -151,250 +136,225 @@ func TestStat(t *testing.T) { } func TestRead(t *testing.T) { - const bufferSize = 20 - - tempDir := t.TempDir() - _ = os.WriteFile(path.Join(tempDir, "bar"), []byte("bar"), 0600) + contentBlob := setupContentBlob(t, []string{"bar", "fuzz", "foo"}) + defer contentBlob.Close() + defer os.Remove(contentBlob.Name()) - _ = os.WriteFile(path.Join(tempDir, "baz"), []byte("baz"), 0600) - openedRootFile, err := os.OpenFile(path.Join(tempDir, "baz"), os.O_RDONLY, filePermission) - if err != nil { - t.Fatalf("Failed to open file: %v", err) - } - // Close the file after the test. The file should be closed via the virtualFile.Close method, - // however, this test explicitly closes the file since the virtualFile.Close method is tested in a - // separate test. - defer openedRootFile.Close() + barSize := int64(len([]byte("bar"))) + fuzzSize := int64(len([]byte("fuzz"))) + fooSize := int64(len([]byte("foo"))) - _ = os.MkdirAll(path.Join(tempDir, "dir1"), 0700) - _ = os.WriteFile(path.Join(tempDir, "dir1/foo"), []byte("foo"), 0600) - - unopenedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", + barVirtualFile := &virtualFile{ virtualPath: "/bar", isWhiteout: false, mode: filePermission, + size: barSize, + reader: io.NewSectionReader(contentBlob, 0, barSize), } - openedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/baz", + fuzzVirtualFile := &virtualFile{ + virtualPath: "/fuzz", isWhiteout: false, mode: filePermission, - file: openedRootFile, + size: fuzzSize, + reader: io.NewSectionReader(contentBlob, barSize, fuzzSize), } - nonRootVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", + nonRootFooVirtualFile := &virtualFile{ virtualPath: "/dir1/foo", isWhiteout: false, mode: filePermission, - } - nonexistentVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/dir1/xyz", - isWhiteout: false, - mode: filePermission, + size: fooSize, + reader: io.NewSectionReader(contentBlob, barSize+fuzzSize, fooSize), } whiteoutVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", virtualPath: "/dir1/abc", isWhiteout: true, mode: filePermission, } + dirVirtualFile := &virtualFile{ + virtualPath: "/dir1", + isWhiteout: false, + mode: fs.ModeDir | dirPermission, + } tests := []struct { - name string - node *virtualFile - want string - wantErr bool + name string + vf *virtualFile + want string + wantSize int64 + wantErr bool }{ { - name: "unopened root file", - node: unopenedVirtualFile, - want: "bar", + name: "unopened root file", + vf: barVirtualFile, + want: "bar", + wantSize: 3, }, { - name: "opened root file", - node: openedVirtualFile, - want: "baz", + name: "opened root file", + vf: fuzzVirtualFile, + want: "fuzz", + wantSize: 4, }, { - name: "non-root file", - node: nonRootVirtualFile, - want: "foo", + name: "non-root file", + vf: nonRootFooVirtualFile, + want: "foo", + wantSize: 3, }, { - name: "nonexistent file", - node: nonexistentVirtualFile, + name: "whiteout file", + vf: whiteoutVirtualFile, wantErr: true, }, { - name: "whiteout file", - node: whiteoutVirtualFile, - wantErr: true, + name: "dir file", + vf: dirVirtualFile, + want: "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - gotBytes := make([]byte, bufferSize) - gotNumBytesRead, gotErr := tc.node.Read(gotBytes) + gotBytes := make([]byte, tc.wantSize) + gotNumBytesRead, gotErr := tc.vf.Read(gotBytes) if gotErr != nil { if tc.wantErr { return } - t.Fatalf("Read(%v) returned error: %v", tc.node, gotErr) + t.Fatalf("Read(%v) returned error: %v", tc.vf, gotErr) } gotContent := string(gotBytes[:gotNumBytesRead]) if gotContent != tc.want { - t.Errorf("Read(%v) = %v, want: %v", tc.node, gotContent, tc.want) + t.Errorf("Read(%v) = %v, want: %v", tc.vf, gotContent, tc.want) } // Close the file. The Close method is tested in a separate test. - _ = tc.node.Close() + _ = tc.vf.Close() }) } } func TestReadAt(t *testing.T) { - const bufferSize = 20 - - tempDir := t.TempDir() - _ = os.WriteFile(path.Join(tempDir, "bar"), []byte("bar"), 0600) + bufferSize := 20 + contentBlob := setupContentBlob(t, []string{"bar", "fuzz", "foo"}) + defer contentBlob.Close() + defer os.Remove(contentBlob.Name()) - _ = os.WriteFile(path.Join(tempDir, "baz"), []byte("baz"), 0600) - openedRootFile, err := os.OpenFile(path.Join(tempDir, "baz"), os.O_RDONLY, filePermission) - if err != nil { - t.Fatalf("Failed to open file: %v", err) - } - // Close the file after the test. The file should be closed via the virtualFile.Close method, - // however, this test explicitly closes the file since the virtualFile.Close method is tested in a - // separate test. - defer openedRootFile.Close() + barSize := int64(len([]byte("bar"))) + fuzzSize := int64(len([]byte("fuzz"))) + fooSize := int64(len([]byte("foo"))) - _ = os.MkdirAll(path.Join(tempDir, "dir1"), 0700) - _ = os.WriteFile(path.Join(tempDir, "dir1/foo"), []byte("foo"), 0600) - - unopenedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", + barVirtualFile := &virtualFile{ virtualPath: "/bar", isWhiteout: false, mode: filePermission, + size: barSize, + reader: io.NewSectionReader(contentBlob, 0, barSize), } - openedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/baz", + fuzzVirtualFile := &virtualFile{ + virtualPath: "/fuzz", isWhiteout: false, mode: filePermission, - file: openedRootFile, + size: fuzzSize, + reader: io.NewSectionReader(contentBlob, barSize, fuzzSize), } - nonRootVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", + nonRootFooVirtualFile := &virtualFile{ virtualPath: "/dir1/foo", isWhiteout: false, mode: filePermission, - } - nonexistentVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/dir1/xyz", - isWhiteout: false, - mode: filePermission, + size: fooSize, + reader: io.NewSectionReader(contentBlob, barSize+fuzzSize, fooSize), } whiteoutVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", virtualPath: "/dir1/abc", isWhiteout: true, mode: filePermission, } + dirVirtualFile := &virtualFile{ + virtualPath: "/dir1", + isWhiteout: false, + mode: fs.ModeDir | dirPermission, + } tests := []struct { name string - node *virtualFile + vf *virtualFile offset int64 want string wantErr error }{ { name: "unopened root file", - node: unopenedVirtualFile, + vf: barVirtualFile, want: "bar", // All successful reads should return EOF wantErr: io.EOF, }, { name: "opened root file", - node: openedVirtualFile, - want: "baz", + vf: fuzzVirtualFile, + want: "fuzz", wantErr: io.EOF, }, { name: "opened root file at offset", - node: unopenedVirtualFile, + vf: barVirtualFile, offset: 2, want: "r", wantErr: io.EOF, }, { name: "opened root file at offset at the end of file", - node: unopenedVirtualFile, + vf: barVirtualFile, offset: 3, want: "", wantErr: io.EOF, }, { name: "opened root file at offset beyond the end of file", - node: unopenedVirtualFile, + vf: barVirtualFile, offset: 4, want: "", wantErr: io.EOF, }, { name: "non-root file", - node: nonRootVirtualFile, + vf: nonRootFooVirtualFile, want: "foo", wantErr: io.EOF, }, { name: "non-root file at offset", - node: nonRootVirtualFile, + vf: nonRootFooVirtualFile, offset: 1, want: "oo", wantErr: io.EOF, }, { - name: "nonexistent file", - node: nonexistentVirtualFile, + name: "whiteout file", + vf: whiteoutVirtualFile, wantErr: os.ErrNotExist, }, { - name: "whiteout file", - node: whiteoutVirtualFile, - wantErr: os.ErrNotExist, + name: "dir file", + vf: dirVirtualFile, + want: "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { gotBytes := make([]byte, bufferSize) - gotNumBytesRead, gotErr := tc.node.ReadAt(gotBytes, tc.offset) + gotNumBytesRead, gotErr := tc.vf.ReadAt(gotBytes, tc.offset) // Close the file. The Close method is tested in a separate test. - defer tc.node.Close() + defer tc.vf.Close() if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" { - t.Errorf("ReadAt(%v) returned unexpected error (-want +got): %v", tc.node, diff) + t.Errorf("ReadAt(%v) returned unexpected error (-want +got): %v", tc.vf, diff) return } gotContent := string(gotBytes[:gotNumBytesRead]) if gotContent != tc.want { - t.Errorf("ReadAt(%v) = %v, want: %v", tc.node, gotContent, tc.want) + t.Errorf("ReadAt(%v) = %v, want: %v", tc.vf, gotContent, tc.want) } }) } @@ -402,8 +362,18 @@ func TestReadAt(t *testing.T) { // Test for the Seek method func TestSeek(t *testing.T) { - tempDir := t.TempDir() - _ = os.WriteFile(path.Join(tempDir, "bar"), []byte("bar"), 0600) + contentBlob := setupContentBlob(t, []string{"foo"}) + defer contentBlob.Close() + defer os.Remove(contentBlob.Name()) + + fooSize := int64(len([]byte("foo"))) + virtualFile := &virtualFile{ + virtualPath: "/foo", + isWhiteout: false, + mode: filePermission, + size: fooSize, + reader: io.NewSectionReader(contentBlob, 0, fooSize), + } // Test seeking to different positions tests := []struct { @@ -452,14 +422,6 @@ func TestSeek(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // Create a virtualFile for the opened file - virtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/bar", - isWhiteout: false, - mode: filePermission, - } gotPos, err := virtualFile.Seek(tc.offset, tc.whence) _ = virtualFile.Close() if err != nil { @@ -473,43 +435,35 @@ func TestSeek(t *testing.T) { } func TestClose(t *testing.T) { - tempDir := t.TempDir() - _ = os.WriteFile(path.Join(tempDir, "bar"), []byte("bar"), 0600) + contentBlob := setupContentBlob(t, []string{"foo"}) + defer contentBlob.Close() + defer os.Remove(contentBlob.Name()) - unopenedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/bar", - isWhiteout: false, - mode: filePermission, - } - nonexistentVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/dir1/xyz", + fooSize := int64(len([]byte("foo"))) + + vf := &virtualFile{ + virtualPath: "/foo", isWhiteout: false, mode: filePermission, + size: int64(len([]byte("foo"))), + reader: io.NewSectionReader(contentBlob, 0, fooSize), } tests := []struct { name string - node *virtualFile + vf *virtualFile }{ { - name: "unopened root file", - node: unopenedVirtualFile, - }, - { - name: "nonexistent file", - node: nonexistentVirtualFile, + name: "close root file", + vf: vf, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - gotErr := tc.node.Close() + gotErr := tc.vf.Close() if gotErr != nil { - t.Fatalf("Read(%v) returned error: %v", tc.node, gotErr) + t.Fatalf("Read(%v) returned error: %v", tc.vf, gotErr) } }) } @@ -519,112 +473,72 @@ func TestReadingAfterClose(t *testing.T) { const bufferSize = 20 const readAndCloseEvents = 2 - tempDir := t.TempDir() - _ = os.WriteFile(path.Join(tempDir, "bar"), []byte("bar"), 0600) - _ = os.WriteFile(path.Join(tempDir, "baz"), []byte("baz"), 0600) - openedRootFile, err := os.OpenFile(path.Join(tempDir, "baz"), os.O_RDONLY, filePermission) - if err != nil { - t.Fatalf("Failed to open file: %v", err) - } + contentBlob := setupContentBlob(t, []string{"foo", "bar"}) + defer contentBlob.Close() + defer os.Remove(contentBlob.Name()) - unopenedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/bar", + fooSize := int64(len([]byte("foo"))) + barSize := int64(len([]byte("bar"))) + + fooVirtualFile := &virtualFile{ + virtualPath: "/foo", isWhiteout: false, mode: filePermission, + size: fooSize, + reader: io.NewSectionReader(contentBlob, 0, fooSize), } - openedVirtualFile := &virtualFile{ - extractDir: tempDir, - layerDir: "", - virtualPath: "/baz", + barVirtualFile := &virtualFile{ + virtualPath: "/bar", isWhiteout: false, mode: filePermission, - file: openedRootFile, + size: barSize, + reader: io.NewSectionReader(contentBlob, fooSize, barSize), } tests := []struct { name string - node *virtualFile + vf *virtualFile want string wantErr bool }{ { name: "unopened root file", - node: unopenedVirtualFile, - want: "bar", + vf: fooVirtualFile, + want: "foo", }, { name: "opened root file", - node: openedVirtualFile, - want: "baz", + vf: barVirtualFile, + want: "bar", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { for range readAndCloseEvents { gotBytes := make([]byte, bufferSize) - gotNumBytesRead, gotErr := tc.node.Read(gotBytes) + gotNumBytesRead, gotErr := tc.vf.Read(gotBytes) if gotErr != nil { if tc.wantErr { return } - t.Fatalf("Read(%v) returned error: %v", tc.node, gotErr) + t.Fatalf("Read(%v) returned error: %v", tc.vf, gotErr) } gotContent := string(gotBytes[:gotNumBytesRead]) if gotContent != tc.want { - t.Errorf("Read(%v) = %v, want: %v", tc.node, gotContent, tc.want) + t.Errorf("Read(%v) = %v, want: %v", tc.vf, gotContent, tc.want) } - err = tc.node.Close() + err := tc.vf.Close() if err != nil { - t.Fatalf("Close(%v) returned error: %v", tc.node, err) + t.Fatalf("Close(%v) returned error: %v", tc.vf, err) } } }) } } -func TestRealFilePath(t *testing.T) { - tests := []struct { - name string - node *virtualFile - want string - }{ - { - name: "root directory", - node: rootDirectory, - want: filepath.FromSlash("/tmp/extract/layer1"), - }, - { - name: "root file", - node: rootFile, - want: filepath.FromSlash("/tmp/extract/layer1/bar"), - }, - { - name: "non-root file", - node: nonRootFile, - want: filepath.FromSlash("/tmp/extract/layer1/dir1/foo"), - }, - { - name: "non-root directory", - node: nonRootDirectory, - want: filepath.FromSlash("/tmp/extract/layer1/dir1/dir2"), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := tc.node.RealFilePath() - if got != tc.want { - t.Errorf("RealFilePath(%v) = %v, want: %v", tc.node, got, tc.want) - } - }) - } -} - func TestName(t *testing.T) { tests := []struct { name string @@ -737,3 +651,19 @@ func TestType(t *testing.T) { }) } } + +// setupContentBlob creates a new file with a temporary content blob containing the given files. +func setupContentBlob(t *testing.T, files []string) *os.File { + t.Helper() + + contentBlob, err := os.CreateTemp(t.TempDir(), "content-blob-*") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + for _, content := range files { + if _, err := contentBlob.ReadFrom(strings.NewReader(content)); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + } + return contentBlob +}