github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/registry/remote/referrers.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 remote
    17  
    18  import (
    19  	"errors"
    20  	"strings"
    21  
    22  	"github.com/opcr-io/oras-go/v2/content"
    23  	"github.com/opcr-io/oras-go/v2/internal/descriptor"
    24  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    25  )
    26  
    27  // zeroDigest represents a digest that consists of zeros. zeroDigest is used
    28  // for pinging Referrers API.
    29  const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
    30  
    31  // referrersState represents the state of Referrers API.
    32  type referrersState = int32
    33  
    34  const (
    35  	// referrersStateUnknown represents an unknown state of Referrers API.
    36  	referrersStateUnknown referrersState = iota
    37  	// referrersStateSupported represents that the repository is known to
    38  	// support Referrers API.
    39  	referrersStateSupported
    40  	// referrersStateUnsupported represents that the repository is known to
    41  	// not support Referrers API.
    42  	referrersStateUnsupported
    43  )
    44  
    45  // referrerOperation represents an operation on a referrer.
    46  type referrerOperation = int32
    47  
    48  const (
    49  	// referrerOperationAdd represents an addition operation on a referrer.
    50  	referrerOperationAdd referrerOperation = iota
    51  	// referrerOperationRemove represents a removal operation on a referrer.
    52  	referrerOperationRemove
    53  )
    54  
    55  // referrerChange represents a change on a referrer.
    56  type referrerChange struct {
    57  	referrer  ocispec.Descriptor
    58  	operation referrerOperation
    59  }
    60  
    61  var (
    62  	// ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability()
    63  	// when the Referrers API capability has been already set.
    64  	ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set")
    65  
    66  	// errNoReferrerUpdate is returned by applyReferrerChanges() when there
    67  	// is no any referrer update.
    68  	errNoReferrerUpdate = errors.New("no referrer update")
    69  )
    70  
    71  // buildReferrersTag builds the referrers tag for the given manifest descriptor.
    72  // Format: <algorithm>-<digest>
    73  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#unavailable-referrers-api
    74  func buildReferrersTag(desc ocispec.Descriptor) string {
    75  	alg := desc.Digest.Algorithm().String()
    76  	encoded := desc.Digest.Encoded()
    77  	return alg + "-" + encoded
    78  }
    79  
    80  // isReferrersFilterApplied checks annotations to see if requested is in the
    81  // applied filter list.
    82  func isReferrersFilterApplied(annotations map[string]string, requested string) bool {
    83  	applied := annotations[ocispec.AnnotationReferrersFiltersApplied]
    84  	if applied == "" || requested == "" {
    85  		return false
    86  	}
    87  	filters := strings.Split(applied, ",")
    88  	for _, f := range filters {
    89  		if f == requested {
    90  			return true
    91  		}
    92  	}
    93  	return false
    94  }
    95  
    96  // filterReferrers filters a slice of referrers by artifactType in place.
    97  // The returned slice contains matching referrers.
    98  func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor {
    99  	if artifactType == "" {
   100  		return refs
   101  	}
   102  	var j int
   103  	for i, ref := range refs {
   104  		if ref.ArtifactType == artifactType {
   105  			if i != j {
   106  				refs[j] = ref
   107  			}
   108  			j++
   109  		}
   110  	}
   111  	return refs[:j]
   112  }
   113  
   114  // applyReferrerChanges applies referrerChanges on referrers and returns the
   115  // updated referrers.
   116  // Returns errNoReferrerUpdate if there is no any referrers updates.
   117  func applyReferrerChanges(referrers []ocispec.Descriptor, referrerChanges []referrerChange) ([]ocispec.Descriptor, error) {
   118  	referrersMap := make(map[descriptor.Descriptor]int, len(referrers)+len(referrerChanges))
   119  	updatedReferrers := make([]ocispec.Descriptor, 0, len(referrers)+len(referrerChanges))
   120  	var updateRequired bool
   121  	for _, r := range referrers {
   122  		if content.Equal(r, ocispec.Descriptor{}) {
   123  			// skip bad entry
   124  			updateRequired = true
   125  			continue
   126  		}
   127  		key := descriptor.FromOCI(r)
   128  		if _, ok := referrersMap[key]; ok {
   129  			// skip duplicates
   130  			updateRequired = true
   131  			continue
   132  		}
   133  		updatedReferrers = append(updatedReferrers, r)
   134  		referrersMap[key] = len(updatedReferrers) - 1
   135  	}
   136  
   137  	// apply changes
   138  	for _, change := range referrerChanges {
   139  		key := descriptor.FromOCI(change.referrer)
   140  		switch change.operation {
   141  		case referrerOperationAdd:
   142  			if _, ok := referrersMap[key]; !ok {
   143  				// add distinct referrers
   144  				updatedReferrers = append(updatedReferrers, change.referrer)
   145  				referrersMap[key] = len(updatedReferrers) - 1
   146  			}
   147  		case referrerOperationRemove:
   148  			if pos, ok := referrersMap[key]; ok {
   149  				// remove referrers that are already in the map
   150  				updatedReferrers[pos] = ocispec.Descriptor{}
   151  				delete(referrersMap, key)
   152  			}
   153  		}
   154  	}
   155  
   156  	// skip unnecessary update
   157  	if !updateRequired && len(referrersMap) == len(referrers) {
   158  		// if the result referrer map contains the same content as the
   159  		// original referrers, consider that there is no update on the
   160  		// referrers.
   161  		for _, r := range referrers {
   162  			key := descriptor.FromOCI(r)
   163  			if _, ok := referrersMap[key]; !ok {
   164  				updateRequired = true
   165  			}
   166  		}
   167  		if !updateRequired {
   168  			return nil, errNoReferrerUpdate
   169  		}
   170  	}
   171  
   172  	return removeEmptyDescriptors(updatedReferrers, len(referrersMap)), nil
   173  }
   174  
   175  // removeEmptyDescriptors in-place removes empty items from descs, given a hint
   176  // of the number of non-empty descriptors.
   177  func removeEmptyDescriptors(descs []ocispec.Descriptor, hint int) []ocispec.Descriptor {
   178  	j := 0
   179  	for i, r := range descs {
   180  		if !content.Equal(r, ocispec.Descriptor{}) {
   181  			if i > j {
   182  				descs[j] = r
   183  			}
   184  			j++
   185  		}
   186  		if j == hint {
   187  			break
   188  		}
   189  	}
   190  	return descs[:j]
   191  }