diff --git a/src/main/java/com/github/dockerjava/core/NameParser.java b/src/main/java/com/github/dockerjava/core/NameParser.java index 2bd99bf31..49a550b1d 100644 --- a/src/main/java/com/github/dockerjava/core/NameParser.java +++ b/src/main/java/com/github/dockerjava/core/NameParser.java @@ -5,15 +5,24 @@ import java.util.regex.Pattern; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; + import com.github.dockerjava.api.model.AuthConfig; public class NameParser { - private static final Pattern VALID_HEX_PATTERN = Pattern.compile("^([a-f0-9]{64})$"); + private static final int RepositoryNameTotalLengthMax = 255; + + private static final Pattern RepositoryNameComponentRegexp = Pattern.compile("[a-z0-9]+(?:[._-][a-z0-9]+)*"); - private static final Pattern VALID_NAMESPACE_PATTERN = Pattern.compile("^([a-z0-9_]{4,30})$"); + private static final Pattern RepositoryNameComponentAnchoredRegexp = Pattern.compile("^" + + RepositoryNameComponentRegexp.pattern() + "$"); - private static final Pattern VALID_REPO_PATTERN = Pattern.compile("^([a-z0-9-_.]+)$"); + // private static final Pattern RepositoryNameRegexp = Pattern.compile("(?:" + + // RepositoryNameComponentRegexp.pattern() + // + "/)*" + RepositoryNameComponentRegexp.pattern()); public static ReposTag parseRepositoryTag(String name) { int n = name.lastIndexOf(':'); @@ -36,30 +45,43 @@ public ReposTag(String repos, String tag) { this.repos = repos; this.tag = tag; } - } - public static void validateRepositoryName(String repositoryName) { - String name; - String namespace; - String[] nameParts = repositoryName.split("/", 2); - if (nameParts.length < 2) { - namespace = "library"; - name = nameParts[0]; - if (VALID_HEX_PATTERN.matcher(name).matches()) { - throw new InvalidRepositoryNameException(String.format( - "Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)); + @Override + public boolean equals(Object obj) { + if (obj instanceof ReposTag) { + ReposTag other = (ReposTag) obj; + return new EqualsBuilder().append(repos, other.repos).append(tag, other.tag).isEquals(); + } else { + return false; } - } else { - namespace = nameParts[0]; - name = nameParts[1]; } - if (!VALID_NAMESPACE_PATTERN.matcher(namespace).matches()) { - throw new InvalidRepositoryNameException(String.format( - "Invalid namespace name (%s), only [a-z0-9_] are allowed, size between 4 and 30", namespace)); + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE); + } + } + + /* + * see https://github.com/docker/distribution/blob/master/registry/api/v2/names.go + */ + public static void validateRepoName(String name) { + if (name.isEmpty()) { + throw new InvalidRepositoryNameException(String.format("Invalid empty repository name \"%s\"", name)); } - if (!VALID_REPO_PATTERN.matcher(name).matches()) { - throw new InvalidRepositoryNameException(String.format( - "Invalid repository name (%s), only [a-z0-9-_.] are allowed", name)); + + if (name.length() > RepositoryNameTotalLengthMax) { + throw new InvalidRepositoryNameException(String.format("Repository name \"%s\" is longer than " + + RepositoryNameTotalLengthMax, name)); + } + + String[] components = name.split("/"); + + for (String component : components) { + if (!RepositoryNameComponentAnchoredRegexp.matcher(component).matches()) { + throw new InvalidRepositoryNameException(String.format( + "Repository name \"%s\" is invalid. Component: %s", name, component)); + } } } @@ -82,7 +104,7 @@ public static HostnameReposName resolveRepositoryName(String reposName) { reposName)); } - validateRepositoryName(reposName); + validateRepoName(reposName); return new HostnameReposName(hostname, reposName); } @@ -96,5 +118,21 @@ public HostnameReposName(String hostname, String reposName) { this.reposName = reposName; } + @Override + public boolean equals(Object obj) { + if (obj instanceof HostnameReposName) { + HostnameReposName other = (HostnameReposName) obj; + return new EqualsBuilder().append(hostname, other.hostname).append(reposName, other.reposName) + .isEquals(); + } else { + return false; + } + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SIMPLE_STYLE); + } + } } diff --git a/src/test/java/com/github/dockerjava/core/NameParserTest.java b/src/test/java/com/github/dockerjava/core/NameParserTest.java new file mode 100644 index 000000000..6b694753a --- /dev/null +++ b/src/test/java/com/github/dockerjava/core/NameParserTest.java @@ -0,0 +1,126 @@ +/* + * Created on 17.08.2015 + */ +package com.github.dockerjava.core; + +import org.apache.commons.lang.StringUtils; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +import com.github.dockerjava.api.model.AuthConfig; +import com.github.dockerjava.core.NameParser.HostnameReposName; +import com.github.dockerjava.core.NameParser.ReposTag; + +/** + * + * + * @author marcus + * + */ +public class NameParserTest { + + @Test + public void testValidateRepoName() throws Exception { + NameParser.validateRepoName("repository"); + NameParser.validateRepoName("namespace/repository"); + NameParser.validateRepoName("namespace-with-dashes/repository"); + NameParser.validateRepoName("namespace/repository-with-dashes"); + NameParser.validateRepoName("namespace.with.dots/repository"); + NameParser.validateRepoName("namespace/repository.with.dots"); + NameParser.validateRepoName("namespace_with_underscores/repository"); + NameParser.validateRepoName("namespace/repository_with_underscores"); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameEmpty() throws Exception { + NameParser.validateRepoName(""); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameExceedsMaxLength() throws Exception { + NameParser.validateRepoName(StringUtils.repeat("repository", 255)); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameEndWithDash() throws Exception { + NameParser.validateRepoName("repository-"); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameStartWithDash() throws Exception { + NameParser.validateRepoName("-repository"); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameEndWithDot() throws Exception { + NameParser.validateRepoName("repository."); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameStartWithDot() throws Exception { + NameParser.validateRepoName(".repository"); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameEndWithUnderscore() throws Exception { + NameParser.validateRepoName("repository_"); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameStartWithUnderscore() throws Exception { + NameParser.validateRepoName("_repository"); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testValidateRepoNameWithColon() throws Exception { + NameParser.validateRepoName("repository:with:colon"); + } + + @Test + public void testResolveSimpleRepositoryName() throws Exception { + HostnameReposName resolved = NameParser.resolveRepositoryName("repository"); + assertEquals(resolved, new HostnameReposName(AuthConfig.DEFAULT_SERVER_ADDRESS, "repository")); + } + + @Test + public void testResolveRepositoryNameWithNamespace() throws Exception { + HostnameReposName resolved = NameParser.resolveRepositoryName("namespace/repository"); + assertEquals(resolved, new HostnameReposName(AuthConfig.DEFAULT_SERVER_ADDRESS, "namespace/repository")); + } + + @Test + public void testResolveRepositoryNameWithNamespaceAndHostname() throws Exception { + HostnameReposName resolved = NameParser.resolveRepositoryName("localhost:5000/namespace/repository"); + assertEquals(resolved, new HostnameReposName("localhost:5000", "namespace/repository")); + } + + @Test(expectedExceptions = InvalidRepositoryNameException.class) + public void testResolveRepositoryNameWithIndex() throws Exception { + NameParser.resolveRepositoryName("index.docker.io/repository"); + } + + @Test + public void testResolveReposTagWithoutTagSimple() throws Exception { + ReposTag resolved = NameParser.parseRepositoryTag("repository"); + assertEquals(resolved, new ReposTag("repository", "")); + + resolved = NameParser.parseRepositoryTag("namespace/repository"); + assertEquals(resolved, new ReposTag("namespace/repository", "")); + + resolved = NameParser.parseRepositoryTag("localhost:5000/namespace/repository"); + assertEquals(resolved, new ReposTag("localhost:5000/namespace/repository", "")); + } + + @Test + public void testResolveReposTagWithTag() throws Exception { + ReposTag resolved = NameParser.parseRepositoryTag("repository:tag"); + assertEquals(resolved, new ReposTag("repository", "tag")); + + resolved = NameParser.parseRepositoryTag("namespace/repository:tag"); + assertEquals(resolved, new ReposTag("namespace/repository", "tag")); + + resolved = NameParser.parseRepositoryTag("localhost:5000/namespace/repository:tag"); + assertEquals(resolved, new ReposTag("localhost:5000/namespace/repository", "tag")); + } +}