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