github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/registry/remote/repository.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  	"bytes"
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"mime"
    26  	"net/http"
    27  	"slices"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"sync/atomic"
    32  
    33  	"github.com/opencontainers/go-digest"
    34  	specs "github.com/opencontainers/image-spec/specs-go"
    35  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    36  	"oras.land/oras-go/v2/content"
    37  	"oras.land/oras-go/v2/errdef"
    38  	"oras.land/oras-go/v2/internal/cas"
    39  	"oras.land/oras-go/v2/internal/httputil"
    40  	"oras.land/oras-go/v2/internal/ioutil"
    41  	"oras.land/oras-go/v2/internal/spec"
    42  	"oras.land/oras-go/v2/internal/syncutil"
    43  	"oras.land/oras-go/v2/registry"
    44  	"oras.land/oras-go/v2/registry/remote/auth"
    45  	"oras.land/oras-go/v2/registry/remote/errcode"
    46  	"oras.land/oras-go/v2/registry/remote/internal/errutil"
    47  )
    48  
    49  const (
    50  	// headerDockerContentDigest is the "Docker-Content-Digest" header.
    51  	// If present on the response, it contains the canonical digest of the
    52  	// uploaded blob.
    53  	//
    54  	// References:
    55  	//   - https://docs.docker.com/registry/spec/api/#digest-header
    56  	//   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pull
    57  	headerDockerContentDigest = "Docker-Content-Digest"
    58  
    59  	// headerOCIFiltersApplied is the "OCI-Filters-Applied" header.
    60  	// If present on the response, it contains a comma-separated list of the
    61  	// applied filters.
    62  	//
    63  	// Reference:
    64  	//   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
    65  	headerOCIFiltersApplied = "OCI-Filters-Applied"
    66  
    67  	// headerOCISubject is the "OCI-Subject" header.
    68  	// If present on the response, it contains the digest of the subject,
    69  	// indicating that Referrers API is supported by the registry.
    70  	headerOCISubject = "OCI-Subject"
    71  )
    72  
    73  // filterTypeArtifactType is the "artifactType" filter applied on the list of
    74  // referrers.
    75  //
    76  // References:
    77  //   - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
    78  //   - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
    79  const filterTypeArtifactType = "artifactType"
    80  
    81  // Client is an interface for a HTTP client.
    82  type Client interface {
    83  	// Do sends an HTTP request and returns an HTTP response.
    84  	//
    85  	// Unlike http.RoundTripper, Client can attempt to interpret the response
    86  	// and handle higher-level protocol details such as redirects and
    87  	// authentication.
    88  	//
    89  	// Like http.RoundTripper, Client should not modify the request, and must
    90  	// always close the request body.
    91  	Do(*http.Request) (*http.Response, error)
    92  }
    93  
    94  // Repository is an HTTP client to a remote repository.
    95  type Repository struct {
    96  	// Client is the underlying HTTP client used to access the remote registry.
    97  	// If nil, auth.DefaultClient is used.
    98  	Client Client
    99  
   100  	// Reference references the remote repository.
   101  	Reference registry.Reference
   102  
   103  	// PlainHTTP signals the transport to access the remote repository via HTTP
   104  	// instead of HTTPS.
   105  	PlainHTTP bool
   106  
   107  	// ManifestMediaTypes is used in `Accept` header for resolving manifests
   108  	// from references. It is also used in identifying manifests and blobs from
   109  	// descriptors. If an empty list is present, default manifest media types
   110  	// are used.
   111  	ManifestMediaTypes []string
   112  
   113  	// TagListPageSize specifies the page size when invoking the tag list API.
   114  	// If zero, the page size is determined by the remote registry.
   115  	// Reference: https://docs.docker.com/registry/spec/api/#tags
   116  	TagListPageSize int
   117  
   118  	// ReferrerListPageSize specifies the page size when invoking the Referrers
   119  	// API.
   120  	// If zero, the page size is determined by the remote registry.
   121  	// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
   122  	ReferrerListPageSize int
   123  
   124  	// MaxMetadataBytes specifies a limit on how many response bytes are allowed
   125  	// in the server's response to the metadata APIs, such as catalog list, tag
   126  	// list, and referrers list.
   127  	// If less than or equal to zero, a default (currently 4MiB) is used.
   128  	MaxMetadataBytes int64
   129  
   130  	// SkipReferrersGC specifies whether to delete the dangling referrers
   131  	// index when referrers tag schema is utilized.
   132  	//  - If false, the old referrers index will be deleted after the new one
   133  	//    is successfully uploaded.
   134  	//  - If true, the old referrers index is kept.
   135  	// By default, it is disabled (set to false). See also:
   136  	//  - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#referrers-tag-schema
   137  	//  - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
   138  	//  - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests
   139  	SkipReferrersGC bool
   140  
   141  	// HandleWarning handles the warning returned by the remote server.
   142  	// Callers SHOULD deduplicate warnings from multiple associated responses.
   143  	//
   144  	// References:
   145  	//   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#warnings
   146  	//   - https://www.rfc-editor.org/rfc/rfc7234#section-5.5
   147  	HandleWarning func(warning Warning)
   148  
   149  	// NOTE: Must keep fields in sync with clone().
   150  
   151  	// referrersState represents that if the repository supports Referrers API.
   152  	// default: referrersStateUnknown
   153  	referrersState referrersState
   154  
   155  	// referrersPingLock locks the pingReferrers() method and allows only
   156  	// one go-routine to send the request.
   157  	referrersPingLock sync.Mutex
   158  
   159  	// referrersMergePool provides a way to manage concurrent updates to a
   160  	// referrers index tagged by referrers tag schema.
   161  	referrersMergePool syncutil.Pool[syncutil.Merge[referrerChange]]
   162  }
   163  
   164  // NewRepository creates a client to the remote repository identified by a
   165  // reference.
   166  // Example: localhost:5000/hello-world
   167  func NewRepository(reference string) (*Repository, error) {
   168  	ref, err := registry.ParseReference(reference)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return &Repository{
   173  		Reference: ref,
   174  	}, nil
   175  }
   176  
   177  // newRepositoryWithOptions returns a Repository with the given Reference and
   178  // RepositoryOptions.
   179  //
   180  // RepositoryOptions are part of the Registry struct and set its defaults.
   181  // RepositoryOptions shares the same struct definition as Repository, which
   182  // contains unexported state that must not be copied to multiple Repositories.
   183  // To handle this we explicitly copy only the fields that we want to reproduce.
   184  func newRepositoryWithOptions(ref registry.Reference, opts *RepositoryOptions) (*Repository, error) {
   185  	if err := ref.ValidateRepository(); err != nil {
   186  		return nil, err
   187  	}
   188  	repo := (*Repository)(opts).clone()
   189  	repo.Reference = ref
   190  	return repo, nil
   191  }
   192  
   193  // clone makes a copy of the Repository being careful not to copy non-copyable fields (sync.Mutex and syncutil.Pool types)
   194  func (r *Repository) clone() *Repository {
   195  	return &Repository{
   196  		Client:               r.Client,
   197  		Reference:            r.Reference,
   198  		PlainHTTP:            r.PlainHTTP,
   199  		ManifestMediaTypes:   slices.Clone(r.ManifestMediaTypes),
   200  		TagListPageSize:      r.TagListPageSize,
   201  		ReferrerListPageSize: r.ReferrerListPageSize,
   202  		MaxMetadataBytes:     r.MaxMetadataBytes,
   203  		SkipReferrersGC:      r.SkipReferrersGC,
   204  		HandleWarning:        r.HandleWarning,
   205  	}
   206  }
   207  
   208  // SetReferrersCapability indicates the Referrers API capability of the remote
   209  // repository. true: capable; false: not capable.
   210  //
   211  // SetReferrersCapability is valid only when it is called for the first time.
   212  // SetReferrersCapability returns ErrReferrersCapabilityAlreadySet if the
   213  // Referrers API capability has been already set.
   214  //   - When the capability is set to true, the Referrers() function will always
   215  //     request the Referrers API. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
   216  //   - When the capability is set to false, the Referrers() function will always
   217  //     request the Referrers Tag. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#referrers-tag-schema
   218  //   - When the capability is not set, the Referrers() function will automatically
   219  //     determine which API to use.
   220  func (r *Repository) SetReferrersCapability(capable bool) error {
   221  	var state referrersState
   222  	if capable {
   223  		state = referrersStateSupported
   224  	} else {
   225  		state = referrersStateUnsupported
   226  	}
   227  	if swapped := atomic.CompareAndSwapInt32(&r.referrersState, referrersStateUnknown, state); !swapped {
   228  		if fact := r.loadReferrersState(); fact != state {
   229  			return fmt.Errorf("%w: current capability = %v, new capability = %v",
   230  				ErrReferrersCapabilityAlreadySet,
   231  				fact == referrersStateSupported,
   232  				capable)
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  // setReferrersState atomically loads r.referrersState.
   239  func (r *Repository) loadReferrersState() referrersState {
   240  	return atomic.LoadInt32(&r.referrersState)
   241  }
   242  
   243  // client returns an HTTP client used to access the remote repository.
   244  // A default HTTP client is return if the client is not configured.
   245  func (r *Repository) client() Client {
   246  	if r.Client == nil {
   247  		return auth.DefaultClient
   248  	}
   249  	return r.Client
   250  }
   251  
   252  // do sends an HTTP request and returns an HTTP response using the HTTP client
   253  // returned by r.client().
   254  func (r *Repository) do(req *http.Request) (*http.Response, error) {
   255  	if r.HandleWarning == nil {
   256  		return r.client().Do(req)
   257  	}
   258  
   259  	resp, err := r.client().Do(req)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning)
   264  	return resp, nil
   265  }
   266  
   267  // FetcherHead fetches content headers.
   268  type FetcherHead interface {
   269  	// Fetch fetches the content identified by the descriptor.
   270  	FetchHead(ctx context.Context, target ocispec.Descriptor) (*http.Header, error)
   271  }
   272  
   273  // BlobStoreHead is a BlobStore with the ability to retrieve content headers.
   274  type BlobStoreHead interface {
   275  	registry.BlobStore
   276  	FetcherHead
   277  }
   278  
   279  // blobStore detects the blob store for the given descriptor.
   280  func (r *Repository) blobStore(desc ocispec.Descriptor) registry.BlobStore {
   281  	if isManifest(r.ManifestMediaTypes, desc) {
   282  		return r.Manifests()
   283  	}
   284  	return r.Blobs()
   285  }
   286  
   287  // Fetch fetches the content identified by the descriptor.
   288  func (r *Repository) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
   289  	return r.blobStore(target).Fetch(ctx, target)
   290  }
   291  
   292  // FetchHead fetches the content headers identified by the descriptor.
   293  func (r *Repository) FetchHead(ctx context.Context, target ocispec.Descriptor) (*http.Header, error) {
   294  	bs := r.blobStore(target)
   295  	if bsh, ok := bs.(BlobStoreHead); ok {
   296  		return bsh.FetchHead(ctx, target)
   297  	}
   298  	return nil, fmt.Errorf("not a blobStore")
   299  }
   300  
   301  // Push pushes the content, matching the expected descriptor.
   302  func (r *Repository) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
   303  	return r.blobStore(expected).Push(ctx, expected, content)
   304  }
   305  
   306  // Mount makes the blob with the given digest in fromRepo
   307  // available in the repository signified by the receiver.
   308  //
   309  // This avoids the need to pull content down from fromRepo only to push it to r.
   310  //
   311  // If the registry does not implement mounting, getContent will be used to get the
   312  // content to push. If getContent is nil, the content will be pulled from the source
   313  // repository. If getContent returns an error, it will be wrapped inside the error
   314  // returned from Mount.
   315  func (r *Repository) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error {
   316  	return r.Blobs().(registry.Mounter).Mount(ctx, desc, fromRepo, getContent)
   317  }
   318  
   319  // Exists returns true if the described content exists.
   320  func (r *Repository) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   321  	return r.blobStore(target).Exists(ctx, target)
   322  }
   323  
   324  // Delete removes the content identified by the descriptor.
   325  func (r *Repository) Delete(ctx context.Context, target ocispec.Descriptor) error {
   326  	return r.blobStore(target).Delete(ctx, target)
   327  }
   328  
   329  // Blobs provides access to the blob CAS only, which contains config blobs,
   330  // layers, and other generic blobs.
   331  func (r *Repository) Blobs() registry.BlobStore {
   332  	return &blobStore{repo: r}
   333  }
   334  
   335  // Manifests provides access to the manifest CAS only.
   336  func (r *Repository) Manifests() registry.ManifestStore {
   337  	return &manifestStore{repo: r}
   338  }
   339  
   340  // Resolve resolves a reference to a manifest descriptor.
   341  // See also `ManifestMediaTypes`.
   342  func (r *Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
   343  	return r.Manifests().Resolve(ctx, reference)
   344  }
   345  
   346  // Tag tags a manifest descriptor with a reference string.
   347  func (r *Repository) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
   348  	return r.Manifests().Tag(ctx, desc, reference)
   349  }
   350  
   351  // PushReference pushes the manifest with a reference tag.
   352  func (r *Repository) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
   353  	return r.Manifests().PushReference(ctx, expected, content, reference)
   354  }
   355  
   356  // FetchReference fetches the manifest identified by the reference.
   357  // The reference can be a tag or digest.
   358  func (r *Repository) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) {
   359  	return r.Manifests().FetchReference(ctx, reference)
   360  }
   361  
   362  // ParseReference resolves a tag or a digest reference to a fully qualified
   363  // reference from a base reference r.Reference.
   364  // Tag, digest, or fully qualified references are accepted as input.
   365  //
   366  // If reference is a fully qualified reference, then ParseReference parses it
   367  // and returns the parsed reference. If the parsed reference does not share
   368  // the same base reference with the Repository r, ParseReference returns a
   369  // wrapped error ErrInvalidReference.
   370  func (r *Repository) ParseReference(reference string) (registry.Reference, error) {
   371  	ref, err := registry.ParseReference(reference)
   372  	if err != nil {
   373  		ref = registry.Reference{
   374  			Registry:   r.Reference.Registry,
   375  			Repository: r.Reference.Repository,
   376  			Reference:  reference,
   377  		}
   378  
   379  		// reference is not a FQDN
   380  		if index := strings.IndexByte(reference, '@'); index != -1 {
   381  			// `@` implies *digest*, so drop the *tag* (irrespective of what it is).
   382  			ref.Reference = reference[index+1:]
   383  			err = ref.ValidateReferenceAsDigest()
   384  		} else {
   385  			err = ref.ValidateReference()
   386  		}
   387  
   388  		if err != nil {
   389  			return registry.Reference{}, err
   390  		}
   391  	} else if ref.Registry != r.Reference.Registry || ref.Repository != r.Reference.Repository {
   392  		return registry.Reference{}, fmt.Errorf(
   393  			"%w: mismatch between received %q and expected %q",
   394  			errdef.ErrInvalidReference, ref, r.Reference,
   395  		)
   396  	}
   397  
   398  	if len(ref.Reference) == 0 {
   399  		return registry.Reference{}, errdef.ErrInvalidReference
   400  	}
   401  
   402  	return ref, nil
   403  }
   404  
   405  // Tags lists the tags available in the repository.
   406  // See also `TagListPageSize`.
   407  // If `last` is NOT empty, the entries in the response start after the
   408  // tag specified by `last`. Otherwise, the response starts from the top
   409  // of the Tags list.
   410  //
   411  // References:
   412  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#content-discovery
   413  //   - https://docs.docker.com/registry/spec/api/#tags
   414  func (r *Repository) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
   415  	ctx = auth.AppendRepositoryScope(ctx, r.Reference, auth.ActionPull)
   416  	url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference)
   417  	var err error
   418  	for err == nil {
   419  		url, err = r.tags(ctx, last, fn, url)
   420  		// clear `last` for subsequent pages
   421  		last = ""
   422  	}
   423  	if err != errNoLink {
   424  		return err
   425  	}
   426  	return nil
   427  }
   428  
   429  // tags returns a single page of tag list with the next link.
   430  func (r *Repository) tags(ctx context.Context, last string, fn func(tags []string) error, url string) (string, error) {
   431  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   432  	if err != nil {
   433  		return "", err
   434  	}
   435  	if r.TagListPageSize > 0 || last != "" {
   436  		q := req.URL.Query()
   437  		if r.TagListPageSize > 0 {
   438  			q.Set("n", strconv.Itoa(r.TagListPageSize))
   439  		}
   440  		if last != "" {
   441  			q.Set("last", last)
   442  		}
   443  		req.URL.RawQuery = q.Encode()
   444  	}
   445  	resp, err := r.do(req)
   446  	if err != nil {
   447  		return "", err
   448  	}
   449  	defer resp.Body.Close()
   450  
   451  	if resp.StatusCode != http.StatusOK {
   452  		return "", errutil.ParseErrorResponse(resp)
   453  	}
   454  	var page struct {
   455  		Tags []string `json:"tags"`
   456  	}
   457  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   458  	if err := json.NewDecoder(lr).Decode(&page); err != nil {
   459  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   460  	}
   461  	if err := fn(page.Tags); err != nil {
   462  		return "", err
   463  	}
   464  
   465  	return parseLink(resp)
   466  }
   467  
   468  // Predecessors returns the descriptors of image or artifact manifests directly
   469  // referencing the given manifest descriptor.
   470  // Predecessors internally leverages Referrers.
   471  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
   472  func (r *Repository) Predecessors(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   473  	var res []ocispec.Descriptor
   474  	if err := r.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
   475  		res = append(res, referrers...)
   476  		return nil
   477  	}); err != nil {
   478  		return nil, err
   479  	}
   480  	return res, nil
   481  }
   482  
   483  // Referrers lists the descriptors of image or artifact manifests directly
   484  // referencing the given manifest descriptor.
   485  //
   486  // fn is called for each page of the referrers result.
   487  // If artifactType is not empty, only referrers of the same artifact type are
   488  // fed to fn.
   489  //
   490  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
   491  func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
   492  	state := r.loadReferrersState()
   493  	if state == referrersStateUnsupported {
   494  		// The repository is known to not support Referrers API, fallback to
   495  		// referrers tag schema.
   496  		return r.referrersByTagSchema(ctx, desc, artifactType, fn)
   497  	}
   498  
   499  	err := r.referrersByAPI(ctx, desc, artifactType, fn)
   500  	if state == referrersStateSupported {
   501  		// The repository is known to support Referrers API, no fallback.
   502  		return err
   503  	}
   504  
   505  	// The referrers state is unknown.
   506  	if err != nil {
   507  		if errors.Is(err, errdef.ErrUnsupported) {
   508  			// Referrers API is not supported, fallback to referrers tag schema.
   509  			r.SetReferrersCapability(false)
   510  			return r.referrersByTagSchema(ctx, desc, artifactType, fn)
   511  		}
   512  		return err
   513  	}
   514  
   515  	r.SetReferrersCapability(true)
   516  	return nil
   517  }
   518  
   519  // referrersByAPI lists the descriptors of manifests directly referencing
   520  // the given manifest descriptor by requesting Referrers API.
   521  // fn is called for the referrers result. If artifactType is not empty,
   522  // only referrers of the same artifact type are fed to fn.
   523  func (r *Repository) referrersByAPI(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
   524  	ref := r.Reference
   525  	ref.Reference = desc.Digest.String()
   526  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
   527  
   528  	url := buildReferrersURL(r.PlainHTTP, ref, artifactType)
   529  	var err error
   530  	for err == nil {
   531  		url, err = r.referrersPageByAPI(ctx, artifactType, fn, url)
   532  	}
   533  	if err == errNoLink {
   534  		return nil
   535  	}
   536  	return err
   537  }
   538  
   539  // referrersPageByAPI lists a single page of the descriptors of manifests
   540  // directly referencing the given manifest descriptor. fn is called for
   541  // a page of referrersPageByAPI result.
   542  // If artifactType is not empty, only referrersPageByAPI of the same
   543  // artifact type are fed to fn.
   544  // referrersPageByAPI returns the link url for the next page.
   545  func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string, fn func(referrers []ocispec.Descriptor) error, url string) (string, error) {
   546  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   547  	if err != nil {
   548  		return "", err
   549  	}
   550  	if r.ReferrerListPageSize > 0 {
   551  		q := req.URL.Query()
   552  		q.Set("n", strconv.Itoa(r.ReferrerListPageSize))
   553  		req.URL.RawQuery = q.Encode()
   554  	}
   555  
   556  	resp, err := r.do(req)
   557  	if err != nil {
   558  		return "", err
   559  	}
   560  	defer resp.Body.Close()
   561  
   562  	switch resp.StatusCode {
   563  	case http.StatusOK:
   564  	case http.StatusNotFound:
   565  		if errResp := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) {
   566  			// The repository is not found, Referrers API status is unknown
   567  			return "", errResp
   568  		}
   569  		// Referrers API is not supported.
   570  		return "", fmt.Errorf("failed to query referrers API: %w", errdef.ErrUnsupported)
   571  	default:
   572  		return "", errutil.ParseErrorResponse(resp)
   573  	}
   574  
   575  	// also check the content type
   576  	if ct := resp.Header.Get("Content-Type"); ct != ocispec.MediaTypeImageIndex {
   577  		return "", fmt.Errorf("unknown content returned (%s), expecting image index: %w", ct, errdef.ErrUnsupported)
   578  	}
   579  
   580  	var index ocispec.Index
   581  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   582  	if err := json.NewDecoder(lr).Decode(&index); err != nil {
   583  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   584  	}
   585  
   586  	referrers := index.Manifests
   587  	if artifactType != "" {
   588  		// check both filters header and filters annotations for compatibility
   589  		// latest spec for filters header: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
   590  		// older spec for filters annotations: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
   591  		filtersHeader := resp.Header.Get(headerOCIFiltersApplied)
   592  		filtersAnnotation := index.Annotations[spec.AnnotationReferrersFiltersApplied]
   593  		if !isReferrersFilterApplied(filtersHeader, filterTypeArtifactType) &&
   594  			!isReferrersFilterApplied(filtersAnnotation, filterTypeArtifactType) {
   595  			// perform client side filtering if the filter is not applied on the server side
   596  			referrers = filterReferrers(referrers, artifactType)
   597  		}
   598  	}
   599  	if len(referrers) > 0 {
   600  		if err := fn(referrers); err != nil {
   601  			return "", err
   602  		}
   603  	}
   604  	return parseLink(resp)
   605  }
   606  
   607  // referrersByTagSchema lists the descriptors of manifests directly
   608  // referencing the given manifest descriptor by requesting referrers tag.
   609  // fn is called for the referrers result. If artifactType is not empty,
   610  // only referrers of the same artifact type are fed to fn.
   611  // reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
   612  func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
   613  	referrersTag := buildReferrersTag(desc)
   614  	_, referrers, err := r.referrersFromIndex(ctx, referrersTag)
   615  	if err != nil {
   616  		if errors.Is(err, errdef.ErrNotFound) {
   617  			// no referrers to the manifest
   618  			return nil
   619  		}
   620  		return err
   621  	}
   622  
   623  	filtered := filterReferrers(referrers, artifactType)
   624  	if len(filtered) == 0 {
   625  		return nil
   626  	}
   627  	return fn(filtered)
   628  }
   629  
   630  // referrersFromIndex queries the referrers index using the the given referrers
   631  // tag. If Succeeded, returns the descriptor of referrers index and the
   632  // referrers list.
   633  func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string) (ocispec.Descriptor, []ocispec.Descriptor, error) {
   634  	desc, rc, err := r.FetchReference(ctx, referrersTag)
   635  	if err != nil {
   636  		return ocispec.Descriptor{}, nil, err
   637  	}
   638  	defer rc.Close()
   639  
   640  	if err := limitSize(desc, r.MaxMetadataBytes); err != nil {
   641  		return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err)
   642  	}
   643  	var index ocispec.Index
   644  	if err := decodeJSON(rc, desc, &index); err != nil {
   645  		return ocispec.Descriptor{}, nil, fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err)
   646  	}
   647  
   648  	return desc, index.Manifests, nil
   649  }
   650  
   651  // pingReferrers returns true if the Referrers API is available for r.
   652  func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
   653  	switch r.loadReferrersState() {
   654  	case referrersStateSupported:
   655  		return true, nil
   656  	case referrersStateUnsupported:
   657  		return false, nil
   658  	}
   659  
   660  	// referrers state is unknown
   661  	// limit the rate of pinging referrers API
   662  	r.referrersPingLock.Lock()
   663  	defer r.referrersPingLock.Unlock()
   664  
   665  	switch r.loadReferrersState() {
   666  	case referrersStateSupported:
   667  		return true, nil
   668  	case referrersStateUnsupported:
   669  		return false, nil
   670  	}
   671  
   672  	ref := r.Reference
   673  	ref.Reference = zeroDigest
   674  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
   675  
   676  	url := buildReferrersURL(r.PlainHTTP, ref, "")
   677  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   678  	if err != nil {
   679  		return false, err
   680  	}
   681  	resp, err := r.do(req)
   682  	if err != nil {
   683  		return false, err
   684  	}
   685  	defer resp.Body.Close()
   686  
   687  	switch resp.StatusCode {
   688  	case http.StatusOK:
   689  		supported := resp.Header.Get("Content-Type") == ocispec.MediaTypeImageIndex
   690  		r.SetReferrersCapability(supported)
   691  		return supported, nil
   692  	case http.StatusNotFound:
   693  		if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) {
   694  			// repository not found
   695  			return false, err
   696  		}
   697  		r.SetReferrersCapability(false)
   698  		return false, nil
   699  	default:
   700  		return false, errutil.ParseErrorResponse(resp)
   701  	}
   702  }
   703  
   704  // delete removes the content identified by the descriptor in the entity "blobs"
   705  // or "manifests".
   706  func (r *Repository) delete(ctx context.Context, target ocispec.Descriptor, isManifest bool) error {
   707  	ref := r.Reference
   708  	ref.Reference = target.Digest.String()
   709  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionDelete)
   710  	buildURL := buildRepositoryBlobURL
   711  	if isManifest {
   712  		buildURL = buildRepositoryManifestURL
   713  	}
   714  	url := buildURL(r.PlainHTTP, ref)
   715  	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
   716  	if err != nil {
   717  		return err
   718  	}
   719  
   720  	resp, err := r.do(req)
   721  	if err != nil {
   722  		return err
   723  	}
   724  	defer resp.Body.Close()
   725  
   726  	switch resp.StatusCode {
   727  	case http.StatusAccepted:
   728  		return verifyContentDigest(resp, target.Digest)
   729  	case http.StatusNotFound:
   730  		return fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
   731  	default:
   732  		return errutil.ParseErrorResponse(resp)
   733  	}
   734  }
   735  
   736  // blobStore accesses the blob part of the repository.
   737  type blobStore struct {
   738  	repo *Repository
   739  }
   740  
   741  // Fetch fetches the content identified by the descriptor.
   742  func (s *blobStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) {
   743  	ref := s.repo.Reference
   744  	ref.Reference = target.Digest.String()
   745  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
   746  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
   747  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   748  	if err != nil {
   749  		return nil, err
   750  	}
   751  
   752  	var ingestSize int64
   753  	resume := target.Annotations != nil && target.Annotations[spec.AnnotationResumeDownload] == "true"
   754  	if resume {
   755  
   756  		// Check if resume is possible
   757  		_, err = s.FetchHead(ctx, target)
   758  		if err != nil {
   759  			// Resume is not possible, ensure it is disabled
   760  			if target.Annotations != nil {
   761  				target.Annotations[spec.AnnotationResumeDownload] = ""
   762  				resume = false
   763  			}
   764  		}
   765  
   766  		// Get size of existing ingestFile to set Range start
   767  		ingestSize, err = strconv.ParseInt(target.Annotations[spec.AnnotationResumeOffset], 10, 64)
   768  		if err != nil {
   769  			ingestSize = 0
   770  			resume = false
   771  		} else {
   772  			if ingestSize < target.Size {
   773  				// Set the Range header and do a chunk right up front if the file is not complete...
   774  				req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", ingestSize, target.Size-1))
   775  			}
   776  		}
   777  	}
   778  
   779  	resp, err := s.repo.do(req)
   780  	if err != nil {
   781  		return nil, err
   782  	}
   783  	defer func() {
   784  		if err != nil {
   785  			resp.Body.Close()
   786  		}
   787  	}()
   788  
   789  	switch resp.StatusCode {
   790  	case http.StatusPartialContent:
   791  		if size := resp.ContentLength; size != -1 && size != target.Size-ingestSize {
   792  			return nil, fmt.Errorf("206 %s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
   793  		}
   794  		return resp.Body, nil
   795  	case http.StatusOK:
   796  		if size := resp.ContentLength; size != -1 && size != target.Size {
   797  			return nil, fmt.Errorf("204 %s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
   798  		}
   799  
   800  		// check server range request capability.
   801  		// Docker spec allows range header form of "Range: bytes=<start>-<end>".
   802  		// However, the remote server may still not RFC 7233 compliant.
   803  		// Reference: https://docs.docker.com/registry/spec/api/#blob
   804  		if rangeUnit := resp.Header.Get("Accept-Ranges"); !resume && rangeUnit == "bytes" {
   805  			return httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, target.Size), nil
   806  		}
   807  		return resp.Body, nil
   808  	case http.StatusNotFound:
   809  		return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
   810  	default:
   811  		return nil, errutil.ParseErrorResponse(resp)
   812  	}
   813  }
   814  
   815  // FetchHead fetches the content identified by the descriptor.
   816  func (s *blobStore) FetchHead(ctx context.Context, target ocispec.Descriptor) (header *http.Header, err error) {
   817  	ref := s.repo.Reference
   818  	ref.Reference = target.Digest.String()
   819  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
   820  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
   821  
   822  	// HEAD
   823  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
   824  	if err != nil {
   825  		return nil, err
   826  	}
   827  
   828  	resp, err := s.repo.do(req)
   829  	if err != nil {
   830  		return nil, err
   831  	}
   832  	defer func() {
   833  		if err != nil {
   834  			resp.Body.Close()
   835  		}
   836  	}()
   837  
   838  	switch resp.StatusCode {
   839  	case http.StatusOK: // server does not support seek as `Range` was ignored.
   840  		if size := resp.ContentLength; size != -1 && size != target.Size {
   841  			return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
   842  		}
   843  
   844  		// check server range request capability.
   845  		// Docker spec allows range header form of "Range: bytes=<start>-<end>".
   846  		// However, the remote server may still not RFC 7233 compliant.
   847  		// Reference: https://docs.docker.com/registry/spec/api/#blob
   848  		if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" {
   849  			if target.Annotations != nil {
   850  				target.Annotations[spec.AnnotationResumeDownload] = "true"
   851  			}
   852  		}
   853  		header = &resp.Header
   854  	case http.StatusNotFound:
   855  		return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
   856  	default:
   857  		return nil, errutil.ParseErrorResponse(resp)
   858  	}
   859  
   860  	return header, nil
   861  }
   862  
   863  // Mount mounts the given descriptor from fromRepo into s.
   864  func (s *blobStore) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error {
   865  	// pushing usually requires both pull and push actions.
   866  	// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
   867  	ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush)
   868  
   869  	// We also need pull access to the source repo.
   870  	fromRef := s.repo.Reference
   871  	fromRef.Repository = fromRepo
   872  	ctx = auth.AppendRepositoryScope(ctx, fromRef, auth.ActionPull)
   873  
   874  	url := buildRepositoryBlobMountURL(s.repo.PlainHTTP, s.repo.Reference, desc.Digest, fromRepo)
   875  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
   876  	if err != nil {
   877  		return err
   878  	}
   879  	resp, err := s.repo.do(req)
   880  	if err != nil {
   881  		return err
   882  	}
   883  	if resp.StatusCode == http.StatusCreated {
   884  		defer resp.Body.Close()
   885  		// Check the server seems to be behaving.
   886  		return verifyContentDigest(resp, desc.Digest)
   887  	}
   888  	if resp.StatusCode != http.StatusAccepted {
   889  		defer resp.Body.Close()
   890  		return errutil.ParseErrorResponse(resp)
   891  	}
   892  	resp.Body.Close()
   893  	// From the [spec]:
   894  	//
   895  	// "If a registry does not support cross-repository mounting
   896  	// or is unable to mount the requested blob,
   897  	// it SHOULD return a 202.
   898  	// This indicates that the upload session has begun
   899  	// and that the client MAY proceed with the upload."
   900  	//
   901  	// So we need to get the content from somewhere in order to
   902  	// push it. If the caller has provided a getContent function, we
   903  	// can use that, otherwise pull the content from the source repository.
   904  	//
   905  	// [spec]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#mounting-a-blob-from-another-repository
   906  
   907  	var r io.ReadCloser
   908  	if getContent != nil {
   909  		r, err = getContent()
   910  	} else {
   911  		r, err = s.sibling(fromRepo).Fetch(ctx, desc)
   912  	}
   913  	if err != nil {
   914  		return fmt.Errorf("cannot read source blob: %w", err)
   915  	}
   916  	defer r.Close()
   917  	return s.completePushAfterInitialPost(ctx, req, resp, desc, r)
   918  }
   919  
   920  // sibling returns a blob store for another repository in the same
   921  // registry.
   922  func (s *blobStore) sibling(otherRepoName string) *blobStore {
   923  	otherRepo := s.repo.clone()
   924  	otherRepo.Reference.Repository = otherRepoName
   925  	return &blobStore{
   926  		repo: otherRepo,
   927  	}
   928  }
   929  
   930  // Push pushes the content, matching the expected descriptor.
   931  // Existing content is not checked by Push() to minimize the number of out-going
   932  // requests.
   933  // Push is done by conventional 2-step monolithic upload instead of a single
   934  // `POST` request for better overall performance. It also allows early fail on
   935  // authentication errors.
   936  //
   937  // References:
   938  //   - https://docs.docker.com/registry/spec/api/#pushing-an-image
   939  //   - https://docs.docker.com/registry/spec/api/#initiate-blob-upload
   940  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-a-blob-monolithically
   941  func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
   942  	// start an upload
   943  	// pushing usually requires both pull and push actions.
   944  	// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
   945  	ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush)
   946  	url := buildRepositoryBlobUploadURL(s.repo.PlainHTTP, s.repo.Reference)
   947  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
   948  	if err != nil {
   949  		return err
   950  	}
   951  
   952  	resp, err := s.repo.do(req)
   953  	if err != nil {
   954  		return err
   955  	}
   956  
   957  	if resp.StatusCode != http.StatusAccepted {
   958  		defer resp.Body.Close()
   959  		return errutil.ParseErrorResponse(resp)
   960  	}
   961  	resp.Body.Close()
   962  	return s.completePushAfterInitialPost(ctx, req, resp, expected, content)
   963  }
   964  
   965  // completePushAfterInitialPost implements step 2 of the push protocol. This can be invoked either by
   966  // Push or by Mount when the receiving repository does not implement the
   967  // mount endpoint.
   968  func (s *blobStore) completePushAfterInitialPost(ctx context.Context, req *http.Request, resp *http.Response, expected ocispec.Descriptor, content io.Reader) error {
   969  	reqHostname := req.URL.Hostname()
   970  	reqPort := req.URL.Port()
   971  	// monolithic upload
   972  	location, err := resp.Location()
   973  	if err != nil {
   974  		return err
   975  	}
   976  	// work-around solution for https://github.com/oras-project/oras-go/issues/177
   977  	// For some registries, if the port 443 is explicitly set to the hostname
   978  	// like registry.wabbit-networks.io:443/myrepo, blob push will fail since
   979  	// the hostname of the Location header in the response is set to
   980  	// registry.wabbit-networks.io instead of registry.wabbit-networks.io:443.
   981  	locationHostname := location.Hostname()
   982  	locationPort := location.Port()
   983  	// if location port 443 is missing, add it back
   984  	if reqPort == "443" && locationHostname == reqHostname && locationPort == "" {
   985  		location.Host = locationHostname + ":" + reqPort
   986  	}
   987  	url := location.String()
   988  	req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content)
   989  	if err != nil {
   990  		return err
   991  	}
   992  	if req.GetBody != nil && req.ContentLength != expected.Size {
   993  		// short circuit a size mismatch for built-in types.
   994  		return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size)
   995  	}
   996  	req.ContentLength = expected.Size
   997  	// the expected media type is ignored as in the API doc.
   998  	req.Header.Set("Content-Type", "application/octet-stream")
   999  	q := req.URL.Query()
  1000  	q.Set("digest", expected.Digest.String())
  1001  	req.URL.RawQuery = q.Encode()
  1002  
  1003  	// reuse credential from previous POST request
  1004  	if auth := resp.Request.Header.Get("Authorization"); auth != "" {
  1005  		req.Header.Set("Authorization", auth)
  1006  	}
  1007  	resp, err = s.repo.do(req)
  1008  	if err != nil {
  1009  		return err
  1010  	}
  1011  	defer resp.Body.Close()
  1012  
  1013  	if resp.StatusCode != http.StatusCreated {
  1014  		return errutil.ParseErrorResponse(resp)
  1015  	}
  1016  	return nil
  1017  }
  1018  
  1019  // Exists returns true if the described content exists.
  1020  func (s *blobStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
  1021  	_, err := s.Resolve(ctx, target.Digest.String())
  1022  	if err == nil {
  1023  		return true, nil
  1024  	}
  1025  	if errors.Is(err, errdef.ErrNotFound) {
  1026  		return false, nil
  1027  	}
  1028  	return false, err
  1029  }
  1030  
  1031  // Delete removes the content identified by the descriptor.
  1032  func (s *blobStore) Delete(ctx context.Context, target ocispec.Descriptor) error {
  1033  	return s.repo.delete(ctx, target, false)
  1034  }
  1035  
  1036  // Resolve resolves a reference to a descriptor.
  1037  func (s *blobStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
  1038  	ref, err := s.repo.ParseReference(reference)
  1039  	if err != nil {
  1040  		return ocispec.Descriptor{}, err
  1041  	}
  1042  	refDigest, err := ref.Digest()
  1043  	if err != nil {
  1044  		return ocispec.Descriptor{}, err
  1045  	}
  1046  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
  1047  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
  1048  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
  1049  	if err != nil {
  1050  		return ocispec.Descriptor{}, err
  1051  	}
  1052  
  1053  	resp, err := s.repo.do(req)
  1054  	if err != nil {
  1055  		return ocispec.Descriptor{}, err
  1056  	}
  1057  	defer resp.Body.Close()
  1058  
  1059  	switch resp.StatusCode {
  1060  	case http.StatusOK:
  1061  		return generateBlobDescriptor(resp, refDigest)
  1062  	case http.StatusNotFound:
  1063  		return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
  1064  	default:
  1065  		return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp)
  1066  	}
  1067  }
  1068  
  1069  // FetchReference fetches the blob identified by the reference.
  1070  // The reference must be a digest.
  1071  func (s *blobStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) {
  1072  	ref, err := s.repo.ParseReference(reference)
  1073  	if err != nil {
  1074  		return ocispec.Descriptor{}, nil, err
  1075  	}
  1076  	refDigest, err := ref.Digest()
  1077  	if err != nil {
  1078  		return ocispec.Descriptor{}, nil, err
  1079  	}
  1080  
  1081  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
  1082  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
  1083  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  1084  	if err != nil {
  1085  		return ocispec.Descriptor{}, nil, err
  1086  	}
  1087  
  1088  	resp, err := s.repo.do(req)
  1089  	if err != nil {
  1090  		return ocispec.Descriptor{}, nil, err
  1091  	}
  1092  	defer func() {
  1093  		if err != nil {
  1094  			resp.Body.Close()
  1095  		}
  1096  	}()
  1097  
  1098  	switch resp.StatusCode {
  1099  	case http.StatusOK: // server does not support seek as `Range` was ignored.
  1100  		if resp.ContentLength == -1 {
  1101  			desc, err = s.Resolve(ctx, reference)
  1102  		} else {
  1103  			desc, err = generateBlobDescriptor(resp, refDigest)
  1104  		}
  1105  		if err != nil {
  1106  			return ocispec.Descriptor{}, nil, err
  1107  		}
  1108  
  1109  		// check server range request capability.
  1110  		// Docker spec allows range header form of "Range: bytes=<start>-<end>".
  1111  		// However, the remote server may still not RFC 7233 compliant.
  1112  		// Reference: https://docs.docker.com/registry/spec/api/#blob
  1113  		if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" {
  1114  			return desc, httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, desc.Size), nil
  1115  		}
  1116  		return desc, resp.Body, nil
  1117  	case http.StatusNotFound:
  1118  		return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
  1119  	default:
  1120  		return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp)
  1121  	}
  1122  }
  1123  
  1124  // generateBlobDescriptor returns a descriptor generated from the response.
  1125  func generateBlobDescriptor(resp *http.Response, refDigest digest.Digest) (ocispec.Descriptor, error) {
  1126  	mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
  1127  	if mediaType == "" {
  1128  		mediaType = "application/octet-stream"
  1129  	}
  1130  
  1131  	size := resp.ContentLength
  1132  	if size == -1 {
  1133  		return ocispec.Descriptor{}, fmt.Errorf("%s %q: unknown response Content-Length", resp.Request.Method, resp.Request.URL)
  1134  	}
  1135  
  1136  	if err := verifyContentDigest(resp, refDigest); err != nil {
  1137  		return ocispec.Descriptor{}, err
  1138  	}
  1139  
  1140  	return ocispec.Descriptor{
  1141  		MediaType: mediaType,
  1142  		Digest:    refDigest,
  1143  		Size:      size,
  1144  	}, nil
  1145  }
  1146  
  1147  // manifestStore accesses the manifest part of the repository.
  1148  type manifestStore struct {
  1149  	repo *Repository
  1150  }
  1151  
  1152  // Fetch fetches the content identified by the descriptor.
  1153  func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) {
  1154  	ref := s.repo.Reference
  1155  	ref.Reference = target.Digest.String()
  1156  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
  1157  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1158  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  1159  	if err != nil {
  1160  		return nil, err
  1161  	}
  1162  	req.Header.Set("Accept", target.MediaType)
  1163  
  1164  	resp, err := s.repo.do(req)
  1165  	if err != nil {
  1166  		return nil, err
  1167  	}
  1168  	defer func() {
  1169  		if err != nil {
  1170  			resp.Body.Close()
  1171  		}
  1172  	}()
  1173  
  1174  	switch resp.StatusCode {
  1175  	case http.StatusOK:
  1176  		// no-op
  1177  	case http.StatusNotFound:
  1178  		return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
  1179  	default:
  1180  		return nil, errutil.ParseErrorResponse(resp)
  1181  	}
  1182  	mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
  1183  	if err != nil {
  1184  		return nil, fmt.Errorf("%s %q: invalid response Content-Type: %w", resp.Request.Method, resp.Request.URL, err)
  1185  	}
  1186  	if mediaType != target.MediaType {
  1187  		return nil, fmt.Errorf("%s %q: mismatch response Content-Type %q: expect %q", resp.Request.Method, resp.Request.URL, mediaType, target.MediaType)
  1188  	}
  1189  	if size := resp.ContentLength; size != -1 && size != target.Size {
  1190  		return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
  1191  	}
  1192  	if err := verifyContentDigest(resp, target.Digest); err != nil {
  1193  		return nil, err
  1194  	}
  1195  	return resp.Body, nil
  1196  }
  1197  
  1198  // Push pushes the content, matching the expected descriptor.
  1199  func (s *manifestStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
  1200  	return s.pushWithIndexing(ctx, expected, content, expected.Digest.String())
  1201  }
  1202  
  1203  // Exists returns true if the described content exists.
  1204  func (s *manifestStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
  1205  	_, err := s.Resolve(ctx, target.Digest.String())
  1206  	if err == nil {
  1207  		return true, nil
  1208  	}
  1209  	if errors.Is(err, errdef.ErrNotFound) {
  1210  		return false, nil
  1211  	}
  1212  	return false, err
  1213  }
  1214  
  1215  // Delete removes the manifest content identified by the descriptor.
  1216  func (s *manifestStore) Delete(ctx context.Context, target ocispec.Descriptor) error {
  1217  	return s.deleteWithIndexing(ctx, target)
  1218  }
  1219  
  1220  // deleteWithIndexing removes the manifest content identified by the descriptor,
  1221  // and indexes referrers for the manifest when needed.
  1222  func (s *manifestStore) deleteWithIndexing(ctx context.Context, target ocispec.Descriptor) error {
  1223  	switch target.MediaType {
  1224  	case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
  1225  		if state := s.repo.loadReferrersState(); state == referrersStateSupported {
  1226  			// referrers API is available, no client-side indexing needed
  1227  			return s.repo.delete(ctx, target, true)
  1228  		}
  1229  
  1230  		if err := limitSize(target, s.repo.MaxMetadataBytes); err != nil {
  1231  			return err
  1232  		}
  1233  		ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionDelete)
  1234  		manifestJSON, err := content.FetchAll(ctx, s, target)
  1235  		if err != nil {
  1236  			return err
  1237  		}
  1238  		if err := s.indexReferrersForDelete(ctx, target, manifestJSON); err != nil {
  1239  			return err
  1240  		}
  1241  	}
  1242  
  1243  	return s.repo.delete(ctx, target, true)
  1244  }
  1245  
  1246  // indexReferrersForDelete indexes referrers for manifests with a subject field
  1247  // on manifest delete.
  1248  //
  1249  // References:
  1250  //   - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests
  1251  //   - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests
  1252  func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error {
  1253  	var manifest struct {
  1254  		Subject *ocispec.Descriptor `json:"subject"`
  1255  	}
  1256  	if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
  1257  		return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
  1258  	}
  1259  	if manifest.Subject == nil {
  1260  		// no subject, no indexing needed
  1261  		return nil
  1262  	}
  1263  
  1264  	subject := *manifest.Subject
  1265  	ok, err := s.repo.pingReferrers(ctx)
  1266  	if err != nil {
  1267  		return err
  1268  	}
  1269  	if ok {
  1270  		// referrers API is available, no client-side indexing needed
  1271  		return nil
  1272  	}
  1273  	return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationRemove})
  1274  }
  1275  
  1276  // Resolve resolves a reference to a descriptor.
  1277  // See also `ManifestMediaTypes`.
  1278  func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
  1279  	ref, err := s.repo.ParseReference(reference)
  1280  	if err != nil {
  1281  		return ocispec.Descriptor{}, err
  1282  	}
  1283  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
  1284  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1285  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
  1286  	if err != nil {
  1287  		return ocispec.Descriptor{}, err
  1288  	}
  1289  	req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))
  1290  
  1291  	resp, err := s.repo.do(req)
  1292  	if err != nil {
  1293  		return ocispec.Descriptor{}, err
  1294  	}
  1295  	defer resp.Body.Close()
  1296  
  1297  	switch resp.StatusCode {
  1298  	case http.StatusOK:
  1299  		return s.generateDescriptor(resp, ref, req.Method)
  1300  	case http.StatusNotFound:
  1301  		return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
  1302  	default:
  1303  		return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp)
  1304  	}
  1305  }
  1306  
  1307  // FetchReference fetches the manifest identified by the reference.
  1308  // The reference can be a tag or digest.
  1309  func (s *manifestStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) {
  1310  	ref, err := s.repo.ParseReference(reference)
  1311  	if err != nil {
  1312  		return ocispec.Descriptor{}, nil, err
  1313  	}
  1314  
  1315  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
  1316  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1317  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  1318  	if err != nil {
  1319  		return ocispec.Descriptor{}, nil, err
  1320  	}
  1321  	req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))
  1322  
  1323  	resp, err := s.repo.do(req)
  1324  	if err != nil {
  1325  		return ocispec.Descriptor{}, nil, err
  1326  	}
  1327  	defer func() {
  1328  		if err != nil {
  1329  			resp.Body.Close()
  1330  		}
  1331  	}()
  1332  
  1333  	switch resp.StatusCode {
  1334  	case http.StatusOK:
  1335  		if resp.ContentLength == -1 {
  1336  			desc, err = s.Resolve(ctx, reference)
  1337  		} else {
  1338  			desc, err = s.generateDescriptor(resp, ref, req.Method)
  1339  		}
  1340  		if err != nil {
  1341  			return ocispec.Descriptor{}, nil, err
  1342  		}
  1343  		return desc, resp.Body, nil
  1344  	case http.StatusNotFound:
  1345  		return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
  1346  	default:
  1347  		return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp)
  1348  	}
  1349  }
  1350  
  1351  // Tag tags a manifest descriptor with a reference string.
  1352  func (s *manifestStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
  1353  	ref, err := s.repo.ParseReference(reference)
  1354  	if err != nil {
  1355  		return err
  1356  	}
  1357  
  1358  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush)
  1359  	rc, err := s.Fetch(ctx, desc)
  1360  	if err != nil {
  1361  		return err
  1362  	}
  1363  	defer rc.Close()
  1364  
  1365  	return s.push(ctx, desc, rc, ref.Reference)
  1366  }
  1367  
  1368  // PushReference pushes the manifest with a reference tag.
  1369  func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
  1370  	ref, err := s.repo.ParseReference(reference)
  1371  	if err != nil {
  1372  		return err
  1373  	}
  1374  	return s.pushWithIndexing(ctx, expected, content, ref.Reference)
  1375  }
  1376  
  1377  // push pushes the manifest content, matching the expected descriptor.
  1378  func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
  1379  	ref := s.repo.Reference
  1380  	ref.Reference = reference
  1381  	// pushing usually requires both pull and push actions.
  1382  	// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
  1383  	ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush)
  1384  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1385  	// unwrap the content for optimizations of built-in types.
  1386  	body := ioutil.UnwrapNopCloser(content)
  1387  	if _, ok := body.(io.ReadCloser); ok {
  1388  		// undo unwrap if the nopCloser is intended.
  1389  		body = content
  1390  	}
  1391  	req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body)
  1392  	if err != nil {
  1393  		return err
  1394  	}
  1395  	if req.GetBody != nil && req.ContentLength != expected.Size {
  1396  		// short circuit a size mismatch for built-in types.
  1397  		return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size)
  1398  	}
  1399  	req.ContentLength = expected.Size
  1400  	req.Header.Set("Content-Type", expected.MediaType)
  1401  
  1402  	// if the underlying client is an auth client, the content might be read
  1403  	// more than once for obtaining the auth challenge and the actual request.
  1404  	// To prevent double reading, the manifest is read and stored in the memory,
  1405  	// and serve from the memory.
  1406  	client := s.repo.client()
  1407  	if _, ok := client.(*auth.Client); ok && req.GetBody == nil {
  1408  		store := cas.NewMemory()
  1409  		err := store.Push(ctx, expected, content)
  1410  		if err != nil {
  1411  			return err
  1412  		}
  1413  		req.GetBody = func() (io.ReadCloser, error) {
  1414  			return store.Fetch(ctx, expected)
  1415  		}
  1416  		req.Body, err = req.GetBody()
  1417  		if err != nil {
  1418  			return err
  1419  		}
  1420  	}
  1421  	resp, err := s.repo.do(req)
  1422  	if err != nil {
  1423  		return err
  1424  	}
  1425  	defer resp.Body.Close()
  1426  
  1427  	if resp.StatusCode != http.StatusCreated {
  1428  		return errutil.ParseErrorResponse(resp)
  1429  	}
  1430  	s.checkOCISubjectHeader(resp)
  1431  	return verifyContentDigest(resp, expected.Digest)
  1432  }
  1433  
  1434  // checkOCISubjectHeader checks the "OCI-Subject" header in the response and
  1435  // sets referrers capability accordingly.
  1436  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
  1437  func (s *manifestStore) checkOCISubjectHeader(resp *http.Response) {
  1438  	// If the "OCI-Subject" header is set, it indicates that the registry
  1439  	// supports the Referrers API and has processed the subject of the manifest.
  1440  	if subjectHeader := resp.Header.Get(headerOCISubject); subjectHeader != "" {
  1441  		s.repo.SetReferrersCapability(true)
  1442  	}
  1443  
  1444  	// If the "OCI-Subject" header is NOT set, it means that either the manifest
  1445  	// has no subject OR the referrers API is NOT supported by the registry.
  1446  	//
  1447  	// Since we don't know whether the pushed manifest has a subject or not,
  1448  	// we do not set the referrers capability to false at here.
  1449  }
  1450  
  1451  // pushWithIndexing pushes the manifest content matching the expected descriptor,
  1452  // and indexes referrers for the manifest when needed.
  1453  func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.Descriptor, r io.Reader, reference string) error {
  1454  	switch expected.MediaType {
  1455  	case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
  1456  		if state := s.repo.loadReferrersState(); state == referrersStateSupported {
  1457  			// referrers API is available, no client-side indexing needed
  1458  			return s.push(ctx, expected, r, reference)
  1459  		}
  1460  
  1461  		if err := limitSize(expected, s.repo.MaxMetadataBytes); err != nil {
  1462  			return err
  1463  		}
  1464  		manifestJSON, err := content.ReadAll(r, expected)
  1465  		if err != nil {
  1466  			return err
  1467  		}
  1468  		if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil {
  1469  			return err
  1470  		}
  1471  		// check referrers API availability again after push
  1472  		if state := s.repo.loadReferrersState(); state == referrersStateSupported {
  1473  			// the subject has been processed the registry, no client-side
  1474  			// indexing needed
  1475  			return nil
  1476  		}
  1477  		return s.indexReferrersForPush(ctx, expected, manifestJSON)
  1478  	default:
  1479  		return s.push(ctx, expected, r, reference)
  1480  	}
  1481  }
  1482  
  1483  // indexReferrersForPush indexes referrers for manifests with a subject field
  1484  // on manifest push.
  1485  //
  1486  // References:
  1487  //   - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
  1488  //   - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject
  1489  func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error {
  1490  	var subject ocispec.Descriptor
  1491  	switch desc.MediaType {
  1492  	case spec.MediaTypeArtifactManifest:
  1493  		var manifest spec.Artifact
  1494  		if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
  1495  			return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
  1496  		}
  1497  		if manifest.Subject == nil {
  1498  			// no subject, no indexing needed
  1499  			return nil
  1500  		}
  1501  		subject = *manifest.Subject
  1502  		desc.ArtifactType = manifest.ArtifactType
  1503  		desc.Annotations = manifest.Annotations
  1504  	case ocispec.MediaTypeImageManifest:
  1505  		var manifest ocispec.Manifest
  1506  		if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
  1507  			return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
  1508  		}
  1509  		if manifest.Subject == nil {
  1510  			// no subject, no indexing needed
  1511  			return nil
  1512  		}
  1513  		subject = *manifest.Subject
  1514  		desc.ArtifactType = manifest.ArtifactType
  1515  		if desc.ArtifactType == "" {
  1516  			desc.ArtifactType = manifest.Config.MediaType
  1517  		}
  1518  		desc.Annotations = manifest.Annotations
  1519  	case ocispec.MediaTypeImageIndex:
  1520  		var manifest ocispec.Index
  1521  		if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
  1522  			return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
  1523  		}
  1524  		if manifest.Subject == nil {
  1525  			// no subject, no indexing needed
  1526  			return nil
  1527  		}
  1528  		subject = *manifest.Subject
  1529  		desc.ArtifactType = manifest.ArtifactType
  1530  		desc.Annotations = manifest.Annotations
  1531  	default:
  1532  		return nil
  1533  	}
  1534  
  1535  	// if the manifest has a subject but the remote registry does not process it,
  1536  	// it means that the Referrers API is not supported by the registry.
  1537  	s.repo.SetReferrersCapability(false)
  1538  	return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationAdd})
  1539  }
  1540  
  1541  // updateReferrersIndex updates the referrers index for desc referencing subject
  1542  // on manifest push and manifest delete.
  1543  // References:
  1544  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
  1545  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests
  1546  func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) {
  1547  	referrersTag := buildReferrersTag(subject)
  1548  
  1549  	var oldIndexDesc *ocispec.Descriptor
  1550  	var oldReferrers []ocispec.Descriptor
  1551  	prepare := func() error {
  1552  		// 1. pull the original referrers list using the referrers tag schema
  1553  		indexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag)
  1554  		if err != nil {
  1555  			if errors.Is(err, errdef.ErrNotFound) {
  1556  				// valid case: no old referrers index
  1557  				return nil
  1558  			}
  1559  			return err
  1560  		}
  1561  		oldIndexDesc = &indexDesc
  1562  		oldReferrers = referrers
  1563  		return nil
  1564  	}
  1565  	update := func(referrerChanges []referrerChange) error {
  1566  		// 2. apply the referrer changes on the referrers list
  1567  		updatedReferrers, err := applyReferrerChanges(oldReferrers, referrerChanges)
  1568  		if err != nil {
  1569  			if err == errNoReferrerUpdate {
  1570  				return nil
  1571  			}
  1572  			return err
  1573  		}
  1574  
  1575  		// 3. push the updated referrers list using referrers tag schema
  1576  		if len(updatedReferrers) > 0 || s.repo.SkipReferrersGC {
  1577  			// push a new index in either case:
  1578  			// 1. the referrers list has been updated with a non-zero size
  1579  			// 2. OR the updated referrers list is empty but referrers GC
  1580  			//    is skipped, in this case an empty index should still be pushed
  1581  			//    as the old index won't get deleted
  1582  			newIndexDesc, newIndex, err := generateIndex(updatedReferrers)
  1583  			if err != nil {
  1584  				return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err)
  1585  			}
  1586  			if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil {
  1587  				return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err)
  1588  			}
  1589  		}
  1590  
  1591  		// 4. delete the dangling original referrers index, if applicable
  1592  		if s.repo.SkipReferrersGC || oldIndexDesc == nil {
  1593  			return nil
  1594  		}
  1595  		if err := s.repo.delete(ctx, *oldIndexDesc, true); err != nil {
  1596  			return &ReferrersError{
  1597  				Op:      opDeleteReferrersIndex,
  1598  				Err:     fmt.Errorf("failed to delete dangling referrers index %s for referrers tag %s: %w", oldIndexDesc.Digest.String(), referrersTag, err),
  1599  				Subject: subject,
  1600  			}
  1601  		}
  1602  		return nil
  1603  	}
  1604  
  1605  	merge, done := s.repo.referrersMergePool.Get(referrersTag)
  1606  	defer done()
  1607  	return merge.Do(change, prepare, update)
  1608  }
  1609  
  1610  // ParseReference parses a reference to a fully qualified reference.
  1611  func (s *manifestStore) ParseReference(reference string) (registry.Reference, error) {
  1612  	return s.repo.ParseReference(reference)
  1613  }
  1614  
  1615  // generateDescriptor returns a descriptor generated from the response.
  1616  // See the truth table at the top of `repository_test.go`
  1617  func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Reference, httpMethod string) (ocispec.Descriptor, error) {
  1618  	// 1. Validate Content-Type
  1619  	mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
  1620  	if err != nil {
  1621  		return ocispec.Descriptor{}, fmt.Errorf(
  1622  			"%s %q: invalid response `Content-Type` header; %w",
  1623  			resp.Request.Method,
  1624  			resp.Request.URL,
  1625  			err,
  1626  		)
  1627  	}
  1628  
  1629  	// 2. Validate Size
  1630  	if resp.ContentLength == -1 {
  1631  		return ocispec.Descriptor{}, fmt.Errorf(
  1632  			"%s %q: unknown response Content-Length",
  1633  			resp.Request.Method,
  1634  			resp.Request.URL,
  1635  		)
  1636  	}
  1637  
  1638  	// 3. Validate Client Reference
  1639  	var refDigest digest.Digest
  1640  	if d, err := ref.Digest(); err == nil {
  1641  		refDigest = d
  1642  	}
  1643  
  1644  	// 4. Validate Server Digest (if present)
  1645  	var serverHeaderDigest digest.Digest
  1646  	if serverHeaderDigestStr := resp.Header.Get(headerDockerContentDigest); serverHeaderDigestStr != "" {
  1647  		if serverHeaderDigest, err = digest.Parse(serverHeaderDigestStr); err != nil {
  1648  			return ocispec.Descriptor{}, fmt.Errorf(
  1649  				"%s %q: invalid response header value: `%s: %s`; %w",
  1650  				resp.Request.Method,
  1651  				resp.Request.URL,
  1652  				headerDockerContentDigest,
  1653  				serverHeaderDigestStr,
  1654  				err,
  1655  			)
  1656  		}
  1657  	}
  1658  
  1659  	/* 5. Now, look for specific error conditions; see truth table in method docstring */
  1660  	var contentDigest digest.Digest
  1661  
  1662  	if len(serverHeaderDigest) == 0 {
  1663  		if httpMethod == http.MethodHead {
  1664  			if len(refDigest) == 0 {
  1665  				// HEAD without server `Docker-Content-Digest` header is an
  1666  				// immediate fail
  1667  				return ocispec.Descriptor{}, fmt.Errorf(
  1668  					"HTTP %s request missing required header %q",
  1669  					httpMethod, headerDockerContentDigest,
  1670  				)
  1671  			}
  1672  			// Otherwise, just trust the client-supplied digest
  1673  			contentDigest = refDigest
  1674  		} else {
  1675  			// GET without server `Docker-Content-Digest` header forces the
  1676  			// expensive calculation
  1677  			var calculatedDigest digest.Digest
  1678  			if calculatedDigest, err = calculateDigestFromResponse(resp, s.repo.MaxMetadataBytes); err != nil {
  1679  				return ocispec.Descriptor{}, fmt.Errorf("failed to calculate digest on response body; %w", err)
  1680  			}
  1681  			contentDigest = calculatedDigest
  1682  		}
  1683  	} else {
  1684  		contentDigest = serverHeaderDigest
  1685  	}
  1686  
  1687  	if len(refDigest) > 0 && refDigest != contentDigest {
  1688  		return ocispec.Descriptor{}, fmt.Errorf(
  1689  			"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
  1690  			resp.Request.Method, resp.Request.URL,
  1691  			headerDockerContentDigest, contentDigest,
  1692  			refDigest,
  1693  		)
  1694  	}
  1695  
  1696  	// 6. Finally, if we made it this far, then all is good; return.
  1697  	return ocispec.Descriptor{
  1698  		MediaType: mediaType,
  1699  		Digest:    contentDigest,
  1700  		Size:      resp.ContentLength,
  1701  	}, nil
  1702  }
  1703  
  1704  // calculateDigestFromResponse calculates the actual digest of the response body
  1705  // taking care not to destroy it in the process.
  1706  func calculateDigestFromResponse(resp *http.Response, maxMetadataBytes int64) (digest.Digest, error) {
  1707  	defer resp.Body.Close()
  1708  
  1709  	body := limitReader(resp.Body, maxMetadataBytes)
  1710  	content, err := io.ReadAll(body)
  1711  	if err != nil {
  1712  		return "", fmt.Errorf("%s %q: failed to read response body: %w", resp.Request.Method, resp.Request.URL, err)
  1713  	}
  1714  	resp.Body = io.NopCloser(bytes.NewReader(content))
  1715  
  1716  	return digest.FromBytes(content), nil
  1717  }
  1718  
  1719  // verifyContentDigest verifies "Docker-Content-Digest" header if present.
  1720  // OCI distribution-spec states the Docker-Content-Digest header is optional.
  1721  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers
  1722  func verifyContentDigest(resp *http.Response, expected digest.Digest) error {
  1723  	digestStr := resp.Header.Get(headerDockerContentDigest)
  1724  
  1725  	if len(digestStr) == 0 {
  1726  		return nil
  1727  	}
  1728  
  1729  	contentDigest, err := digest.Parse(digestStr)
  1730  	if err != nil {
  1731  		return fmt.Errorf(
  1732  			"%s %q: invalid response header: `%s: %s`",
  1733  			resp.Request.Method, resp.Request.URL,
  1734  			headerDockerContentDigest, digestStr,
  1735  		)
  1736  	}
  1737  
  1738  	if contentDigest != expected {
  1739  		return fmt.Errorf(
  1740  			"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
  1741  			resp.Request.Method, resp.Request.URL,
  1742  			headerDockerContentDigest, contentDigest,
  1743  			expected,
  1744  		)
  1745  	}
  1746  
  1747  	return nil
  1748  }
  1749  
  1750  // generateIndex generates an image index containing the given manifests list.
  1751  func generateIndex(manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) {
  1752  	if manifests == nil {
  1753  		manifests = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
  1754  	}
  1755  	index := ocispec.Index{
  1756  		Versioned: specs.Versioned{
  1757  			SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
  1758  		},
  1759  		MediaType: ocispec.MediaTypeImageIndex,
  1760  		Manifests: manifests,
  1761  	}
  1762  	indexJSON, err := json.Marshal(index)
  1763  	if err != nil {
  1764  		return ocispec.Descriptor{}, nil, err
  1765  	}
  1766  	indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON)
  1767  	return indexDesc, indexJSON, nil
  1768  }