Skip to content

Commit

Permalink
refactor(initramfs): combine virtual and regular file
Browse files Browse the repository at this point in the history
  • Loading branch information
aibor committed Oct 22, 2024
1 parent 877ae83 commit 89682db
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 151 deletions.
4 changes: 0 additions & 4 deletions internal/initramfs/file_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,4 @@ const (

// TreeNodeTypeLink is a symbolic link in the archive.
TreeNodeTypeLink

// TreeNodeTypeVirtual is like [TreeNodeTypeRegular] but with its content
// written from an io.Reader instead of being copied from the fs.
TreeNodeTypeVirtual
)
38 changes: 24 additions & 14 deletions internal/initramfs/initramfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,26 @@ import (

const fileMode = 0o755

func OSFileOpen(path string) (fs.File, error) {
return os.Open(path) //nolint:wrapcheck
}

type openFile struct {
fs.File
}

func (f openFile) self(_ string) (fs.File, error) {
return f.File, nil
}

// Initramfs represents a file tree that can be used as an initramfs for the
// Linux kernel.
//
// Create a new instance using [New]. Additional files can be added with
// [Initramfs.AddFiles]. Dynamically linked ELF libraries can be resolved
// and added for all already added regular files by calling
// [Initramfs.AddRequiredSharedObjects]. Once ready, write the [Initramfs] with
// [Initramfs.WriteCPIOInto].
// [Initramfs.WriteCPIO].
type Initramfs struct {
fileTree Tree
libDir string
Expand All @@ -31,14 +43,14 @@ type Initramfs struct {
func WithRealInitFile(path string) func(*TreeNode) {
return func(rootDir *TreeNode) {
// Never fails on a new tree.
_, _ = rootDir.AddRegular("init", path)
_, _ = rootDir.AddRegular("init", path, OSFileOpen)
}
}

func WithVirtualInitFile(file fs.File) func(*TreeNode) {
return func(rootDir *TreeNode) {
// Never fails on a new tree.
_, _ = rootDir.AddVirtual("init", file)
_, _ = rootDir.AddRegular("init", "", openFile{file}.self)
}
}

Expand Down Expand Up @@ -151,7 +163,7 @@ func (i *Initramfs) collectLibs() (map[string]bool, error) {
// interpreter). Collect the absolute paths of the found shared objects
// deduplicated in a set.
for path, node := range i.fileTree.All() {
if node.Type != TreeNodeTypeRegular {
if node.Type != TreeNodeTypeRegular || node.RelatedPath == "" {
continue
}

Expand Down Expand Up @@ -230,7 +242,7 @@ func (i *Initramfs) WriteToTempFile(tmpDir string) (string, error) {
}
defer file.Close()

err = i.WriteCPIOInto(file, os.DirFS("/"))
err = i.WriteCPIO(file)
if err != nil {
_ = os.Remove(file.Name())
return "", fmt.Errorf("create archive: %w", err)
Expand All @@ -239,20 +251,18 @@ func (i *Initramfs) WriteToTempFile(tmpDir string) (string, error) {
return file.Name(), nil
}

// WriteCPIOInto writes the [Initramfs] as CPIO archive to the given [Writer]
// from the given source [fs.FS].
func (i *Initramfs) WriteCPIOInto(writer io.Writer, source fs.FS) error {
// WriteCPIO writes the [Initramfs] as CPIO archive to the given [Writer],.
func (i *Initramfs) WriteCPIO(writer io.Writer) error {
w := NewCPIOWriter(writer)
defer w.Close()

return i.writeTo(w, source)
return i.writeTo(w)
}

// writeTo writes all collected files into the given writer. Regular files are
// copied from the given source [fs.FS].
func (i *Initramfs) writeTo(writer Writer, source fs.FS) error {
// writeTo writes all collected files into the given writer.
func (i *Initramfs) writeTo(writer Writer) error {
for path, node := range i.fileTree.All() {
err := node.WriteTo(writer, path, source)
err := node.WriteTo(writer, path)
if err != nil {
return &PathError{
Op: "archive write",
Expand All @@ -266,7 +276,7 @@ func (i *Initramfs) writeTo(writer Writer, source fs.FS) error {
}

func addFile(dirNode *TreeNode, name, path string) error {
_, err := dirNode.AddRegular(name, path)
_, err := dirNode.AddRegular(name, path, OSFileOpen)
if err != nil {
return &PathError{
Op: "add file",
Expand Down
40 changes: 17 additions & 23 deletions internal/initramfs/initramfs_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func assertNode(t *testing.T, i *Initramfs, p string, e TreeNode) {

node, err := i.fileTree.GetNode(p)
require.NoError(t, err)
assert.Equal(t, e, *node)
assert.Equal(t, e.String(), node.String())
}

func TestInitramfsNew(t *testing.T) {
Expand All @@ -38,24 +38,25 @@ func TestInitramfsNew(t *testing.T) {
name: "real file",
initFunc: WithRealInitFile("first"),
expected: TreeNode{
Type: TreeNodeTypeRegular,
RelatedPath: "first",
Type: TreeNodeTypeRegular,
RelatedPath: "first",
SourceOpenFunc: OSFileOpen,
},
},
{
name: "virtual file",
initFunc: WithVirtualInitFile(initFile),
expected: TreeNode{
Type: TreeNodeTypeVirtual,
Source: initFile,
Type: TreeNodeTypeRegular,
SourceOpenFunc: openFile{initFile}.self,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := New(tt.initFunc)
assertNode(t, i, "/init", tt.expected)
assertNode(t, i, "init", tt.expected)
})
}
}
Expand Down Expand Up @@ -115,7 +116,7 @@ func TestInitramfsWriteTo(t *testing.T) {
_, err := i.fileTree.GetRoot().AddNode("init", node)
require.NoError(t, err)

return i.writeTo(w, testFS)
return i.writeTo(w)
}

t.Run("unknown file type", func(t *testing.T) {
Expand All @@ -125,8 +126,10 @@ func TestInitramfsWriteTo(t *testing.T) {

t.Run("nonexisting source", func(t *testing.T) {
node := &TreeNode{
Type: TreeNodeTypeRegular,
RelatedPath: "nonexisting",
Type: TreeNodeTypeRegular,
SourceOpenFunc: func(_ string) (fs.File, error) {
return nil, fs.ErrNotExist
},
}
err := test(node, &MockWriter{})
assert.ErrorIs(t, err, fs.ErrNotExist)
Expand All @@ -143,6 +146,9 @@ func TestInitramfsWriteTo(t *testing.T) {
node: TreeNode{
Type: TreeNodeTypeRegular,
RelatedPath: "/input",
SourceOpenFunc: func(_ string) (fs.File, error) {
return testFile, nil
},
},
mock: MockWriter{
Path: "/init",
Expand Down Expand Up @@ -170,18 +176,6 @@ func TestInitramfsWriteTo(t *testing.T) {
RelatedPath: "/lib",
},
},
{
name: "virtual",
node: TreeNode{
Type: TreeNodeTypeVirtual,
Source: testFile,
},
mock: MockWriter{
Path: "/init",
Source: testFile,
Mode: 0o755,
},
},
}

for _, tt := range tests {
Expand All @@ -192,7 +186,7 @@ func TestInitramfsWriteTo(t *testing.T) {
require.NoError(t, err)

mock := MockWriter{}
err = i.writeTo(&mock, testFS)
err = i.writeTo(&mock)
require.NoError(t, err)
assert.Equal(t, tt.mock, mock)
})
Expand All @@ -202,7 +196,7 @@ func TestInitramfsWriteTo(t *testing.T) {
require.NoError(t, err)

mock := MockWriter{Err: assert.AnError}
err = i.writeTo(&mock, testFS)
err = i.writeTo(&mock)
require.ErrorIs(t, err, assert.AnError)
})
})
Expand Down
60 changes: 37 additions & 23 deletions internal/initramfs/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"iter"
"path/filepath"
"strings"
)

// Tree represents a simple file tree.
Expand All @@ -18,7 +19,7 @@ type Tree struct {
}

func isRoot(path string) bool {
switch filepath.Clean(path) {
switch path {
case "", ".", "..", string(filepath.Separator):
return true
default:
Expand All @@ -37,45 +38,57 @@ func (t *Tree) GetRoot() *TreeNode {
return t.root
}

func (t *Tree) Nodes(path string) iter.Seq[string] {
return func(yield func(string) bool) {
relPath := strings.TrimPrefix(path, "/")
cleaned := filepath.Clean(relPath)

if isRoot(cleaned) {
return
}

nodes := strings.Split(cleaned, string(filepath.Separator))
for _, name := range nodes {
if !yield(name) {
return
}
}
}
}

// GetNode returns the node for the given path. Returns ErrNodeNotExists if
// the node does not exist.
func (t *Tree) GetNode(path string) (*TreeNode, error) {
if isRoot(path) {
return t.GetRoot(), nil
}
node := t.GetRoot()

dir, name := filepath.Split(filepath.Clean(path))
for name := range t.Nodes(path) {
var err error

parent, err := t.GetNode(dir)
if err != nil {
return nil, err
node, err = node.GetNode(name)
if err != nil {
return nil, err
}
}

return parent.GetNode(name)
return node, nil
}

// Mkdir adds a directory node for the given path. Non existing parents
// are created recursively. If any of the parents exists but is not a directory
// ErrNodeNotDir is returned.
func (t *Tree) Mkdir(path string) (*TreeNode, error) {
cleaned := filepath.Clean(path)
if isRoot(cleaned) {
return t.GetRoot(), nil
}
node := t.GetRoot()

dir, name := filepath.Split(cleaned)
for name := range t.Nodes(path) {
var err error

parent, err := t.Mkdir(dir)
if err != nil {
return nil, err
}

node, err := parent.AddDirectory(name)
if errors.Is(err, ErrTreeNodeExists) && node.IsDir() {
err = nil
node, err = node.AddDirectory(name)
if err != nil && (!errors.Is(err, ErrTreeNodeExists) || !node.IsDir()) {
return nil, err
}
}

return node, err
return node, nil
}

// Ln adds links to target for the given path.
Expand Down Expand Up @@ -106,6 +119,7 @@ func (t *Tree) All() iter.Seq2[string, *TreeNode] {
return
}

// Collect iterators for each sub directory. Start with root directory.
iterators := []iter.Seq2[string, *TreeNode]{
t.root.prefixedPaths(base),
}
Expand Down
4 changes: 2 additions & 2 deletions internal/initramfs/tree_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestTreeIsRoot(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.assert(t, isRoot(tt.path))
tt.assert(t, isRoot(filepath.Clean(tt.path)))
})
}
}
Expand Down Expand Up @@ -135,7 +135,7 @@ func TestTreeGetNode(t *testing.T) {
},
{
name: "dir node",
path: "/dir",
path: "dir",
expect: &dirNode,
},
}
Expand Down
Loading

0 comments on commit 89682db

Please sign in to comment.