cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociref/reference_test.go (about) 1 // Copyright 2023 CUE Labs AG 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ociref 16 17 import ( 18 _ "crypto/sha256" 19 _ "crypto/sha512" 20 "fmt" 21 "regexp" 22 "strings" 23 "testing" 24 25 "github.com/go-quicktest/qt" 26 ) 27 28 var parseReferenceTests = []struct { 29 testName string 30 // input is the repository name or name component testcase 31 input string 32 // err is the error expected from Parse, or nil 33 wantErr string 34 wantRef Reference 35 }{ 36 { 37 input: "test_com", 38 wantRef: Reference{ 39 Repository: "test_com", 40 }, 41 }, 42 { 43 input: "test.com:tag", 44 wantRef: Reference{ 45 Repository: "test.com", 46 Tag: "tag", 47 }, 48 }, 49 { 50 input: "test.com:5000", 51 wantRef: Reference{ 52 Repository: "test.com", 53 Tag: "5000", 54 }, 55 }, 56 { 57 input: "test.com/repo:tag", 58 wantRef: Reference{ 59 Host: "test.com", 60 Repository: "repo", 61 Tag: "tag", 62 }, 63 }, 64 { 65 input: "test:5000/repo", 66 wantRef: Reference{ 67 Host: "test:5000", 68 Repository: "repo", 69 }, 70 }, 71 { 72 input: "test:5000/repo:tag", 73 wantRef: Reference{ 74 Host: "test:5000", 75 Repository: "repo", 76 Tag: "tag", 77 }, 78 }, 79 { 80 input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 81 wantRef: Reference{ 82 Host: "test:5000", 83 Repository: "repo", 84 Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 85 }, 86 }, 87 { 88 input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 89 wantRef: Reference{ 90 Host: "test:5000", 91 Repository: "repo", 92 Tag: "tag", 93 Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 94 }, 95 }, 96 { 97 input: "test:5000/repo", 98 wantRef: Reference{ 99 Host: "test:5000", 100 Repository: "repo", 101 }, 102 }, 103 { 104 testName: "EmptyString", 105 input: "", 106 wantErr: `invalid reference syntax`, 107 }, 108 { 109 input: ":justtag", 110 wantErr: `invalid reference syntax`, 111 }, 112 { 113 input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 114 wantErr: `invalid reference syntax`, 115 }, 116 { 117 input: "repo@sha256:ffffffffffffffffffffffffffffffffff", 118 wantErr: `invalid digest "sha256:ffffffffffffffffffffffffffffffffff": invalid checksum digest length`, 119 }, 120 { 121 input: "validname@invalidDigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 122 wantErr: `invalid digest "invalidDigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff": invalid checksum digest format`, 123 }, 124 { 125 input: "Uppercase:tag", 126 wantErr: `invalid reference syntax`, 127 }, 128 // FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes. 129 // See https://github.com/distribution/distribution/pull/1778, and https://github.com/docker/docker/pull/20175 130 // { 131 // input: "Uppercase/lowercase:tag", 132 // err: ErrNameContainsUppercase, 133 // }, 134 { 135 input: "test:5000/Uppercase/lowercase:tag", 136 wantErr: `tag "5000/Uppercase/lowercase:tag" contains invalid invalid character '/'`, 137 }, 138 { 139 input: "lowercase:Uppercase", 140 wantRef: Reference{ 141 Repository: "lowercase", 142 Tag: "Uppercase", 143 }, 144 }, 145 { 146 testName: "RepoTooLong", 147 input: strings.Repeat("a/", 128) + "a:tag", 148 wantErr: `repository name too long`, 149 }, 150 { 151 testName: "RepoAlmostTooLong", 152 input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", 153 wantRef: Reference{ 154 // Note: docker/reference parses Host as "a". 155 Repository: strings.Repeat("a/", 127) + "a", 156 Tag: "tag-puts-this-over-max", 157 }, 158 }, 159 { 160 input: "aa/asdf$$^/aa", 161 wantErr: `invalid reference syntax`, 162 }, 163 { 164 input: "sub-dom1.foo.com/bar/baz/quux", 165 wantRef: Reference{ 166 Host: "sub-dom1.foo.com", 167 Repository: "bar/baz/quux", 168 }, 169 }, 170 { 171 input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag", 172 wantRef: Reference{ 173 Host: "sub-dom1.foo.com", 174 Repository: "bar/baz/quux", 175 Tag: "some-long-tag", 176 }, 177 }, 178 { 179 input: "b.gcr.io/test.example.com/my-app:test.example.com", 180 wantRef: Reference{ 181 Host: "b.gcr.io", 182 Repository: "test.example.com/my-app", 183 Tag: "test.example.com", 184 }, 185 }, 186 { 187 input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode 188 wantRef: Reference{ 189 Host: "xn--n3h.com", 190 Repository: "myimage", 191 Tag: "xn--n3h.com", 192 }, 193 }, 194 { 195 input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode 196 wantRef: Reference{ 197 Host: "xn--7o8h.com", 198 Repository: "myimage", 199 Tag: "xn--7o8h.com", 200 Digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 201 }, 202 }, 203 { 204 input: "foo_bar.com:8080", 205 wantRef: Reference{ 206 Repository: "foo_bar.com", 207 Tag: "8080", 208 }, 209 }, 210 { 211 input: "foo.com/bar:8080", 212 wantRef: Reference{ 213 Host: "foo.com", 214 Repository: "bar", 215 Tag: "8080", 216 }, 217 }, 218 { 219 input: "foo/foo_bar.com:8080", 220 wantRef: Reference{ 221 Repository: "foo/foo_bar.com", 222 Tag: "8080", 223 }, 224 }, 225 { 226 input: "192.168.1.1", 227 wantRef: Reference{ 228 Repository: "192.168.1.1", 229 }, 230 }, 231 { 232 input: "192.168.1.1:tag", 233 wantRef: Reference{ 234 Repository: "192.168.1.1", 235 Tag: "tag", 236 }, 237 }, 238 { 239 input: "192.168.1.1:5000", 240 wantRef: Reference{ 241 Repository: "192.168.1.1", 242 Tag: "5000", 243 }, 244 }, 245 { 246 input: "192.168.1.1/repo", 247 wantRef: Reference{ 248 Host: "192.168.1.1", 249 Repository: "repo", 250 }, 251 }, 252 { 253 input: "192.168.1.1:5000/repo", 254 wantRef: Reference{ 255 Host: "192.168.1.1:5000", 256 Repository: "repo", 257 }, 258 }, 259 { 260 input: "192.168.1.1:5000/repo:5050", 261 wantRef: Reference{ 262 Host: "192.168.1.1:5000", 263 Repository: "repo", 264 Tag: "5050", 265 }, 266 }, 267 { 268 input: "[2001:db8::1]", 269 wantErr: `invalid reference syntax`, 270 }, 271 { 272 input: "[2001:db8::1]:5000", 273 wantErr: `invalid reference syntax`, 274 }, 275 { 276 input: "[2001:db8::1]:tag", 277 wantErr: `invalid reference syntax`, 278 }, 279 { 280 input: "[2001:db8::1]/repo", 281 wantRef: Reference{ 282 Host: "[2001:db8::1]", 283 Repository: "repo", 284 }, 285 }, 286 { 287 input: "[2001:db8:1:2:3:4:5:6]/repo:tag", 288 wantRef: Reference{ 289 Host: "[2001:db8:1:2:3:4:5:6]", 290 Repository: "repo", 291 Tag: "tag", 292 }, 293 }, 294 { 295 input: "[2001:db8::1]:5000/repo", 296 wantRef: Reference{ 297 Host: "[2001:db8::1]:5000", 298 Repository: "repo", 299 }, 300 }, 301 { 302 input: "[2001:db8::1]:5000/repo:tag", 303 wantRef: Reference{ 304 Host: "[2001:db8::1]:5000", 305 Repository: "repo", 306 Tag: "tag", 307 }, 308 }, 309 { 310 input: "[2001:db8::1]:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 311 wantRef: Reference{ 312 Host: "[2001:db8::1]:5000", 313 Repository: "repo", 314 Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 315 }, 316 }, 317 { 318 input: "[2001:db8::1]:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 319 wantRef: Reference{ 320 Host: "[2001:db8::1]:5000", 321 Repository: "repo", 322 Tag: "tag", 323 Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 324 }, 325 }, 326 { 327 input: "[2001:db8::]:5000/repo", 328 wantRef: Reference{ 329 Host: "[2001:db8::]:5000", 330 Repository: "repo", 331 }, 332 }, 333 { 334 input: "[::1]:5000/repo", 335 wantRef: Reference{ 336 Host: "[::1]:5000", 337 Repository: "repo", 338 }, 339 }, 340 { 341 input: "[fe80::1%eth0]:5000/repo", 342 wantErr: `invalid reference syntax`, 343 }, 344 { 345 input: "[fe80::1%@invalidzone]:5000/repo", 346 wantErr: `invalid reference syntax`, 347 }, 348 } 349 350 func TestParseReference(t *testing.T) { 351 for _, test := range parseReferenceTests { 352 if test.testName == "" { 353 test.testName = test.input 354 } 355 t.Run(test.testName, func(t *testing.T) { 356 ref, err := ParseRelative(test.input) 357 t.Logf("ref: %#v", ref) 358 if test.wantErr != "" { 359 if test.wantErr == "invalid reference syntax" { 360 test.wantErr += regexp.QuoteMeta(fmt.Sprintf(" (%q)", test.input)) 361 } 362 qt.Assert(t, qt.ErrorMatches(err, test.wantErr)) 363 return 364 } 365 qt.Assert(t, qt.IsNil(err)) 366 qt.Check(t, qt.Equals(ref, test.wantRef)) 367 qt.Check(t, qt.Equals(ref.String(), test.input)) 368 if test.wantRef.Host != "" { 369 ref1, err := Parse(test.input) 370 qt.Assert(t, qt.IsNil(err)) 371 qt.Check(t, qt.Equals(ref1, test.wantRef)) 372 } else { 373 _, err := Parse(test.input) 374 qt.Assert(t, qt.ErrorMatches(err, `reference does not contain host name`)) 375 } 376 }) 377 } 378 } 379 380 var isValidHostTests = []struct { 381 host string 382 want bool 383 }{{ 384 host: "foo.com:5000", 385 want: true, 386 }, { 387 host: "foo.com", 388 want: true, 389 }, { 390 host: "localhost:1234", 391 want: true, 392 }, { 393 host: "localhost", 394 want: false, 395 }, { 396 host: "foo", 397 want: false, 398 }, { 399 host: "foo..com", 400 want: false, 401 }, { 402 host: "[::1]", 403 want: true, 404 }, { 405 host: "[::1]:3456", 406 want: true, 407 }} 408 409 func TestIsValidHost(t *testing.T) { 410 for _, test := range isValidHostTests { 411 t.Run(test.host, func(t *testing.T) { 412 qt.Assert(t, qt.Equals(IsValidHost(test.host), test.want)) 413 }) 414 } 415 } 416 417 var isValidRepositoryTests = []struct { 418 repo string 419 want bool 420 }{{ 421 repo: "foo", 422 want: true, 423 }, { 424 repo: "00123456789abcdefghijklmnopqrstuvwxyz", 425 want: true, 426 }, { 427 repo: "a-b---c", 428 want: true, 429 }, { 430 repo: "a.b.c9", 431 want: true, 432 }, { 433 repo: "a..b", 434 want: false, 435 }, { 436 repo: ".a", 437 want: false, 438 }, { 439 repo: "a.", 440 want: false, 441 }, { 442 repo: "-a", 443 want: false, 444 }, { 445 repo: "a-", 446 want: false, 447 }, { 448 repo: "A", 449 want: false, 450 }, { 451 repo: "_a", 452 want: false, 453 }, { 454 repo: "a_", 455 want: false, 456 }, { 457 repo: "foo/bar/baz", 458 want: true, 459 }, { 460 repo: "café", 461 want: false, 462 }, { 463 repo: "foo@bar", 464 want: false, 465 }} 466 467 func TestIsValidRepository(t *testing.T) { 468 for _, test := range isValidRepositoryTests { 469 t.Run(test.repo, func(t *testing.T) { 470 qt.Assert(t, qt.Equals(IsValidRepository(test.repo), test.want)) 471 }) 472 } 473 } 474 475 var isValidTagTests = []struct { 476 tag string 477 want bool 478 }{{ 479 tag: "hello", 480 want: true, 481 }, { 482 tag: "v1.2.3-alpha.0", 483 want: true, 484 }, { 485 tag: "foo", 486 want: true, 487 }, { 488 tag: "_", 489 want: true, 490 }, { 491 tag: "_x", 492 want: true, 493 }, { 494 tag: "___", 495 want: true, 496 }, { 497 tag: "ABC_D", 498 want: true, 499 }, { 500 tag: strings.Repeat("a", 128), 501 want: true, 502 }, { 503 tag: strings.Repeat("a", 129), 504 want: false, 505 }, { 506 tag: "foo....___---", 507 want: true, 508 }, { 509 tag: "café", 510 want: false, 511 }, { 512 tag: "foo@bar", 513 want: false, 514 }, { 515 tag: "v4.3+something", 516 want: false, 517 }, { 518 tag: "xxx", 519 want: true, 520 }} 521 522 func TestIsValidTag(t *testing.T) { 523 for _, test := range isValidTagTests { 524 t.Run(test.tag, func(t *testing.T) { 525 qt.Assert(t, qt.Equals(IsValidTag(test.tag), test.want)) 526 }) 527 } 528 }