diff --git a/artifact/image/layerscanning/image/file_node.go b/artifact/image/layerscanning/image/file_node.go index 0071f4ca..40740a94 100644 --- a/artifact/image/layerscanning/image/file_node.go +++ b/artifact/image/layerscanning/image/file_node.go @@ -19,10 +19,10 @@ import ( "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 @@ -31,15 +31,27 @@ const ( dirPermission = 0700 ) -// FileNode represents a file in a virtual filesystem. +// fileNode represents a file in a virtual filesystem. type fileNode struct { + // extractDir and originLayerID are used to construct the real file path of the fileNode. extractDir string originLayerID string - isWhiteout bool - virtualPath string - targetPath string - mode fs.FileMode - file *os.File + + // isWhiteout is true if the fileNode represents a whiteout file + isWhiteout bool + + // virtualPath is the path of the fileNode in the virtual filesystem. + virtualPath string + // targetPath is reserved for symlinks. It is the path that the symlink points to. + targetPath string + + // size, mode, and modTime are used to implement the fs.FileInfo interface. + size int64 + mode fs.FileMode + modTime time.Time + + // file is the file object for the real file referred to by the fileNode. + file *os.File } // ======================================================== @@ -47,12 +59,11 @@ type fileNode struct { // ======================================================== // Stat returns the file info of real file referred by the fileNode. -// TODO: b/378130598 - Need to replace the os stat permission with the permissions on the filenode. func (f *fileNode) Stat() (fs.FileInfo, error) { if f.isWhiteout { return nil, fs.ErrNotExist } - return os.Stat(f.RealFilePath()) + return f, nil } // Read reads the real file referred to by the fileNode. @@ -103,13 +114,14 @@ func (f *fileNode) RealFilePath() string { // fs.DirEntry METHODS // ======================================================== -// Name returns the name of the fileNode. +// Name returns the name of the fileNode. Name is also used to implement the fs.FileInfo interface. func (f *fileNode) Name() string { _, filename := path.Split(f.virtualPath) return filename } -// IsDir returns whether the fileNode represents a directory. +// IsDir returns whether the fileNode represents a directory. IsDir is also used to implement the +// fs.FileInfo interface. func (f *fileNode) IsDir() bool { return f.Type().IsDir() } @@ -123,3 +135,22 @@ func (f *fileNode) Type() fs.FileMode { func (f *fileNode) Info() (fs.FileInfo, error) { return f.Stat() } + +// ======================================================== +// fs.FileInfo METHODS +// ======================================================== +func (f *fileNode) Size() int64 { + return f.size +} + +func (f *fileNode) Mode() fs.FileMode { + return f.mode +} + +func (f *fileNode) ModTime() time.Time { + return f.modTime +} + +func (f *fileNode) Sys() any { + return nil +} diff --git a/artifact/image/layerscanning/image/file_node_test.go b/artifact/image/layerscanning/image/file_node_test.go index 8e56ebff..992a979e 100644 --- a/artifact/image/layerscanning/image/file_node_test.go +++ b/artifact/image/layerscanning/image/file_node_test.go @@ -21,6 +21,7 @@ import ( "path" "path/filepath" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -59,7 +60,94 @@ var ( // TODO: b/377551664 - Add tests for the Stat method for the fileNode type. func TestStat(t *testing.T) { - return + baseTime := time.Now() + regularFileNode := &fileNode{ + extractDir: "tempDir", + originLayerID: "", + virtualPath: "/bar", + isWhiteout: false, + mode: filePermission, + size: 1, + modTime: baseTime, + } + symlinkFileNode := &fileNode{ + extractDir: "tempDir", + originLayerID: "", + virtualPath: "/symlink-to-bar", + targetPath: "/bar", + isWhiteout: false, + mode: fs.ModeSymlink | filePermission, + size: 1, + modTime: baseTime, + } + whiteoutFileNode := &fileNode{ + extractDir: "tempDir", + originLayerID: "", + virtualPath: "/bar", + isWhiteout: true, + mode: filePermission, + } + + type info struct { + name string + size int64 + mode fs.FileMode + modTime time.Time + } + + tests := []struct { + name string + node *fileNode + wantInfo info + wantErr error + }{ + { + name: "regular file", + node: regularFileNode, + wantInfo: info{ + name: "bar", + size: 1, + mode: filePermission, + modTime: baseTime, + }, + }, + { + name: "symlink", + node: symlinkFileNode, + wantInfo: info{ + name: "symlink-to-bar", + size: 1, + mode: fs.ModeSymlink | filePermission, + modTime: baseTime, + }, + }, + { + name: "whiteout file", + node: whiteoutFileNode, + wantErr: fs.ErrNotExist, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotFileNode, gotErr := tc.node.Stat() + if tc.wantErr != nil { + if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" { + t.Errorf("Stat(%v) returned unexpected error (-want +got): %v", tc.node, diff) + } + return + } + + gotInfo := info{ + name: gotFileNode.Name(), + size: gotFileNode.Size(), + mode: gotFileNode.Mode(), + modTime: gotFileNode.ModTime(), + } + if diff := cmp.Diff(tc.wantInfo, gotInfo, cmp.AllowUnexported(info{})); diff != "" { + t.Errorf("Stat(%v) returned unexpected fileNode (-want +got): %v", tc.node, diff) + } + }) + } } func TestRead(t *testing.T) { diff --git a/artifact/image/layerscanning/image/image.go b/artifact/image/layerscanning/image/image.go index fd8bf887..dfff48be 100644 --- a/artifact/image/layerscanning/image/image.go +++ b/artifact/image/layerscanning/image/image.go @@ -385,12 +385,17 @@ func (img *Image) handleDir(realFilePath, virtualPath, originLayerID string, tar return nil, fmt.Errorf("failed to create directory with realFilePath %s: %w", realFilePath, err) } } + + fileInfo := header.FileInfo() + return &fileNode{ extractDir: img.ExtractDir, originLayerID: originLayerID, virtualPath: virtualPath, isWhiteout: isWhiteout, - mode: fs.FileMode(header.Mode) | fs.ModeDir, + mode: fileInfo.Mode() | fs.ModeDir, + size: fileInfo.Size(), + modTime: fileInfo.ModTime(), }, nil } @@ -415,12 +420,16 @@ func (img *Image) handleFile(realFilePath, virtualPath, originLayerID string, ta return nil, fmt.Errorf("unable to copy file: %w", err) } + fileInfo := header.FileInfo() + return &fileNode{ extractDir: img.ExtractDir, originLayerID: originLayerID, virtualPath: virtualPath, isWhiteout: isWhiteout, - mode: fs.FileMode(header.Mode), + mode: fileInfo.Mode(), + size: fileInfo.Size(), + modTime: fileInfo.ModTime(), }, nil }