mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-07-15 01:07:30 +08:00
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
This commit is contained in:
parent
f55e4c08f5
commit
617f7b9587
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,9 +16,6 @@
|
|||||||
|
|
||||||
package org.springframework.boot.buildpack.platform.docker.type;
|
package org.springframework.boot.buildpack.platform.docker.type;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,8 +29,6 @@ import org.springframework.util.Assert;
|
|||||||
*/
|
*/
|
||||||
public class ImageName {
|
public class ImageName {
|
||||||
|
|
||||||
private static final Pattern PATTERN = Regex.IMAGE_NAME.compile();
|
|
||||||
|
|
||||||
private static final String DEFAULT_DOMAIN = "docker.io";
|
private static final String DEFAULT_DOMAIN = "docker.io";
|
||||||
|
|
||||||
private static final String OFFICIAL_REPOSITORY_NAME = "library";
|
private static final String OFFICIAL_REPOSITORY_NAME = "library";
|
||||||
@ -132,12 +127,22 @@ public class ImageName {
|
|||||||
*/
|
*/
|
||||||
public static ImageName of(String value) {
|
public static ImageName of(String value) {
|
||||||
Assert.hasText(value, "Value must not be empty");
|
Assert.hasText(value, "Value must not be empty");
|
||||||
Matcher matcher = PATTERN.matcher(value);
|
String domain = parseDomain(value);
|
||||||
Assert.isTrue(matcher.matches(),
|
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
|
||||||
|
Assert.isTrue(Regex.PATH.matcher(path).matches(),
|
||||||
() -> "Unable to parse name \"" + value + "\". "
|
() -> "Unable to parse name \"" + value + "\". "
|
||||||
+ "Image name must be in the form '[domainHost:port/][path/]name', "
|
+ "Image name must be in the form '[domainHost:port/][path/]name', "
|
||||||
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
|
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
|
||||||
return new ImageName(matcher.group("domain"), matcher.group("path"));
|
return new ImageName(domain, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String parseDomain(String value) {
|
||||||
|
int firstSlash = value.indexOf('/');
|
||||||
|
String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null;
|
||||||
|
if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -33,8 +33,6 @@ import org.springframework.util.ObjectUtils;
|
|||||||
*/
|
*/
|
||||||
public final class ImageReference {
|
public final class ImageReference {
|
||||||
|
|
||||||
private static final Pattern PATTERN = Regex.IMAGE_REFERENCE.compile();
|
|
||||||
|
|
||||||
private static final Pattern JAR_VERSION_PATTERN = Pattern.compile("^(.*)(\\-\\d+)$");
|
private static final Pattern JAR_VERSION_PATTERN = Pattern.compile("^(.*)(\\-\\d+)$");
|
||||||
|
|
||||||
private static final String LATEST = "latest";
|
private static final String LATEST = "latest";
|
||||||
@ -225,13 +223,36 @@ public final class ImageReference {
|
|||||||
*/
|
*/
|
||||||
public static ImageReference of(String value) {
|
public static ImageReference of(String value) {
|
||||||
Assert.hasText(value, "Value must not be null");
|
Assert.hasText(value, "Value must not be null");
|
||||||
Matcher matcher = PATTERN.matcher(value);
|
String domain = ImageName.parseDomain(value);
|
||||||
Assert.isTrue(matcher.matches(),
|
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
|
||||||
|
String digest = null;
|
||||||
|
int digestSplit = path.indexOf("@");
|
||||||
|
if (digestSplit != -1) {
|
||||||
|
String remainder = path.substring(digestSplit + 1);
|
||||||
|
Matcher matcher = Regex.DIGEST.matcher(remainder);
|
||||||
|
if (matcher.find()) {
|
||||||
|
digest = remainder.substring(0, matcher.end());
|
||||||
|
remainder = remainder.substring(matcher.end());
|
||||||
|
path = path.substring(0, digestSplit) + remainder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String tag = null;
|
||||||
|
int tagSplit = path.lastIndexOf(":");
|
||||||
|
if (tagSplit != -1) {
|
||||||
|
String remainder = path.substring(tagSplit + 1);
|
||||||
|
Matcher matcher = Regex.TAG.matcher(remainder);
|
||||||
|
if (matcher.find()) {
|
||||||
|
tag = remainder.substring(0, matcher.end());
|
||||||
|
remainder = remainder.substring(matcher.end());
|
||||||
|
path = path.substring(0, tagSplit) + remainder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.isTrue(Regex.PATH.matcher(path).matches(),
|
||||||
() -> "Unable to parse image reference \"" + value + "\". "
|
() -> "Unable to parse image reference \"" + value + "\". "
|
||||||
+ "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', "
|
+ "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', "
|
||||||
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
|
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
|
||||||
ImageName name = new ImageName(matcher.group("domain"), matcher.group("path"));
|
ImageName name = new ImageName(domain, path);
|
||||||
return new ImageReference(name, matcher.group("tag"), matcher.group("digest"));
|
return new ImageReference(name, tag, digest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -36,7 +36,7 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
final class Regex implements CharSequence {
|
final class Regex implements CharSequence {
|
||||||
|
|
||||||
private static final Regex DOMAIN;
|
static final Pattern DOMAIN;
|
||||||
static {
|
static {
|
||||||
Regex component = Regex.oneOf("[a-zA-Z0-9]", "[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]");
|
Regex component = Regex.oneOf("[a-zA-Z0-9]", "[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]");
|
||||||
Regex dotComponent = Regex.group("[.]", component);
|
Regex dotComponent = Regex.group("[.]", component);
|
||||||
@ -44,7 +44,7 @@ final class Regex implements CharSequence {
|
|||||||
Regex dottedDomain = Regex.group(component, dotComponent.oneOrMoreTimes());
|
Regex dottedDomain = Regex.group(component, dotComponent.oneOrMoreTimes());
|
||||||
Regex dottedDomainAndPort = Regex.group(component, dotComponent.oneOrMoreTimes(), colonPort);
|
Regex dottedDomainAndPort = Regex.group(component, dotComponent.oneOrMoreTimes(), colonPort);
|
||||||
Regex nameAndPort = Regex.group(component, colonPort);
|
Regex nameAndPort = Regex.group(component, colonPort);
|
||||||
DOMAIN = Regex.oneOf(dottedDomain, nameAndPort, dottedDomainAndPort, "localhost");
|
DOMAIN = Regex.oneOf(dottedDomain, nameAndPort, dottedDomainAndPort, "localhost").compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Regex PATH_COMPONENT;
|
private static final Regex PATH_COMPONENT;
|
||||||
@ -55,36 +55,18 @@ final class Regex implements CharSequence {
|
|||||||
PATH_COMPONENT = Regex.of(segment, Regex.group(separatedSegment).zeroOrOnce());
|
PATH_COMPONENT = Regex.of(segment, Regex.group(separatedSegment).zeroOrOnce());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Regex PATH;
|
static final Pattern PATH;
|
||||||
static {
|
static {
|
||||||
Regex component = PATH_COMPONENT;
|
Regex component = PATH_COMPONENT;
|
||||||
Regex slashComponent = Regex.group("[/]", component);
|
Regex slashComponent = Regex.group("[/]", component);
|
||||||
Regex slashComponents = Regex.group(slashComponent.oneOrMoreTimes());
|
Regex slashComponents = Regex.group(slashComponent.oneOrMoreTimes());
|
||||||
PATH = Regex.of(component, slashComponents.zeroOrOnce());
|
PATH = Regex.of(component, slashComponents.zeroOrOnce()).compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Regex IMAGE_NAME;
|
static final Pattern TAG = Regex.of("^[\\w][\\w.-]{0,127}").compile();
|
||||||
static {
|
|
||||||
Regex domain = DOMAIN.capturedAs("domain");
|
|
||||||
Regex domainSlash = Regex.group(domain, "[/]");
|
|
||||||
Regex path = PATH.capturedAs("path");
|
|
||||||
Regex optionalDomainSlash = domainSlash.zeroOrOnce();
|
|
||||||
IMAGE_NAME = Regex.of(optionalDomainSlash, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Regex TAG_REGEX = Regex.of("[\\w][\\w.-]{0,127}");
|
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,}")
|
||||||
|
.compile();
|
||||||
private static final Regex DIGEST_REGEX = Regex
|
|
||||||
.of("[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[A-Fa-f0-9]]{32,}");
|
|
||||||
|
|
||||||
static final Regex IMAGE_REFERENCE;
|
|
||||||
static {
|
|
||||||
Regex tag = TAG_REGEX.capturedAs("tag");
|
|
||||||
Regex digest = DIGEST_REGEX.capturedAs("digest");
|
|
||||||
Regex atDigest = Regex.group("[@]", digest);
|
|
||||||
Regex colonTag = Regex.group("[:]", tag);
|
|
||||||
IMAGE_REFERENCE = Regex.of(IMAGE_NAME, colonTag.zeroOrOnce(), atDigest.zeroOrOnce());
|
|
||||||
}
|
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
@ -100,10 +82,6 @@ final class Regex implements CharSequence {
|
|||||||
return new Regex(this.value + "?");
|
return new Regex(this.value + "?");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Regex capturedAs(String name) {
|
|
||||||
return new Regex("(?<" + name + ">" + this + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern compile() {
|
Pattern compile() {
|
||||||
return Pattern.compile("^" + this.value + "$");
|
return Pattern.compile("^" + this.value + "$");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -171,6 +171,14 @@ class ImageReferenceTests {
|
|||||||
"docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
|
"docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofWhenHasIllegalCharacter() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ImageReference
|
||||||
|
.of("registry.example.com/example/example-app:1.6.0-dev.2.uncommitted+wip.foo.c75795d"))
|
||||||
|
.withMessageContaining("Unable to parse image reference");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void forJarFile() {
|
void forJarFile() {
|
||||||
assertForJarFile("spring-boot.2.0.0.BUILD-SNAPSHOT.jar", "library/spring-boot", "2.0.0.BUILD-SNAPSHOT");
|
assertForJarFile("spring-boot.2.0.0.BUILD-SNAPSHOT.jar", "library/spring-boot", "2.0.0.BUILD-SNAPSHOT");
|
||||||
|
Loading…
Reference in New Issue
Block a user