diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 76bf31e..318d617 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 - diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 9b4419d..919944b 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run FOSSA scan and upload build data uses: fossa-contrib/fossa-action@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1382d34..3d345e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,15 +17,15 @@ jobs: build: strategy: matrix: - go-version: [1.20.x, 1.21.x] + go-version: [1.21.x, 1.22.x] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} @@ -34,9 +34,9 @@ jobs: make build - name: lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.53 + version: v1.59 args: --print-resources-usage --timeout=10m --verbose - name: Test @@ -44,6 +44,6 @@ jobs: make coverage - name: Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: directory: ./ diff --git a/fuzz_test.go b/fuzz_test.go index c64c54e..1aaebba 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -8,7 +8,7 @@ import ( // that targets ParseNormalizedNamed // nolint:deadcode func FuzzParseNormalizedNamed(f *testing.F) { - f.Fuzz(func(t *testing.T, data string) { + f.Fuzz(func(_ *testing.T, data string) { _, _ = ParseNormalizedNamed(data) }) } diff --git a/go.mod b/go.mod index 25cf64a..e82b093 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/distribution/reference -go 1.20 +go 1.21 require github.com/opencontainers/go-digest v1.0.0 diff --git a/normalize.go b/normalize.go index f412831..9e081d7 100644 --- a/normalize.go +++ b/normalize.go @@ -54,7 +54,7 @@ type normalizedNamed interface { // qualified reference. If the value may be an identifier // use ParseAnyReference. func ParseNormalizedNamed(s string) (Named, error) { - if ok := anchoredIdentifierRegexp.MatchString(s); ok { + 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) @@ -244,7 +244,7 @@ func TagNameOnly(ref Named) Named { // 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 { + if ok := anchoredIdentifierRegexp().MatchString(ref); ok { return digestReference("sha256:" + ref), nil } if dgst, err := digest.Parse(ref); err == nil { diff --git a/reference.go b/reference.go index 900398b..ec3a22a 100644 --- a/reference.go +++ b/reference.go @@ -174,7 +174,7 @@ func Path(named Named) (name string) { // If no valid hostname is found, the hostname is empty and the full value // is returned as name func splitDomain(name string) (string, string) { - match := anchoredNameRegexp.FindStringSubmatch(name) + match := anchoredNameRegexp().FindStringSubmatch(name) if len(match) != 3 { return "", name } @@ -197,7 +197,7 @@ func Parse(s string) (Reference, error) { var repo repository - nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1]) + nameMatch := anchoredNameRegexp().FindStringSubmatch(matches[1]) if len(nameMatch) == 3 { repo.domain = nameMatch[1] repo.path = nameMatch[2] @@ -248,7 +248,7 @@ func ParseNamed(s string) (Named, error) { // WithName returns a named object representing the given string. If the input // is invalid ErrReferenceInvalidFormat will be returned. func WithName(name string) (Named, error) { - match := anchoredNameRegexp.FindStringSubmatch(name) + match := anchoredNameRegexp().FindStringSubmatch(name) if match == nil || len(match) != 3 { return nil, ErrReferenceInvalidFormat } @@ -266,7 +266,7 @@ func WithName(name string) (Named, error) { // 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) { + if !anchoredTagRegexp().MatchString(tag) { return nil, ErrTagInvalidFormat } var repo repository @@ -292,7 +292,7 @@ func WithTag(name Named, tag string) (NamedTagged, error) { // 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()) { + if !anchoredDigestRegexp().MatchString(digest.String()) { return nil, ErrDigestInvalidFormat } var repo repository diff --git a/regexp.go b/regexp.go index 65bc49d..3d65c39 100644 --- a/regexp.go +++ b/regexp.go @@ -3,6 +3,7 @@ package reference import ( "regexp" "strings" + "sync" ) // DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:"). @@ -111,11 +112,15 @@ var ( // anchoredTagRegexp matches valid tag names, anchored at the start and // end of the matched string. - anchoredTagRegexp = regexp.MustCompile(anchored(tag)) + anchoredTagRegexp = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(anchored(tag)) + }) // anchoredDigestRegexp matches valid digests, anchored at the start and // end of the matched string. - anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat)) + anchoredDigestRegexp = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(anchored(digestPat)) + }) // pathComponent restricts path-components to start with an alphanumeric // character, with following parts able to be separated by a separator @@ -131,13 +136,17 @@ var ( // anchoredNameRegexp is used to parse a name value, capturing the // domain and trailing components. - anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName))) + anchoredNameRegexp = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName))) + }) referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat))) // anchoredIdentifierRegexp is used to check or match an // identifier value, anchored at start and end of string. - anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier)) + anchoredIdentifierRegexp = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(anchored(identifier)) + }) ) // optional wraps the expression in a non-capturing group and makes the diff --git a/regexp_test.go b/regexp_test.go index ca4680d..b92ed6a 100644 --- a/regexp_test.go +++ b/regexp_test.go @@ -176,9 +176,9 @@ func TestDomainRegexp(t *testing.T) { func TestFullNameRegexp(t *testing.T) { t.Parallel() - if anchoredNameRegexp.NumSubexp() != 2 { + if anchoredNameRegexp().NumSubexp() != 2 { t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2", - anchoredNameRegexp, anchoredNameRegexp.NumSubexp()) + anchoredNameRegexp(), anchoredNameRegexp().NumSubexp()) } tests := []regexpMatch{ @@ -469,7 +469,7 @@ func TestFullNameRegexp(t *testing.T) { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() - checkRegexp(t, anchoredNameRegexp, tc) + checkRegexp(t, anchoredNameRegexp(), tc) }) } } @@ -580,7 +580,7 @@ func TestIdentifierRegexp(t *testing.T) { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() - match := anchoredIdentifierRegexp.MatchString(tc.input) + match := anchoredIdentifierRegexp().MatchString(tc.input) if match != tc.match { t.Errorf("Expected match=%t, got %t", tc.match, match) }