Skip to content

Commit

Permalink
refactor(initramfs): change into fs.FS
Browse files Browse the repository at this point in the history
This commit changes the `initramfs` package completely by refactoring it
into a `fs.FS`. The `initramfs.FS` is tested with `fstest.TestFS` for
proper implementation of `fs.FS` and `fs.File`.

It changes the `Writer` into a `FileWriter` working with any `fs.File`.

The virtrun's packages use is refactored to reflect those changes.
Readability of the initramfs construction is improved.

With this, the components are much more decoupled, which should allow for
improved testability.
  • Loading branch information
aibor committed Oct 26, 2024
1 parent 89682db commit 8ab7a41
Show file tree
Hide file tree
Showing 42 changed files with 1,376 additions and 1,567 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later

/dist
/internal/initramfs/testdata/bin/
/internal/initramfs/testdata/lib/
/internal/elf/testdata/bin/
/internal/elf/testdata/lib/
/testing/kernels/
/testing/bin/
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ issues:
source: "^\\s+defer \\S+\\.Close()"
- linters:
- err113
path: ".*_test.go"
- wrapcheck
path: "(.+_test|testing).go"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
Expand Down
15 changes: 15 additions & 0 deletions internal/elf/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2024 Tobias Böhm <code@aibor.de>
//
// SPDX-License-Identifier: GPL-3.0-or-later

package elf

import "errors"

