Skip to content

Commit 4ca1403

Browse files
committed
use sync.OnceValue for various regular expressions, require go1.21
Using regex.MustCompile consumes a significant amount of memory when importing the package, even if those regular expressions are not used. This changes compiling the regular expressions to use a sync.OnceValue so that they're only compiled the first time they're used. There are various regular expressions remaining that are still compiled on import, but these are exported, so changing them to a sync.OnceValue would be a breaking change; we can still decide to do so, but leaving that for a follow-up. It's worth noting that sync.OnceValue requires go1.21 or up, so raising the minimum version accordingly. Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent 343e590 commit 4ca1403

File tree

5 files changed

+25
-16
lines changed

5 files changed

+25
-16
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module github.com/distribution/reference
22

3-
go 1.20
3+
go 1.21
44

55
require github.com/opencontainers/go-digest v1.0.0

normalize.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type normalizedNamed interface {
5454
// qualified reference. If the value may be an identifier
5555
// use ParseAnyReference.
5656
func ParseNormalizedNamed(s string) (Named, error) {
57-
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
57+
if ok := anchoredIdentifierRegexp().MatchString(s); ok {
5858
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
5959
}
6060
domain, remainder := splitDockerDomain(s)
@@ -244,7 +244,7 @@ func TagNameOnly(ref Named) Named {
244244
// ParseAnyReference parses a reference string as a possible identifier,
245245
// full digest, or familiar name.
246246
func ParseAnyReference(ref string) (Reference, error) {
247-
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
247+
if ok := anchoredIdentifierRegexp().MatchString(ref); ok {
248248
return digestReference("sha256:" + ref), nil
249249
}
250250
if dgst, err := digest.Parse(ref); err == nil {

reference.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func Path(named Named) (name string) {
174174
// If no valid hostname is found, the hostname is empty and the full value
175175
// is returned as name
176176
func splitDomain(name string) (string, string) {
177-
match := anchoredNameRegexp.FindStringSubmatch(name)
177+
match := anchoredNameRegexp().FindStringSubmatch(name)
178178
if len(match) != 3 {
179179
return "", name
180180
}
@@ -197,7 +197,7 @@ func Parse(s string) (Reference, error) {
197197

198198
var repo repository
199199

200-
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
200+
nameMatch := anchoredNameRegexp().FindStringSubmatch(matches[1])
201201
if len(nameMatch) == 3 {
202202
repo.domain = nameMatch[1]
203203
repo.path = nameMatch[2]
@@ -248,7 +248,7 @@ func ParseNamed(s string) (Named, error) {
248248
// WithName returns a named object representing the given string. If the input
249249
// is invalid ErrReferenceInvalidFormat will be returned.
250250
func WithName(name string) (Named, error) {
251-
match := anchoredNameRegexp.FindStringSubmatch(name)
251+
match := anchoredNameRegexp().FindStringSubmatch(name)
252252
if match == nil || len(match) != 3 {
253253
return nil, ErrReferenceInvalidFormat
254254
}
@@ -266,7 +266,7 @@ func WithName(name string) (Named, error) {
266266
// WithTag combines the name from "name" and the tag from "tag" to form a
267267
// reference incorporating both the name and the tag.
268268
func WithTag(name Named, tag string) (NamedTagged, error) {
269-
if !anchoredTagRegexp.MatchString(tag) {
269+
if !anchoredTagRegexp().MatchString(tag) {
270270
return nil, ErrTagInvalidFormat
271271
}
272272
var repo repository
@@ -292,7 +292,7 @@ func WithTag(name Named, tag string) (NamedTagged, error) {
292292
// WithDigest combines the name from "name" and the digest from "digest" to form
293293
// a reference incorporating both the name and the digest.
294294
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
295-
if !anchoredDigestRegexp.MatchString(digest.String()) {
295+
if !anchoredDigestRegexp().MatchString(digest.String()) {
296296
return nil, ErrDigestInvalidFormat
297297
}
298298
var repo repository

regexp.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package reference
33
import (
44
"regexp"
55
"strings"
6+
"sync"
67
)
78

89
// DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:<encoded>").
@@ -111,11 +112,15 @@ var (
111112

112113
// anchoredTagRegexp matches valid tag names, anchored at the start and
113114
// end of the matched string.
114-
anchoredTagRegexp = regexp.MustCompile(anchored(tag))
115+
anchoredTagRegexp = sync.OnceValue(func() *regexp.Regexp {
116+
return regexp.MustCompile(anchored(tag))
117+
})
115118

116119
// anchoredDigestRegexp matches valid digests, anchored at the start and
117120
// end of the matched string.
118-
anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat))
121+
anchoredDigestRegexp = sync.OnceValue(func() *regexp.Regexp {
122+
return regexp.MustCompile(anchored(digestPat))
123+
})
119124

120125
// pathComponent restricts path-components to start with an alphanumeric
121126
// character, with following parts able to be separated by a separator
@@ -131,13 +136,17 @@ var (
131136

132137
// anchoredNameRegexp is used to parse a name value, capturing the
133138
// domain and trailing components.
134-
anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName)))
139+
anchoredNameRegexp = sync.OnceValue(func() *regexp.Regexp {
140+
return regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName)))
141+
})
135142

136143
referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat)))
137144

138145
// anchoredIdentifierRegexp is used to check or match an
139146
// identifier value, anchored at start and end of string.
140-
anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier))
147+
anchoredIdentifierRegexp = sync.OnceValue(func() *regexp.Regexp {
148+
return regexp.MustCompile(anchored(identifier))
149+
})
141150
)
142151

143152
// optional wraps the expression in a non-capturing group and makes the

regexp_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,9 @@ func TestDomainRegexp(t *testing.T) {
176176

177177
func TestFullNameRegexp(t *testing.T) {
178178
t.Parallel()
179-
if anchoredNameRegexp.NumSubexp() != 2 {
179+
if anchoredNameRegexp().NumSubexp() != 2 {
180180
t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2",
181-
anchoredNameRegexp, anchoredNameRegexp.NumSubexp())
181+
anchoredNameRegexp(), anchoredNameRegexp().NumSubexp())
182182
}
183183

184184
tests := []regexpMatch{
@@ -469,7 +469,7 @@ func TestFullNameRegexp(t *testing.T) {
469469
tc := tc
470470
t.Run(tc.input, func(t *testing.T) {
471471
t.Parallel()
472-
checkRegexp(t, anchoredNameRegexp, tc)
472+
checkRegexp(t, anchoredNameRegexp(), tc)
473473
})
474474
}
475475
}
@@ -580,7 +580,7 @@ func TestIdentifierRegexp(t *testing.T) {
580580
tc := tc
581581
t.Run(tc.input, func(t *testing.T) {
582582
t.Parallel()
583-
match := anchoredIdentifierRegexp.MatchString(tc.input)
583+
match := anchoredIdentifierRegexp().MatchString(tc.input)
584584
if match != tc.match {
585585
t.Errorf("Expected match=%t, got %t", tc.match, match)
586586
}

0 commit comments

Comments
 (0)