Skip to content

Commit 617f7b9

Browse files
committed
Improve ImageName/ImageReference parse performance
Update `ImageName` and `ImageReference` to use distinct regex patterns to parse specific parts of the value. Prior to this commit a single regex pattern was used which could hang given certain input strings. Fixes gh-23115
1 parent f55e4c0 commit 617f7b9

File tree

4 files changed

+59
-47
lines changed

4 files changed

+59
-47
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,6 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker.type;
1818

19-
import java.util.regex.Matcher;
20-
import java.util.regex.Pattern;
21-
2219
import org.springframework.util.Assert;
2320

2421
/**
@@ -32,8 +29,6 @@
3229
*/
3330
public class ImageName {
3431

35-
private static final Pattern PATTERN = Regex.IMAGE_NAME.compile();
36-
3732
private static final String DEFAULT_DOMAIN = "docker.io";
3833

3934
private static final String OFFICIAL_REPOSITORY_NAME = "library";
@@ -132,12 +127,22 @@ private String getNameWithDefaultPath(String domain, String name) {
132127
*/
133128
public static ImageName of(String value) {
134129
Assert.hasText(value, "Value must not be empty");
135-
Matcher matcher = PATTERN.matcher(value);
136-
Assert.isTrue(matcher.matches(),
130+
String domain = parseDomain(value);
131+
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
132+
Assert.isTrue(Regex.PATH.matcher(path).matches(),
137133
() -> "Unable to parse name \"" + value + "\". "
138134
+ "Image name must be in the form '[domainHost:port/][path/]name', "
139135
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
140-
return new ImageName(matcher.group("domain"), matcher.group("path"));
136+
return new ImageName(domain, path);
137+
}
138+
139+
static String parseDomain(String value) {
140+
int firstSlash = value.indexOf('/');
141+
String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null;
142+
if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) {
143+
return candidate;
144+
}
145+
return null;
141146
}
142147

143148
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,8 +33,6 @@
3333
*/
3434
public final class ImageReference {
3535

36-
private static final Pattern PATTERN = Regex.IMAGE_REFERENCE.compile();
37-
3836
private static final Pattern JAR_VERSION_PATTERN = Pattern.compile("^(.*)(\\-\\d+)$");
3937

4038
private static final String LATEST = "latest";
@@ -225,13 +223,36 @@ public static ImageReference random(String prefix, int randomLength) {
225223
*/
226224
public static ImageReference of(String value) {
227225
Assert.hasText(value, "Value must not be null");
228-
Matcher matcher = PATTERN.matcher(value);
229-
Assert.isTrue(matcher.matches(),
226+
String domain = ImageName.parseDomain(value);
227+
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
228+
String digest = null;
229+
int digestSplit = path.indexOf("@");
230+
if (digestSplit != -1) {
231+
String remainder = path.substring(digestSplit + 1);
232+
Matcher matcher = Regex.DIGEST.matcher(remainder);
233+
if (matcher.find()) {
234+
digest = remainder.substring(0, matcher.end());
235+
remainder = remainder.substring(matcher.end());
236+
path = path.substring(0, digestSplit) + remainder;
237+
}
238+
}
239+
String tag = null;
240+
int tagSplit = path.lastIndexOf(":");
241+
if (tagSplit != -1) {
242+
String remainder = path.substring(tagSplit + 1);
243+
Matcher matcher = Regex.TAG.matcher(remainder);
244+
if (matcher.find()) {
245+
tag = remainder.substring(0, matcher.end());
246+
remainder = remainder.substring(matcher.end());
247+
path = path.substring(0, tagSplit) + remainder;
248+
}
249+
}
250+
Assert.isTrue(Regex.PATH.matcher(path).matches(),
230251
() -> "Unable to parse image reference \"" + value + "\". "
231252
+ "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', "
232253
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
233-
ImageName name = new ImageName(matcher.group("domain"), matcher.group("path"));
234-
return new ImageReference(name, matcher.group("tag"), matcher.group("digest"));
254+
ImageName name = new ImageName(domain, path);
255+
return new ImageReference(name, tag, digest);
235256
}
236257

237258
/**

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,15 +36,15 @@
3636
*/
3737
final class Regex implements CharSequence {
3838

39-
private static final Regex DOMAIN;
39+
static final Pattern DOMAIN;
4040
static {
4141
Regex component = Regex.oneOf("[a-zA-Z0-9]", "[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]");
4242
Regex dotComponent = Regex.group("[.]", component);
4343
Regex colonPort = Regex.of("[:][0-9]+");
4444
Regex dottedDomain = Regex.group(component, dotComponent.oneOrMoreTimes());
4545
Regex dottedDomainAndPort = Regex.group(component, dotComponent.oneOrMoreTimes(), colonPort);
4646
Regex nameAndPort = Regex.group(component, colonPort);
47-
DOMAIN = Regex.oneOf(dottedDomain, nameAndPort, dottedDomainAndPort, "localhost");
47+
DOMAIN = Regex.oneOf(dottedDomain, nameAndPort, dottedDomainAndPort, "localhost").compile();
4848
}
4949

5050
private static final Regex PATH_COMPONENT;
@@ -55,36 +55,18 @@ final class Regex implements CharSequence {
5555
PATH_COMPONENT = Regex.of(segment, Regex.group(separatedSegment).zeroOrOnce());
5656
}
5757

58-
private static final Regex PATH;
58+
static final Pattern PATH;
5959
static {
6060
Regex component = PATH_COMPONENT;
6161
Regex slashComponent = Regex.group("[/]", component);
6262
Regex slashComponents = Regex.group(slashComponent.oneOrMoreTimes());
63-
PATH = Regex.of(component, slashComponents.zeroOrOnce());
63+
PATH = Regex.of(component, slashComponents.zeroOrOnce()).compile();
6464
}
6565

66-
static final Regex IMAGE_NAME;
67-
static {
68-
Regex domain = DOMAIN.capturedAs("domain");
69-
Regex domainSlash = Regex.group(domain, "[/]");
70-
Regex path = PATH.capturedAs("path");
71-
Regex optionalDomainSlash = domainSlash.zeroOrOnce();
72-
IMAGE_NAME = Regex.of(optionalDomainSlash, path);
73-
}
74-
75-
private static final Regex TAG_REGEX = Regex.of("[\\w][\\w.-]{0,127}");
76-
77-
private static final Regex DIGEST_REGEX = Regex
78-
.of("[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[A-Fa-f0-9]]{32,}");
66+
static final Pattern TAG = Regex.of("^[\\w][\\w.-]{0,127}").compile();
7967

80-
static final Regex IMAGE_REFERENCE;
81-
static {
82-
Regex tag = TAG_REGEX.capturedAs("tag");
83-
Regex digest = DIGEST_REGEX.capturedAs("digest");
84-
Regex atDigest = Regex.group("[@]", digest);
85-
Regex colonTag = Regex.group("[:]", tag);
86-
IMAGE_REFERENCE = Regex.of(IMAGE_NAME, colonTag.zeroOrOnce(), atDigest.zeroOrOnce());
87-
}
68+
static final Pattern DIGEST = Regex.of("^[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[A-Fa-f0-9]]{32,}")
69+
.compile();
8870

8971
private final String value;
9072

@@ -100,10 +82,6 @@ private Regex zeroOrOnce() {
10082
return new Regex(this.value + "?");
10183
}
10284

103-
private Regex capturedAs(String name) {
104-
return new Regex("(?<" + name + ">" + this + ")");
105-
}
106-
10785
Pattern compile() {
10886
return Pattern.compile("^" + this.value + "$");
10987
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -171,6 +171,14 @@ void ofImageNameTagAndDigest() {
171171
"docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
172172
}
173173

174+
@Test
175+
void ofWhenHasIllegalCharacter() {
176+
assertThatIllegalArgumentException()
177+
.isThrownBy(() -> ImageReference
178+
.of("registry.example.com/example/example-app:1.6.0-dev.2.uncommitted+wip.foo.c75795d"))
179+
.withMessageContaining("Unable to parse image reference");
180+
}
181+
174182
@Test
175183
void forJarFile() {
176184
assertForJarFile("spring-boot.2.0.0.BUILD-SNAPSHOT.jar", "library/spring-boot", "2.0.0.BUILD-SNAPSHOT");

0 commit comments

Comments
 (0)