mirror of https://github.com/k3d-io/k3d
parent
b2c790b51f
commit
9a0ca62ec2
@ -1,247 +0,0 @@ |
||||
package digestset |
||||
|
||||
import ( |
||||
"errors" |
||||
"sort" |
||||
"strings" |
||||
"sync" |
||||
|
||||
digest "github.com/opencontainers/go-digest" |
||||
) |
||||
|
||||
var ( |
||||
// ErrDigestNotFound is used when a matching digest
|
||||
// could not be found in a set.
|
||||
ErrDigestNotFound = errors.New("digest not found") |
||||
|
||||
// ErrDigestAmbiguous is used when multiple digests
|
||||
// are found in a set. None of the matching digests
|
||||
// should be considered valid matches.
|
||||
ErrDigestAmbiguous = errors.New("ambiguous digest string") |
||||
) |
||||
|
||||
// Set is used to hold a unique set of digests which
|
||||
// may be easily referenced by easily referenced by a string
|
||||
// representation of the digest as well as short representation.
|
||||
// The uniqueness of the short representation is based on other
|
||||
// digests in the set. If digests are omitted from this set,
|
||||
// collisions in a larger set may not be detected, therefore it
|
||||
// is important to always do short representation lookups on
|
||||
// the complete set of digests. To mitigate collisions, an
|
||||
// appropriately long short code should be used.
|
||||
type Set struct { |
||||
mutex sync.RWMutex |
||||
entries digestEntries |
||||
} |
||||
|
||||
// NewSet creates an empty set of digests
|
||||
// which may have digests added.
|
||||
func NewSet() *Set { |
||||
return &Set{ |
||||
entries: digestEntries{}, |
||||
} |
||||
} |
||||
|
||||
// checkShortMatch checks whether two digests match as either whole
|
||||
// values or short values. This function does not test equality,
|
||||
// rather whether the second value could match against the first
|
||||
// value.
|
||||
func checkShortMatch(alg digest.Algorithm, hex, shortAlg, shortHex string) bool { |
||||
if len(hex) == len(shortHex) { |
||||
if hex != shortHex { |
||||
return false |
||||
} |
||||
if len(shortAlg) > 0 && string(alg) != shortAlg { |
||||
return false |
||||
} |
||||
} else if !strings.HasPrefix(hex, shortHex) { |
||||
return false |
||||
} else if len(shortAlg) > 0 && string(alg) != shortAlg { |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// Lookup looks for a digest matching the given string representation.
|
||||
// If no digests could be found ErrDigestNotFound will be returned
|
||||
// with an empty digest value. If multiple matches are found
|
||||
// ErrDigestAmbiguous will be returned with an empty digest value.
|
||||
func (dst *Set) Lookup(d string) (digest.Digest, error) { |
||||
dst.mutex.RLock() |
||||
defer dst.mutex.RUnlock() |
||||
if len(dst.entries) == 0 { |
||||
return "", ErrDigestNotFound |
||||
} |
||||
var ( |
||||
searchFunc func(int) bool |
||||
alg digest.Algorithm |
||||
hex string |
||||
) |
||||
dgst, err := digest.Parse(d) |
||||
if err == digest.ErrDigestInvalidFormat { |
||||
hex = d |
||||
searchFunc = func(i int) bool { |
||||
return dst.entries[i].val >= d |
||||
} |
||||
} else { |
||||
hex = dgst.Hex() |
||||
alg = dgst.Algorithm() |
||||
searchFunc = func(i int) bool { |
||||
if dst.entries[i].val == hex { |
||||
return dst.entries[i].alg >= alg |
||||
} |
||||
return dst.entries[i].val >= hex |
||||
} |
||||
} |
||||
idx := sort.Search(len(dst.entries), searchFunc) |
||||
if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) { |
||||
return "", ErrDigestNotFound |
||||
} |
||||
if dst.entries[idx].alg == alg && dst.entries[idx].val == hex { |
||||
return dst.entries[idx].digest, nil |
||||
} |
||||
if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) { |
||||
return "", ErrDigestAmbiguous |
||||
} |
||||
|
||||
return dst.entries[idx].digest, nil |
||||
} |
||||
|
||||
// Add adds the given digest to the set. An error will be returned
|
||||
// if the given digest is invalid. If the digest already exists in the
|
||||
// set, this operation will be a no-op.
|
||||
func (dst *Set) Add(d digest.Digest) error { |
||||
if err := d.Validate(); err != nil { |
||||
return err |
||||
} |
||||
dst.mutex.Lock() |
||||
defer dst.mutex.Unlock() |
||||
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d} |
||||
searchFunc := func(i int) bool { |
||||
if dst.entries[i].val == entry.val { |
||||
return dst.entries[i].alg >= entry.alg |
||||
} |
||||
return dst.entries[i].val >= entry.val |
||||
} |
||||
idx := sort.Search(len(dst.entries), searchFunc) |
||||
if idx == len(dst.entries) { |
||||
dst.entries = append(dst.entries, entry) |
||||
return nil |
||||
} else if dst.entries[idx].digest == d { |
||||
return nil |
||||
} |
||||
|
||||
entries := append(dst.entries, nil) |
||||
copy(entries[idx+1:], entries[idx:len(entries)-1]) |
||||
entries[idx] = entry |
||||
dst.entries = entries |
||||
return nil |
||||
} |
||||
|
||||
// Remove removes the given digest from the set. An err will be
|
||||
// returned if the given digest is invalid. If the digest does
|
||||
// not exist in the set, this operation will be a no-op.
|
||||
func (dst *Set) Remove(d digest.Digest) error { |
||||
if err := d.Validate(); err != nil { |
||||
return err |
||||
} |
||||
dst.mutex.Lock() |
||||
defer dst.mutex.Unlock() |
||||
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d} |
||||
searchFunc := func(i int) bool { |
||||
if dst.entries[i].val == entry.val { |
||||
return dst.entries[i].alg >= entry.alg |
||||
} |
||||
return dst.entries[i].val >= entry.val |
||||
} |
||||
idx := sort.Search(len(dst.entries), searchFunc) |
||||
// Not found if idx is after or value at idx is not digest
|
||||
if idx == len(dst.entries) || dst.entries[idx].digest != d { |
||||
return nil |
||||
} |
||||
|
||||
entries := dst.entries |
||||
copy(entries[idx:], entries[idx+1:]) |
||||
entries = entries[:len(entries)-1] |
||||
dst.entries = entries |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// All returns all the digests in the set
|
||||
func (dst *Set) All() []digest.Digest { |
||||
dst.mutex.RLock() |
||||
defer dst.mutex.RUnlock() |
||||
retValues := make([]digest.Digest, len(dst.entries)) |
||||
for i := range dst.entries { |
||||
retValues[i] = dst.entries[i].digest |
||||
} |
||||
|
||||
return retValues |
||||
} |
||||
|
||||
// ShortCodeTable returns a map of Digest to unique short codes. The
|
||||
// length represents the minimum value, the maximum length may be the
|
||||
// entire value of digest if uniqueness cannot be achieved without the
|
||||
// full value. This function will attempt to make short codes as short
|
||||
// as possible to be unique.
|
||||
func ShortCodeTable(dst *Set, length int) map[digest.Digest]string { |
||||
dst.mutex.RLock() |
||||
defer dst.mutex.RUnlock() |
||||
m := make(map[digest.Digest]string, len(dst.entries)) |
||||
l := length |
||||
resetIdx := 0 |
||||
for i := 0; i < len(dst.entries); i++ { |
||||
var short string |
||||
extended := true |
||||
for extended { |
||||
extended = false |
||||
if len(dst.entries[i].val) <= l { |
||||
short = dst.entries[i].digest.String() |
||||
} else { |
||||
short = dst.entries[i].val[:l] |
||||
for j := i + 1; j < len(dst.entries); j++ { |
||||
if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) { |
||||
if j > resetIdx { |
||||
resetIdx = j |
||||
} |
||||
extended = true |
||||
} else { |
||||
break |
||||
} |
||||
} |
||||
if extended { |
||||
l++ |
||||
} |
||||
} |
||||
} |
||||
m[dst.entries[i].digest] = short |
||||
if i >= resetIdx { |
||||
l = length |
||||
} |
||||
} |
||||
return m |
||||
} |
||||
|
||||
type digestEntry struct { |
||||
alg digest.Algorithm |
||||
val string |
||||
digest digest.Digest |
||||
} |
||||
|
||||
type digestEntries []*digestEntry |
||||
|
||||
func (d digestEntries) Len() int { |
||||
return len(d) |
||||
} |
||||
|
||||
func (d digestEntries) Less(i, j int) bool { |
||||
if d[i].val != d[j].val { |
||||
return d[i].val < d[j].val |
||||
} |
||||
return d[i].alg < d[j].alg |
||||
} |
||||
|
||||
func (d digestEntries) Swap(i, j int) { |
||||
d[i], d[j] = d[j], d[i] |
||||
} |
@ -1,42 +0,0 @@ |
||||
package reference |
||||
|
||||
import "path" |
||||
|
||||
// IsNameOnly returns true if reference only contains a repo name.
|
||||
func IsNameOnly(ref Named) bool { |
||||
if _, ok := ref.(NamedTagged); ok { |
||||
return false |
||||
} |
||||
if _, ok := ref.(Canonical); ok { |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// FamiliarName returns the familiar name string
|
||||
// for the given named, familiarizing if needed.
|
||||
func FamiliarName(ref Named) string { |
||||
if nn, ok := ref.(normalizedNamed); ok { |
||||
return nn.Familiar().Name() |
||||
} |
||||
return ref.Name() |
||||
} |
||||
|
||||
// FamiliarString returns the familiar string representation
|
||||
// for the given reference, familiarizing if needed.
|
||||
func FamiliarString(ref Reference) string { |
||||
if nn, ok := ref.(normalizedNamed); ok { |
||||
return nn.Familiar().String() |
||||
} |
||||
return ref.String() |
||||
} |
||||
|
||||
// FamiliarMatch reports whether ref matches the specified pattern.
|
||||
// See https://godoc.org/path#Match for supported patterns.
|
||||
func FamiliarMatch(pattern string, ref Reference) (bool, error) { |
||||
matched, err := path.Match(pattern, FamiliarString(ref)) |
||||
if namedRef, isNamed := ref.(Named); isNamed && !matched { |
||||
matched, _ = path.Match(pattern, FamiliarName(namedRef)) |
||||
} |
||||
return matched, err |
||||
} |
@ -1,199 +0,0 @@ |
||||
package reference |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/docker/distribution/digestset" |
||||
"github.com/opencontainers/go-digest" |
||||
) |
||||
|
||||
var ( |
||||
legacyDefaultDomain = "index.docker.io" |
||||
defaultDomain = "docker.io" |
||||
officialRepoName = "library" |
||||
defaultTag = "latest" |
||||
) |
||||
|
||||
// normalizedNamed represents a name which has been
|
||||
// normalized and has a familiar form. A familiar name
|
||||
// is what is used in Docker UI. An example normalized
|
||||
// name is "docker.io/library/ubuntu" and corresponding
|
||||
// familiar name of "ubuntu".
|
||||
type normalizedNamed interface { |
||||
Named |
||||
Familiar() Named |
||||
} |
||||
|
||||
// ParseNormalizedNamed parses a string into a named reference
|
||||
// transforming a familiar name from Docker UI to a fully
|
||||
// qualified reference. If the value may be an identifier
|
||||
// use ParseAnyReference.
|
||||
func ParseNormalizedNamed(s string) (Named, error) { |
||||
if ok := anchoredIdentifierRegexp.MatchString(s); ok { |
||||
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s) |
||||
} |
||||
domain, remainder := splitDockerDomain(s) |
||||
var remoteName string |
||||
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 { |
||||
remoteName = remainder[:tagSep] |
||||
} else { |
||||
remoteName = remainder |
||||
} |
||||
if strings.ToLower(remoteName) != remoteName { |
||||
return nil, errors.New("invalid reference format: repository name must be lowercase") |
||||
} |
||||
|
||||
ref, err := Parse(domain + "/" + remainder) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
named, isNamed := ref.(Named) |
||||
if !isNamed { |
||||
return nil, fmt.Errorf("reference %s has no name", ref.String()) |
||||
} |
||||
return named, nil |
||||
} |
||||
|
||||
// ParseDockerRef normalizes the image reference following the docker convention. This is added
|
||||
// mainly for backward compatibility.
|
||||
// The reference returned can only be either tagged or digested. For reference contains both tag
|
||||
// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
|
||||
// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
|
||||
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
|
||||
func ParseDockerRef(ref string) (Named, error) { |
||||
named, err := ParseNormalizedNamed(ref) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if _, ok := named.(NamedTagged); ok { |
||||
if canonical, ok := named.(Canonical); ok { |
||||
// The reference is both tagged and digested, only
|
||||
// return digested.
|
||||
newNamed, err := WithName(canonical.Name()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
newCanonical, err := WithDigest(newNamed, canonical.Digest()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return newCanonical, nil |
||||
} |
||||
} |
||||
return TagNameOnly(named), nil |
||||
} |
||||
|
||||
// splitDockerDomain splits a repository name to domain and remotename string.
|
||||
// If no valid domain is found, the default domain is used. Repository name
|
||||
// needs to be already validated before.
|
||||
func splitDockerDomain(name string) (domain, remainder string) { |
||||
i := strings.IndexRune(name, '/') |
||||
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") { |
||||
domain, remainder = defaultDomain, name |
||||
} else { |
||||
domain, remainder = name[:i], name[i+1:] |
||||
} |
||||
if domain == legacyDefaultDomain { |
||||
domain = defaultDomain |
||||
} |
||||
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') { |
||||
remainder = officialRepoName + "/" + remainder |
||||
} |
||||
return |
||||
} |
||||
|
||||
// familiarizeName returns a shortened version of the name familiar
|
||||
// to to the Docker UI. Familiar names have the default domain
|
||||
// "docker.io" and "library/" repository prefix removed.
|
||||
// For example, "docker.io/library/redis" will have the familiar
|
||||
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
|
||||
// Returns a familiarized named only reference.
|
||||
func familiarizeName(named namedRepository) repository { |
||||
repo := repository{ |
||||
domain: named.Domain(), |
||||
path: named.Path(), |
||||
} |
||||
|
||||
if repo.domain == defaultDomain { |
||||
repo.domain = "" |
||||
// Handle official repositories which have the pattern "library/<official repo name>"
|
||||
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName { |
||||
repo.path = split[1] |
||||
} |
||||
} |
||||
return repo |
||||
} |
||||
|
||||
func (r reference) Familiar() Named { |
||||
return reference{ |
||||
namedRepository: familiarizeName(r.namedRepository), |
||||
tag: r.tag, |
||||
digest: r.digest, |
||||
} |
||||
} |
||||
|
||||
func (r repository) Familiar() Named { |
||||
return familiarizeName(r) |
||||
} |
||||
|
||||
func (t taggedReference) Familiar() Named { |
||||
return taggedReference{ |
||||
namedRepository: familiarizeName(t.namedRepository), |
||||
tag: t.tag, |
||||
} |
||||
} |
||||
|
||||
func (c canonicalReference) Familiar() Named { |
||||
return canonicalReference{ |
||||
namedRepository: familiarizeName(c.namedRepository), |
||||
digest: c.digest, |
||||
} |
||||
} |
||||
|
||||
// TagNameOnly adds the default tag "latest" to a reference if it only has
|
||||
// a repo name.
|
||||
func TagNameOnly(ref Named) Named { |
||||
if IsNameOnly(ref) { |
||||
namedTagged, err := WithTag(ref, defaultTag) |
||||
if err != nil { |
||||
// Default tag must be valid, to create a NamedTagged
|
||||
// type with non-validated input the WithTag function
|
||||
// should be used instead
|
||||
panic(err) |
||||
} |
||||
return namedTagged |
||||
} |
||||
return ref |
||||
} |
||||
|
||||
// ParseAnyReference parses a reference string as a possible identifier,
|
||||
// full digest, or familiar name.
|
||||
func ParseAnyReference(ref string) (Reference, error) { |
||||
if ok := anchoredIdentifierRegexp.MatchString(ref); ok { |
||||
return digestReference("sha256:" + ref), nil |
||||
} |
||||
if dgst, err := digest.Parse(ref); err == nil { |
||||
return digestReference(dgst), nil |
||||
} |
||||
|
||||
return ParseNormalizedNamed(ref) |
||||
} |
||||
|
||||
// ParseAnyReferenceWithSet parses a reference string as a possible short
|
||||
// identifier to be matched in a digest set, a full digest, or familiar name.
|
||||
func ParseAnyReferenceWithSet(ref string, ds *digestset.Set) (Reference, error) { |
||||
if ok := anchoredShortIdentifierRegexp.MatchString(ref); ok { |
||||
dgst, err := ds.Lookup(ref) |
||||
if err == nil { |
||||
return digestReference(dgst), nil |
||||
} |
||||
} else { |
||||
if dgst, err := digest.Parse(ref); err == nil { |
||||
return digestReference(dgst), nil |
||||
} |
||||
} |
||||
|
||||
return ParseNormalizedNamed(ref) |
||||
} |
@ -1,433 +0,0 @@ |
||||
// Package reference provides a general type to represent any way of referencing images within the registry.
|
||||
// Its main purpose is to abstract tags and digests (content-addressable hash).
|
||||
//
|
||||
// Grammar
|
||||
//
|
||||
// reference := name [ ":" tag ] [ "@" digest ]
|
||||
// name := [domain '/'] path-component ['/' path-component]*
|
||||
// domain := domain-component ['.' domain-component]* [':' port-number]
|
||||
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
|
||||
// port-number := /[0-9]+/
|
||||
// path-component := alpha-numeric [separator alpha-numeric]*
|
||||
// alpha-numeric := /[a-z0-9]+/
|
||||
// separator := /[_.]|__|[-]*/
|
||||
//
|
||||
// tag := /[\w][\w.-]{0,127}/
|
||||
//
|
||||
// digest := digest-algorithm ":" digest-hex
|
||||
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
|
||||
// digest-algorithm-separator := /[+.-_]/
|
||||
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
|
||||
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
|
||||
//
|
||||
// identifier := /[a-f0-9]{64}/
|
||||
// short-identifier := /[a-f0-9]{6,64}/
|
||||
package reference |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/opencontainers/go-digest" |
||||
) |
||||
|
||||
const ( |
||||
// NameTotalLengthMax is the maximum total number of characters in a repository name.
|
||||
NameTotalLengthMax = 255 |
||||
) |
||||
|
||||
var ( |
||||
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
|
||||
ErrReferenceInvalidFormat = errors.New("invalid reference format") |
||||
|
||||
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
|
||||
ErrTagInvalidFormat = errors.New("invalid tag format") |
||||
|
||||
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
|
||||
ErrDigestInvalidFormat = errors.New("invalid digest format") |
||||
|
||||
// ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
|
||||
ErrNameContainsUppercase = errors.New("repository name must be lowercase") |
||||
|
||||
// ErrNameEmpty is returned for empty, invalid repository names.
|
||||
ErrNameEmpty = errors.New("repository name must have at least one component") |
||||
|
||||
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
|
||||
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax) |
||||
|
||||
// ErrNameNotCanonical is returned when a name is not canonical.
|
||||
ErrNameNotCanonical = errors.New("repository name must be canonical") |
||||
) |
||||
|
||||
// Reference is an opaque object reference identifier that may include
|
||||
// modifiers such as a hostname, name, tag, and digest.
|
||||
type Reference interface { |
||||
// String returns the full reference
|
||||
String() string |
||||
} |
||||
|
||||
// Field provides a wrapper type for resolving correct reference types when
|
||||
// working with encoding.
|
||||
type Field struct { |
||||
reference Reference |
||||
} |
||||
|
||||
// AsField wraps a reference in a Field for encoding.
|
||||
func AsField(reference Reference) Field { |
||||
return Field{reference} |
||||
} |
||||
|
||||
// Reference unwraps the reference type from the field to
|
||||
// return the Reference object. This object should be
|
||||
// of the appropriate type to further check for different
|
||||
// reference types.
|
||||
func (f Field) Reference() Reference { |
||||
return f.reference |
||||
} |
||||
|
||||
// MarshalText serializes the field to byte text which
|
||||
// is the string of the reference.
|
||||
func (f Field) MarshalText() (p []byte, err error) { |
||||
return []byte(f.reference.String()), nil |
||||
} |
||||
|
||||
// UnmarshalText parses text bytes by invoking the
|
||||
// reference parser to ensure the appropriately
|
||||
// typed reference object is wrapped by field.
|
||||
func (f *Field) UnmarshalText(p []byte) error { |
||||
r, err := Parse(string(p)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
f.reference = r |
||||
return nil |
||||
} |
||||
|
||||
// Named is an object with a full name
|
||||
type Named interface { |
||||
Reference |
||||
Name() string |
||||
} |
||||
|
||||
// Tagged is an object which has a tag
|
||||
type Tagged interface { |
||||
Reference |
||||
Tag() string |
||||
} |
||||
|
||||
// NamedTagged is an object including a name and tag.
|
||||
type NamedTagged interface { |
||||
Named |
||||
Tag() string |
||||
} |
||||
|
||||
// Digested is an object which has a digest
|
||||
// in which it can be referenced by
|
||||
type Digested interface { |
||||
Reference |
||||
Digest() digest.Digest |
||||
} |
||||
|
||||
// Canonical reference is an object with a fully unique
|
||||
// name including a name with domain and digest
|
||||
type Canonical interface { |
||||
Named |
||||
Digest() digest.Digest |
||||
} |
||||
|
||||
// namedRepository is a reference to a repository with a name.
|
||||
// A namedRepository has both domain and path components.
|
||||
type namedRepository interface { |
||||
Named |
||||
Domain() string |
||||
Path() string |
||||
} |
||||
|
||||
// Domain returns the domain part of the Named reference
|
||||
func Domain(named Named) string { |
||||
if r, ok := named.(namedRepository); ok { |
||||
return r.Domain() |
||||
} |
||||
domain, _ := splitDomain(named.Name()) |
||||
return domain |
||||
} |
||||
|
||||
// Path returns the name without the domain part of the Named reference
|
||||
func Path(named Named) (name string) { |
||||
if r, ok := named.(namedRepository); ok { |
||||
return r.Path() |
||||
} |
||||
_, path := splitDomain(named.Name()) |
||||
return path |
||||
} |
||||
|
||||
func splitDomain(name string) (string, string) { |
||||
match := anchoredNameRegexp.FindStringSubmatch(name) |
||||
if len(match) != 3 { |
||||
return "", name |
||||
} |
||||
return match[1], match[2] |
||||
} |
||||
|
||||
// SplitHostname splits a named reference into a
|
||||
// hostname and name string. If no valid hostname is
|
||||
// found, the hostname is empty and the full value
|
||||
// is returned as name
|
||||
// DEPRECATED: Use Domain or Path
|
||||
func SplitHostname(named Named) (string, string) { |
||||
if r, ok := named.(namedRepository); ok { |
||||
return r.Domain(), r.Path() |
||||
} |
||||
return splitDomain(named.Name()) |
||||
} |
||||
|
||||
// Parse parses s and returns a syntactically valid Reference.
|
||||
// If an error was encountered it is returned, along with a nil Reference.
|
||||
// NOTE: Parse will not handle short digests.
|
||||
func Parse(s string) (Reference, error) { |
||||
matches := ReferenceRegexp.FindStringSubmatch(s) |
||||
if matches == nil { |
||||
if s == "" { |
||||
return nil, ErrNameEmpty |
||||
} |
||||
if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil { |
||||
return nil, ErrNameContainsUppercase |
||||
} |
||||
return nil, ErrReferenceInvalidFormat |
||||
} |
||||
|
||||
if len(matches[1]) > NameTotalLengthMax { |
||||
return nil, ErrNameTooLong |
||||
} |
||||
|
||||
var repo repository |
||||
|
||||
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1]) |
||||
if len(nameMatch) == 3 { |
||||
repo.domain = nameMatch[1] |
||||
repo.path = nameMatch[2] |
||||
} else { |
||||
repo.domain = "" |
||||
repo.path = matches[1] |
||||
} |
||||
|
||||
ref := reference{ |
||||
namedRepository: repo, |
||||
tag: matches[2], |
||||
} |
||||
if matches[3] != "" { |
||||
var err error |
||||
ref.digest, err = digest.Parse(matches[3]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
r := getBestReferenceType(ref) |
||||
if r == nil { |
||||
return nil, ErrNameEmpty |
||||
} |
||||
|
||||
return r, nil |
||||
} |
||||
|
||||
// ParseNamed parses s and returns a syntactically valid reference implementing
|
||||
// the Named interface. The reference must have a name and be in the canonical
|
||||
// form, otherwise an error is returned.
|
||||
// If an error was encountered it is returned, along with a nil Reference.
|
||||
// NOTE: ParseNamed will not handle short digests.
|
||||
func ParseNamed(s string) (Named, error) { |
||||
named, err := ParseNormalizedNamed(s) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if named.String() != s { |
||||
return nil, ErrNameNotCanonical |
||||
} |
||||
return named, nil |
||||
} |
||||
|
||||
// WithName returns a named object representing the given string. If the input
|
||||
// is invalid ErrReferenceInvalidFormat will be returned.
|
||||
func WithName(name string) (Named, error) { |
||||
if len(name) > NameTotalLengthMax { |
||||
return nil, ErrNameTooLong |
||||
} |
||||
|
||||
match := anchoredNameRegexp.FindStringSubmatch(name) |
||||
if match == nil || len(match) != 3 { |
||||
return nil, ErrReferenceInvalidFormat |
||||
} |
||||
return repository{ |
||||
domain: match[1], |
||||
path: match[2], |
||||
}, nil |
||||
} |
||||
|
||||
// WithTag combines the name from "name" and the tag from "tag" to form a
|
||||
// reference incorporating both the name and the tag.
|
||||
func WithTag(name Named, tag string) (NamedTagged, error) { |
||||
if !anchoredTagRegexp.MatchString(tag) { |
||||
return nil, ErrTagInvalidFormat |
||||
} |
||||
var repo repository |
||||
if r, ok := name.(namedRepository); ok { |
||||
repo.domain = r.Domain() |
||||
repo.path = r.Path() |
||||
} else { |
||||
repo.path = name.Name() |
||||
} |
||||
if canonical, ok := name.(Canonical); ok { |
||||
return reference{ |
||||
namedRepository: repo, |
||||
tag: tag, |
||||
digest: canonical.Digest(), |
||||
}, nil |
||||
} |
||||
return taggedReference{ |
||||
namedRepository: repo, |
||||
tag: tag, |
||||
}, nil |
||||
} |
||||
|
||||
// WithDigest combines the name from "name" and the digest from "digest" to form
|
||||
// a reference incorporating both the name and the digest.
|
||||
func WithDigest(name Named, digest digest.Digest) (Canonical, error) { |
||||
if !anchoredDigestRegexp.MatchString(digest.String()) { |
||||
return nil, ErrDigestInvalidFormat |
||||
} |
||||
var repo repository |
||||
if r, ok := name.(namedRepository); ok { |
||||
repo.domain = r.Domain() |
||||
repo.path = r.Path() |
||||
} else { |
||||
repo.path = name.Name() |
||||
} |
||||
if tagged, ok := name.(Tagged); ok { |
||||
return reference{ |
||||
namedRepository: repo, |
||||
tag: tagged.Tag(), |
||||
digest: digest, |
||||
}, nil |
||||
} |
||||
return canonicalReference{ |
||||
namedRepository: repo, |
||||
digest: digest, |
||||
}, nil |
||||
} |
||||
|
||||
// TrimNamed removes any tag or digest from the named reference.
|
||||
func TrimNamed(ref Named) Named { |
||||
domain, path := SplitHostname(ref) |
||||
return repository{ |
||||
domain: domain, |
||||
path: path, |
||||
} |
||||
} |
||||
|
||||
func getBestReferenceType(ref reference) Reference { |
||||
if ref.Name() == "" { |
||||
// Allow digest only references
|
||||
if ref.digest != "" { |
||||
return digestReference(ref.digest) |
||||
} |
||||
return nil |
||||
} |
||||
if ref.tag == "" { |
||||
if ref.digest != "" { |
||||
return canonicalReference{ |
||||
namedRepository: ref.namedRepository, |
||||
digest: ref.digest, |
||||
} |
||||
} |
||||
return ref.namedRepository |
||||
} |
||||
if ref.digest == "" { |
||||
return taggedReference{ |
||||
namedRepository: ref.namedRepository, |
||||
tag: ref.tag, |
||||
} |
||||
} |
||||
|
||||
return ref |
||||
} |
||||
|
||||
type reference struct { |
||||
namedRepository |
||||
tag string |
||||
digest digest.Digest |
||||
} |
||||
|
||||
func (r reference) String() string { |
||||
return r.Name() + ":" + r.tag + "@" + r.digest.String() |
||||
} |
||||
|
||||
func (r reference) Tag() string { |
||||
return r.tag |
||||
} |
||||
|
||||
func (r reference) Digest() digest.Digest { |
||||
return r.digest |
||||
} |
||||
|
||||
type repository struct { |
||||
domain string |
||||
path string |
||||
} |
||||
|
||||
func (r repository) String() string { |
||||
return r.Name() |
||||
} |
||||
|
||||
func (r repository) Name() string { |
||||
if r.domain == "" { |
||||
return r.path |
||||
} |
||||
return r.domain + "/" + r.path |
||||
} |
||||
|
||||
func (r repository) Domain() string { |
||||
return r.domain |
||||
} |
||||
|
||||
func (r repository) Path() string { |
||||
return r.path |
||||
} |
||||
|
||||
type digestReference digest.Digest |
||||
|
||||
func (d digestReference) String() string { |
||||
return digest.Digest(d).String() |
||||
} |
||||
|
||||
func (d digestReference) Digest() digest.Digest { |
||||
return digest.Digest(d) |
||||
} |
||||
|
||||
type taggedReference struct { |
||||
namedRepository |
||||
tag string |
||||
} |
||||
|
||||
func (t taggedReference) String() string { |
||||
return t.Name() + ":" + t.tag |
||||
} |
||||
|
||||
func (t taggedReference) Tag() string { |
||||
return t.tag |
||||
} |
||||
|
||||
type canonicalReference struct { |
||||
namedRepository |
||||
digest digest.Digest |
||||
} |
||||
|
||||
func (c canonicalReference) String() string { |
||||
return c.Name() + "@" + c.digest.String() |
||||
} |
||||
|
||||
func (c canonicalReference) Digest() digest.Digest { |
||||
return c.digest |
||||
} |
@ -1,143 +0,0 @@ |
||||
package reference |
||||
|
||||
import "regexp" |
||||
|
||||
var ( |
||||
// alphaNumericRegexp defines the alpha numeric atom, typically a
|
||||
// component of names. This only allows lower case characters and digits.
|
||||
alphaNumericRegexp = match(`[a-z0-9]+`) |
||||
|
||||
// separatorRegexp defines the separators allowed to be embedded in name
|
||||
// components. This allow one period, one or two underscore and multiple
|
||||
// dashes.
|
||||
separatorRegexp = match(`(?:[._]|__|[-]*)`) |
||||
|
||||
// nameComponentRegexp restricts registry path component names to start
|
||||
// with at least one letter or number, with following parts able to be
|
||||
// separated by one period, one or two underscore and multiple dashes.
|
||||
nameComponentRegexp = expression( |
||||
alphaNumericRegexp, |
||||
optional(repeated(separatorRegexp, alphaNumericRegexp))) |
||||
|
||||
// domainComponentRegexp restricts the registry domain component of a
|
||||
// repository name to start with a component as defined by DomainRegexp
|
||||
// and followed by an optional port.
|
||||
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) |
||||
|
||||
// DomainRegexp defines the structure of potential domain components
|
||||
// that may be part of image names. This is purposely a subset of what is
|
||||
// allowed by DNS to ensure backwards compatibility with Docker image
|
||||
// names.
|
||||
DomainRegexp = expression( |
||||
domainComponentRegexp, |
||||
optional(repeated(literal(`.`), domainComponentRegexp)), |
||||
optional(literal(`:`), match(`[0-9]+`))) |
||||
|
||||
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||
TagRegexp = match(`[\w][\w.-]{0,127}`) |
||||
|
||||
// anchoredTagRegexp matches valid tag names, anchored at the start and
|
||||
// end of the matched string.
|
||||
anchoredTagRegexp = anchored(TagRegexp) |
||||
|
||||
// DigestRegexp matches valid digests.
|
||||
DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`) |
||||
|
||||
// anchoredDigestRegexp matches valid digests, anchored at the start and
|
||||
// end of the matched string.
|
||||
anchoredDigestRegexp = anchored(DigestRegexp) |
||||
|
||||
// NameRegexp is the format for the name component of references. The
|
||||
// regexp has capturing groups for the domain and name part omitting
|
||||
// the separating forward slash from either.
|
||||
NameRegexp = expression( |
||||
optional(DomainRegexp, literal(`/`)), |
||||
nameComponentRegexp, |
||||
optional(repeated(literal(`/`), nameComponentRegexp))) |
||||
|
||||
// anchoredNameRegexp is used to parse a name value, capturing the
|
||||
// domain and trailing components.
|
||||
anchoredNameRegexp = anchored( |
||||
optional(capture(DomainRegexp), literal(`/`)), |
||||
capture(nameComponentRegexp, |
||||
optional(repeated(literal(`/`), nameComponentRegexp)))) |
||||
|
||||
// ReferenceRegexp is the full supported format of a reference. The regexp
|
||||
// is anchored and has capturing groups for name, tag, and digest
|
||||
// components.
|
||||
ReferenceRegexp = anchored(capture(NameRegexp), |
||||
optional(literal(":"), capture(TagRegexp)), |
||||
optional(literal("@"), capture(DigestRegexp))) |
||||
|
||||
// IdentifierRegexp is the format for string identifier used as a
|
||||
// content addressable identifier using sha256. These identifiers
|
||||
// are like digests without the algorithm, since sha256 is used.
|
||||
IdentifierRegexp = match(`([a-f0-9]{64})`) |
||||
|
||||
// ShortIdentifierRegexp is the format used to represent a prefix
|
||||
// of an identifier. A prefix may be used to match a sha256 identifier
|
||||
// within a list of trusted identifiers.
|
||||
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`) |
||||
|
||||
// anchoredIdentifierRegexp is used to check or match an
|
||||
// identifier value, anchored at start and end of string.
|
||||
anchoredIdentifierRegexp = anchored(IdentifierRegexp) |
||||
|
||||
// anchoredShortIdentifierRegexp is used to check if a value
|
||||
// is a possible identifier prefix, anchored at start and end
|
||||
// of string.
|
||||
anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp) |
||||
) |
||||
|
||||
// match compiles the string to a regular expression.
|
||||
var match = regexp.MustCompile |
||||
|
||||
// literal compiles s into a literal regular expression, escaping any regexp
|
||||
// reserved characters.
|
||||
func literal(s string) *regexp.Regexp { |
||||
re := match(regexp.QuoteMeta(s)) |
||||
|
||||
if _, complete := re.LiteralPrefix(); !complete { |
||||
panic("must be a literal") |
||||
} |
||||
|
||||
return re |
||||
} |
||||
|
||||
// expression defines a full expression, where each regular expression must
|
||||
// follow the previous.
|
||||
func expression(res ...*regexp.Regexp) *regexp.Regexp { |
||||
var s string |
||||
for _, re := range res { |
||||
s += re.String() |
||||
} |
||||
|
||||
return match(s) |
||||
} |
||||
|
||||
// optional wraps the expression in a non-capturing group and makes the
|
||||
// production optional.
|
||||
func optional(res ...*regexp.Regexp) *regexp.Regexp { |
||||
return match(group(expression(res...)).String() + `?`) |
||||
} |
||||
|
||||
// repeated wraps the regexp in a non-capturing group to get one or more
|
||||
// matches.
|
||||
func repeated(res ...*regexp.Regexp) *regexp.Regexp { |
||||
return match(group(expression(res...)).String() + `+`) |
||||
} |
||||
|
||||
// group wraps the regexp in a non-capturing group.
|
||||
func group(res ...*regexp.Regexp) *regexp.Regexp { |
||||
return match(`(?:` + expression(res...).String() + `)`) |
||||
} |
||||
|
||||
// capture wraps the expression in a capturing group.
|
||||
func capture(res ...*regexp.Regexp) *regexp.Regexp { |
||||
return match(`(` + expression(res...).String() + `)`) |
||||
} |
||||
|
||||
// anchored anchors the regular expression by adding start and end delimiters.
|
||||
func anchored(res ...*regexp.Regexp) *regexp.Regexp { |
||||
return match(`^` + expression(res...).String() + `$`) |
||||
} |
@ -0,0 +1,331 @@ |
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package http2 |
||||
|
||||
import ( |
||||
"context" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
// testSyncHooks coordinates goroutines in tests.
|
||||
//
|
||||
// For example, a call to ClientConn.RoundTrip involves several goroutines, including:
|
||||
// - the goroutine running RoundTrip;
|
||||
// - the clientStream.doRequest goroutine, which writes the request; and
|
||||
// - the clientStream.readLoop goroutine, which reads the response.
|
||||
//
|
||||
// Using testSyncHooks, a test can start a RoundTrip and identify when all these goroutines
|
||||
// are blocked waiting for some condition such as reading the Request.Body or waiting for
|
||||
// flow control to become available.
|
||||
//
|
||||
// The testSyncHooks also manage timers and synthetic time in tests.
|
||||
// This permits us to, for example, start a request and cause it to time out waiting for
|
||||
// response headers without resorting to time.Sleep calls.
|
||||
type testSyncHooks struct { |
||||
// active/inactive act as a mutex and condition variable.
|
||||
//
|
||||
// - neither chan contains a value: testSyncHooks is locked.
|
||||
// - active contains a value: unlocked, and at least one goroutine is not blocked
|
||||
// - inactive contains a value: unlocked, and all goroutines are blocked
|
||||
active chan struct{} |
||||
inactive chan struct{} |
||||
|
||||
// goroutine counts
|
||||
total int // total goroutines
|
||||
condwait map[*sync.Cond]int // blocked in sync.Cond.Wait
|
||||
blocked []*testBlockedGoroutine // otherwise blocked
|
||||
|
||||
// fake time
|
||||
now time.Time |
||||
timers []*fakeTimer |
||||
|
||||
// Transport testing: Report various events.
|
||||
newclientconn func(*ClientConn) |
||||
newstream func(*clientStream) |
||||
} |
||||
|
||||
// testBlockedGoroutine is a blocked goroutine.
|
||||
type testBlockedGoroutine struct { |
||||
f func() bool // blocked until f returns true
|
||||
ch chan struct{} // closed when unblocked
|
||||
} |
||||
|
||||
func newTestSyncHooks() *testSyncHooks { |
||||
h := &testSyncHooks{ |
||||
active: make(chan struct{}, 1), |
||||
inactive: make(chan struct{}, 1), |
||||
condwait: map[*sync.Cond]int{}, |
||||
} |
||||
h.inactive <- struct{}{} |
||||
h.now = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) |
||||
return h |
||||
} |
||||
|
||||
// lock acquires the testSyncHooks mutex.
|
||||
func (h *testSyncHooks) lock() { |
||||
select { |
||||
case <-h.active: |
||||
case <-h.inactive: |
||||
} |
||||
} |
||||
|
||||
// waitInactive waits for all goroutines to become inactive.
|
||||
func (h *testSyncHooks) waitInactive() { |
||||
for { |
||||
<-h.inactive |
||||
if !h.unlock() { |
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
// unlock releases the testSyncHooks mutex.
|
||||
// It reports whether any goroutines are active.
|
||||
func (h *testSyncHooks) unlock() (active bool) { |
||||
// Look for a blocked goroutine which can be unblocked.
|
||||
blocked := h.blocked[:0] |
||||
unblocked := false |
||||
for _, b := range h.blocked { |
||||
if !unblocked && b.f() { |
||||
unblocked = true |
||||
close(b.ch) |
||||
} else { |
||||
blocked = append(blocked, b) |
||||
} |
||||
} |
||||
h.blocked = blocked |
||||
|
||||
// Count goroutines blocked on condition variables.
|
||||
condwait := 0 |
||||
for _, count := range h.condwait { |
||||
condwait += count |
||||
} |
||||
|
||||
if h.total > condwait+len(blocked) { |
||||
h.active <- struct{}{} |
||||
return true |
||||
} else { |
||||
h.inactive <- struct{}{} |
||||
return false |
||||
} |
||||
} |
||||
|
||||
// goRun starts a new goroutine.
|
||||
func (h *testSyncHooks) goRun(f func()) { |
||||
h.lock() |
||||
h.total++ |
||||
h.unlock() |
||||
go func() { |
||||
defer func() { |
||||
h.lock() |
||||
h.total-- |
||||
h.unlock() |
||||
}() |
||||
f() |
||||
}() |
||||
} |
||||
|
||||
// blockUntil indicates that a goroutine is blocked waiting for some condition to become true.
|
||||
// It waits until f returns true before proceeding.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// h.blockUntil(func() bool {
|
||||
// // Is the context done yet?
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// default:
|
||||
// return false
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
// // Wait for the context to become done.
|
||||
// <-ctx.Done()
|
||||
//
|
||||
// The function f passed to blockUntil must be non-blocking and idempotent.
|
||||
func (h *testSyncHooks) blockUntil(f func() bool) { |
||||
if f() { |
||||
return |
||||
} |
||||
ch := make(chan struct{}) |
||||
h.lock() |
||||
h.blocked = append(h.blocked, &testBlockedGoroutine{ |
||||
f: f, |
||||
ch: ch, |
||||
}) |
||||
h.unlock() |
||||
<-ch |
||||
} |
||||
|
||||
// broadcast is sync.Cond.Broadcast.
|
||||
func (h *testSyncHooks) condBroadcast(cond *sync.Cond) { |
||||
h.lock() |
||||
delete(h.condwait, cond) |
||||
h.unlock() |
||||
cond.Broadcast() |
||||
} |
||||
|
||||
// broadcast is sync.Cond.Wait.
|
||||
func (h *testSyncHooks) condWait(cond *sync.Cond) { |
||||
h.lock() |
||||
h.condwait[cond]++ |
||||
h.unlock() |
||||
} |
||||
|
||||
// newTimer creates a new fake timer.
|
||||
func (h *testSyncHooks) newTimer(d time.Duration) timer { |
||||
h.lock() |
||||
defer h.unlock() |
||||
t := &fakeTimer{ |
||||
hooks: h, |
||||
when: h.now.Add(d), |
||||
c: make(chan time.Time), |
||||
} |
||||
h.timers = append(h.timers, t) |
||||
return t |
||||
} |
||||
|
||||
// afterFunc creates a new fake AfterFunc timer.
|
||||
func (h *testSyncHooks) afterFunc(d time.Duration, f func()) timer { |
||||
h.lock() |
||||
defer h.unlock() |
||||
t := &fakeTimer{ |
||||
hooks: h, |
||||
when: h.now.Add(d), |
||||
f: f, |
||||
} |
||||
h.timers = append(h.timers, t) |
||||
return t |
||||
} |
||||
|
||||
func (h *testSyncHooks) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { |
||||
ctx, cancel := context.WithCancel(ctx) |
||||
t := h.afterFunc(d, cancel) |
||||
return ctx, func() { |
||||
t.Stop() |
||||
cancel() |
||||
} |
||||
} |
||||
|
||||
func (h *testSyncHooks) timeUntilEvent() time.Duration { |
||||
h.lock() |
||||
defer h.unlock() |
||||
var next time.Time |
||||
for _, t := range h.timers { |
||||
if next.IsZero() || t.when.Before(next) { |
||||
next = t.when |
||||
} |
||||
} |
||||
if d := next.Sub(h.now); d > 0 { |
||||
return d |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
// advance advances time and causes synthetic timers to fire.
|
||||
func (h *testSyncHooks) advance(d time.Duration) { |
||||
h.lock() |
||||
defer h.unlock() |
||||
h.now = h.now.Add(d) |
||||
timers := h.timers[:0] |
||||
for _, t := range h.timers { |
||||
t := t // remove after go.mod depends on go1.22
|
||||
t.mu.Lock() |
||||
switch { |
||||
case t.when.After(h.now): |
||||
timers = append(timers, t) |
||||
case t.when.IsZero(): |
||||
// stopped timer
|
||||
default: |
||||
t.when = time.Time{} |
||||
if t.c != nil { |
||||
close(t.c) |
||||
} |
||||
if t.f != nil { |
||||
h.total++ |
||||
go func() { |
||||
defer func() { |
||||
h.lock() |
||||
h.total-- |
||||
h.unlock() |
||||
}() |
||||
t.f() |
||||
}() |
||||
} |
||||
} |
||||
t.mu.Unlock() |
||||
} |
||||
h.timers = timers |
||||
} |
||||
|
||||
// A timer wraps a time.Timer, or a synthetic equivalent in tests.
|
||||
// Unlike time.Timer, timer is single-use: The timer channel is closed when the timer expires.
|
||||
type timer interface { |
||||
C() <-chan time.Time |
||||
Stop() bool |
||||
Reset(d time.Duration) bool |
||||
} |
||||
|
||||
// timeTimer implements timer using real time.
|
||||
type timeTimer struct { |
||||
t *time.Timer |
||||
c chan time.Time |
||||
} |
||||
|
||||
// newTimeTimer creates a new timer using real time.
|
||||
func newTimeTimer(d time.Duration) timer { |
||||
ch := make(chan time.Time) |
||||
t := time.AfterFunc(d, func() { |
||||
close(ch) |
||||
}) |
||||
return &timeTimer{t, ch} |
||||
} |
||||
|
||||
// newTimeAfterFunc creates an AfterFunc timer using real time.
|
||||
func newTimeAfterFunc(d time.Duration, f func()) timer { |
||||
return &timeTimer{ |
||||
t: time.AfterFunc(d, f), |
||||
} |
||||
} |
||||
|
||||
func (t timeTimer) C() <-chan time.Time { return t.c } |
||||
func (t timeTimer) Stop() bool { return t.t.Stop() } |
||||
func (t timeTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } |
||||
|
||||
// fakeTimer implements timer using fake time.
|
||||
type fakeTimer struct { |
||||
hooks *testSyncHooks |
||||
|
||||
mu sync.Mutex |
||||
when time.Time // when the timer will fire
|
||||
c chan time.Time // closed when the timer fires; mutually exclusive with f
|
||||
f func() // called when the timer fires; mutually exclusive with c
|
||||
} |
||||
|
||||
func (t *fakeTimer) C() <-chan time.Time { return t.c } |
||||
|
||||
func (t *fakeTimer) Stop() bool { |
||||
t.mu.Lock() |
||||
defer t.mu.Unlock() |
||||
stopped := t.when.IsZero() |
||||
t.when = time.Time{} |
||||
return stopped |
||||
} |
||||
|
||||
func (t *fakeTimer) Reset(d time.Duration) bool { |
||||
if t.c != nil || t.f == nil { |
||||
panic("fakeTimer only supports Reset on AfterFunc timers") |
||||
} |
||||
t.mu.Lock() |
||||
defer t.mu.Unlock() |
||||
t.hooks.lock() |
||||
defer t.hooks.unlock() |
||||
active := !t.when.IsZero() |
||||
t.when = t.hooks.now.Add(d) |
||||
if !active { |
||||
t.hooks.timers = append(t.hooks.timers, t) |
||||
} |
||||
return active |
||||
} |
Loading…
Reference in new issue