Repository: regclient/regclient
Pull request #536 (2023-09-18 00:03:50)

Add ability to filter artifacts in an index
<!--

Commits must be signed indicating your agreement to the [DCO](https://developercertificate.org/).
See [DCO missing](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) for steps to fix a missing signoff.

-->

### Fixes issue

<!-- If this is a bug fix, include "fixes #xxxx", or "closes #xxxx" -->

### Describe the change

- Move descriptor filter/sort into descriptor funcs
- Reuse those methods for referrers get/list
- Use those methods in artifact get when an index is seen

<!-- Include the type of change: bug fix, new feature, breaking change, documentation update -->
<!-- Describe what was changed, why the change was made, and how it was implemented -->

### How to verify it

```shell
regctl artifact get --platform ... --filter-aritfact-type ... --filter-annotation ... $ref
```

Where `$ref` is an index of artifacts.
<!-- Include steps that can be taken to verify the change -->

### Changelog text

- Support getting an artifact from an index of artifacts
<!-- If the release changelog should have an entry for this, include it here -->

### Please verify and check that the pull request fulfills the following requirements

<!-- Mark the following with an [X] to verify they are included -->

- [X] Tests have been added or not applicable
- [X] Documentation has been added, updated, or not applicable
- [X] Changes have been rebased to main
- [X] Multiple commits to the same code have been squashed



Patch:
diff --git a/cmd/regctl/artifact.go b/cmd/regctl/artifact.go
index d2c0316..431694a 100644
--- a/cmd/regctl/artifact.go
+++ b/cmd/regctl/artifact.go
@@ -208,37 +208,48 @@ func runArtifactGet(cmd *cobra.Command, args []string) error {
 	}
 
 	r := ref.Ref{}
+	matchOpts := types.MatchOpt{
+		ArtifactType:   artifactOpts.filterAT,
+		SortAnnotation: artifactOpts.sortAnnot,
+		SortDesc:       artifactOpts.sortDesc,
+	}
+	if artifactOpts.filterAnnot != nil {
+		matchOpts.Annotations = map[string]string{}
+		for _, kv := range artifactOpts.filterAnnot {
+			kvSplit := strings.SplitN(kv, "=", 2)
+			if len(kvSplit) == 2 {
+				matchOpts.Annotations[kvSplit[0]] = kvSplit[1]
+			} else {
+				matchOpts.Annotations[kv] = ""
+			}
+		}
+	}
+	if artifactOpts.latest {
+		matchOpts.SortAnnotation = types.AnnotationCreated
+		matchOpts.SortDesc = true
+	}
+	if artifactOpts.platform != "" {
+		p, err := platform.Parse(artifactOpts.platform)
+		if err != nil {
+			return fmt.Errorf("platform could not be parsed: %w", err)
+		}
+		matchOpts.Platform = &p
+	}
+
+	// lookup referrers to the subject
 	if len(args) == 0 && artifactOpts.subject != "" {
 		rSubject, err := ref.New(artifactOpts.subject)
 		if err != nil {
 			return err
 		}
-		// lookup referrers to the subject
-		referrerOpts := []scheme.ReferrerOpts{}
-		if artifactOpts.filterAT != "" {
-			referrerOpts = append(referrerOpts, scheme.WithReferrerAT(artifactOpts.filterAT))
-		}
-		if artifactOpts.filterAnnot != nil {
-			af := map[string]string{}
-			for _, kv := range artifactOpts.filterAnnot {
-				kvSplit := strings.SplitN(kv, "=", 2)
-				if len(kvSplit) == 2 {
-					af[kvSplit[0]] = kvSplit[1]
-				} else {
-					af[kv] = ""
-				}
-			}
-			referrerOpts = append(referrerOpts, scheme.WithReferrerAnnotations(af))
+		referrerMatchOpts := matchOpts
+		referrerMatchOpts.Platform = nil
+		referrerOpts := []scheme.ReferrerOpts{
+			scheme.WithReferrerMatchOpt(referrerMatchOpts),
 		}
 		if artifactOpts.platform != "" {
 			referrerOpts = append(referrerOpts, scheme.WithReferrerPlatform(artifactOpts.platform))
 		}
-		if artifactOpts.latest {
-			referrerOpts = append(referrerOpts, scheme.WithReferrerSort(types.AnnotationCreated, true))
-		} else if artifactOpts.sortAnnot != "" {
-			referrerOpts = append(referrerOpts, scheme.WithReferrerSort(artifactOpts.sortAnnot, artifactOpts.sortDesc))
-		}
-
 		rl, err := rc.ReferrerList(ctx, rSubject, referrerOpts...)
 		if err != nil {
 			return err
@@ -263,11 +274,31 @@ func runArtifactGet(cmd *cobra.Command, args []string) error {
 	defer rc.Close(ctx, r)
 
 	// pull the manifest
-	mm, err := rc.ManifestGet(ctx, r)
+	m, err := rc.ManifestGet(ctx, r)
 	if err != nil {
 		return err
 	}
-	mi, ok := mm.(manifest.Imager)
+	// lookup descriptor if index / manifest list is returned
+	if m.IsList() {
+		mi, ok := m.(manifest.Indexer)
+		if !ok {
+			return fmt.Errorf("manifest list does not support index methods%.0w", types.ErrUnsupportedMediaType)
+		}
+		dl, err := mi.GetManifestList()
+		if err != nil {
+			return fmt.Errorf("failed to get descriptor list: %w", err)
+		}
+		d, err := types.DescriptorListSearch(dl, matchOpts)
+		if err != nil {
+			return fmt.Errorf("no matching artifacts found in index: %w", err)
+		}
+		r.Digest = d.Digest.String()
+		m, err = rc.ManifestGet(ctx, r)
+		if err != nil {
+			return err
+		}
+	}
+	mi, ok := m.(manifest.Imager)
 	if !ok {
 		return fmt.Errorf("manifest does not support image methods%.0w", types.ErrUnsupportedMediaType)
 	}
@@ -435,30 +466,32 @@ func runArtifactList(cmd *cobra.Command, args []string) error {
 	rc := newRegClient()
 	defer rc.Close(ctx, rSubject)
 
-	referrerOpts := []scheme.ReferrerOpts{}
-	if artifactOpts.filterAT != "" {
-		referrerOpts = append(referrerOpts, scheme.WithReferrerAT(artifactOpts.filterAT))
+	matchOpts := types.MatchOpt{
+		ArtifactType:   artifactOpts.filterAT,
+		SortAnnotation: artifactOpts.sortAnnot,
+		SortDesc:       artifactOpts.sortDesc,
 	}
 	if artifactOpts.filterAnnot != nil {
-		af := map[string]string{}
+		matchOpts.Annotations = map[string]string{}
 		for _, kv := range artifactOpts.filterAnnot {
 			kvSplit := strings.SplitN(kv, "=", 2)
 			if len(kvSplit) == 2 {
-				af[kvSplit[0]] = kvSplit[1]
+				matchOpts.Annotations[kvSplit[0]] = kvSplit[1]
 			} else {
-				af[kv] = ""
+				matchOpts.Annotations[kv] = ""
 			}
 		}
-		referrerOpts = append(referrerOpts, scheme.WithReferrerAnnotations(af))
+	}
+	if artifactOpts.latest {
+		matchOpts.SortAnnotation = types.AnnotationCreated
+		matchOpts.SortDesc = true
+	}
+	referrerOpts := []scheme.ReferrerOpts{
+		scheme.WithReferrerMatchOpt(matchOpts),
 	}
 	if artifactOpts.platform != "" {
 		referrerOpts = append(referrerOpts, scheme.WithReferrerPlatform(artifactOpts.platform))
 	}
-	if artifactOpts.latest {
-		referrerOpts = append(referrerOpts, scheme.WithReferrerSort(types.AnnotationCreated, true))
-	} else if artifactOpts.sortAnnot != "" {
-		referrerOpts = append(referrerOpts, scheme.WithReferrerSort(artifactOpts.sortAnnot, artifactOpts.sortDesc))
-	}
 
 	rl, err := rc.ReferrerList(ctx, rSubject, referrerOpts...)
 	if err != nil {
@@ -834,7 +867,7 @@ func runArtifactTree(cmd *cobra.Command, args []string) error {
 
 	referrerOpts := []scheme.ReferrerOpts{}
 	if artifactOpts.filterAT != "" {
-		referrerOpts = append(referrerOpts, scheme.WithReferrerAT(artifactOpts.filterAT))
+		referrerOpts = append(referrerOpts, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: artifactOpts.filterAT}))
 	}
 	if artifactOpts.filterAnnot != nil {
 		af := map[string]string{}
@@ -846,7 +879,7 @@ func runArtifactTree(cmd *cobra.Command, args []string) error {
 				af[kv] = ""
 			}
 		}
-		referrerOpts = append(referrerOpts, scheme.WithReferrerAnnotations(af))
+		referrerOpts = append(referrerOpts, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: af}))
 	}
 
 	// include digest tags if requested
diff --git a/cmd/regsync/root.go b/cmd/regsync/root.go
index 6fbad15..73d4b54 100644
--- a/cmd/regsync/root.go
+++ b/cmd/regsync/root.go
@@ -793,10 +793,10 @@ func (s ConfigSync) processRef(ctx context.Context, src, tgt ref.Ref, action act
 			for _, filter := range s.ReferrerFilters {
 				rOpts := []scheme.ReferrerOpts{}
 				if filter.ArtifactType != "" {
-					rOpts = append(rOpts, scheme.WithReferrerAT(filter.ArtifactType))
+					rOpts = append(rOpts, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: filter.ArtifactType}))
 				}
 				if filter.Annotations != nil {
-					rOpts = append(rOpts, scheme.WithReferrerAnnotations(filter.Annotations))
+					rOpts = append(rOpts, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: filter.Annotations}))
 				}
 				opts = append(opts, regclient.ImageWithReferrers(rOpts...))
 			}
diff --git a/scheme/reg/referrer.go b/scheme/reg/referrer.go
index ad2aa34..ba0617f 100644
--- a/scheme/reg/referrer.go
+++ b/scheme/reg/referrer.go
@@ -83,7 +83,7 @@ func (reg *Reg) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme.Refe
 				reg.featureSet("referrer", r.Registry, r.Repository, err == nil)
 			}
 			if err == nil {
-				if config.FilterArtifactType == "" {
+				if config.MatchOpt.ArtifactType == "" {
 					// only cache if successful and artifactType is not filtered
 					reg.cacheRL.Set(rCache, rl)
 				}
@@ -157,8 +157,8 @@ func (reg *Reg) referrerListByAPIPage(ctx context.Context, r ref.Ref, config sch
 		Tags:    []string{},
 	}
 	query := url.Values{}
-	if config.FilterArtifactType != "" {
-		query.Set("artifactType", config.FilterArtifactType)
+	if config.MatchOpt.ArtifactType != "" {
+		query.Set("artifactType", config.MatchOpt.ArtifactType)
 	}
 	req := &reghttp.Req{
 		Host: r.Registry,
diff --git a/scheme/scheme.go b/scheme/scheme.go
index 6313ea1..dd1f76c 100644
--- a/scheme/scheme.go
+++ b/scheme/scheme.go
@@ -4,8 +4,6 @@ package scheme
 import (
 	"context"
 	"io"
-	"sort"
-	"strings"
 
 	"github.com/regclient/regclient/internal/throttle"
 	"github.com/regclient/regclient/types"
@@ -103,102 +101,69 @@ func WithManifest(m manifest.Manifest) ManifestOpts {
 
 // ReferrerConfig is used by schemes to import ReferrerOpts
 type ReferrerConfig struct {
-	FilterArtifactType string
-	FilterAnnotation   map[string]string
-	Platform           string
-	SortAnnotation     string
-	SortDesc           bool
+	MatchOpt types.MatchOpt // filter/sort results
+	Platform string         // get referrers for a specific platform
 }
 
 // ReferrerOpts is used to set options on referrer APIs
 type ReferrerOpts func(*ReferrerConfig)
 
+// WithReferrerMatchOpt filters results using MatchOpt
+func WithReferrerMatchOpt(mo types.MatchOpt) ReferrerOpts {
+	return func(config *ReferrerConfig) {
+		config.MatchOpt = mo
+	}
+}
+
+// WithReferrerPlatform gets referrers for a single platform from a multi-platform manifest
+func WithReferrerPlatform(p string) ReferrerOpts {
+	return func(config *ReferrerConfig) {
+		config.Platform = p
+	}
+}
+
 // WithReferrerAT filters by a specific artifactType value
+//
+// Deprecated: replace with WithReferrerMatchOpt
 func WithReferrerAT(at string) ReferrerOpts {
 	return func(config *ReferrerConfig) {
-		config.FilterArtifactType = at
+		config.MatchOpt.ArtifactType = at
 	}
 }
 
 // WithReferrerAnnotations filters by a list of annotations, all of which must match
+//
+// Deprecated: replace with WithReferrerMatchOpt
 func WithReferrerAnnotations(annotations map[string]string) ReferrerOpts {
 	return func(config *ReferrerConfig) {
-		if config.FilterAnnotation == nil {
-			config.FilterAnnotation = annotations
+		if config.MatchOpt.Annotations == nil {
+			config.MatchOpt.Annotations = annotations
 		} else {
 			for k, v := range annotations {
-				config.FilterAnnotation[k] = v
+				config.MatchOpt.Annotations[k] = v
 			}
 		}
 	}
 }
 
+// WithReferrerSort orders the resulting referrers listing according to a specified annotation.
+//
+// Deprecated: replace with WithReferrerMatchOpt
+func WithReferrerSort(annotation string, desc bool) ReferrerOpts {
+	return func(config *ReferrerConfig) {
+		config.MatchOpt.SortAnnotation = annotation
+		config.MatchOpt.SortDesc = desc
+	}
+}
+
 // ReferrerFilter filters the referrer list according to the config
 func ReferrerFilter(config ReferrerConfig, rlIn referrer.ReferrerList) referrer.ReferrerList {
-	rlOut := referrer.ReferrerList{
+	return referrer.ReferrerList{
 		Subject:     rlIn.Subject,
 		Manifest:    rlIn.Manifest,
 		Annotations: rlIn.Annotations,
 		Tags:        rlIn.Tags,
-	}
-	rlOut.Descriptors = make([]types.Descriptor, len(rlIn.Descriptors))
-	copy(rlOut.Descriptors, rlIn.Descriptors)
-	if config.FilterArtifactType != "" && len(rlOut.Descriptors) > 0 {
-		for i := len(rlOut.Descriptors) - 1; i >= 0; i-- {
-			if rlOut.Descriptors[i].ArtifactType != config.FilterArtifactType {
-				rlOut.Descriptors = append(rlOut.Descriptors[:i], rlOut.Descriptors[i+1:]...)
-			}
-		}
-	}
-	for k, v := range config.FilterAnnotation {
-		if len(rlOut.Descriptors) > 0 {
-			for i := len(rlOut.Descriptors) - 1; i >= 0; i-- {
-				if rlOut.Descriptors[i].Annotations == nil {
-					rlOut.Descriptors = append(rlOut.Descriptors[:i], rlOut.Descriptors[i+1:]...)
-				} else if rlVal, ok := rlOut.Descriptors[i].Annotations[k]; !ok || v != "" && rlVal != v {
-					rlOut.Descriptors = append(rlOut.Descriptors[:i], rlOut.Descriptors[i+1:]...)
-				}
-			}
-		}
-	}
-	// sort the results if requested
-	if config.SortAnnotation != "" {
-		sort.Slice(rlOut.Descriptors, func(i, j int) bool {
-			// if annotations are not defined, sort to the very end
-			if rlOut.Descriptors[i].Annotations == nil {
-				return false
-			}
-			if _, ok := rlOut.Descriptors[i].Annotations[config.SortAnnotation]; !ok {
-				return false
-			}
-			if rlOut.Descriptors[j].Annotations == nil {
-				return true
-			}
-			if _, ok := rlOut.Descriptors[j].Annotations[config.SortAnnotation]; !ok {
-				return true
-			}
-			// else sort by string
-			if strings.Compare(rlOut.Descriptors[i].Annotations[config.SortAnnotation], rlOut.Descriptors[j].Annotations[config.SortAnnotation]) < 0 {
-				return !config.SortDesc
-			}
-			return config.SortDesc
-		})
-	}
-	return rlOut
-}
-
-// WithReferrerPlatform gets referrers for a single platform from a multi-platform manifest
-func WithReferrerPlatform(platform string) ReferrerOpts {
-	return func(config *ReferrerConfig) {
-		config.Platform = platform
-	}
-}
-
-// WithReferrerSort orders the resulting referrers listing according to a specified annotation.
-func WithReferrerSort(annotation string, desc bool) ReferrerOpts {
-	return func(config *ReferrerConfig) {
-		config.SortAnnotation = annotation
-		config.SortDesc = desc
+		Descriptors: types.DescriptorListFilter(rlIn.Descriptors, config.MatchOpt),
 	}
 }
 
diff --git a/types/descriptor.go b/types/descriptor.go
index 4b44434..5e9c09c 100644
--- a/types/descriptor.go
+++ b/types/descriptor.go
@@ -2,6 +2,7 @@ package types
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 	"text/tabwriter"
 
@@ -182,3 +183,89 @@ func (d Descriptor) MarshalPrettyTW(tw *tabwriter.Writer, prefix string) error {
 	}
 	return nil
 }
+
+// MatchOpt defines conditions for a match descriptor
+type MatchOpt struct {
+	Platform       *platform.Platform // Platform to match including compatible platforms (darwin/arm64 matches linux/arm64)
+	ArtifactType   string             // Match ArtifactType in the descriptor
+	Annotations    map[string]string  // Match each of the specified annotations and their value, an empty value verifies the key is set
+	SortAnnotation string             // Sort the results by an annotation, string based comparison, descriptors without the annotation are sorted last
+	SortDesc       bool               // Set to true to sort in descending order
+}
+
+// Match returns true if the descriptor matches the options, including compatible platforms
+func (d Descriptor) Match(opt MatchOpt) bool {
+	if opt.ArtifactType != "" && d.ArtifactType != opt.ArtifactType {
+		return false
+	}
+	if opt.Annotations != nil && len(opt.Annotations) > 0 {
+		if d.Annotations == nil {
+			return false
+		}
+		for k, v := range opt.Annotations {
+			if dv, ok := d.Annotations[k]; !ok || (v != "" && v != dv) {
+				return false
+			}
+		}
+	}
+	if opt.Platform != nil {
+		if d.Platform == nil {
+			return false
+		}
+		if !platform.Compatible(*opt.Platform, *d.Platform) {
+			return false
+		}
+	}
+	return true
+}
+
+// DescriptorListFilter returns a list of descriptors from the list matching the search options.
+// When opt.SortAnnotation is set, the order of descriptors with matching annotations is undefined.
+func DescriptorListFilter(dl []Descriptor, opt MatchOpt) []Descriptor {
+	ret := []Descriptor{}
+	for _, d := range dl {
+		if d.Match(opt) {
+			ret = append(ret, d)
+		}
+	}
+	if opt.SortAnnotation != "" {
+		sort.Slice(ret, func(i, j int) bool {
+			// if annotations are not defined, sort to the very end
+			if ret[i].Annotations == nil {
+				return false
+			}
+			if _, ok := ret[i].Annotations[opt.SortAnnotation]; !ok {
+				return false
+			}
+			if ret[j].Annotations == nil {
+				return true
+			}
+			if _, ok := ret[j].Annotations[opt.SortAnnotation]; !ok {
+				return true
+			}
+			// else sort by string
+			if strings.Compare(ret[i].Annotations[opt.SortAnnotation], ret[j].Annotations[opt.SortAnnotation]) < 0 {
+				return !opt.SortDesc
+			}
+			return opt.SortDesc
+		})
+	}
+	return ret
+}
+
+// DescriptorListSearch returns the first descriptor from the list matching the search options
+func DescriptorListSearch(dl []Descriptor, opt MatchOpt) (Descriptor, error) {
+	filter := DescriptorListFilter(dl, opt)
+	if len(filter) < 1 {
+		return Descriptor{}, ErrNotFound
+	}
+	// prefer exact platform match when available
+	if opt.Platform != nil {
+		for _, d := range filter {
+			if platform.Match(*opt.Platform, *d.Platform) {
+				return d, nil
+			}
+		}
+	}
+	return filter[0], nil
+}
diff --git a/types/manifest/docker2.go b/types/manifest/docker2.go
index 7bb338c..9f559e5 100644
--- a/types/manifest/docker2.go
+++ b/types/manifest/docker2.go
@@ -96,7 +96,17 @@ func (m *docker2Manifest) GetPlatformDesc(p *platform.Platform) (*types.Descript
 	return nil, fmt.Errorf("platform lookup not available for media type %s%.0w", m.desc.MediaType, types.ErrUnsupportedMediaType)
 }
 func (m *docker2ManifestList) GetPlatformDesc(p *platform.Platform) (*types.Descriptor, error) {
-	return getPlatformDesc(p, m.Manifests)
+	if !m.manifSet {
+		return nil, types.ErrManifestNotSet
+	}
+	if p == nil {
+		return nil, fmt.Errorf("invalid input, platform is nil%.0w", types.ErrNotFound)
+	}
+	d, err := types.DescriptorListSearch(m.Manifests, types.MatchOpt{Platform: p})
+	if err != nil {
+		return nil, fmt.Errorf("platform not found: %s%.0w", *p, err)
+	}
+	return &d, nil
 }
 
 func (m *docker2Manifest) GetPlatformList() ([]*platform.Platform, error) {
diff --git a/types/manifest/manifest.go b/types/manifest/manifest.go
index be895ea..11069e6 100644
--- a/types/manifest/manifest.go
+++ b/types/manifest/manifest.go
@@ -176,7 +176,14 @@ func GetPlatformDesc(m Manifest, p *platform.Platform) (*types.Descriptor, error
 	if err != nil {
 		return nil, err
 	}
-	return getPlatformDesc(p, dl)
+	if p == nil {
+		return nil, fmt.Errorf("invalid input, platform is nil%.0w", types.ErrNotFound)
+	}
+	d, err := types.DescriptorListSearch(dl, types.MatchOpt{Platform: p})
+	if err != nil {
+		return nil, fmt.Errorf("platform not found: %s%.0w", *p, err)
+	}
+	return &d, nil
 }
 
 // GetPlatformList returns the list of platforms from an index
@@ -551,24 +558,6 @@ func verifyMT(expected, received string) error {
 	return nil
 }
 
-func getPlatformDesc(p *platform.Platform, dl []types.Descriptor) (*types.Descriptor, error) {
-	if p == nil {
-		return nil, fmt.Errorf("invalid input, platform is nil%.0w", types.ErrNotFound)
-	}
-	for _, d := range dl {
-		if d.Platform != nil && platform.Match(*p, *d.Platform) {
-			return &d, nil
-		}
-	}
-	// if no platforms match, fall back to searching for a compatible platform (Mac runs Linux images)
-	for _, d := range dl {
-		if d.Platform != nil && platform.Compatible(*p, *d.Platform) {
-			return &d, nil
-		}
-	}
-	return nil, fmt.Errorf("platform not found: %s%.0w", *p, types.ErrNotFound)
-}
-
 func getPlatformList(dl []types.Descriptor) ([]*platform.Platform, error) {
 	var l []*platform.Platform
 	for _, d := range dl {
diff --git a/types/manifest/oci1.go b/types/manifest/oci1.go
index 9623280..ace54fd 100644
--- a/types/manifest/oci1.go
+++ b/types/manifest/oci1.go
@@ -126,11 +126,17 @@ func (m *oci1Manifest) GetPlatformDesc(p *platform.Platform) (*types.Descriptor,
 	return nil, fmt.Errorf("platform lookup not available for media type %s%.0w", m.desc.MediaType, types.ErrUnsupportedMediaType)
 }
 func (m *oci1Index) GetPlatformDesc(p *platform.Platform) (*types.Descriptor, error) {
-	dl, err := m.GetManifestList()
+	if !m.manifSet {
+		return nil, types.ErrManifestNotSet
+	}
+	if p == nil {
+		return nil, fmt.Errorf("invalid input, platform is nil%.0w", types.ErrNotFound)
+	}
+	d, err := types.DescriptorListSearch(m.Manifests, types.MatchOpt{Platform: p})
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("platform not found: %s%.0w", *p, err)
 	}
-	return getPlatformDesc(p, dl)
+	return &d, nil
 }
 func (m *oci1Artifact) GetPlatformDesc(p *platform.Platform) (*types.Descriptor, error) {
 	return nil, fmt.Errorf("platform lookup not available for media type %s%.0w", m.desc.MediaType, types.ErrUnsupportedMediaType)


Test patch:
diff --git a/cmd/regctl/artifact_test.go b/cmd/regctl/artifact_test.go
index b05f3ff..ee67495 100644
--- a/cmd/regctl/artifact_test.go
+++ b/cmd/regctl/artifact_test.go
@@ -12,6 +12,69 @@ import (
 	"github.com/regclient/regclient/types"
 )
 
+func TestArtifactGet(t *testing.T) {
+	saveArtifactOpts := artifactOpts
+	tt := []struct {
+		name        string
+		args        []string
+		expectErr   error
+		expectOut   string
+		outContains bool
+	}{
+		{
+			name:      "Missing arg",
+			args:      []string{"artifact", "get"},
+			expectErr: fmt.Errorf("either a reference or subject must be provided"),
+		},
+		{
+			name:      "Invalid ref",
+			args:      []string{"artifact", "get", "invalid*ref"},
+			expectErr: types.ErrInvalidReference,
+		},
+		{
+			name:      "Missing manifest",
+			args:      []string{"artifact", "get", "ocidir://../../testdata/testrepo:missing"},
+			expectErr: types.ErrNotFound,
+		},
+		{
+			name:      "By Manifest",
+			args:      []string{"artifact", "get", "ocidir://../../testdata/testrepo:a1"},
+			expectOut: "eggs",
+		},
+		{
+			name:      "By Subject",
+			args:      []string{"artifact", "get", "--subject", "ocidir://../../testdata/testrepo:v2", "--filter-artifact-type", "application/example.sbom"},
+			expectOut: "eggs",
+		},
+		{
+			name:      "By Index",
+			args:      []string{"artifact", "get", "ocidir://../../testdata/testrepo:ai", "--filter-annotation", "type=sbom"},
+			expectOut: "eggs",
+		},
+	}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			out, err := cobraTest(t, tc.args...)
+			artifactOpts = saveArtifactOpts
+			if tc.expectErr != nil {
+				if err == nil {
+					t.Errorf("did not receive expected error: %v", tc.expectErr)
+				} else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
+					t.Errorf("unexpected error, received %v, expected %v", err, tc.expectErr)
+				}
+				return
+			}
+			if err != nil {
+				t.Errorf("returned unexpected error: %v", err)
+				return
+			}
+			if (!tc.outContains && out != tc.expectOut) || (tc.outContains && !strings.Contains(out, tc.expectOut)) {
+				t.Errorf("unexpected output, expected %s, received %s", tc.expectOut, out)
+			}
+		})
+	}
+}
+
 func TestArtifactList(t *testing.T) {
 	saveArtifactOpts := artifactOpts
 	tt := []struct {
diff --git a/cmd/regsync/regsync_test.go b/cmd/regsync/regsync_test.go
index 490a8b2..1e8eafe 100644
--- a/cmd/regsync/regsync_test.go
+++ b/cmd/regsync/regsync_test.go
@@ -100,12 +100,12 @@ func TestProcess(t *testing.T) {
 		t.Errorf("failed to get platform ")
 	}
 	d2AMD := desc2AMD.Digest
-	desc2SBOM, err := rc.ReferrerList(ctx, r2, scheme.WithReferrerAT("application/example.sbom"))
+	desc2SBOM, err := rc.ReferrerList(ctx, r2, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: "application/example.sbom"}))
 	if err != nil || len(desc2SBOM.Descriptors) == 0 {
 		t.Errorf("failed to get SBOM for v2: %v", err)
 	}
 	d2SBOM := desc2SBOM.Descriptors[0].Digest
-	desc2Sig, err := rc.ReferrerList(ctx, r2, scheme.WithReferrerAT("application/example.signature"))
+	desc2Sig, err := rc.ReferrerList(ctx, r2, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: "application/example.signature"}))
 	if err != nil || len(desc2Sig.Descriptors) == 0 {
 		t.Errorf("failed to get signature for v2: %v", err)
 	}
diff --git a/scheme/ocidir/referrer_test.go b/scheme/ocidir/referrer_test.go
index 67d26e7..8533b34 100644
--- a/scheme/ocidir/referrer_test.go
+++ b/scheme/ocidir/referrer_test.go
@@ -196,7 +196,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("Failed creating getRef: %v", err)
 			return
 		}
-		rl, err := o.ReferrerList(ctx, r, scheme.WithReferrerSort(timeAnnot, false))
+		rl, err := o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{SortAnnotation: timeAnnot}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -224,7 +224,7 @@ func TestReferrer(t *testing.T) {
 		if len(rl.Tags) != 1 || rl.Tags[0] != tagRef {
 			t.Errorf("tag list missing entries, received: %v", rl.Tags)
 		}
-		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerSort(timeAnnot, true))
+		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{SortAnnotation: timeAnnot, SortDesc: true}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList reverse: %v", err)
 			return
@@ -245,7 +245,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("Failed creating getRef: %v", err)
 			return
 		}
-		rl, err := o.ReferrerList(ctx, r, scheme.WithReferrerAT(aType))
+		rl, err := o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: aType}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -254,7 +254,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("descriptor list length, expected 1, received %d", len(rl.Descriptors))
 			return
 		}
