Skip to content

Commit dff5628

Browse files
authored
fix: handle dot-prefixed path for tarfs (#875)
1. Handle dot-prefixed paths such as `./index.json` when indexing and reading entries in tarfs 2. Add corresponding tests 3. Refactor original tests Fixes: #851 Signed-off-by: Lixia (Sylvia) Lei <[email protected]>
1 parent a15da41 commit dff5628

File tree

7 files changed

+482
-289
lines changed

7 files changed

+482
-289
lines changed

content/oci/readonlyoci_test.go

Lines changed: 106 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -416,91 +416,117 @@ func TestReadOnlyStore_DirFS(t *testing.T) {
416416
}
417417

418418
/*
419-
testdata/hello-world.tar contains:
420-
421-
blobs/
422-
sha256/
423-
2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
424-
f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
425-
faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
426-
feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config
427-
index.json
428-
manifest.json
429-
oci-layout
419+
=== Contents of testdata/hello-world.tar ===
420+
421+
blobs/
422+
423+
blobs/sha256/
424+
blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
425+
blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
426+
blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
427+
blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config
428+
429+
index.json
430+
manifest.json
431+
oci-layout
432+
433+
=== Contents of testdata/hello-world-prefixed-path.tar ===
434+
435+
./
436+
./blobs/
437+
438+
./blobs/sha256/
439+
./blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
440+
./blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
441+
./blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
442+
./blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config
443+
444+
./index.json
445+
./manifest.json
446+
./oci-layout
430447
*/
448+
431449
func TestReadOnlyStore_TarFS(t *testing.T) {
432-
ctx := context.Background()
433-
s, err := NewFromTar(ctx, "testdata/hello-world.tar")
434-
if err != nil {
435-
t.Fatal("New() error =", err)
436-
}
450+
tarPaths := []string{
451+
"testdata/hello-world.tar",
452+
"testdata/hello-world-prefixed-path.tar",
453+
}
454+
for _, tarPath := range tarPaths {
455+
t.Run(tarPath, func(t *testing.T) {
456+
ctx := context.Background()
457+
s, err := NewFromTar(ctx, tarPath)
458+
if err != nil {
459+
t.Fatal("New() error =", err)
460+
}
437461

438-
// test data in testdata/hello-world.tar
439-
descs := []ocispec.Descriptor{
440-
// desc 0: config
441-
{
442-
MediaType: "application/vnd.docker.container.image.v1+json",
443-
Size: 1469,
444-
Digest: "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
445-
},
446-
// desc 1: layer
447-
{
448-
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
449-
Size: 2479,
450-
Digest: "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54",
451-
},
452-
// desc 2: image manifest
453-
{
454-
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
455-
Digest: "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4",
456-
Size: 525,
457-
Platform: &ocispec.Platform{
458-
Architecture: "amd64",
459-
OS: "linux",
460-
},
461-
},
462-
// desc 3: manifest list
463-
{
464-
MediaType: docker.MediaTypeManifestList,
465-
Size: 2561,
466-
Digest: "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af",
467-
},
468-
}
462+
// test data in testdata/hello-world.tar
463+
descs := []ocispec.Descriptor{
464+
// desc 0: config
465+
{
466+
MediaType: "application/vnd.docker.container.image.v1+json",
467+
Size: 1469,
468+
Digest: "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
469+
},
470+
// desc 1: layer
471+
{
472+
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
473+
Size: 2479,
474+
Digest: "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54",
475+
},
476+
// desc 2: image manifest
477+
{
478+
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
479+
Digest: "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4",
480+
Size: 525,
481+
Platform: &ocispec.Platform{
482+
Architecture: "amd64",
483+
OS: "linux",
484+
},
485+
},
486+
// desc 3: manifest list
487+
{
488+
MediaType: docker.MediaTypeManifestList,
489+
Size: 2561,
490+
Digest: "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af",
491+
},
492+
}
469493

470-
// test resolving by tag
471-
for _, desc := range descs {
472-
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
473-
if err != nil {
474-
t.Fatal("ReadOnlyStore: Resolve() error =", err)
475-
}
476-
if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
477-
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
478-
}
479-
}
480-
// test resolving by tag
481-
gotDesc, err := s.Resolve(ctx, "latest")
482-
if err != nil {
483-
t.Fatal("ReadOnlyStore: Resolve() error =", err)
484-
}
485-
if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
486-
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
487-
}
494+
// test resolving by tag
495+
for _, desc := range descs {
496+
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
497+
if err != nil {
498+
t.Fatal("ReadOnlyStore: Resolve() error =", err)
499+
}
500+
if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
501+
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
502+
}
503+
}
504+
// test resolving by tag
505+
gotDesc, err := s.Resolve(ctx, "latest")
506+
if err != nil {
507+
t.Fatal("ReadOnlyStore: Resolve() error =", err)
508+
}
509+
if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
510+
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
511+
}
488512

489-
// test Predecessors
490-
wantPredecessors := [][]ocispec.Descriptor{
491-
{descs[2]}, // desc 0
492-
{descs[2]}, // desc 1
493-
{descs[3]}, // desc 2
494-
{}, // desc 3
495-
}
496-
for i, want := range wantPredecessors {
497-
predecessors, err := s.Predecessors(ctx, descs[i])
498-
if err != nil {
499-
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
500-
}
501-
if !equalDescriptorSet(predecessors, want) {
502-
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
503-
}
513+
// test Predecessors
514+
wantPredecessors := [][]ocispec.Descriptor{
515+
{descs[2]}, // desc 0
516+
{descs[2]}, // desc 1
517+
{descs[3]}, // desc 2
518+
{}, // desc 3
519+
}
520+
for i, want := range wantPredecessors {
521+
predecessors, err := s.Predecessors(ctx, descs[i])
522+
if err != nil {
523+
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
524+
}
525+
if !equalDescriptorSet(predecessors, want) {
526+
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
527+
}
528+
}
529+
})
504530
}
505531
}
506532

