270 lines
8.5 KiB
Go
270 lines
8.5 KiB
Go
/*
|
||
Copyright The ORAS Authors.
|
||
Licensed under the Apache License, Version 2.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
You may obtain a copy of the License at
|
||
|
||
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
||
Unless required by applicable law or agreed to in writing, software
|
||
distributed under the License is distributed on an "AS IS" BASIS,
|
||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
*/
|
||
|
||
package registry
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"github.com/opencontainers/go-digest"
|
||
"oras.land/oras-go/v2/errdef"
|
||
)
|
||
|
||
// regular expressions for components.
|
||
var (
|
||
// repositoryRegexp is adapted from the distribution implementation. The
|
||
// repository name set under OCI distribution spec is a subset of the docker
|
||
// spec. For maximum compatability, the docker spec is verified client-side.
|
||
// Further checks are left to the server-side.
|
||
// References:
|
||
// - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53
|
||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pulling-manifests
|
||
repositoryRegexp = regexp.MustCompile(`^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$`)
|
||
|
||
// tagRegexp checks the tag name.
|
||
// The docker and OCI spec have the same regular expression.
|
||
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pulling-manifests
|
||
tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`)
|
||
)
|
||
|
||
// Reference references either a resource descriptor (where Reference.Reference
|
||
// is a tag or a digest), or a resource repository (where Reference.Reference
|
||
// is the empty string).
|
||
type Reference struct {
|
||
// Registry is the name of the registry. It is usually the domain name of
|
||
// the registry optionally with a port.
|
||
Registry string
|
||
|
||
// Repository is the name of the repository.
|
||
Repository string
|
||
|
||
// Reference is the reference of the object in the repository. This field
|
||
// can take any one of the four valid forms (see ParseReference). In the
|
||
// case where it's the empty string, it necessarily implies valid form D,
|
||
// and where it is non-empty, then it is either a tag, or a digest
|
||
// (implying one of valid forms A, B, or C).
|
||
Reference string
|
||
}
|
||
|
||
// ParseReference parses a string (artifact) into an `artifact reference`.
|
||
//
|
||
// Note: An "image" is an "artifact", however, an "artifact" is not necessarily
|
||
// an "image".
|
||
//
|
||
// The token `artifact` is composed of other tokens, and those in turn are
|
||
// composed of others. This definition recursivity requires a notation capable
|
||
// of recursion, thus the following two forms have been adopted:
|
||
//
|
||
// 1. Backus–Naur Form (BNF) has been adopted to address the recursive nature
|
||
// of the definition.
|
||
// 2. Token opacity is revealed via its label letter-casing. That is, "opaque"
|
||
// tokens (i.e., tokens that are not final, and must therefore be further
|
||
// broken down into their constituents) are denoted in *lowercase*, while
|
||
// final tokens (i.e., leaf-node tokens that are final) are denoted in
|
||
// *uppercase*.
|
||
//
|
||
// Finally, note that a number of the opaque tokens are polymorphic in nature;
|
||
// that is, they can take on one of numerous forms, not restricted to a single
|
||
// defining form.
|
||
//
|
||
// The top-level token, `artifact`, is composed of two (opaque) tokens; namely
|
||
// `socketaddr` and `path`:
|
||
//
|
||
// <artifact> ::= <socketaddr> "/" <path>
|
||
//
|
||
// The former is described as follows:
|
||
//
|
||
// <socketaddr> ::= <host> | <host> ":" <PORT>
|
||
// <host> ::= <ip> | <FQDN>
|
||
// <ip> ::= <IPV4-ADDR> | <IPV6-ADDR>
|
||
//
|
||
// The latter, which is of greater interest here, is described as follows:
|
||
//
|
||
// <path> ::= <REPOSITORY> | <REPOSITORY> <reference>
|
||
// <reference> ::= "@" <digest> | ":" <TAG> "@" <DIGEST> | ":" <TAG>
|
||
// <digest> ::= <ALGO> ":" <HASH>
|
||
//
|
||
// This second token--`path`--can take on exactly four forms, each of which will
|
||
// now be illustrated:
|
||
//
|
||
// <--- path --------------------------------------------> | - Decode `path`
|
||
// <=== REPOSITORY ===> <--- reference ------------------> | - Decode `reference`
|
||
// <=== REPOSITORY ===> @ <=================== digest ===> | - Valid Form A
|
||
// <=== REPOSITORY ===> : <!!! TAG !!!> @ <=== digest ===> | - Valid Form B (tag is dropped)
|
||
// <=== REPOSITORY ===> : <=== TAG ======================> | - Valid Form C
|
||
// <=== REPOSITORY ======================================> | - Valid Form D
|
||
//
|
||
// Note: In the case of Valid Form B, TAG is dropped without any validation or
|
||
// further consideration.
|
||
func ParseReference(artifact string) (Reference, error) {
|
||
parts := strings.SplitN(artifact, "/", 2)
|
||
if len(parts) == 1 {
|
||
// Invalid Form
|
||
return Reference{}, fmt.Errorf("%w: missing repository", errdef.ErrInvalidReference)
|
||
}
|
||
registry, path := parts[0], parts[1]
|
||
|
||
var isTag bool
|
||
var repository string
|
||
var reference string
|
||
if index := strings.Index(path, "@"); index != -1 {
|
||
// `digest` found; Valid Form A (if not B)
|
||
isTag = false
|
||
repository = path[:index]
|
||
reference = path[index+1:]
|
||
|
||
if index = strings.Index(repository, ":"); index != -1 {
|
||
// `tag` found (and now dropped without validation) since `the
|
||
// `digest` already present; Valid Form B
|
||
repository = repository[:index]
|
||
}
|
||
} else if index = strings.Index(path, ":"); index != -1 {
|
||
// `tag` found; Valid Form C
|
||
isTag = true
|
||
repository = path[:index]
|
||
reference = path[index+1:]
|
||
} else {
|
||
// empty `reference`; Valid Form D
|
||
repository = path
|
||
}
|
||
ref := Reference{
|
||
Registry: registry,
|
||
Repository: repository,
|
||
Reference: reference,
|
||
}
|
||
|
||
if err := ref.ValidateRegistry(); err != nil {
|
||
return Reference{}, err
|
||
}
|
||
|
||
if err := ref.ValidateRepository(); err != nil {
|
||
return Reference{}, err
|
||
}
|
||
|
||
if len(ref.Reference) == 0 {
|
||
return ref, nil
|
||
}
|
||
|
||
validator := ref.ValidateReferenceAsDigest
|
||
if isTag {
|
||
validator = ref.ValidateReferenceAsTag
|
||
}
|
||
if err := validator(); err != nil {
|
||
return Reference{}, err
|
||
}
|
||
|
||
return ref, nil
|
||
}
|
||
|
||
// Validate the entire reference object; the registry, the repository, and the
|
||
// reference.
|
||
func (r Reference) Validate() error {
|
||
if err := r.ValidateRegistry(); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := r.ValidateRepository(); err != nil {
|
||
return err
|
||
}
|
||
|
||
return r.ValidateReference()
|
||
}
|
||
|
||
// ValidateRegistry validates the registry.
|
||
func (r Reference) ValidateRegistry() error {
|
||
if uri, err := url.ParseRequestURI("dummy://" + r.Registry); err != nil || uri.Host != r.Registry {
|
||
return fmt.Errorf("%w: invalid registry", errdef.ErrInvalidReference)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateRepository validates the repository.
|
||
func (r Reference) ValidateRepository() error {
|
||
if !repositoryRegexp.MatchString(r.Repository) {
|
||
return fmt.Errorf("%w: invalid repository", errdef.ErrInvalidReference)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateReferenceAsTag validates the reference as a tag.
|
||
func (r Reference) ValidateReferenceAsTag() error {
|
||
if !tagRegexp.MatchString(r.Reference) {
|
||
return fmt.Errorf("%w: invalid tag", errdef.ErrInvalidReference)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateReferenceAsDigest validates the reference as a digest.
|
||
func (r Reference) ValidateReferenceAsDigest() error {
|
||
if _, err := r.Digest(); err != nil {
|
||
return fmt.Errorf("%w: invalid digest; %v", errdef.ErrInvalidReference, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateReference where the reference is first tried as an ampty string, then
|
||
// as a digest, and if that fails, as a tag.
|
||
func (r Reference) ValidateReference() error {
|
||
if len(r.Reference) == 0 {
|
||
return nil
|
||
}
|
||
|
||
if index := strings.IndexByte(r.Reference, ':'); index != -1 {
|
||
return r.ValidateReferenceAsDigest()
|
||
}
|
||
|
||
return r.ValidateReferenceAsTag()
|
||
}
|
||
|
||
// Host returns the host name of the registry.
|
||
func (r Reference) Host() string {
|
||
if r.Registry == "docker.io" {
|
||
return "registry-1.docker.io"
|
||
}
|
||
return r.Registry
|
||
}
|
||
|
||
// ReferenceOrDefault returns the reference or the default reference if empty.
|
||
func (r Reference) ReferenceOrDefault() string {
|
||
if r.Reference == "" {
|
||
return "latest"
|
||
}
|
||
return r.Reference
|
||
}
|
||
|
||
// Digest returns the reference as a digest.
|
||
func (r Reference) Digest() (digest.Digest, error) {
|
||
return digest.Parse(r.Reference)
|
||
}
|
||
|
||
// String implements `fmt.Stringer` and returns the reference string.
|
||
// The resulted string is meaningful only if the reference is valid.
|
||
func (r Reference) String() string {
|
||
if r.Repository == "" {
|
||
return r.Registry
|
||
}
|
||
ref := r.Registry + "/" + r.Repository
|
||
if r.Reference == "" {
|
||
return ref
|
||
}
|
||
if d, err := r.Digest(); err == nil {
|
||
return ref + "@" + d.String()
|
||
}
|
||
return ref + ":" + r.Reference
|
||
}
|