var (
// ErrNoInterpreter is returned if no interpreter is found in an ELF file.
ErrNoInterpreter = errors.New("no interpreter in ELF file")

// ErrNotELFFile is returned if the file does not have an ELF magic number.
ErrNotELFFile = errors.New("is not an ELF file")
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

//go:generate go generate -v ./testdata/cmd

package initramfs_test
package elf_test
6 changes: 3 additions & 3 deletions internal/initramfs/elf.go → internal/elf/ldd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later

package initramfs
package elf

import (
"bufio"
Expand Down Expand Up @@ -71,10 +71,10 @@ func readInterpreter(path string) (string, error) {
elfFile, err := elf.Open(path)
if err != nil {
if strings.Contains(err.Error(), "bad magic number") {
return "", ErrNotELFFile
err = ErrNotELFFile
}

return "", fmt.Errorf("open: %w", err)
return "", fmt.Errorf("open %s: %w", path, err)
}
defer elfFile.Close()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later

package initramfs
package elf

import (
"bytes"
Expand Down
8 changes: 4 additions & 4 deletions internal/initramfs/elf_test.go → internal/elf/ldd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later

package initramfs_test
package elf_test

import (
"os/exec"
"testing"

"github.com/aibor/virtrun/internal/initramfs"
"github.com/aibor/virtrun/internal/elf"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -24,7 +24,7 @@ func TestTestdata(t *testing.T) {
}

func TestFilesLdd(t *testing.T) {
actual, err := initramfs.Ldd("testdata/bin/main")
actual, err := elf.Ldd("testdata/bin/main")
require.NoErrorf(t, err, "must resolve")

expected := []string{
Expand All @@ -33,5 +33,5 @@ func TestFilesLdd(t *testing.T) {
"testdata/lib/libfunc1.so",
}

initramfs.AssertContainsPaths(t, actual, expected)
elf.AssertContainsPaths(t, actual, expected)
}
117 changes: 117 additions & 0 deletions internal/elf/lib_collection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2024 Tobias Böhm <code@aibor.de>
//
// SPDX-License-Identifier: GPL-3.0-or-later

package elf

import (
"errors"
"fmt"
"iter"
"path/filepath"
)

// LibCollection is a deduplicated collection of dynamically linked libraries
// and paths they are found at.
type LibCollection struct {
libs map[string]int
searchPaths map[string]int
}

func (c *LibCollection) Libs() iter.Seq[string] {
return func(yield func(string) bool) {
for name := range c.libs {
if !yield(name) {
return
}
}
}
}

func (c *LibCollection) SearchPaths() iter.Seq[string] {
return func(yield func(string) bool) {
for name := range c.searchPaths {
if !yield(name) {
return
}
}
}
}

// CollectLibsFor recursively resolves the dynamically linked shared objects of
// all given ELF files.
//
// The dynamic linker consumed LD_LIBRARY_PATH from the environment.
func CollectLibsFor(files ...string) (LibCollection, error) {
collection := LibCollection{
libs: make(map[string]int),
searchPaths: make(map[string]int),
}

for _, name := range files {
err := collectLibsFor(collection.libs, name)
if err != nil {
return collection, fmt.Errorf("[%s]: %w", name, err)
}
}

for name := range collection.libs {
dir, _ := filepath.Split(name)

err := collectSearchPathsFor(collection.searchPaths, dir)
if err != nil {
return collection, fmt.Errorf("[%s]: %w", name, err)
}
}

return collection, nil
}

func collectLibsFor(libs map[string]int, name string) error {
// For each regular file, try to get linked shared objects.
// Ignore if it is not an ELF file or if it is statically linked (has no
// interpreter). Collect the absolute paths of the found shared objects
// deduplicated in a set.
paths, err := Ldd(name)
if err != nil {
if errors.Is(err, ErrNotELFFile) ||
errors.Is(err, ErrNoInterpreter) {
return nil
}

return err
}

for _, p := range paths {
absPath, err := filepath.Abs(p)
if err != nil {
return fmt.Errorf("absolute path: %w", err)
}

libs[absPath]++
}

return nil
}

func collectSearchPathsFor(paths map[string]int, dir string) error {
dir = filepath.Clean(dir)
if dir == "" {
return nil
}

paths[dir]++

// Try if the directory has symbolic links and resolve them, so we
// get the real path that the dynamic linker needs.
canonicalDir, err := filepath.EvalSymlinks(dir)
if err != nil {
return fmt.Errorf("resolve symlinks: %w", err)
}

if canonicalDir != dir {
paths[canonicalDir]++
}

return nil
}
41 changes: 41 additions & 0 deletions internal/elf/lib_collection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Tobias Böhm <code@aibor.de>
//
// SPDX-License-Identifier: GPL-3.0-or-later

package elf_test

import (
"slices"
"testing"

"github.com/aibor/virtrun/internal/elf"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLibCollection_CollectLibsFor(t *testing.T) {
collection, err := elf.CollectLibsFor("testdata/bin/main")
require.NoError(t, err)

expectedLibs := []string{
"testdata/lib/libfunc2.so",
"testdata/lib/libfunc3.so",
"testdata/lib/libfunc1.so",
}

expectedLinks := []string{
"testdata/lib",
}

for _, name := range expectedLibs {
expected := elf.MustAbsPath(t, name)
actual := slices.Collect(collection.Libs())
assert.Contains(t, actual, expected, name)
}

for _, name := range expectedLinks {
expected := elf.MustAbsPath(t, name)
actual := slices.Collect(collection.SearchPaths())
assert.Contains(t, actual, expected, name)
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
53 changes: 53 additions & 0 deletions internal/elf/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2024 Tobias Böhm <code@aibor.de>
//
// SPDX-License-Identifier: GPL-3.0-or-later

package elf

import (
"path/filepath"
"slices"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func AssertContainsPaths(tb testing.TB, actual, expected []string) bool {
tb.Helper()

expectedAbs := make(map[string]string, len(expected))

for _, path := range expected {
abs, err := filepath.Abs(path)
require.NoErrorf(tb, err, "must absolute path %s", path)

expectedAbs[abs] = path
}

for _, path := range actual {
abs, err := filepath.Abs(path)
require.NoErrorf(tb, err, "must absolute path %s", path)

relPath, exists := expectedAbs[abs]
if !exists {
continue
}

idx := slices.Index(expected, relPath)
if idx >= 0 {
expected = slices.Delete(expected, idx, idx+1)
}
}

return assert.Empty(tb, expected)
}

func MustAbsPath(tb testing.TB, path string) string {
tb.Helper()

abs, err := filepath.Abs(path)
require.NoError(tb, err)

return abs
}
Loading

0 comments on commit 8ab7a41

Please sign in to comment.