From 3ec5a6849a6cad4c5f5f3bafb5f74b5827fec14c Mon Sep 17 00:00:00 2001 From: Sven Date: Wed, 3 Jan 2024 16:48:45 +0100 Subject: [PATCH] fix(build): tag regex should allow ports (#3196) Update the regex and add test cases. (There are some xfails here for cases that the regex is not currently handling. It's too strict for IPv6 domains at the moment.) Closes: https://github.com/docker/docker-py/issues/3195 Related: https://github.com/opencontainers/distribution-spec/pull/498 Signed-off-by: Sven Kieske Signed-off-by: Milas Bowman Co-authored-by: Milas Bowman --- docker/utils/build.py | 5 ++-- tests/unit/utils_build_test.py | 53 ++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docker/utils/build.py b/docker/utils/build.py index a5c4b0c2..86a4423f 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -10,8 +10,9 @@ from ..constants import IS_WINDOWS_PLATFORM _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/') _TAG = re.compile( - r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" \ - + "(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$" + r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*" + r"(?::[0-9]+)?(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" + r"(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$" ) diff --git a/tests/unit/utils_build_test.py b/tests/unit/utils_build_test.py index fa7d833d..5f1bb1ec 100644 --- a/tests/unit/utils_build_test.py +++ b/tests/unit/utils_build_test.py @@ -6,11 +6,10 @@ import tarfile import tempfile import unittest +import pytest from docker.constants import IS_WINDOWS_PLATFORM -from docker.utils import exclude_paths, tar - -import pytest +from docker.utils import exclude_paths, tar, match_tag from ..helpers import make_tree @@ -489,3 +488,51 @@ class TarTest(unittest.TestCase): assert member in names assert 'a/c/b' in names assert 'a/c/b/utils.py' not in names + + +# selected test cases from https://github.com/distribution/reference/blob/8507c7fcf0da9f570540c958ea7b972c30eeaeca/reference_test.go#L13-L328 +@pytest.mark.parametrize("tag,expected", [ + ("test_com", True), + ("test.com:tag", True), + # N.B. this implicitly means "docker.io/library/test.com:5000" + # i.e. the `5000` is a tag, not a port here! + ("test.com:5000", True), + ("test.com/repo:tag", True), + ("test:5000/repo", True), + ("test:5000/repo:tag", True), + ("test:5000/repo", True), + ("", False), + (":justtag", False), + ("Uppercase:tag", False), + ("test:5000/Uppercase/lowercase:tag", False), + ("lowercase:Uppercase", True), + # length limits not enforced + pytest.param("a/"*128 + "a:tag", False, marks=pytest.mark.xfail), + ("a/"*127 + "a:tag-puts-this-over-max", True), + ("aa/asdf$$^/aa", False), + ("sub-dom1.foo.com/bar/baz/quux", True), + ("sub-dom1.foo.com/bar/baz/quux:some-long-tag", True), + ("b.gcr.io/test.example.com/my-app:test.example.com", True), + ("xn--n3h.com/myimage:xn--n3h.com", True), + ("foo_bar.com:8080", True), + ("foo/foo_bar.com:8080", True), + ("192.168.1.1", True), + ("192.168.1.1:tag", True), + ("192.168.1.1:5000", True), + ("192.168.1.1/repo", True), + ("192.168.1.1:5000/repo", True), + ("192.168.1.1:5000/repo:5050", True), + # regex does not properly handle ipv6 + pytest.param("[2001:db8::1]", False, marks=pytest.mark.xfail), + ("[2001:db8::1]:5000", False), + pytest.param("[2001:db8::1]/repo", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8:1:2:3:4:5:6]/repo:tag", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8::1]:5000/repo", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8::1]:5000/repo:tag", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8::]:5000/repo", True, marks=pytest.mark.xfail), + pytest.param("[::1]:5000/repo", True, marks=pytest.mark.xfail), + ("[fe80::1%eth0]:5000/repo", False), + ("[fe80::1%@invalidzone]:5000/repo", False), +]) +def test_match_tag(tag: str, expected: bool): + assert match_tag(tag) == expected