From 65af42d1a34e602091fb8acc746aa3966edbf6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20B=C3=B6hm?= Date: Sun, 17 Nov 2024 22:36:37 +0100 Subject: [PATCH] feat(initramfs): add ReadLinkFS In preparation for https://github.com/golang/go/issues/49580, this commit adds an own simplified implementation of ReadLinkFS. It adds helpers to extend a simple fstest.MapFS into a ReadLinkFS. --- internal/initramfs/cpio.go | 54 +++++++++++++------- internal/initramfs/cpio_test.go | 6 +-- internal/initramfs/readlinkfs.go | 87 ++++++++++++++++++++++++++++++++ internal/virtrun/initramfs.go | 7 ++- 4 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 internal/initramfs/readlinkfs.go diff --git a/internal/initramfs/cpio.go b/internal/initramfs/cpio.go index 055c855..68e2bf1 100644 --- a/internal/initramfs/cpio.go +++ b/internal/initramfs/cpio.go @@ -7,6 +7,7 @@ package initramfs import ( "io" "io/fs" + "os" "github.com/cavaliergopher/cpio" ) @@ -26,14 +27,7 @@ func NewCPIOFSWriter(w io.Writer) *CPIOFSWriter { // // It walks the directory tree starting at the root of the filesystem adding // each file to the tar archive while maintaining the directory structure. -// The [fs.FS.Open] must not follow symlinks. This is the case for [FS] and -// most implementations in the standard library, except for [os.DirFS]. -// -// TODO: Consider switching to [fs.ReadLinkFS] once available. See -// https://github.com/golang/go/issues/49580 -// -//nolint:godox -func (w *CPIOFSWriter) AddFS(fsys fs.FS) error { +func (w *CPIOFSWriter) AddFS(fsys ReadLinkFS) error { return fs.WalkDir(fsys, ".", func( //nolint:wrapcheck name string, d fs.DirEntry, err error, ) error { @@ -68,11 +62,38 @@ func (w *CPIOFSWriter) AddFS(fsys fs.FS) error { } } + err = w.writeBody(fsys, name, info.Mode().Type()) + if err != nil { + return &PathError{ + Op: "write body", + Path: name, + Err: err, + } + } + + return nil + }) +} + +func (w *CPIOFSWriter) writeBody( + fsys ReadLinkFS, + name string, + typ fs.FileMode, +) error { + switch typ { + case os.ModeDir: // Directories do not have a body and fail on [fs.File.Read]. - if info.IsDir() { - return nil + return nil + case fs.ModeSymlink: + linkName, err := fsys.ReadLink(name) + if err != nil { + return err //nolint:wrapcheck } + _, err = w.Write([]byte(linkName)) + + return err //nolint:wrapcheck + case 0: file, err := fsys.Open(name) if err != nil { return err //nolint:wrapcheck @@ -80,14 +101,9 @@ func (w *CPIOFSWriter) AddFS(fsys fs.FS) error { defer file.Close() _, err = io.Copy(w, file) - if err != nil { - return &PathError{ - Op: "write body", - Path: name, - Err: err, - } - } - return nil - }) + return err //nolint:wrapcheck + default: + return ErrFileInvalid + } } diff --git a/internal/initramfs/cpio_test.go b/internal/initramfs/cpio_test.go index b5874ef..7c24482 100644 --- a/internal/initramfs/cpio_test.go +++ b/internal/initramfs/cpio_test.go @@ -20,7 +20,7 @@ import ( ) func TestCPIOFSWriter_AddFS(t *testing.T) { - testFS := fstest.MapFS{ + sourceFS := fstest.MapFS{ ".": &fstest.MapFile{ Mode: fs.ModeDir, }, @@ -47,7 +47,7 @@ func TestCPIOFSWriter_AddFS(t *testing.T) { w := initramfs.NewCPIOFSWriter(&archive) - err := w.AddFS(testFS) + err := w.AddFS(initramfs.WithReadLinkNoFollowOpen(sourceFS)) require.NoError(t, err) r := cpio.NewReader(&archive) @@ -77,5 +77,5 @@ func TestCPIOFSWriter_AddFS(t *testing.T) { } } - assert.Equal(t, testFS, extractedFS) + assert.Equal(t, sourceFS, extractedFS) } diff --git a/internal/initramfs/readlinkfs.go b/internal/initramfs/readlinkfs.go new file mode 100644 index 0000000..7c5a295 --- /dev/null +++ b/internal/initramfs/readlinkfs.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 Tobias Böhm +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package initramfs + +import "io/fs" + +// ReadLinkFS is a [fs.FS] with an additional method for reading the target of +// a symbolic link. +// +// Replace with [fs.ReadLinkFS] once available. See +// https://github.com/golang/go/issues/49580 +type ReadLinkFS interface { + fs.FS + + // ReadLink returns the destination a symbolic link points to. Returns an + // error if the file at name is not a symbolic link or cannot be read. + ReadLink(name string) (string, error) +} + +type readLinkFS struct { + fs.FS + readLinkFn ReadLinkFunc +} + +// ReadLink implements [ReadLinkFS]. +func (fsys *readLinkFS) ReadLink(name string) (string, error) { + return fsys.readLinkFn(name) +} + +// ReadLinkFunc returns the destination of a symbolic link or an error in case +// the file at name is not a symbolic link or cannot be read. +type ReadLinkFunc func(name string) (string, error) + +// WithReadLinkFunc extends the given [fs.FS] with the given [ReadLinkFunc] +// into a new [ReadLinkFS]. +// +//nolint:ireturn +func WithReadLinkFunc(fsys fs.FS, readLinkFn ReadLinkFunc) ReadLinkFS { + return &readLinkFS{ + FS: fsys, + readLinkFn: readLinkFn, + } +} + +// WithReadLinkNoFollowOpen extends the given [fs.FS] into a new [ReadLinkFS]. +// +// The source [fs.FS]'s Open method must not follow symbolic links. It must open +// them directly so the destination can be read and returned by the ReadLink +// method. +// +//nolint:ireturn +func WithReadLinkNoFollowOpen(fsys fs.FS) ReadLinkFS { + return WithReadLinkFunc(fsys, func(name string) (string, error) { + file, err := fsys.Open(name) + if err != nil { + return "", err //nolint:wrapcheck + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + return "", err //nolint:wrapcheck + } + + if info.Mode().Type() != fs.ModeSymlink { + return "", &PathError{ + Op: "readlink", + Path: name, + Err: ErrFileInvalid, + } + } + + b := make([]byte, info.Size()) + + if _, err := file.Read(b); err != nil { + return "", &PathError{ + Op: "readlink", + Path: name, + Err: err, + } + } + + return string(b), nil + }) +} diff --git a/internal/virtrun/initramfs.go b/internal/virtrun/initramfs.go index e1a02c1..ea7b513 100644 --- a/internal/virtrun/initramfs.go +++ b/internal/virtrun/initramfs.go @@ -7,7 +7,6 @@ package virtrun import ( "context" "fmt" - "io/fs" "log/slog" "os" "slices" @@ -113,13 +112,13 @@ func buildFS(f initramfs.FSAdder, cfg Initramfs, libs sys.LibCollection) error { // WriteFSToTempFile writes the given [fs.FS] as CPIO archive into a new // temporary file in the given directory. // -// It returns the path to the created file. If tmpDir is the empty string the +// It returns the path to the created file. If dir is the empty string the // default directory is used as returned by [os.TempDir]. // // The caller is responsible for removing the file once it is not needed // anymore. -func WriteFSToTempFile(fsys fs.FS, tmpDir string) (string, error) { - file, err := os.CreateTemp(tmpDir, "initramfs") +func WriteFSToTempFile(fsys initramfs.ReadLinkFS, dir string) (string, error) { + file, err := os.CreateTemp(dir, "initramfs") if err != nil { return "", fmt.Errorf("create temp file: %w", err) }