// Copyright 2023 CUE Labs AG // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ociref import ( _ "crypto/sha256" _ "crypto/sha512" "fmt" "regexp" "strings" "testing" "github.com/go-quicktest/qt" ) var parseReferenceTests = []struct { testName string // input is the repository name or name component testcase input string // err is the error expected from Parse, or nil wantErr string wantRef Reference }{ { input: "test_com", wantRef: Reference{ Repository: "test_com", }, }, { input: "test.com:tag", wantRef: Reference{ Repository: "test.com", Tag: "tag", }, }, { input: "test.com:5000", wantRef: Reference{ Repository: "test.com", Tag: "5000", }, }, { input: "test.com/repo:tag", wantRef: Reference{ Host: "test.com", Repository: "repo", Tag: "tag", }, }, { input: "test:5000/repo", wantRef: Reference{ Host: "test:5000", Repository: "repo", }, }, { input: "test:5000/repo:tag", wantRef: Reference{ Host: "test:5000", Repository: "repo", Tag: "tag", }, }, { input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", wantRef: Reference{ Host: "test:5000", Repository: "repo", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }, { input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", wantRef: Reference{ Host: "test:5000", Repository: "repo", Tag: "tag", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }, { input: "test:5000/repo", wantRef: Reference{ Host: "test:5000", Repository: "repo", }, }, { testName: "EmptyString", input: "", wantErr: `invalid reference syntax`, }, { input: ":justtag", wantErr: `invalid reference syntax`, }, { input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", wantErr: `invalid reference syntax`, }, { input: "repo@sha256:ffffffffffffffffffffffffffffffffff", wantErr: `invalid digest "sha256:ffffffffffffffffffffffffffffffffff": invalid checksum digest length`, }, { input: "validname@invalidDigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", wantErr: `invalid digest "invalidDigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff": invalid checksum digest format`, }, { input: "Uppercase:tag", wantErr: `invalid reference syntax`, }, // FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes. // See https://github.com/distribution/distribution/pull/1778, and https://github.com/docker/docker/pull/20175 // { // input: "Uppercase/lowercase:tag", // err: ErrNameContainsUppercase, // }, { input: "test:5000/Uppercase/lowercase:tag", wantErr: `tag "5000/Uppercase/lowercase:tag" contains invalid invalid character '/'`, }, { input: "lowercase:Uppercase", wantRef: Reference{ Repository: "lowercase", Tag: "Uppercase", }, }, { testName: "RepoTooLong", input: strings.Repeat("a/", 128) + "a:tag", wantErr: `repository name too long`, }, { testName: "RepoAlmostTooLong", input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", wantRef: Reference{ // Note: docker/reference parses Host as "a". Repository: strings.Repeat("a/", 127) + "a", Tag: "tag-puts-this-over-max", }, }, { input: "aa/asdf$$^/aa", wantErr: `invalid reference syntax`, }, { input: "sub-dom1.foo.com/bar/baz/quux", wantRef: Reference{ Host: "sub-dom1.foo.com", Repository: "bar/baz/quux", }, }, { input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag", wantRef: Reference{ Host: "sub-dom1.foo.com", Repository: "bar/baz/quux", Tag: "some-long-tag", }, }, { input: "b.gcr.io/test.example.com/my-app:test.example.com", wantRef: Reference{ Host: "b.gcr.io", Repository: "test.example.com/my-app", Tag: "test.example.com", }, }, { input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode wantRef: Reference{ Host: "xn--n3h.com", Repository: "myimage", Tag: "xn--n3h.com", }, }, { input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode wantRef: Reference{ Host: "xn--7o8h.com", Repository: "myimage", Tag: "xn--7o8h.com", Digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }, { input: "foo_bar.com:8080", wantRef: Reference{ Repository: "foo_bar.com", Tag: "8080", }, }, { input: "foo.com/bar:8080", wantRef: Reference{ Host: "foo.com", Repository: "bar", Tag: "8080", }, }, { input: "foo/foo_bar.com:8080", wantRef: Reference{ Repository: "foo/foo_bar.com", Tag: "8080", }, }, { input: "192.168.1.1", wantRef: Reference{ Repository: "192.168.1.1", }, }, { input: "192.168.1.1:tag", wantRef: Reference{ Repository: "192.168.1.1", Tag: "tag", }, }, { input: "192.168.1.1:5000", wantRef: Reference{ Repository: "192.168.1.1", Tag: "5000", }, }, { input: "192.168.1.1/repo", wantRef: Reference{ Host: "192.168.1.1", Repository: "repo", }, }, { input: "192.168.1.1:5000/repo", wantRef: Reference{ Host: "192.168.1.1:5000", Repository: "repo", }, }, { input: "192.168.1.1:5000/repo:5050", wantRef: Reference{ Host: "192.168.1.1:5000", Repository: "repo", Tag: "5050", }, }, { input: "[2001:db8::1]", wantErr: `invalid reference syntax`, }, { input: "[2001:db8::1]:5000", wantErr: `invalid reference syntax`, }, { input: "[2001:db8::1]:tag", wantErr: `invalid reference syntax`, }, { input: "[2001:db8::1]/repo", wantRef: Reference{ Host: "[2001:db8::1]", Repository: "repo", }, }, { input: "[2001:db8:1:2:3:4:5:6]/repo:tag", wantRef: Reference{ Host: "[2001:db8:1:2:3:4:5:6]", Repository: "repo", Tag: "tag", }, }, { input: "[2001:db8::1]:5000/repo", wantRef: Reference{ Host: "[2001:db8::1]:5000", Repository: "repo", }, }, { input: "[2001:db8::1]:5000/repo:tag", wantRef: Reference{ Host: "[2001:db8::1]:5000", Repository: "repo", Tag: "tag", }, }, { input: "[2001:db8::1]:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", wantRef: Reference{ Host: "[2001:db8::1]:5000", Repository: "repo", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }, { input: "[2001:db8::1]:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", wantRef: Reference{ Host: "[2001:db8::1]:5000", Repository: "repo", Tag: "tag", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }, { input: "[2001:db8::]:5000/repo", wantRef: Reference{ Host: "[2001:db8::]:5000", Repository: "repo", }, }, { input: "[::1]:5000/repo", wantRef: Reference{ Host: "[::1]:5000", Repository: "repo", }, }, { input: "[fe80::1%eth0]:5000/repo", wantErr: `invalid reference syntax`, }, { input: "[fe80::1%@invalidzone]:5000/repo", wantErr: `invalid reference syntax`, }, } func TestParseReference(t *testing.T) { for _, test := range parseReferenceTests { if test.testName == "" { test.testName = test.input } t.Run(test.testName, func(t *testing.T) { ref, err := ParseRelative(test.input) t.Logf("ref: %#v", ref) if test.wantErr != "" { if test.wantErr == "invalid reference syntax" { test.wantErr += regexp.QuoteMeta(fmt.Sprintf(" (%q)", test.input)) } qt.Assert(t, qt.ErrorMatches(err, test.wantErr)) return } qt.Assert(t, qt.IsNil(err)) qt.Check(t, qt.Equals(ref, test.wantRef)) qt.Check(t, qt.Equals(ref.String(), test.input)) if test.wantRef.Host != "" { ref1, err := Parse(test.input) qt.Assert(t, qt.IsNil(err)) qt.Check(t, qt.Equals(ref1, test.wantRef)) } else { _, err := Parse(test.input) qt.Assert(t, qt.ErrorMatches(err, `reference does not contain host name`)) } }) } } var isValidHostTests = []struct { host string want bool }{{ host: "foo.com:5000", want: true, }, { host: "foo.com", want: true, }, { host: "localhost:1234", want: true, }, { host: "localhost", want: false, }, { host: "foo", want: false, }, { host: "foo..com", want: false, }, { host: "[::1]", want: true, }, { host: "[::1]:3456", want: true, }} func TestIsValidHost(t *testing.T) { for _, test := range isValidHostTests { t.Run(test.host, func(t *testing.T) { qt.Assert(t, qt.Equals(IsValidHost(test.host), test.want)) }) } } var isValidRepositoryTests = []struct { repo string want bool }{{ repo: "foo", want: true, }, { repo: "00123456789abcdefghijklmnopqrstuvwxyz", want: true, }, { repo: "a-b---c", want: true, }, { repo: "a.b.c9", want: true, }, { repo: "a..b", want: false, }, { repo: ".a", want: false, }, { repo: "a.", want: false, }, { repo: "-a", want: false, }, { repo: "a-", want: false, }, { repo: "A", want: false, }, { repo: "_a", want: false, }, { repo: "a_", want: false, }, { repo: "foo/bar/baz", want: true, }, { repo: "café", want: false, }, { repo: "foo@bar", want: false, }} func TestIsValidRepository(t *testing.T) { for _, test := range isValidRepositoryTests { t.Run(test.repo, func(t *testing.T) { qt.Assert(t, qt.Equals(IsValidRepository(test.repo), test.want)) }) } } var isValidTagTests = []struct { tag string want bool }{{ tag: "hello", want: true, }, { tag: "v1.2.3-alpha.0", want: true, }, { tag: "foo", want: true, }, { tag: "_", want: true, }, { tag: "_x", want: true, }, { tag: "___", want: true, }, { tag: "ABC_D", want: true, }, { tag: strings.Repeat("a", 128), want: true, }, { tag: strings.Repeat("a", 129), want: false, }, { tag: "foo....___---", want: true, }, { tag: "café", want: false, }, { tag: "foo@bar", want: false, }, { tag: "v4.3+something", want: false, }, { tag: "xxx", want: true, }} func TestIsValidTag(t *testing.T) { for _, test := range isValidTagTests { t.Run(test.tag, func(t *testing.T) { qt.Assert(t, qt.Equals(IsValidTag(test.tag), test.want)) }) } }