Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ linker resolved against an external sysroot via `--sysroot`.

## OCI Images

`elfuse-oci` is a separate Go binary that pulls OCI images into a local
OCI image layout. It does not add container isolation. See
`elfuse-oci` is a separate Go binary that pulls and unpacks OCI images. It
does not add container isolation. See
[docs/usage.md](docs/usage.md#oci-images) and
[docs/oci-images.md](docs/oci-images.md).

Expand Down Expand Up @@ -159,7 +159,7 @@ The build signs `build/elfuse` before use. Override the signing identity with
`make check` flow, the QEMU and Rosetta cross-check matrices, and
fixture handling.
- [docs/oci-images.md](docs/oci-images.md): the `elfuse-oci` store,
pull behavior, and validation.
pull and unpack behavior, and validation.
- [docs/filenames.md](docs/filenames.md): how a guest filename becomes a
name on disk and back: case folding and normalization on the sysroot
volume, the escape encoding, and the length limits both systems impose.
Expand Down
9 changes: 9 additions & 0 deletions cmd/oci/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ func (cf *commonFlags) openStore() (*store, ocispec.Platform, error) {
s, err := openStore(root)
return s, platform, err
}

func (cf *commonFlags) openStoreForRead() (*store, ocispec.Platform, error) {
root, platform, err := cf.values()
if err != nil {
return nil, ocispec.Platform{}, err
}
s, err := openStoreForRead(root)
return s, platform, err
}
78 changes: 75 additions & 3 deletions cmd/oci/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,59 @@ import (
"context"
"encoding/json"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"
"time"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

type tarEntry struct {
Name string
Body string
Name string
Body string
Link string
Mode int64
Type byte
Major int64
ModTime time.Time
}

func buildLayerTar(t *testing.T, entries []tarEntry) []byte {
t.Helper()
var b bytes.Buffer
tw := tar.NewWriter(&b)
for _, e := range entries {
hdr := &tar.Header{Name: e.Name, Mode: 0o644, Size: int64(len(e.Body)), Typeflag: tar.TypeReg}
hdr := &tar.Header{Name: e.Name, Mode: e.Mode, Size: int64(len(e.Body)), Typeflag: tar.TypeReg, ModTime: e.ModTime}
if hdr.Mode == 0 {
hdr.Mode = 0o644
}
// A sub-second timestamp only survives in PAX, and the writer picks
// USTAR unless it is told otherwise.
if e.ModTime.Nanosecond() != 0 {
hdr.Format = tar.FormatPAX
}
switch {
case e.Type != 0:
hdr.Typeflag = e.Type
hdr.Size = 0
hdr.Linkname = e.Link
hdr.Devmajor = e.Major
case e.Link != "":
hdr.Typeflag = tar.TypeSymlink
hdr.Linkname = e.Link
hdr.Size = 0
case e.Name[len(e.Name)-1] == '/':

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When a tarEntry has an empty Name with Type==0 and Link=="", e.Name[len(e.Name)-1] indexes index -1 and panics the test. Guard for the empty Name before indexing, or produce a clear t.Fatal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/oci/helpers_test.go, line 59:

<comment>When a tarEntry has an empty Name with Type==0 and Link=="", `e.Name[len(e.Name)-1]` indexes index -1 and panics the test. Guard for the empty Name before indexing, or produce a clear t.Fatal.</comment>

<file context>
@@ -10,27 +10,59 @@ import (
+			hdr.Typeflag = tar.TypeSymlink
+			hdr.Linkname = e.Link
+			hdr.Size = 0
+		case e.Name[len(e.Name)-1] == '/':
+			hdr.Typeflag = tar.TypeDir
+			if e.Mode == 0 {
</file context>

hdr.Typeflag = tar.TypeDir
if e.Mode == 0 {
hdr.Mode = 0o755
}
hdr.Size = 0
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -220,3 +252,43 @@ func mustContain(t *testing.T, got string, wants ...string) {
}
}
}

func manifestOf(t *testing.T, s *store, digest string) ocispec.Manifest {
t.Helper()
manifest, err := s.manifestFor(context.Background(), digest)
if err != nil {
t.Fatal(err)
}
return manifest
}

func runCaptured(t *testing.T, args ...string) (string, error) {
t.Helper()
var err error
_, stderr := captureOutput(t, func() { err = run(args) })
return stderr, err
}

func unpackFresh(t *testing.T, s *store, digest string) string {
t.Helper()
dest := filepath.Join(t.TempDir(), "rootfs")
var err error
captureOutput(t, func() {
err = unpackImageFresh(context.Background(), s, manifestOf(t, s, digest), dest)
})
if err != nil {
t.Fatal(err)
}
return dest
}

// incompressibleBody returns deterministic bytes that gzip cannot shrink, so a
// fixture layer is streamed from the blob in many reads.
func incompressibleBody(seed, n int) string {
r := rand.New(rand.NewSource(int64(seed)))
b := make([]byte, n)
for i := range b {
b[i] = byte(r.Intn(256))
}
return string(b)
}
3 changes: 2 additions & 1 deletion cmd/oci/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ func main() {
}

type cli struct {
Pull pullCommand `cmd:"" help:"Pull an image into the local store"`
Pull pullCommand `cmd:"" help:"Pull an image into the local store"`
Unpack unpackCommand `cmd:"" help:"Unpack a stored image into a rootfs"`
}

func newParser(stdout, stderr io.Writer, target *cli) (*kong.Kong, error) {
Expand Down
16 changes: 14 additions & 2 deletions cmd/oci/main_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestUsageAndErrors(t *testing.T) {
if err == nil {
t.Fatal("missing command must fail")
}
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull")
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull", "unpack")
if stderr != "" {
t.Fatalf("parse error wrote to stderr: %q", stderr)
}
Expand All @@ -24,7 +24,7 @@ func TestUsageAndErrors(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "unexpected argument bogus") {
t.Fatalf("unknown command error = %v", err)
}
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull")
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull", "unpack")
if stderr != "" {
t.Fatalf("unknown command wrote to stderr: %q", stderr)
}
Expand Down Expand Up @@ -78,3 +78,15 @@ func TestParserWritesToConfiguredStreams(t *testing.T) {
t.Fatalf("parse wrote stdout %q stderr %q", stdout.String(), stderr.String())
}
}

func TestUnpackUsage(t *testing.T) {
var err error
stdout, stderr := captureOutput(t, func() { err = run([]string{"unpack", "--nope", "x"}) })
if err == nil || !strings.Contains(err.Error(), "unknown flag --nope") {
t.Fatalf("unknown flag error = %v", err)
}
mustContain(t, stdout, "Usage: elfuse-oci unpack", "--platform", "--store", "--rootfs")
if stderr != "" {
t.Fatalf("unknown flag wrote to stderr: %q", stderr)
}
}
Loading
Loading