content/oci/readonlystorage_test.go

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -161,57 +161,93 @@ func TestReadOnlyStorage_DirFS(t *testing.T) {
161161
}
162162
}
163163

164-
func TestReadOnlyStorage_TarFS(t *testing.T) {
165-
s, err := NewStorageFromTar("testdata/hello-world.tar")
166-
if err != nil {
167-
t.Fatal("NewStorageFromTar() error =", err)
168-
}
169-
ctx := context.Background()
170-
171-
// test data in testdata/hello-world.tar
172-
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"]}}`)
173-
desc := content.NewDescriptorFromBytes(docker.MediaTypeManifest, blob)
174-
175-
// test Exists
176-
exists, err := s.Exists(ctx, desc)
177-
if err != nil {
178-
t.Fatal("ReadOnlyStorage.Exists() error =", err)
179-
}
180-
if want := true; exists != want {
181-
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
182-
}
183-
184-
// test Fetch
185-
rc, err := s.Fetch(ctx, desc)
186-
if err != nil {
187-
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
188-
}
189-
got, err := io.ReadAll(rc)
190-
if err != nil {
191-
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
192-
}
193-
err = rc.Close()
194-
if err != nil {
195-
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
196-
}
197-
if !bytes.Equal(got, blob) {
198-
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
199-
}
164+
/*
165+
=== Contents of testdata/hello-world.tar ===
166+
167+
blobs/
168+
blobs/sha256/
169+
blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
170+
blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4
171+
blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
172+
blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412
173+
index.json
174+
manifest.json
175+
oci-layout
176+
177+
=== Contents of testdata/hello-world-prefixed-path.tar ===
178+
179+
./
180+
./blobs/
181+
./blobs/sha256/
182+
./blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
183+
./blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4
184+
./blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
185+
./blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412
186+
./index.json
187+
./manifest.json
188+
./oci-layout
200189
201-
// test Exists against a non-existing content
202-
blob = []byte("whatever")
203-
desc = content.NewDescriptorFromBytes("", blob)
204-
exists, err = s.Exists(ctx, desc)
205-
if err != nil {
206-
t.Fatal("ReadOnlyStorage.Exists() error =", err)
207-
}
208-
if want := false; exists != want {
209-
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
210-
}
190+
*/
211191

212-
// test Fetch against a non-existing content
213-
_, err = s.Fetch(ctx, desc)
214-
if want := errdef.ErrNotFound; !errors.Is(err, want) {
215-
t.Errorf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, want)
192+
func TestReadOnlyStorage_TarFS(t *testing.T) {
193+
tarPaths := []string{
194+
"testdata/hello-world.tar",
195+
"testdata/hello-world-prefixed-path.tar",
196+
}
197+
for _, tarPath := range tarPaths {
198+
t.Run(tarPath, func(t *testing.T) {
199+
s, err := NewStorageFromTar(tarPath)
200+
if err != nil {
201+
t.Fatal("NewStorageFromTar() error =", err)
202+
}
203+
ctx := context.Background()
204+
205+
// test data in testdata/hello-world.tar and testdata/hello-world-prefixed-path.tar
206+
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"]}}`)
207+
desc := content.NewDescriptorFromBytes(docker.MediaTypeManifest, blob)
208+
209+
// test Exists
210+
exists, err := s.Exists(ctx, desc)
211+
if err != nil {
212+
t.Fatal("ReadOnlyStorage.Exists() error =", err)
213+
}
214+
if want := true; exists != want {
215+
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
216+
}
217+
218+
// test Fetch
219+
rc, err := s.Fetch(ctx, desc)
220+
if err != nil {
221+
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
222+
}
223+
got, err := io.ReadAll(rc)
224+
if err != nil {
225+
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
226+
}
227+
err = rc.Close()
228+
if err != nil {
229+
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
230+
}
231+
if !bytes.Equal(got, blob) {
232+
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
233+
}
234+
235+
// test Exists against a non-existing content
236+
blob = []byte("whatever")
237+
desc = content.NewDescriptorFromBytes("", blob)
238+
exists, err = s.Exists(ctx, desc)
239+
if err != nil {
240+
t.Fatal("ReadOnlyStorage.Exists() error =", err)
241+
}
242+
if want := false; exists != want {
243+
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
244+
}
245+
246+
// test Fetch against a non-existing content
247+
_, err = s.Fetch(ctx, desc)
248+
if want := errdef.ErrNotFound; !errors.Is(err, want) {
249+
t.Errorf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, want)
250+
}
251+
})
216252
}
217253
}
Binary file not shown.

internal/fs/tarfs/tarfs.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"io"
2323
"io/fs"
2424
"os"
25+
"path"
2526
"path/filepath"
2627

2728
"oras.land/oras-go/v2/errdef"
@@ -143,12 +144,13 @@ func (tfs *TarFS) indexEntries() error {
143144
if err != nil {
144145
return err
145146
}
146-
tfs.entries[header.Name] = &entry{
147+
148+
name := path.Clean(header.Name)
149+
tfs.entries[name] = &entry{
147150
header: header,
148151
pos: pos - blockSize,
149152
}
150153
}
151-
152154
return nil
153155
}
154156

0 commit comments

Comments
 (0)