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