github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/content/oci/readonlyoci_test.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package oci
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"reflect"
    27  	"strconv"
    28  	"strings"
    29  	"testing"
    30  	"testing/fstest"
    31  
    32  	"github.com/opcr-io/oras-go/v2"
    33  	"github.com/opcr-io/oras-go/v2/content"
    34  	"github.com/opcr-io/oras-go/v2/content/memory"
    35  	"github.com/opcr-io/oras-go/v2/internal/docker"
    36  	"github.com/opcr-io/oras-go/v2/registry"
    37  	"github.com/opencontainers/go-digest"
    38  	specs "github.com/opencontainers/image-spec/specs-go"
    39  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    40  	"golang.org/x/sync/errgroup"
    41  )
    42  
    43  func TestReadonlyStoreInterface(t *testing.T) {
    44  	var store interface{} = &ReadOnlyStore{}
    45  	if _, ok := store.(oras.ReadOnlyGraphTarget); !ok {
    46  		t.Error("&ReadOnlyStore{} does not conform oras.ReadOnlyGraphTarget")
    47  	}
    48  	if _, ok := store.(registry.TagLister); !ok {
    49  		t.Error("&ReadOnlyStore{} does not conform registry.TagLister")
    50  	}
    51  }
    52  
    53  func TestReadOnlyStore(t *testing.T) {
    54  	// generate test content
    55  	var blobs [][]byte
    56  	var descs []ocispec.Descriptor
    57  	appendBlob := func(mediaType string, blob []byte) {
    58  		blobs = append(blobs, blob)
    59  		descs = append(descs, ocispec.Descriptor{
    60  			MediaType: mediaType,
    61  			Digest:    digest.FromBytes(blob),
    62  			Size:      int64(len(blob)),
    63  		})
    64  	}
    65  	generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
    66  		manifest := ocispec.Manifest{
    67  			MediaType: ocispec.MediaTypeImageManifest,
    68  			Config:    config,
    69  			Layers:    layers,
    70  		}
    71  		manifestJSON, err := json.Marshal(manifest)
    72  		if err != nil {
    73  			t.Fatal(err)
    74  		}
    75  		appendBlob(manifest.MediaType, manifestJSON)
    76  	}
    77  	generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
    78  		manifest := ocispec.Artifact{
    79  			MediaType: ocispec.MediaTypeArtifactManifest,
    80  			Subject:   &subject,
    81  			Blobs:     blobs,
    82  		}
    83  		manifestJSON, err := json.Marshal(manifest)
    84  		if err != nil {
    85  			t.Fatal(err)
    86  		}
    87  		appendBlob(manifest.MediaType, manifestJSON)
    88  	}
    89  
    90  	appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
    91  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar"))  // Blob 1
    92  	generateManifest(descs[0], descs[1])                       // Blob 2
    93  	generateArtifactManifest(descs[2])                         // Blob 3
    94  	subjectTag := "subject"
    95  
    96  	layout := ocispec.ImageLayout{
    97  		Version: ocispec.ImageLayoutVersion,
    98  	}
    99  	layoutJSON, err := json.Marshal(layout)
   100  	if err != nil {
   101  		t.Fatalf("failed to marshal OCI layout: %v", err)
   102  	}
   103  	index := ocispec.Index{
   104  		Versioned: specs.Versioned{
   105  			SchemaVersion: 2, // historical value
   106  		},
   107  		Manifests: []ocispec.Descriptor{
   108  			{
   109  				MediaType:   descs[2].MediaType,
   110  				Size:        descs[2].Size,
   111  				Digest:      descs[2].Digest,
   112  				Annotations: map[string]string{ocispec.AnnotationRefName: subjectTag},
   113  			},
   114  			descs[3],
   115  		},
   116  	}
   117  	indexJSON, err := json.Marshal(index)
   118  	if err != nil {
   119  		t.Fatalf("failed to marshal index: %v", err)
   120  	}
   121  
   122  	// build fs
   123  	fsys := fstest.MapFS{}
   124  	for i, desc := range descs {
   125  		path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/")
   126  		fsys[path] = &fstest.MapFile{Data: blobs[i]}
   127  	}
   128  	fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON}
   129  	fsys[ociImageIndexFile] = &fstest.MapFile{Data: indexJSON}
   130  
   131  	// test read-only store
   132  	ctx := context.Background()
   133  	s, err := NewFromFS(ctx, fsys)
   134  	if err != nil {
   135  		t.Fatal("NewFromFS() error =", err)
   136  	}
   137  
   138  	// test resolving subject by digest
   139  	gotDesc, err := s.Resolve(ctx, descs[2].Digest.String())
   140  	if err != nil {
   141  		t.Error("ReadOnlyStore.Resolve() error =", err)
   142  	}
   143  	if want := descs[2]; !reflect.DeepEqual(gotDesc, want) {
   144  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   145  	}
   146  
   147  	// test resolving subject by tag
   148  	gotDesc, err = s.Resolve(ctx, subjectTag)
   149  	if err != nil {
   150  		t.Error("ReadOnlyStore.Resolve() error =", err)
   151  	}
   152  	if want := descs[2]; !content.Equal(gotDesc, want) {
   153  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   154  	}
   155  
   156  	// descriptor resolved by tag should have annotations
   157  	if gotDesc.Annotations[ocispec.AnnotationRefName] != subjectTag {
   158  		t.Errorf("ReadOnlyStore.Resolve() returned descriptor without annotations %v, want %v",
   159  			gotDesc.Annotations,
   160  			map[string]string{ocispec.AnnotationRefName: subjectTag})
   161  	}
   162  
   163  	// test resolving artifact by digest
   164  	gotDesc, err = s.Resolve(ctx, descs[3].Digest.String())
   165  	if err != nil {
   166  		t.Error("ReadOnlyStore.Resolve() error =", err)
   167  	}
   168  	if want := descs[3]; !reflect.DeepEqual(gotDesc, want) {
   169  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   170  	}
   171  
   172  	// test resolving blob by digest
   173  	gotDesc, err = s.Resolve(ctx, descs[0].Digest.String())
   174  	if err != nil {
   175  		t.Error("ReadOnlyStore.Resolve() error =", err)
   176  	}
   177  	if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
   178  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   179  	}
   180  
   181  	// test fetching blobs
   182  	eg, egCtx := errgroup.WithContext(ctx)
   183  	for i := range blobs {
   184  		eg.Go(func(i int) func() error {
   185  			return func() error {
   186  				rc, err := s.Fetch(egCtx, descs[i])
   187  				if err != nil {
   188  					return fmt.Errorf("ReadOnlyStore.Fetch(%d) error = %v", i, err)
   189  				}
   190  				got, err := io.ReadAll(rc)
   191  				if err != nil {
   192  					return fmt.Errorf("ReadOnlyStore.Fetch(%d).Read() error = %v", i, err)
   193  				}
   194  				err = rc.Close()
   195  				if err != nil {
   196  					return fmt.Errorf("ReadOnlyStore.Fetch(%d).Close() error = %v", i, err)
   197  				}
   198  				if !bytes.Equal(got, blobs[i]) {
   199  					return fmt.Errorf("ReadOnlyStore.Fetch(%d) = %v, want %v", i, got, blobs[i])
   200  				}
   201  				return nil
   202  			}
   203  		}(i))
   204  	}
   205  	if err := eg.Wait(); err != nil {
   206  		t.Fatal(err)
   207  	}
   208  
   209  	// test predecessors
   210  	wants := [][]ocispec.Descriptor{
   211  		{descs[2]}, // blob 0
   212  		{descs[2]}, // blob 1
   213  		{descs[3]}, // blob 2,
   214  		{},         // blob 3
   215  	}
   216  	for i, want := range wants {
   217  		predecessors, err := s.Predecessors(ctx, descs[i])
   218  		if err != nil {
   219  			t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
   220  		}
   221  		if !equalDescriptorSet(predecessors, want) {
   222  			t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
   223  		}
   224  	}
   225  }
   226  
   227  func TestReadOnlyStore_DirFS(t *testing.T) {
   228  	tempDir := t.TempDir()
   229  	// build an OCI layout on disk
   230  	s, err := New(tempDir)
   231  	if err != nil {
   232  		t.Fatal("New() error =", err)
   233  	}
   234  
   235  	// generate test content
   236  	var blobs [][]byte
   237  	var descs []ocispec.Descriptor
   238  	appendBlob := func(mediaType string, blob []byte) {
   239  		blobs = append(blobs, blob)
   240  		descs = append(descs, ocispec.Descriptor{
   241  			MediaType: mediaType,
   242  			Digest:    digest.FromBytes(blob),
   243  			Size:      int64(len(blob)),
   244  		})
   245  	}
   246  	generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
   247  		manifest := ocispec.Manifest{
   248  			Config: config,
   249  			Layers: layers,
   250  		}
   251  		manifestJSON, err := json.Marshal(manifest)
   252  		if err != nil {
   253  			t.Fatal(err)
   254  		}
   255  		appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
   256  	}
   257  	generateIndex := func(manifests ...ocispec.Descriptor) {
   258  		index := ocispec.Index{
   259  			Manifests: manifests,
   260  		}
   261  		indexJSON, err := json.Marshal(index)
   262  		if err != nil {
   263  			t.Fatal(err)
   264  		}
   265  		appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
   266  	}
   267  	generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
   268  		var manifest ocispec.Artifact
   269  		manifest.Subject = &subject
   270  		manifest.Blobs = append(manifest.Blobs, blobs...)
   271  		manifestJSON, err := json.Marshal(manifest)
   272  		if err != nil {
   273  			t.Fatal(err)
   274  		}
   275  		appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON)
   276  	}
   277  
   278  	appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
   279  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foo"))     // Blob 1
   280  	appendBlob(ocispec.MediaTypeImageLayer, []byte("bar"))     // Blob 2
   281  	appendBlob(ocispec.MediaTypeImageLayer, []byte("hello"))   // Blob 3
   282  	generateManifest(descs[0], descs[1:3]...)                  // Blob 4
   283  	generateManifest(descs[0], descs[3])                       // Blob 5
   284  	generateManifest(descs[0], descs[1:4]...)                  // Blob 6
   285  	generateIndex(descs[4:6]...)                               // Blob 7
   286  	generateIndex(descs[6])                                    // Blob 8
   287  	generateIndex()                                            // Blob 9
   288  	generateIndex(descs[7:10]...)                              // Blob 10
   289  	appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1"))   // Blob 11
   290  	generateArtifactManifest(descs[6], descs[11])              // Blob 12
   291  	appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2"))   // Blob 13
   292  	generateArtifactManifest(descs[10], descs[13])             // Blob 14
   293  
   294  	ctx := context.Background()
   295  	eg, egCtx := errgroup.WithContext(ctx)
   296  	for i := range blobs {
   297  		eg.Go(func(i int) func() error {
   298  			return func() error {
   299  				err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
   300  				if err != nil {
   301  					return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
   302  				}
   303  				return nil
   304  			}
   305  		}(i))
   306  	}
   307  	if err := eg.Wait(); err != nil {
   308  		t.Fatal(err)
   309  	}
   310  
   311  	// tag index root
   312  	indexRoot := descs[10]
   313  	tag := "latest"
   314  	if err := s.Tag(ctx, indexRoot, tag); err != nil {
   315  		t.Fatal("Tag() error =", err)
   316  	}
   317  
   318  	// test read-only store
   319  	readonlyS, err := NewFromFS(ctx, os.DirFS(tempDir))
   320  	if err != nil {
   321  		t.Fatal("New() error =", err)
   322  	}
   323  
   324  	// test resolving index root by tag
   325  	gotDesc, err := readonlyS.Resolve(ctx, tag)
   326  	if err != nil {
   327  		t.Fatal("ReadOnlyStore: Resolve() error =", err)
   328  	}
   329  	if !content.Equal(gotDesc, indexRoot) {
   330  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, indexRoot)
   331  	}
   332  
   333  	// test resolving index root by digest
   334  	gotDesc, err = readonlyS.Resolve(ctx, indexRoot.Digest.String())
   335  	if err != nil {
   336  		t.Fatal("ReadOnlyStore: Resolve() error =", err)
   337  	}
   338  	if !reflect.DeepEqual(gotDesc, indexRoot) {
   339  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, indexRoot)
   340  	}
   341  
   342  	// test resolving artifact manifest by digest
   343  	artifactRootDesc := descs[12]
   344  	gotDesc, err = readonlyS.Resolve(ctx, artifactRootDesc.Digest.String())
   345  	if err != nil {
   346  		t.Fatal("ReadOnlyStore: Resolve() error =", err)
   347  	}
   348  	if !reflect.DeepEqual(gotDesc, artifactRootDesc) {
   349  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, artifactRootDesc)
   350  	}
   351  
   352  	// test resolving blob by digest
   353  	gotDesc, err = readonlyS.Resolve(ctx, descs[0].Digest.String())
   354  	if err != nil {
   355  		t.Fatal("ReadOnlyStore: Resolve() error =", err)
   356  	}
   357  	if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
   358  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   359  	}
   360  
   361  	// test fetching blobs
   362  	for i := range blobs {
   363  		eg.Go(func(i int) func() error {
   364  			return func() error {
   365  				rc, err := s.Fetch(egCtx, descs[i])
   366  				if err != nil {
   367  					return fmt.Errorf("ReadOnlyStore.Fetch(%d) error = %v", i, err)
   368  				}
   369  				got, err := io.ReadAll(rc)
   370  				if err != nil {
   371  					return fmt.Errorf("ReadOnlyStore.Fetch(%d).Read() error = %v", i, err)
   372  				}
   373  				err = rc.Close()
   374  				if err != nil {
   375  					return fmt.Errorf("ReadOnlyStore.Fetch(%d).Close() error = %v", i, err)
   376  				}
   377  				if !bytes.Equal(got, blobs[i]) {
   378  					return fmt.Errorf("ReadOnlyStore.Fetch(%d) = %v, want %v", i, got, blobs[i])
   379  				}
   380  				return nil
   381  			}
   382  		}(i))
   383  	}
   384  	if err := eg.Wait(); err != nil {
   385  		t.Fatal(err)
   386  	}
   387  
   388  	// verify predecessors
   389  	wants := [][]ocispec.Descriptor{
   390  		descs[4:7],            // Blob 0
   391  		{descs[4], descs[6]},  // Blob 1
   392  		{descs[4], descs[6]},  // Blob 2
   393  		{descs[5], descs[6]},  // Blob 3
   394  		{descs[7]},            // Blob 4
   395  		{descs[7]},            // Blob 5
   396  		{descs[8], descs[12]}, // Blob 6
   397  		{descs[10]},           // Blob 7
   398  		{descs[10]},           // Blob 8
   399  		{descs[10]},           // Blob 9
   400  		{descs[14]},           // Blob 10
   401  		{descs[12]},           // Blob 11
   402  		nil,                   // Blob 12, no predecessors
   403  		{descs[14]},           // Blob 13
   404  		nil,                   // Blob 14, no predecessors
   405  	}
   406  	for i, want := range wants {
   407  		predecessors, err := readonlyS.Predecessors(ctx, descs[i])
   408  		if err != nil {
   409  			t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
   410  		}
   411  		if !equalDescriptorSet(predecessors, want) {
   412  			t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
   413  		}
   414  	}
   415  }
   416  
   417  /*
   418  testdata/hello-world.tar contains:
   419  
   420  	blobs/
   421  		sha256/
   422  			2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
   423  			f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
   424  			faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
   425  			feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config
   426  	index.json
   427  	manifest.json
   428  	oci-layout
   429  */
   430  func TestReadOnlyStore_TarFS(t *testing.T) {
   431  	ctx := context.Background()
   432  	s, err := NewFromTar(ctx, "testdata/hello-world.tar")
   433  	if err != nil {
   434  		t.Fatal("New() error =", err)
   435  	}
   436  
   437  	// test data in testdata/hello-world.tar
   438  	descs := []ocispec.Descriptor{
   439  		// desc 0: config
   440  		{
   441  			MediaType: "application/vnd.docker.container.image.v1+json",
   442  			Size:      1469,
   443  			Digest:    "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
   444  		},
   445  		// desc 1: layer
   446  		{
   447  			MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
   448  			Size:      2479,
   449  			Digest:    "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54",
   450  		},
   451  		// desc 2: image manifest
   452  		{
   453  			MediaType: "application/vnd.docker.distribution.manifest.v2+json",
   454  			Digest:    "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4",
   455  			Size:      525,
   456  			Platform: &ocispec.Platform{
   457  				Architecture: "amd64",
   458  				OS:           "linux",
   459  			},
   460  		},
   461  		// desc 3: manifest list
   462  		{
   463  			MediaType: docker.MediaTypeManifestList,
   464  			Size:      2561,
   465  			Digest:    "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af",
   466  		},
   467  	}
   468  
   469  	// test resolving by tag
   470  	for _, desc := range descs {
   471  		gotDesc, err := s.Resolve(ctx, desc.Digest.String())
   472  		if err != nil {
   473  			t.Fatal("ReadOnlyStore: Resolve() error =", err)
   474  		}
   475  		if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
   476  			t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   477  		}
   478  	}
   479  	// test resolving by tag
   480  	gotDesc, err := s.Resolve(ctx, "latest")
   481  	if err != nil {
   482  		t.Fatal("ReadOnlyStore: Resolve() error =", err)
   483  	}
   484  	if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
   485  		t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
   486  	}
   487  
   488  	// test Predecessors
   489  	wantPredecessors := [][]ocispec.Descriptor{
   490  		{descs[2]}, // desc 0
   491  		{descs[2]}, // desc 1
   492  		{descs[3]}, // desc 2
   493  		{},         // desc 3
   494  	}
   495  	for i, want := range wantPredecessors {
   496  		predecessors, err := s.Predecessors(ctx, descs[i])
   497  		if err != nil {
   498  			t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
   499  		}
   500  		if !equalDescriptorSet(predecessors, want) {
   501  			t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
   502  		}
   503  	}
   504  }
   505  
   506  func TestReadOnlyStore_BadIndex(t *testing.T) {
   507  	content := []byte("whatever")
   508  	fsys := fstest.MapFS{
   509  		ociImageIndexFile: &fstest.MapFile{Data: content},
   510  	}
   511  
   512  	ctx := context.Background()
   513  	_, err := NewFromFS(ctx, fsys)
   514  	if err == nil {
   515  		t.Errorf("NewFromFS() error = %v, wantErr %v", err, true)
   516  	}
   517  }
   518  
   519  func TestReadOnlyStore_BadLayout(t *testing.T) {
   520  	content := []byte("whatever")
   521  	fsys := fstest.MapFS{
   522  		ocispec.ImageLayoutFile: &fstest.MapFile{Data: content},
   523  	}
   524  
   525  	ctx := context.Background()
   526  	_, err := NewFromFS(ctx, fsys)
   527  	if err == nil {
   528  		t.Errorf("NewFromFS() error = %v, wantErr %v", err, true)
   529  	}
   530  }
   531  
   532  func TestReadOnlyStore_Copy_OCIToMemory(t *testing.T) {
   533  	// generate test content
   534  	var blobs [][]byte
   535  	var descs []ocispec.Descriptor
   536  	appendBlob := func(mediaType string, blob []byte) {
   537  		blobs = append(blobs, blob)
   538  		descs = append(descs, ocispec.Descriptor{
   539  			MediaType: mediaType,
   540  			Digest:    digest.FromBytes(blob),
   541  			Size:      int64(len(blob)),
   542  		})
   543  	}
   544  	generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
   545  		manifest := ocispec.Manifest{
   546  			MediaType: ocispec.MediaTypeImageManifest,
   547  			Config:    config,
   548  			Layers:    layers,
   549  		}
   550  		manifestJSON, err := json.Marshal(manifest)
   551  		if err != nil {
   552  			t.Fatal(err)
   553  		}
   554  		appendBlob(manifest.MediaType, manifestJSON)
   555  	}
   556  	generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
   557  		manifest := ocispec.Artifact{
   558  			MediaType: ocispec.MediaTypeArtifactManifest,
   559  			Subject:   &subject,
   560  			Blobs:     blobs,
   561  		}
   562  		manifestJSON, err := json.Marshal(manifest)
   563  		if err != nil {
   564  			t.Fatal(err)
   565  		}
   566  		appendBlob(manifest.MediaType, manifestJSON)
   567  	}
   568  
   569  	appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
   570  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar"))  // Blob 1
   571  	generateManifest(descs[0], descs[1])                       // Blob 2
   572  	generateArtifactManifest(descs[2])                         // Blob 3
   573  	tag := "foobar"
   574  	root := descs[3]
   575  
   576  	layout := ocispec.ImageLayout{
   577  		Version: ocispec.ImageLayoutVersion,
   578  	}
   579  	layoutJSON, err := json.Marshal(layout)
   580  	if err != nil {
   581  		t.Fatalf("failed to marshal OCI layout: %v", err)
   582  	}
   583  	index := ocispec.Index{
   584  		Versioned: specs.Versioned{
   585  			SchemaVersion: 2, // historical value
   586  		},
   587  		Manifests: []ocispec.Descriptor{
   588  			{
   589  				MediaType: descs[3].MediaType,
   590  				Digest:    descs[3].Digest,
   591  				Size:      descs[3].Size,
   592  				Annotations: map[string]string{
   593  					ocispec.AnnotationRefName: tag,
   594  				},
   595  			},
   596  		},
   597  	}
   598  	indexJSON, err := json.Marshal(index)
   599  	if err != nil {
   600  		t.Fatalf("failed to marshal index: %v", err)
   601  	}
   602  	// build fs
   603  	fsys := fstest.MapFS{}
   604  	for i, desc := range descs {
   605  		path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/")
   606  		fsys[path] = &fstest.MapFile{Data: blobs[i]}
   607  	}
   608  	fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON}
   609  	fsys[ociImageIndexFile] = &fstest.MapFile{Data: indexJSON}
   610  
   611  	// test read-only store
   612  	ctx := context.Background()
   613  	src, err := NewFromFS(ctx, fsys)
   614  	if err != nil {
   615  		t.Fatal("NewFromFS() error =", err)
   616  	}
   617  
   618  	// test copy
   619  	dst := memory.New()
   620  	gotDesc, err := oras.Copy(ctx, src, tag, dst, "", oras.DefaultCopyOptions)
   621  	if err != nil {
   622  		t.Fatalf("Copy() error = %v, wantErr %v", err, false)
   623  	}
   624  	if !content.Equal(gotDesc, root) {
   625  		t.Errorf("Copy() = %v, want %v", gotDesc, root)
   626  	}
   627  
   628  	// verify contents
   629  	for i, desc := range descs {
   630  		exists, err := dst.Exists(ctx, desc)
   631  		if err != nil {
   632  			t.Fatalf("dst.Exists(%d) error = %v", i, err)
   633  		}
   634  		if !exists {
   635  			t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
   636  		}
   637  	}
   638  
   639  	// verify tag
   640  	gotDesc, err = dst.Resolve(ctx, tag)
   641  	if err != nil {
   642  		t.Fatal("dst.Resolve() error =", err)
   643  	}
   644  	if !content.Equal(gotDesc, root) {
   645  		t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
   646  	}
   647  }
   648  
   649  func TestReadOnlyStore_Tags(t *testing.T) {
   650  	// generate test content
   651  	var blobs [][]byte
   652  	var descs []ocispec.Descriptor
   653  	appendBlob := func(mediaType string, blob []byte) {
   654  		blobs = append(blobs, blob)
   655  		descs = append(descs, ocispec.Descriptor{
   656  			MediaType: mediaType,
   657  			Digest:    digest.FromBytes(blob),
   658  			Size:      int64(len(blob)),
   659  		})
   660  	}
   661  	generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
   662  		manifest := ocispec.Manifest{
   663  			MediaType: ocispec.MediaTypeImageManifest,
   664  			Config:    config,
   665  			Layers:    layers,
   666  		}
   667  		// add annotation to make each manifest unique
   668  		manifest.Annotations = map[string]string{
   669  			"blob_index": strconv.Itoa(len(blobs)),
   670  		}
   671  		manifestJSON, err := json.Marshal(manifest)
   672  		if err != nil {
   673  			t.Fatal(err)
   674  		}
   675  		appendBlob(manifest.MediaType, manifestJSON)
   676  	}
   677  
   678  	appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
   679  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar"))  // Blob 1
   680  	generateManifest(descs[0], descs[1])                       // Blob 2
   681  	generateManifest(descs[0], descs[1])                       // Blob 3
   682  	generateManifest(descs[0], descs[1])                       // Blob 4
   683  	generateManifest(descs[0], descs[1])                       // Blob 5
   684  	generateManifest(descs[0], descs[1])                       // Blob 6
   685  
   686  	layout := ocispec.ImageLayout{
   687  		Version: ocispec.ImageLayoutVersion,
   688  	}
   689  	layoutJSON, err := json.Marshal(layout)
   690  	if err != nil {
   691  		t.Fatalf("failed to marshal OCI layout: %v", err)
   692  	}
   693  
   694  	index := ocispec.Index{
   695  		Versioned: specs.Versioned{
   696  			SchemaVersion: 2, // historical value
   697  		},
   698  	}
   699  	for _, desc := range descs[2:] {
   700  		index.Manifests = append(index.Manifests, ocispec.Descriptor{
   701  			MediaType: desc.MediaType,
   702  			Size:      desc.Size,
   703  			Digest:    desc.Digest,
   704  		})
   705  	}
   706  	index.Manifests[1].Annotations = map[string]string{ocispec.AnnotationRefName: "v2"}
   707  	index.Manifests[2].Annotations = map[string]string{ocispec.AnnotationRefName: "v3"}
   708  	index.Manifests[3].Annotations = map[string]string{ocispec.AnnotationRefName: "v1"}
   709  	index.Manifests[4].Annotations = map[string]string{ocispec.AnnotationRefName: "v4"}
   710  
   711  	indexJSON, err := json.Marshal(index)
   712  	if err != nil {
   713  		t.Fatalf("failed to marshal index: %v", err)
   714  	}
   715  
   716  	// build fs
   717  	fsys := fstest.MapFS{}
   718  	for i, desc := range descs {
   719  		path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/")
   720  		fsys[path] = &fstest.MapFile{Data: blobs[i]}
   721  	}
   722  	fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON}
   723  	fsys[ociImageIndexFile] = &fstest.MapFile{Data: indexJSON}
   724  
   725  	// test read-only store
   726  	ctx := context.Background()
   727  	s, err := NewFromFS(ctx, fsys)
   728  	if err != nil {
   729  		t.Fatal("NewFromFS() error =", err)
   730  	}
   731  
   732  	// test tags
   733  	tests := []struct {
   734  		name string
   735  		last string
   736  		want []string
   737  	}{
   738  		{
   739  			name: "list all tags",
   740  			want: []string{"v1", "v2", "v3", "v4"},
   741  		},
   742  		{
   743  			name: "list from middle",
   744  			last: "v2",
   745  			want: []string{"v3", "v4"},
   746  		},
   747  		{
   748  			name: "list from end",
   749  			last: "v4",
   750  			want: nil,
   751  		},
   752  	}
   753  	for _, tt := range tests {
   754  		t.Run(tt.name, func(t *testing.T) {
   755  			if err := s.Tags(ctx, tt.last, func(got []string) error {
   756  				if !reflect.DeepEqual(got, tt.want) {
   757  					t.Errorf("ReadOnlyStore.Tags() = %v, want %v", got, tt.want)
   758  				}
   759  				return nil
   760  			}); err != nil {
   761  				t.Errorf("ReadOnlyStore.Tags() error = %v", err)
   762  			}
   763  		})
   764  	}
   765  
   766  	wantErr := errors.New("expected error")
   767  	if err := s.Tags(ctx, "", func(got []string) error {
   768  		return wantErr
   769  	}); err != wantErr {
   770  		t.Errorf("ReadOnlyStore.Tags() error = %v, wantErr %v", err, wantErr)
   771  	}
   772  }
   773  
   774  func Test_deleteAnnotationRefName(t *testing.T) {
   775  	tests := []struct {
   776  		name string
   777  		desc ocispec.Descriptor
   778  		want ocispec.Descriptor
   779  	}{
   780  		{
   781  			name: "No annotation",
   782  			desc: ocispec.Descriptor{},
   783  			want: ocispec.Descriptor{},
   784  		},
   785  		{
   786  			name: "Nil annotation",
   787  			desc: ocispec.Descriptor{Annotations: nil},
   788  			want: ocispec.Descriptor{},
   789  		},
   790  		{
   791  			name: "Empty annotation",
   792  			desc: ocispec.Descriptor{Annotations: map[string]string{}},
   793  			want: ocispec.Descriptor{Annotations: map[string]string{}},
   794  		},
   795  		{
   796  			name: "No RefName",
   797  			desc: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
   798  			want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
   799  		},
   800  		{
   801  			name: "Empty RefName",
   802  			desc: ocispec.Descriptor{Annotations: map[string]string{
   803  				"foo":                     "bar",
   804  				ocispec.AnnotationRefName: "",
   805  			}},
   806  			want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
   807  		},
   808  		{
   809  			name: "RefName only",
   810  			desc: ocispec.Descriptor{Annotations: map[string]string{ocispec.AnnotationRefName: "foobar"}},
   811  			want: ocispec.Descriptor{},
   812  		},
   813  		{
   814  			name: "Multiple annotations with RefName",
   815  			desc: ocispec.Descriptor{Annotations: map[string]string{
   816  				"foo":                     "bar",
   817  				ocispec.AnnotationRefName: "foobar",
   818  			}},
   819  			want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
   820  		},
   821  		{
   822  			name: "Multiple annotations with empty RefName",
   823  			desc: ocispec.Descriptor{Annotations: map[string]string{
   824  				"foo":                     "bar",
   825  				ocispec.AnnotationRefName: "",
   826  			}},
   827  			want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
   828  		},
   829  	}
   830  	for _, tt := range tests {
   831  		t.Run(tt.name, func(t *testing.T) {
   832  			if got := deleteAnnotationRefName(tt.desc); !reflect.DeepEqual(got, tt.want) {
   833  				t.Errorf("deleteAnnotationRefName() = %v, want %v", got, tt.want)
   834  			}
   835  		})
   836  	}
   837  }