From 617f7b9587436f81fc1e82c01566154010d2ae5c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 28 May 2021 12:38:41 -0700 Subject: [PATCH] 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 --- .../platform/docker/type/ImageName.java | 23 ++++++----- .../platform/docker/type/ImageReference.java | 35 +++++++++++++---- .../buildpack/platform/docker/type/Regex.java | 38 ++++--------------- .../docker/type/ImageReferenceTests.java | 10 ++++- 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java index 889fee126318..073871c39692 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java @@ -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"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.boot.buildpack.platform.docker.type; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.springframework.util.Assert; /** @@ -32,8 +29,6 @@ */ public class ImageName { - private static final Pattern PATTERN = Regex.IMAGE_NAME.compile(); - private static final String DEFAULT_DOMAIN = "docker.io"; private static final String OFFICIAL_REPOSITORY_NAME = "library"; @@ -132,12 +127,22 @@ private String getNameWithDefaultPath(String domain, String name) { */ public static ImageName of(String value) { Assert.hasText(value, "Value must not be empty"); - Matcher matcher = PATTERN.matcher(value); - Assert.isTrue(matcher.matches(), + String domain = parseDomain(value); + String path = (domain != null) ? value.substring(domain.length() + 1) : value; + Assert.isTrue(Regex.PATH.matcher(path).matches(), () -> "Unable to parse name \"" + value + "\". " + "Image name must be in the form '[domainHost:port/][path/]name', " + "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; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java index 4393cc6fe528..f3b29ab19e00 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java @@ -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"); * you may not use this file except in compliance with the License. @@ -33,8 +33,6 @@ */ 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 String LATEST = "latest"; @@ -225,13 +223,36 @@ public static ImageReference random(String prefix, int randomLength) { */ public static ImageReference of(String value) { Assert.hasText(value, "Value must not be null"); - Matcher matcher = PATTERN.matcher(value); - Assert.isTrue(matcher.matches(), + String domain = ImageName.parseDomain(value); + 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 + "\". " + "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', " + "with 'path' and 'name' containing only [a-z0-9][.][_][-]"); - ImageName name = new ImageName(matcher.group("domain"), matcher.group("path")); - return new ImageReference(name, matcher.group("tag"), matcher.group("digest")); + ImageName name = new ImageName(domain, path); + return new ImageReference(name, tag, digest); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java index 2307ccc4e5e8..c4ebb66520a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java @@ -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"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ */ final class Regex implements CharSequence { - private static final Regex DOMAIN; + static final Pattern DOMAIN; static { 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); @@ -44,7 +44,7 @@ final class Regex implements CharSequence { Regex dottedDomain = Regex.group(component, dotComponent.oneOrMoreTimes()); Regex dottedDomainAndPort = Regex.group(component, dotComponent.oneOrMoreTimes(), 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; @@ -55,36 +55,18 @@ final class Regex implements CharSequence { PATH_COMPONENT = Regex.of(segment, Regex.group(separatedSegment).zeroOrOnce()); } - private static final Regex PATH; + static final Pattern PATH; static { Regex component = PATH_COMPONENT; Regex slashComponent = Regex.group("[/]", component); 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 { - 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}"); - - 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 Pattern TAG = Regex.of("^[\\w][\\w.-]{0,127}").compile(); - 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()); - } + 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 final String value; @@ -100,10 +82,6 @@ private Regex zeroOrOnce() { return new Regex(this.value + "?"); } - private Regex capturedAs(String name) { - return new Regex("(?<" + name + ">" + this + ")"); - } - Pattern compile() { return Pattern.compile("^" + this.value + "$"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java index 134db76f11e0..e19c0e14fb31 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -171,6 +171,14 @@ void ofImageNameTagAndDigest() { "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 void forJarFile() { assertForJarFile("spring-boot.2.0.0.BUILD-SNAPSHOT.jar", "library/spring-boot", "2.0.0.BUILD-SNAPSHOT");