-		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerAT("application/vnd.example.unknown"))
+		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: "application/vnd.example.unknown"}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -270,7 +270,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("Failed creating getRef: %v", err)
 			return
 		}
-		rl, err := o.ReferrerList(ctx, r, scheme.WithReferrerAnnotations(map[string]string{extraAnnot: extraValueB}))
+		rl, err := o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: map[string]string{extraAnnot: extraValueB}}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -279,7 +279,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("descriptor list length, expected 1, received %d", len(rl.Descriptors))
 			return
 		}
-		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerAnnotations(map[string]string{extraAnnot: "unknown value"}))
+		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: map[string]string{extraAnnot: "unknown value"}}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -288,7 +288,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("unexpected descriptors")
 			return
 		}
-		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerAnnotations(map[string]string{extraAnnot: ""}))
+		rl, err = o.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: map[string]string{extraAnnot: ""}}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
diff --git a/scheme/reg/referrer_test.go b/scheme/reg/referrer_test.go
index 86e1906..f274d63 100644
--- a/scheme/reg/referrer_test.go
+++ b/scheme/reg/referrer_test.go
@@ -1017,7 +1017,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("Failed creating ref: %v", err)
 			return
 		}
-		rl, err := reg.ReferrerList(ctx, r, scheme.WithReferrerAT(configMTA))
+		rl, err := reg.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: configMTA}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -1026,7 +1026,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("descriptor list mismatch: %v", rl.Descriptors)
 			return
 		}
