Skip to content

fix: handle dot-prefixed path for tarfs #875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 20, 2025
Merged
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
186 changes: 106 additions & 80 deletions content/oci/readonlyoci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,91 +416,117 @@ func TestReadOnlyStore_DirFS(t *testing.T) {
}

/*
testdata/hello-world.tar contains:

blobs/
sha256/
2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config
index.json
manifest.json
oci-layout
=== Contents of testdata/hello-world.tar ===

blobs/

blobs/sha256/
blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config

index.json
manifest.json
oci-layout

=== Contents of testdata/hello-world-prefixed-path.tar ===

./
./blobs/

./blobs/sha256/
./blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
./blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
./blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
./blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config

./index.json
./manifest.json
./oci-layout
*/

func TestReadOnlyStore_TarFS(t *testing.T) {
ctx := context.Background()
s, err := NewFromTar(ctx, "testdata/hello-world.tar")
if err != nil {
t.Fatal("New() error =", err)
}
tarPaths := []string{
"testdata/hello-world.tar",
"testdata/hello-world-prefixed-path.tar",
}
for _, tarPath := range tarPaths {
t.Run(tarPath, func(t *testing.T) {
ctx := context.Background()
s, err := NewFromTar(ctx, tarPath)
if err != nil {
t.Fatal("New() error =", err)
}

// test data in testdata/hello-world.tar
descs := []ocispec.Descriptor{
// desc 0: config
{
MediaType: "application/vnd.docker.container.image.v1+json",
Size: 1469,
Digest: "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
},
// desc 1: layer
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Size: 2479,
Digest: "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54",
},
// desc 2: image manifest
{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4",
Size: 525,
Platform: &ocispec.Platform{
Architecture: "amd64",
OS: "linux",
},
},
// desc 3: manifest list
{
MediaType: docker.MediaTypeManifestList,
Size: 2561,
Digest: "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af",
},
}
// test data in testdata/hello-world.tar
descs := []ocispec.Descriptor{
// desc 0: config
{
MediaType: "application/vnd.docker.container.image.v1+json",
Size: 1469,
Digest: "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
},
// desc 1: layer
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Size: 2479,
Digest: "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54",
},
// desc 2: image manifest
{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4",
Size: 525,
Platform: &ocispec.Platform{
Architecture: "amd64",
OS: "linux",
},
},
// desc 3: manifest list
{
MediaType: docker.MediaTypeManifestList,
Size: 2561,
Digest: "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af",
},
}

// test resolving by tag
for _, desc := range descs {
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
}
// test resolving by tag
gotDesc, err := s.Resolve(ctx, "latest")
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// test resolving by tag
for _, desc := range descs {
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
}
// test resolving by tag
gotDesc, err := s.Resolve(ctx, "latest")
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}

// test Predecessors
wantPredecessors := [][]ocispec.Descriptor{
{descs[2]}, // desc 0
{descs[2]}, // desc 1
{descs[3]}, // desc 2
{}, // desc 3
}
for i, want := range wantPredecessors {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
// test Predecessors
wantPredecessors := [][]ocispec.Descriptor{
{descs[2]}, // desc 0
{descs[2]}, // desc 1
{descs[3]}, // desc 2
{}, // desc 3
}
for i, want := range wantPredecessors {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
})
}
}

Expand Down
136 changes: 86 additions & 50 deletions content/oci/readonlystorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,57 +161,93 @@ func TestReadOnlyStorage_DirFS(t *testing.T) {
}
}

func TestReadOnlyStorage_TarFS(t *testing.T) {
s, err := NewStorageFromTar("testdata/hello-world.tar")
if err != nil {
t.Fatal("NewStorageFromTar() error =", err)
}
ctx := context.Background()

// test data in testdata/hello-world.tar
blob := []byte(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"8746661ca3c2f215da94e6d3f7dfdcafaff5ec0b21c9aff6af3dc379a82fbc72","container_config":{"Hostname":"8746661ca3c2","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/hello\"]"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-09-23T23:47:57.442225064Z","docker_version":"20.10.7","history":[{"created":"2021-09-23T23:47:57.098990892Z","created_by":"/bin/sh -c #(nop) COPY file:50563a97010fd7ce1ceebd1fa4f4891ac3decdf428333fb2683696f4358af6c2 in / "},{"created":"2021-09-23T23:47:57.442225064Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:e07ee1baac5fae6a26f30cabfe54a36d3402f96afda318fe0a96cec4ca393359"]}}`)
desc := content.NewDescriptorFromBytes(docker.MediaTypeManifest, blob)

// test Exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := true; exists != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
}

// test Fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
}
/*
=== Contents of testdata/hello-world.tar ===

blobs/
blobs/sha256/
blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4
blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412
index.json
manifest.json
oci-layout

=== Contents of testdata/hello-world-prefixed-path.tar ===

./
./blobs/
./blobs/sha256/
./blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
./blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4
./blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
./blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412
./index.json
./manifest.json
./oci-layout

// test Exists against a non-existing content
blob = []byte("whatever")
desc = content.NewDescriptorFromBytes("", blob)
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := false; exists != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
}
*/

// test Fetch against a non-existing content
_, err = s.Fetch(ctx, desc)
if want := errdef.ErrNotFound; !errors.Is(err, want) {
t.Errorf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, want)
func TestReadOnlyStorage_TarFS(t *testing.T) {
tarPaths := []string{
"testdata/hello-world.tar",
"testdata/hello-world-prefixed-path.tar",
}
for _, tarPath := range tarPaths {
t.Run(tarPath, func(t *testing.T) {
s, err := NewStorageFromTar(tarPath)
if err != nil {
t.Fatal("NewStorageFromTar() error =", err)
}
ctx := context.Background()

// test data in testdata/hello-world.tar and testdata/hello-world-prefixed-path.tar
blob := []byte(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"8746661ca3c2f215da94e6d3f7dfdcafaff5ec0b21c9aff6af3dc379a82fbc72","container_config":{"Hostname":"8746661ca3c2","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/hello\"]"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-09-23T23:47:57.442225064Z","docker_version":"20.10.7","history":[{"created":"2021-09-23T23:47:57.098990892Z","created_by":"/bin/sh -c #(nop) COPY file:50563a97010fd7ce1ceebd1fa4f4891ac3decdf428333fb2683696f4358af6c2 in / "},{"created":"2021-09-23T23:47:57.442225064Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:e07ee1baac5fae6a26f30cabfe54a36d3402f96afda318fe0a96cec4ca393359"]}}`)
desc := content.NewDescriptorFromBytes(docker.MediaTypeManifest, blob)

// test Exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := true; exists != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
}

// test Fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
}

// test Exists against a non-existing content
blob = []byte("whatever")
desc = content.NewDescriptorFromBytes("", blob)
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := false; exists != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
}

// test Fetch against a non-existing content
_, err = s.Fetch(ctx, desc)
if want := errdef.ErrNotFound; !errors.Is(err, want) {
t.Errorf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, want)
}
})
}
}
Binary file not shown.
6 changes: 4 additions & 2 deletions internal/fs/tarfs/tarfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"

"oras.land/oras-go/v2/errdef"
Expand Down Expand Up @@ -143,12 +144,13 @@ func (tfs *TarFS) indexEntries() error {
if err != nil {
return err
}
tfs.entries[header.Name] = &entry{

name := path.Clean(header.Name)
tfs.entries[name] = &entry{
header: header,
pos: pos - blockSize,
}
}

return nil
}

Expand Down
Loading
Loading