Skip to content

Commit aeed5e6

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 e3db434 commit aeed5e6

File tree

4 files changed

+24
-15
lines changed

4 files changed

+24
-15
lines changed

normalize.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type normalizedNamed interface {
6464
// _ "crypto/sha256"
6565
// )
6666
func ParseNormalizedNamed(s string) (Named, error) {
67-
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
67+
if ok := anchoredIdentifierRegexp().MatchString(s); ok {
6868
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
6969
}
7070
domain, remainder := splitDockerDomain(s)
@@ -274,7 +274,7 @@ func TagNameOnly(ref Named) Named {
274274
// _ "crypto/sha256"
275275
// )
276276
func ParseAnyReference(ref string) (Reference, error) {
277-
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
277+
if ok := anchoredIdentifierRegexp().MatchString(ref); ok {
278278
return digestReference("sha256:" + ref), nil
279279
}
280280
if dgst, err := digest.Parse(ref); err == nil {

reference.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func Path(named Named) (name string) {
207207
// If no valid hostname is found, the hostname is empty and the full value
208208
// is returned as name
209209
func splitDomain(name string) (string, string) {
210-
match := anchoredNameRegexp.FindStringSubmatch(name)
210+
match := anchoredNameRegexp().FindStringSubmatch(name)
211211
if len(match) != 3 {
212212
return "", name
213213
}
@@ -241,7 +241,7 @@ func Parse(s string) (Reference, error) {
241241

242242
var repo repository
243243

244-
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
244+
nameMatch := anchoredNameRegexp().FindStringSubmatch(matches[1])
245245
if len(nameMatch) == 3 {
246246
repo.domain = nameMatch[1]
247247
repo.path = nameMatch[2]
@@ -292,7 +292,7 @@ func ParseNamed(s string) (Named, error) {
292292
// WithName returns a named object representing the given string. If the input
293293
// is invalid ErrReferenceInvalidFormat will be returned.
294294
func WithName(name string) (Named, error) {
295-
match := anchoredNameRegexp.FindStringSubmatch(name)
295+
match := anchoredNameRegexp().FindStringSubmatch(name)
296296
if match == nil || len(match) != 3 {
297297
return nil, ErrReferenceInvalidFormat
298298
}
@@ -310,7 +310,7 @@ func WithName(name string) (Named, error) {
310310
// WithTag combines the name from "name" and the tag from "tag" to form a
311311
// reference incorporating both the name and the tag.
312312
func WithTag(name Named, tag string) (NamedTagged, error) {
313-
if !anchoredTagRegexp.MatchString(tag) {
313+
if !anchoredTagRegexp().MatchString(tag) {
314314
return nil, ErrTagInvalidFormat
315315
}
316316
var repo repository
@@ -336,7 +336,7 @@ func WithTag(name Named, tag string) (NamedTagged, error) {
336336
// WithDigest combines the name from "name" and the digest from "digest" to form
337337
// a reference incorporating both the name and the digest.
338338
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
339-
if !anchoredDigestRegexp.MatchString(digest.String()) {
339+
if !anchoredDigestRegexp().MatchString(digest.String()) {
340340
return nil, ErrDigestInvalidFormat
341341
}
342342
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)