-		rl, err = reg.ReferrerList(ctx, r, scheme.WithReferrerAT("application/vnd.example.unknown"))
+		rl, err = reg.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{ArtifactType: "application/vnd.example.unknown"}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -1042,7 +1042,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("Failed creating ref: %v", err)
 			return
 		}
-		rl, err := reg.ReferrerList(ctx, r, scheme.WithReferrerAnnotations(map[string]string{extraAnnot: extraValue2}))
+		rl, err := reg.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: map[string]string{extraAnnot: extraValue2}}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -1051,7 +1051,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("descriptor list mismatch: %v", rl.Descriptors)
 			return
 		}
-		rl, err = reg.ReferrerList(ctx, r, scheme.WithReferrerAnnotations(map[string]string{extraAnnot: "unknown value"}))
+		rl, err = reg.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: map[string]string{extraAnnot: "unknown value"}}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
@@ -1060,7 +1060,7 @@ func TestReferrer(t *testing.T) {
 			t.Errorf("unexpected descriptors: %v", rl.Descriptors)
 			return
 		}
-		rl, err = reg.ReferrerList(ctx, r, scheme.WithReferrerAnnotations(map[string]string{extraAnnot: ""}))
+		rl, err = reg.ReferrerList(ctx, r, scheme.WithReferrerMatchOpt(types.MatchOpt{Annotations: map[string]string{extraAnnot: ""}}))
 		if err != nil {
 			t.Errorf("Failed running ReferrerList: %v", err)
 			return
diff --git a/testdata/build-testrepo.sh b/testdata/build-testrepo.sh
index 0938c16..a0650bd 100755
--- a/testdata/build-testrepo.sh
+++ b/testdata/build-testrepo.sh
@@ -105,6 +105,17 @@ echo 64 arms | regctl artifact put \
   --artifact-type application/example.arms -m application/example.arms \
   --subject ocidir://testrepo:v2 --platform linux/arm64
 
+# create an index of artifacts
+regctl index create \
+  --artifact-type application/example.artifacts \
+  --ref ocidir://testrepo:a1 \
+  --desc-annotation "type=sbom" \
+  ocidir://testrepo:ai
+regctl index add \
+  --ref ocidir://testrepo:a2 \
+  --desc-annotation "type=sig" \
+  ocidir://testrepo:ai
+
 # create a digest tag from v3 pointing to v1
 v1_dig="$(regctl image digest ocidir://testrepo:v1)"
 v1_dig=${v1_dig#*:}
diff --git a/testdata/testrepo/blobs/sha256/5a9860a0d47070db1d8057670874d78f87abf352c34efb2913ee11f5484366b8 b/testdata/testrepo/blobs/sha256/5a9860a0d47070db1d8057670874d78f87abf352c34efb2913ee11f5484366b8
new file mode 100644
index 0000000..1b54050
--- /dev/null
+++ b/testdata/testrepo/blobs/sha256/5a9860a0d47070db1d8057670874d78f87abf352c34efb2913ee11f5484366b8
@@ -0,0 +1,1 @@
+{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/example.artifacts","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":583,"digest":"sha256:54dd4e105916f7a95f7911c0328ce753d7bd08034538933c042cfae3f63bc573","annotations":{"type":"sbom"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":588,"digest":"sha256:559c971b91100f44900af38bb1391a7b2637afe674087ec68e5e6a92abcce57b","annotations":{"type":"sig"}}]}
\ No newline at end of file
diff --git a/testdata/testrepo/index.json b/testdata/testrepo/index.json
index 9d9a188..164df37 100644
--- a/testdata/testrepo/index.json
+++ b/testdata/testrepo/index.json
@@ -1,1 +1,1 @@
-{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","size":964,"digest":"sha256:7ba20869dedc0090a7074b8574eb1f096f705f6f30082d08a3160b86848089d0","annotations":{"org.opencontainers.image.ref.name":"b1"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":964,"digest":"sha256:15a3231581118fd8ce33fd581cbae6958e990cbc99a6a7a1f50c63cce95d4ec5","annotations":{"org.opencontainers.image.ref.name":"b2"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":964,"digest":"sha256:98c0bb8411acb5dafef5223e13e8f553190ab3d1994062b5785788733cdbfca3","annotations":{"org.opencontainers.image.ref.name":"b3"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":1262,"digest":"sha256:0d4ea07d06d42ad1cb756a5d9e71f62288e426b22a230b224410164181998c4c","annotations":{"org.opencontainers.image.ref.name":"v1"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":922,"digest":"sha256:4eef5c8459d36adab1f354e332af99915cb75dfe4b91e19cdd53ccfb33aa0418","annotations":{"org.opencontainers.image.ref.name":"v2"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":1141,"digest":"sha256:d0b099bf44f68230c262a2a2397d4caf9c74da80374b4674b8e41bc4c8579275","annotations":{"org.opencontainers.image.ref.name":"v3"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":394,"digest":"sha256:fcb04a33e5e9ecd98d30b05a784de6fe65a26e84316a995c55a12dfbb02132e8","annotations":{"org.opencontainers.image.ref.name":"a-docker"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":583,"digest":"sha256:54dd4e105916f7a95f7911c0328ce753d7bd08034538933c042cfae3f63bc573","annotations":{"org.opencontainers.image.ref.name":"a1"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":482,"digest":"sha256:7df92b086d7ec46a64ae0ff5652d371a7cf524d7076a925130807f464355c0d4","annotations":{"org.opencontainers.image.ref.name":"sha256-4eef5c8459d36adab1f354e332af99915cb75dfe4b91e19cdd53ccfb33aa0418"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":588,"digest":"sha256:559c971b91100f44900af38bb1391a7b2637afe674087ec68e5e6a92abcce57b","annotations":{"org.opencontainers.image.ref.name":"a2"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":576,"digest":"sha256:05d037f7d6b8fa8438c2d5f0cabd089358a56c057143527d8f60139e6799e171"},{"mediaType":"application/vnd.oci.image.index.v1+json","size":282,"digest":"sha256:876d3d65d4df98cdee450fb417721d1b568a4c56af1851126c18a5e22f2a1830","annotations":{"org.opencontainers.image.ref.name":"sha256-ef05efc8cfd478ac3140fce1297bd6b72dc5f5f1df31bfce690aa903a2c20310"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":576,"digest":"sha256:045226b7a97d5cc8a7c585f6816815ed662b0833734c6cbfa901137079e0997c"},{"mediaType":"application/vnd.oci.image.index.v1+json","size":282,"digest":"sha256:acf88dd4fa9307038a4099ff4ccd13ae77fe4d52a5f3d5edcb5a77142627ab52","annotations":{"org.opencontainers.image.ref.name":"sha256-4638b261096d1b87665ca154599c4ac1664a16ee1bc12b302f8aae24e336e071"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":576,"digest":"sha256:49fa909609a7f6c7589fb7cfa966e2cb04b682c5b2ca1da1de943a21e26bfe70"},{"mediaType":"application/vnd.oci.image.index.v1+json","size":282,"digest":"sha256:cf7f350eaff86a56089a93aff69bfa26aeac72d086bb5f0490bd46179260a77f","annotations":{"org.opencontainers.image.ref.name":"sha256-56ed84e8d4f983411218751bcb5d544e14e28d210101a12c63a1be03108de01b"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":1141,"digest":"sha256:d0b099bf44f68230c262a2a2397d4caf9c74da80374b4674b8e41bc4c8579275","annotations":{"org.opencontainers.image.ref.name":"sha256-0d4ea07d06d42ad1cb756a5d9e71f62288e426b22a230b224410164181998c4c.d0b099bf44f68230.meta"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":414,"digest":"sha256:507bd066139b92463a30828d26fb9b9e10b2bb6b0e22a875b8062798c8bf9e5b","annotations":{"org.opencontainers.image.ref.name":"child"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":445,"digest":"sha256:c65509594eca7ea7a70208fb49a3bf2f74150ca4ec6ce66dcc92f3ff381290ff","annotations":{"org.opencontainers.image.ref.name":"loop"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":279,"digest":"sha256:e986d78d171eceec4b7233606ecbfa5a2c4dd9c64bbe50609df282815a37675c","annotations":{"org.opencontainers.image.ref.name":"sha256-507bd066139b92463a30828d26fb9b9e10b2bb6b0e22a875b8062798c8bf9e5b"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":415,"digest":"sha256:b86ce43f2d9bf91965bd7d310744d1590ad38f6243cf656677b002f696c30482","annotations":{"org.opencontainers.image.ref.name":"mirror"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":415,"digest":"sha256:b86ce43f2d9bf91965bd7d310744d1590ad38f6243cf656677b002f696c30482","annotations":{"org.opencontainers.image.ref.name":"sha256-b86ce43f2d9bf91965bd7d310744d1590ad38f6243cf656677b002f696c30482"}}]}
\ No newline at end of file
+{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","size":964,"digest":"sha256:7ba20869dedc0090a7074b8574eb1f096f705f6f30082d08a3160b86848089d0","annotations":{"org.opencontainers.image.ref.name":"b1"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":964,"digest":"sha256:15a3231581118fd8ce33fd581cbae6958e990cbc99a6a7a1f50c63cce95d4ec5","annotations":{"org.opencontainers.image.ref.name":"b2"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":964,"digest":"sha256:98c0bb8411acb5dafef5223e13e8f553190ab3d1994062b5785788733cdbfca3","annotations":{"org.opencontainers.image.ref.name":"b3"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":1262,"digest":"sha256:0d4ea07d06d42ad1cb756a5d9e71f62288e426b22a230b224410164181998c4c","annotations":{"org.opencontainers.image.ref.name":"v1"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":922,"digest":"sha256:4eef5c8459d36adab1f354e332af99915cb75dfe4b91e19cdd53ccfb33aa0418","annotations":{"org.opencontainers.image.ref.name":"v2"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":1141,"digest":"sha256:d0b099bf44f68230c262a2a2397d4caf9c74da80374b4674b8e41bc4c8579275","annotations":{"org.opencontainers.image.ref.name":"v3"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":394,"digest":"sha256:fcb04a33e5e9ecd98d30b05a784de6fe65a26e84316a995c55a12dfbb02132e8","annotations":{"org.opencontainers.image.ref.name":"a-docker"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":583,"digest":"sha256:54dd4e105916f7a95f7911c0328ce753d7bd08034538933c042cfae3f63bc573","annotations":{"org.opencontainers.image.ref.name":"a1"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":482,"digest":"sha256:7df92b086d7ec46a64ae0ff5652d371a7cf524d7076a925130807f464355c0d4","annotations":{"org.opencontainers.image.ref.name":"sha256-4eef5c8459d36adab1f354e332af99915cb75dfe4b91e19cdd53ccfb33aa0418"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":588,"digest":"sha256:559c971b91100f44900af38bb1391a7b2637afe674087ec68e5e6a92abcce57b","annotations":{"org.opencontainers.image.ref.name":"a2"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":576,"digest":"sha256:05d037f7d6b8fa8438c2d5f0cabd089358a56c057143527d8f60139e6799e171"},{"mediaType":"application/vnd.oci.image.index.v1+json","size":282,"digest":"sha256:876d3d65d4df98cdee450fb417721d1b568a4c56af1851126c18a5e22f2a1830","annotations":{"org.opencontainers.image.ref.name":"sha256-ef05efc8cfd478ac3140fce1297bd6b72dc5f5f1df31bfce690aa903a2c20310"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":576,"digest":"sha256:045226b7a97d5cc8a7c585f6816815ed662b0833734c6cbfa901137079e0997c"},{"mediaType":"application/vnd.oci.image.index.v1+json","size":282,"digest":"sha256:acf88dd4fa9307038a4099ff4ccd13ae77fe4d52a5f3d5edcb5a77142627ab52","annotations":{"org.opencontainers.image.ref.name":"sha256-4638b261096d1b87665ca154599c4ac1664a16ee1bc12b302f8aae24e336e071"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":576,"digest":"sha256:49fa909609a7f6c7589fb7cfa966e2cb04b682c5b2ca1da1de943a21e26bfe70"},{"mediaType":"application/vnd.oci.image.index.v1+json","size":282,"digest":"sha256:cf7f350eaff86a56089a93aff69bfa26aeac72d086bb5f0490bd46179260a77f","annotations":{"org.opencontainers.image.ref.name":"sha256-56ed84e8d4f983411218751bcb5d544e14e28d210101a12c63a1be03108de01b"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":1141,"digest":"sha256:d0b099bf44f68230c262a2a2397d4caf9c74da80374b4674b8e41bc4c8579275","annotations":{"org.opencontainers.image.ref.name":"sha256-0d4ea07d06d42ad1cb756a5d9e71f62288e426b22a230b224410164181998c4c.d0b099bf44f68230.meta"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":414,"digest":"sha256:507bd066139b92463a30828d26fb9b9e10b2bb6b0e22a875b8062798c8bf9e5b","annotations":{"org.opencontainers.image.ref.name":"child"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":445,"digest":"sha256:c65509594eca7ea7a70208fb49a3bf2f74150ca4ec6ce66dcc92f3ff381290ff","annotations":{"org.opencontainers.image.ref.name":"loop"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":279,"digest":"sha256:e986d78d171eceec4b7233606ecbfa5a2c4dd9c64bbe50609df282815a37675c","annotations":{"org.opencontainers.image.ref.name":"sha256-507bd066139b92463a30828d26fb9b9e10b2bb6b0e22a875b8062798c8bf9e5b"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":415,"digest":"sha256:b86ce43f2d9bf91965bd7d310744d1590ad38f6243cf656677b002f696c30482","annotations":{"org.opencontainers.image.ref.name":"mirror"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":415,"digest":"sha256:b86ce43f2d9bf91965bd7d310744d1590ad38f6243cf656677b002f696c30482","annotations":{"org.opencontainers.image.ref.name":"sha256-b86ce43f2d9bf91965bd7d310744d1590ad38f6243cf656677b002f696c30482"}},{"mediaType":"application/vnd.oci.image.index.v1+json","size":499,"digest":"sha256:5a9860a0d47070db1d8057670874d78f87abf352c34efb2913ee11f5484366b8","annotations":{"org.opencontainers.image.ref.name":"ai"}}]}
\ No newline at end of file
diff --git a/types/descriptor_test.go b/types/descriptor_test.go
index e36505a..1f54151 100644
--- a/types/descriptor_test.go
+++ b/types/descriptor_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestDescriptorData(t *testing.T) {
-	tests := []struct {
+	tt := []struct {
 		name     string
 		d        Descriptor
 		wantData []byte
@@ -58,12 +58,12 @@ func TestDescriptorData(t *testing.T) {
 			wantData: []byte("example data"),
 		},
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			out, err := tt.d.GetData()
-			if tt.wantErr != nil {
-				if err == nil || (!errors.Is(err, tt.wantErr) && err.Error() != tt.wantErr.Error()) {
-					t.Errorf("expected error %v, received %v", tt.wantErr, err)
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			out, err := tc.d.GetData()
+			if tc.wantErr != nil {
+				if err == nil || (!errors.Is(err, tc.wantErr) && err.Error() != tc.wantErr.Error()) {
+					t.Errorf("expected error %v, received %v", tc.wantErr, err)
 				}
 				return
 			}
@@ -71,8 +71,8 @@ func TestDescriptorData(t *testing.T) {
 				t.Errorf("received error %v", err)
 				return
 			}
-			if !bytes.Equal(out, tt.wantData) {
-				t.Errorf("data mismatch, expected %s, received %s", string(tt.wantData), string(out))
+			if !bytes.Equal(out, tc.wantData) {
+				t.Errorf("data mismatch, expected %s, received %s", string(tc.wantData), string(out))
 			}
 		})
 	}
@@ -81,7 +81,7 @@ func TestDescriptorData(t *testing.T) {
 func TestDescriptorEq(t *testing.T) {
 	digA := digest.FromString("test A")
 	digB := digest.FromString("test B")
-	tests := []struct {
+	tt := []struct {
 		name        string
 		d1, d2      Descriptor
 		expectEqual bool
@@ -433,13 +433,13 @@ func TestDescriptorEq(t *testing.T) {
 			expectSame:  true,
 		},
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if tt.d1.Equal(tt.d2) != tt.expectEqual {
-				t.Errorf("equal is not %v", tt.expectEqual)
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.d1.Equal(tc.d2) != tc.expectEqual {
+				t.Errorf("equal is not %v", tc.expectEqual)
 			}
-			if tt.d1.Same(tt.d2) != tt.expectSame {
-				t.Errorf("same is not %v", tt.expectSame)
+			if tc.d1.Same(tc.d2) != tc.expectSame {
+				t.Errorf("same is not %v", tc.expectSame)
 			}
 		})
 	}
@@ -531,3 +531,254 @@ func TestDataJSON(t *testing.T) {
 		})
 	}
 }
+
+func TestDescriptorSearch(t *testing.T) {
+	dAMD64 := Descriptor{
+		MediaType: MediaTypeOCI1Manifest,
+		Size:      12345,
+		Digest:    EmptyDigest,
+		Platform: &platform.Platform{
+			OS:           "linux",
+			Architecture: "amd64",
+		},
+	}
+	dAMD64Win := Descriptor{
+		MediaType: MediaTypeOCI1Manifest,
+		Size:      12345,
+		Digest:    EmptyDigest,
+		Platform: &platform.Platform{
+			OS:           "windows",
+			Architecture: "amd64",
+		},
+	}
+	dARM64 := Descriptor{
+		MediaType: MediaTypeOCI1Manifest,
+		Size:      12345,
+		Digest:    EmptyDigest,
+		Platform: &platform.Platform{
+			OS:           "linux",
+			Architecture: "arm64",
+		},
+	}
+	dAnnotations := Descriptor{
+		MediaType: MediaTypeOCI1Manifest,
+		Size:      12345,
+		Digest:    EmptyDigest,
+		Platform: &platform.Platform{
+			OS:           "linux",
+			Architecture: "amd64",
+		},
+		Annotations: map[string]string{
+			"runtime": "special runtime",
+			"version": "1.2.3",
+			"date":    "2022-01-01 12:34:56",
+		},
+	}
+	dAnnotations2 := Descriptor{
+		MediaType: MediaTypeOCI1Manifest,
+		Size:      12345,
+		Digest:    EmptyDigest,
+		Platform: &platform.Platform{
+			OS:           "linux",
+			Architecture: "amd64",
+		},
+		Annotations: map[string]string{
+			"runtime": "special runtime",
+			"version": "1.3.0",
+			"date":    "2022-04-01 01:02:03",
+		},
+	}
+	dArtifact := Descriptor{
+		MediaType:    MediaTypeOCI1Manifest,
+		Size:         12345,
+		Digest:       EmptyDigest,
+		ArtifactType: "application/example.artifact",
+		Annotations: map[string]string{
+			"version": "1.2.3",
+			"date":    "2022-01-01 12:34:56",
+		},
+	}
+	dArtifact2 := Descriptor{
+		MediaType:    MediaTypeOCI1Manifest,
+		Size:         12345,
+		Digest:       EmptyDigest,
+		ArtifactType: "application/example.artifact",
+		Annotations: map[string]string{
+			"version": "1.2.9",
+			"date":    "2022-04-01 01:02:03",
+			"unique":  "x",
+		},
+	}
+	dArtifact3 := Descriptor{
+		MediaType:    MediaTypeOCI1Manifest,
+		Size:         12345,
+		Digest:       EmptyDigest,
+		ArtifactType: "application/example.artifact",
+		Annotations: map[string]string{
+			"version": "1.3.0",
+			"date":    "2022-02-28 02:04:08",
+		},
+	}
+	testDL := []Descriptor{
+		dAMD64,
+		dARM64,
+		dAMD64Win,
+		dAnnotations,
+		dAnnotations2,
+		dArtifact,
+		dArtifact2,
+		dArtifact3,
+	}
+	tt := []struct {
+		name   string
+		dl     []Descriptor
+		opt    MatchOpt
+		expect Descriptor
+		err    error
+	}{
+		{
+			name: "empty",
+			err:  ErrNotFound,
+		},
+		{
+			name: "amd64",
+			dl:   testDL,
+			opt: MatchOpt{
+				Platform: &platform.Platform{
+					OS:           "linux",
+					Architecture: "amd64",
+				},
+			},
+			expect: dAMD64,
+		},
+		{
+			name: "amd64 compat",
+			dl:   testDL,
+			opt: MatchOpt{
+				Platform: &platform.Platform{
+					OS:           "darwin",
+					Architecture: "amd64",
+				},
+			},
+			expect: dAMD64,
+		},
+		{
+			name: "amd64 windows",
+			dl:   testDL,
+			opt: MatchOpt{
+				Platform: &platform.Platform{
+					OS:           "windows",
+					Architecture: "amd64",
+				},
+			},
+			expect: dAMD64Win,
+		},
+		{
+			name: "amd64 annotations",
+			dl:   testDL,
+			opt: MatchOpt{
+				Annotations: map[string]string{
+					"runtime": "special runtime",
+				},
+				Platform: &platform.Platform{
+					OS:           "linux",
+					Architecture: "amd64",
+				},
+			},
+			expect: dAnnotations,
+		},
+		{
+			name: "artifact missing",
+			dl:   testDL,
+			opt: MatchOpt{
+				ArtifactType: "application/example.artifact",
+				Platform: &platform.Platform{
+					OS:           "linux",
+					Architecture: "amd64",
+				},
+			},
+			err: ErrNotFound,
+		},
+		{
+			name: "artifact",
+			dl:   testDL,
+			opt: MatchOpt{
+				ArtifactType: "application/example.artifact",
+				Annotations:  map[string]string{},
+			},
+			expect: dArtifact,
+		},
+		{
+			name: "artifact sort",
+			dl:   testDL,
+			opt: MatchOpt{
+				ArtifactType:   "application/example.artifact",
+				Annotations:    map[string]string{},
+				SortAnnotation: "date",
+			},
+			expect: dArtifact,
+		},
+		{
+			name: "artifact sort desc",
+			dl:   testDL,
+			opt: MatchOpt{
+				ArtifactType:   "application/example.artifact",
+				Annotations:    map[string]string{},
+				SortAnnotation: "date",
+				SortDesc:       true,
+			},
+			expect: dArtifact2,
+		},
+		{
+			name: "artifact sort unset",
+			dl:   testDL,
+			opt: MatchOpt{
+				ArtifactType:   "application/example.artifact",
+				Annotations:    map[string]string{},
+				SortAnnotation: "unique",
+			},
+			expect: dArtifact2,
+		},
+		{
+			name: "artifact sort unset desc",
+			dl:   testDL,
+			opt: MatchOpt{
+				ArtifactType:   "application/example.artifact",
+				Annotations:    map[string]string{},
+				SortAnnotation: "unique",
+				SortDesc:       true,
+			},
+			expect: dArtifact2,
+		},
+		{
+			name: "artifact sort all unique desc",
+			dl:   testDL,
+			opt: MatchOpt{
+				SortAnnotation: "unique",
+				SortDesc:       true,
+			},
+			expect: dArtifact2,
+		},
+	}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			result, err := DescriptorListSearch(tc.dl, tc.opt)
+			if tc.err != nil {
+				if err == nil {
+					t.Error("did not fail")
+				} else if !errors.Is(err, tc.err) && err.Error() != tc.err.Error() {
+					t.Errorf("unexpected error, expected %v, received %v", tc.err, err)
+				}
+				return
+			}
+			if err != nil {
+				t.Errorf("unexpected error, received %v", err)
+				return
+			}
+			if !tc.expect.Equal(result) {
+				t.Errorf("unexpected result, expected %v, received %v", tc.expect, result)
+			}
+		})
+	}
+
+}