1package reference 2 3import ( 4 "regexp" 5 "strings" 6 "testing" 7) 8 9type regexpMatch struct { 10 input string 11 match bool 12 subs []string 13} 14 15func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) { 16 matches := r.FindStringSubmatch(m.input) 17 if m.match && matches != nil { 18 if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input { 19 t.Fatalf("Bad match result %#v for %q", matches, m.input) 20 } 21 if len(matches) < (len(m.subs) + 1) { 22 t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input) 23 } 24 for i := range m.subs { 25 if m.subs[i] != matches[i+1] { 26 t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input) 27 } 28 } 29 } else if m.match { 30 t.Errorf("Expected match for %q", m.input) 31 } else if matches != nil { 32 t.Errorf("Unexpected match for %q", m.input) 33 } 34} 35 36func TestDomainRegexp(t *testing.T) { 37 hostcases := []regexpMatch{ 38 { 39 input: "test.com", 40 match: true, 41 }, 42 { 43 input: "test.com:10304", 44 match: true, 45 }, 46 { 47 input: "test.com:http", 48 match: false, 49 }, 50 { 51 input: "localhost", 52 match: true, 53 }, 54 { 55 input: "localhost:8080", 56 match: true, 57 }, 58 { 59 input: "a", 60 match: true, 61 }, 62 { 63 input: "a.b", 64 match: true, 65 }, 66 { 67 input: "ab.cd.com", 68 match: true, 69 }, 70 { 71 input: "a-b.com", 72 match: true, 73 }, 74 { 75 input: "-ab.com", 76 match: false, 77 }, 78 { 79 input: "ab-.com", 80 match: false, 81 }, 82 { 83 input: "ab.c-om", 84 match: true, 85 }, 86 { 87 input: "ab.-com", 88 match: false, 89 }, 90 { 91 input: "ab.com-", 92 match: false, 93 }, 94 { 95 input: "0101.com", 96 match: true, // TODO(dmcgowan): valid if this should be allowed 97 }, 98 { 99 input: "001a.com", 100 match: true, 101 }, 102 { 103 input: "b.gbc.io:443", 104 match: true, 105 }, 106 { 107 input: "b.gbc.io", 108 match: true, 109 }, 110 { 111 input: "xn--n3h.com", // ☃.com in punycode 112 match: true, 113 }, 114 { 115 input: "Asdf.com", // uppercase character 116 match: true, 117 }, 118 } 119 r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`) 120 for i := range hostcases { 121 checkRegexp(t, r, hostcases[i]) 122 } 123} 124 125func TestFullNameRegexp(t *testing.T) { 126 if anchoredNameRegexp.NumSubexp() != 2 { 127 t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2", 128 anchoredNameRegexp, anchoredNameRegexp.NumSubexp()) 129 } 130 131 testcases := []regexpMatch{ 132 { 133 input: "", 134 match: false, 135 }, 136 { 137 input: "short", 138 match: true, 139 subs: []string{"", "short"}, 140 }, 141 { 142 input: "simple/name", 143 match: true, 144 subs: []string{"simple", "name"}, 145 }, 146 { 147 input: "library/ubuntu", 148 match: true, 149 subs: []string{"library", "ubuntu"}, 150 }, 151 { 152 input: "docker/stevvooe/app", 153 match: true, 154 subs: []string{"docker", "stevvooe/app"}, 155 }, 156 { 157 input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", 158 match: true, 159 subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"}, 160 }, 161 { 162 input: "aa/aa/bb/bb/bb", 163 match: true, 164 subs: []string{"aa", "aa/bb/bb/bb"}, 165 }, 166 { 167 input: "a/a/a/a", 168 match: true, 169 subs: []string{"a", "a/a/a"}, 170 }, 171 { 172 input: "a/a/a/a/", 173 match: false, 174 }, 175 { 176 input: "a//a/a", 177 match: false, 178 }, 179 { 180 input: "a", 181 match: true, 182 subs: []string{"", "a"}, 183 }, 184 { 185 input: "a/aa", 186 match: true, 187 subs: []string{"a", "aa"}, 188 }, 189 { 190 input: "a/aa/a", 191 match: true, 192 subs: []string{"a", "aa/a"}, 193 }, 194 { 195 input: "foo.com", 196 match: true, 197 subs: []string{"", "foo.com"}, 198 }, 199 { 200 input: "foo.com/", 201 match: false, 202 }, 203 { 204 input: "foo.com:8080/bar", 205 match: true, 206 subs: []string{"foo.com:8080", "bar"}, 207 }, 208 { 209 input: "foo.com:http/bar", 210 match: false, 211 }, 212 { 213 input: "foo.com/bar", 214 match: true, 215 subs: []string{"foo.com", "bar"}, 216 }, 217 { 218 input: "foo.com/bar/baz", 219 match: true, 220 subs: []string{"foo.com", "bar/baz"}, 221 }, 222 { 223 input: "localhost:8080/bar", 224 match: true, 225 subs: []string{"localhost:8080", "bar"}, 226 }, 227 { 228 input: "sub-dom1.foo.com/bar/baz/quux", 229 match: true, 230 subs: []string{"sub-dom1.foo.com", "bar/baz/quux"}, 231 }, 232 { 233 input: "blog.foo.com/bar/baz", 234 match: true, 235 subs: []string{"blog.foo.com", "bar/baz"}, 236 }, 237 { 238 input: "a^a", 239 match: false, 240 }, 241 { 242 input: "aa/asdf$$^/aa", 243 match: false, 244 }, 245 { 246 input: "asdf$$^/aa", 247 match: false, 248 }, 249 { 250 input: "aa-a/a", 251 match: true, 252 subs: []string{"aa-a", "a"}, 253 }, 254 { 255 input: strings.Repeat("a/", 128) + "a", 256 match: true, 257 subs: []string{"a", strings.Repeat("a/", 127) + "a"}, 258 }, 259 { 260 input: "a-/a/a/a", 261 match: false, 262 }, 263 { 264 input: "foo.com/a-/a/a", 265 match: false, 266 }, 267 { 268 input: "-foo/bar", 269 match: false, 270 }, 271 { 272 input: "foo/bar-", 273 match: false, 274 }, 275 { 276 input: "foo-/bar", 277 match: false, 278 }, 279 { 280 input: "foo/-bar", 281 match: false, 282 }, 283 { 284 input: "_foo/bar", 285 match: false, 286 }, 287 { 288 input: "foo_bar", 289 match: true, 290 subs: []string{"", "foo_bar"}, 291 }, 292 { 293 input: "foo_bar.com", 294 match: true, 295 subs: []string{"", "foo_bar.com"}, 296 }, 297 { 298 input: "foo_bar.com:8080", 299 match: false, 300 }, 301 { 302 input: "foo_bar.com:8080/app", 303 match: false, 304 }, 305 { 306 input: "foo.com/foo_bar", 307 match: true, 308 subs: []string{"foo.com", "foo_bar"}, 309 }, 310 { 311 input: "____/____", 312 match: false, 313 }, 314 { 315 input: "_docker/_docker", 316 match: false, 317 }, 318 { 319 input: "docker_/docker_", 320 match: false, 321 }, 322 { 323 input: "b.gcr.io/test.example.com/my-app", 324 match: true, 325 subs: []string{"b.gcr.io", "test.example.com/my-app"}, 326 }, 327 { 328 input: "xn--n3h.com/myimage", // ☃.com in punycode 329 match: true, 330 subs: []string{"xn--n3h.com", "myimage"}, 331 }, 332 { 333 input: "xn--7o8h.com/myimage", // .com in punycode 334 match: true, 335 subs: []string{"xn--7o8h.com", "myimage"}, 336 }, 337 { 338 input: "example.com/xn--7o8h.com/myimage", // .com in punycode 339 match: true, 340 subs: []string{"example.com", "xn--7o8h.com/myimage"}, 341 }, 342 { 343 input: "example.com/some_separator__underscore/myimage", 344 match: true, 345 subs: []string{"example.com", "some_separator__underscore/myimage"}, 346 }, 347 { 348 input: "example.com/__underscore/myimage", 349 match: false, 350 }, 351 { 352 input: "example.com/..dots/myimage", 353 match: false, 354 }, 355 { 356 input: "example.com/.dots/myimage", 357 match: false, 358 }, 359 { 360 input: "example.com/nodouble..dots/myimage", 361 match: false, 362 }, 363 { 364 input: "example.com/nodouble..dots/myimage", 365 match: false, 366 }, 367 { 368 input: "docker./docker", 369 match: false, 370 }, 371 { 372 input: ".docker/docker", 373 match: false, 374 }, 375 { 376 input: "docker-/docker", 377 match: false, 378 }, 379 { 380 input: "-docker/docker", 381 match: false, 382 }, 383 { 384 input: "do..cker/docker", 385 match: false, 386 }, 387 { 388 input: "do__cker:8080/docker", 389 match: false, 390 }, 391 { 392 input: "do__cker/docker", 393 match: true, 394 subs: []string{"", "do__cker/docker"}, 395 }, 396 { 397 input: "b.gcr.io/test.example.com/my-app", 398 match: true, 399 subs: []string{"b.gcr.io", "test.example.com/my-app"}, 400 }, 401 { 402 input: "registry.io/foo/project--id.module--name.ver---sion--name", 403 match: true, 404 subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"}, 405 }, 406 { 407 input: "Asdf.com/foo/bar", // uppercase character in hostname 408 match: true, 409 }, 410 { 411 input: "Foo/FarB", // uppercase characters in remote name 412 match: false, 413 }, 414 } 415 for i := range testcases { 416 checkRegexp(t, anchoredNameRegexp, testcases[i]) 417 } 418} 419 420func TestReferenceRegexp(t *testing.T) { 421 if ReferenceRegexp.NumSubexp() != 3 { 422 t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3", 423 ReferenceRegexp, ReferenceRegexp.NumSubexp()) 424 } 425 426 testcases := []regexpMatch{ 427 { 428 input: "registry.com:8080/myapp:tag", 429 match: true, 430 subs: []string{"registry.com:8080/myapp", "tag", ""}, 431 }, 432 { 433 input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", 434 match: true, 435 subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, 436 }, 437 { 438 input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", 439 match: true, 440 subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, 441 }, 442 { 443 input: "registry.com:8080/myapp@sha256:badbadbadbad", 444 match: false, 445 }, 446 { 447 input: "registry.com:8080/myapp:invalid~tag", 448 match: false, 449 }, 450 { 451 input: "bad_hostname.com:8080/myapp:tag", 452 match: false, 453 }, 454 { 455 input:// localhost treated as name, missing tag with 8080 as tag 456 "localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", 457 match: true, 458 subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, 459 }, 460 { 461 input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", 462 match: true, 463 subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, 464 }, 465 { 466 input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", 467 match: false, 468 }, 469 { 470 // localhost will be treated as an image name without a host 471 input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", 472 match: true, 473 subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, 474 }, 475 { 476 input: "registry.com:8080/myapp@bad", 477 match: false, 478 }, 479 { 480 input: "registry.com:8080/myapp@2bad", 481 match: false, // TODO(dmcgowan): Support this as valid 482 }, 483 } 484 485 for i := range testcases { 486 checkRegexp(t, ReferenceRegexp, testcases[i]) 487 } 488 489} 490 491func TestIdentifierRegexp(t *testing.T) { 492 fullCases := []regexpMatch{ 493 { 494 input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", 495 match: true, 496 }, 497 { 498 input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C", 499 match: false, 500 }, 501 { 502 input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf", 503 match: false, 504 }, 505 { 506 input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", 507 match: false, 508 }, 509 { 510 input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482", 511 match: false, 512 }, 513 } 514 515 shortCases := []regexpMatch{ 516 { 517 input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", 518 match: true, 519 }, 520 { 521 input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C", 522 match: false, 523 }, 524 { 525 input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf", 526 match: true, 527 }, 528 { 529 input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", 530 match: false, 531 }, 532 { 533 input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482", 534 match: false, 535 }, 536 { 537 input: "da304", 538 match: false, 539 }, 540 { 541 input: "da304e", 542 match: true, 543 }, 544 } 545 546 for i := range fullCases { 547 checkRegexp(t, anchoredIdentifierRegexp, fullCases[i]) 548 } 549 550 for i := range shortCases { 551 checkRegexp(t, anchoredShortIdentifierRegexp, shortCases[i]) 552 } 553} 554