• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 Google LLC
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
15package main
16
17import (
18	"bufio"
19	"bytes"
20	"fmt"
21	"html"
22	"os"
23	"reflect"
24	"regexp"
25	"strings"
26	"testing"
27
28	"android/soong/tools/compliance"
29)
30
31var (
32	horizontalRule = regexp.MustCompile(`^\s*<hr>\s*$`)
33	bodyTag        = regexp.MustCompile(`^\s*<body>\s*$`)
34	boilerPlate    = regexp.MustCompile(`^\s*(?:<ul class="file-list">|<ul>|</.*)\s*$`)
35	tocTag         = regexp.MustCompile(`^\s*<ul class="toc">\s*$`)
36	libraryName    = regexp.MustCompile(`^\s*<strong>(.*)</strong>\s\s*used\s\s*by\s*:\s*$`)
37	licenseText    = regexp.MustCompile(`^\s*<a id="[^"]{32}"/><pre class="license-text">(.*)$`)
38	titleTag       = regexp.MustCompile(`^\s*<title>(.*)</title>\s*$`)
39	h1Tag          = regexp.MustCompile(`^\s*<h1>(.*)</h1>\s*$`)
40	usedByTarget   = regexp.MustCompile(`^\s*<li>(?:<a href="#id[0-9]+">)?((?:out/(?:[^/<]*/)+)[^/<]*)(?:</a>)?\s*$`)
41	installTarget  = regexp.MustCompile(`^\s*<li id="id[0-9]+"><strong>(.*)</strong>\s*$`)
42	libReference   = regexp.MustCompile(`^\s*<li><a href="#[^"]{32}">(.*)</a>\s*$`)
43)
44
45func TestMain(m *testing.M) {
46	// Change into the parent directory before running the tests
47	// so they can find the testdata directory.
48	if err := os.Chdir(".."); err != nil {
49		fmt.Printf("failed to change to testdata directory: %s\n", err)
50		os.Exit(1)
51	}
52	os.Exit(m.Run())
53}
54
55func Test(t *testing.T) {
56	tests := []struct {
57		condition    string
58		name         string
59		outDir       string
60		roots        []string
61		includeTOC   bool
62		stripPrefix  string
63		title        string
64		expectedOut  []matcher
65		expectedDeps []string
66	}{
67		{
68			condition: "firstparty",
69			name:      "apex",
70			roots:     []string{"highest.apex.meta_lic"},
71			expectedOut: []matcher{
72				hr{},
73				library{"Android"},
74				usedBy{"highest.apex"},
75				usedBy{"highest.apex/bin/bin1"},
76				usedBy{"highest.apex/bin/bin2"},
77				usedBy{"highest.apex/lib/liba.so"},
78				usedBy{"highest.apex/lib/libb.so"},
79				firstParty{},
80			},
81			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
82		},
83		{
84			condition:  "firstparty",
85			name:       "apex+toc",
86			roots:      []string{"highest.apex.meta_lic"},
87			includeTOC: true,
88			expectedOut: []matcher{
89				toc{},
90				target{"highest.apex"},
91				uses{"Android"},
92				target{"highest.apex/bin/bin1"},
93				uses{"Android"},
94				target{"highest.apex/bin/bin2"},
95				uses{"Android"},
96				target{"highest.apex/lib/liba.so"},
97				uses{"Android"},
98				target{"highest.apex/lib/libb.so"},
99				uses{"Android"},
100				hr{},
101				library{"Android"},
102				usedBy{"highest.apex"},
103				usedBy{"highest.apex/bin/bin1"},
104				usedBy{"highest.apex/bin/bin2"},
105				usedBy{"highest.apex/lib/liba.so"},
106				usedBy{"highest.apex/lib/libb.so"},
107				firstParty{},
108			},
109			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
110		},
111		{
112			condition: "firstparty",
113			name:      "apex-with-title",
114			roots:     []string{"highest.apex.meta_lic"},
115			title:     "Emperor",
116			expectedOut: []matcher{
117				pageTitle{"Emperor"},
118				hr{},
119				library{"Android"},
120				usedBy{"highest.apex"},
121				usedBy{"highest.apex/bin/bin1"},
122				usedBy{"highest.apex/bin/bin2"},
123				usedBy{"highest.apex/lib/liba.so"},
124				usedBy{"highest.apex/lib/libb.so"},
125				firstParty{},
126			},
127			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
128		},
129		{
130			condition:  "firstparty",
131			name:       "apex-with-title+toc",
132			roots:      []string{"highest.apex.meta_lic"},
133			includeTOC: true,
134			title:      "Emperor",
135			expectedOut: []matcher{
136				pageTitle{"Emperor"},
137				toc{},
138				target{"highest.apex"},
139				uses{"Android"},
140				target{"highest.apex/bin/bin1"},
141				uses{"Android"},
142				target{"highest.apex/bin/bin2"},
143				uses{"Android"},
144				target{"highest.apex/lib/liba.so"},
145				uses{"Android"},
146				target{"highest.apex/lib/libb.so"},
147				uses{"Android"},
148				hr{},
149				library{"Android"},
150				usedBy{"highest.apex"},
151				usedBy{"highest.apex/bin/bin1"},
152				usedBy{"highest.apex/bin/bin2"},
153				usedBy{"highest.apex/lib/liba.so"},
154				usedBy{"highest.apex/lib/libb.so"},
155				firstParty{},
156			},
157			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
158		},
159		{
160			condition: "firstparty",
161			name:      "container",
162			roots:     []string{"container.zip.meta_lic"},
163			expectedOut: []matcher{
164				hr{},
165				library{"Android"},
166				usedBy{"container.zip"},
167				usedBy{"container.zip/bin1"},
168				usedBy{"container.zip/bin2"},
169				usedBy{"container.zip/liba.so"},
170				usedBy{"container.zip/libb.so"},
171				firstParty{},
172			},
173			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
174		},
175		{
176			condition: "firstparty",
177			name:      "application",
178			roots:     []string{"application.meta_lic"},
179			expectedOut: []matcher{
180				hr{},
181				library{"Android"},
182				usedBy{"application"},
183				firstParty{},
184			},
185			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
186		},
187		{
188			condition: "firstparty",
189			name:      "binary",
190			roots:     []string{"bin/bin1.meta_lic"},
191			expectedOut: []matcher{
192				hr{},
193				library{"Android"},
194				usedBy{"bin/bin1"},
195				firstParty{},
196			},
197			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
198		},
199		{
200			condition: "firstparty",
201			name:      "library",
202			roots:     []string{"lib/libd.so.meta_lic"},
203			expectedOut: []matcher{
204				hr{},
205				library{"Android"},
206				usedBy{"lib/libd.so"},
207				firstParty{},
208			},
209			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
210		},
211		{
212			condition: "notice",
213			name:      "apex",
214			roots:     []string{"highest.apex.meta_lic"},
215			expectedOut: []matcher{
216				hr{},
217				library{"Android"},
218				usedBy{"highest.apex"},
219				usedBy{"highest.apex/bin/bin1"},
220				usedBy{"highest.apex/bin/bin2"},
221				usedBy{"highest.apex/lib/libb.so"},
222				firstParty{},
223				hr{},
224				library{"Device"},
225				usedBy{"highest.apex/bin/bin1"},
226				usedBy{"highest.apex/lib/liba.so"},
227				library{"External"},
228				usedBy{"highest.apex/bin/bin1"},
229				notice{},
230			},
231			expectedDeps: []string{
232				"testdata/firstparty/FIRST_PARTY_LICENSE",
233				"testdata/notice/NOTICE_LICENSE",
234			},
235		},
236		{
237			condition: "notice",
238			name:      "container",
239			roots:     []string{"container.zip.meta_lic"},
240			expectedOut: []matcher{
241				hr{},
242				library{"Android"},
243				usedBy{"container.zip"},
244				usedBy{"container.zip/bin1"},
245				usedBy{"container.zip/bin2"},
246				usedBy{"container.zip/libb.so"},
247				firstParty{},
248				hr{},
249				library{"Device"},
250				usedBy{"container.zip/bin1"},
251				usedBy{"container.zip/liba.so"},
252				library{"External"},
253				usedBy{"container.zip/bin1"},
254				notice{},
255			},
256			expectedDeps: []string{
257				"testdata/firstparty/FIRST_PARTY_LICENSE",
258				"testdata/notice/NOTICE_LICENSE",
259			},
260		},
261		{
262			condition: "notice",
263			name:      "application",
264			roots:     []string{"application.meta_lic"},
265			expectedOut: []matcher{
266				hr{},
267				library{"Android"},
268				usedBy{"application"},
269				firstParty{},
270				hr{},
271				library{"Device"},
272				usedBy{"application"},
273				notice{},
274			},
275			expectedDeps: []string{
276				"testdata/firstparty/FIRST_PARTY_LICENSE",
277				"testdata/notice/NOTICE_LICENSE",
278			},
279		},
280		{
281			condition: "notice",
282			name:      "binary",
283			roots:     []string{"bin/bin1.meta_lic"},
284			expectedOut: []matcher{
285				hr{},
286				library{"Android"},
287				usedBy{"bin/bin1"},
288				firstParty{},
289				hr{},
290				library{"Device"},
291				usedBy{"bin/bin1"},
292				library{"External"},
293				usedBy{"bin/bin1"},
294				notice{},
295			},
296			expectedDeps: []string{
297				"testdata/firstparty/FIRST_PARTY_LICENSE",
298				"testdata/notice/NOTICE_LICENSE",
299			},
300		},
301		{
302			condition: "notice",
303			name:      "library",
304			roots:     []string{"lib/libd.so.meta_lic"},
305			expectedOut: []matcher{
306				hr{},
307				library{"External"},
308				usedBy{"lib/libd.so"},
309				notice{},
310			},
311			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
312		},
313		{
314			condition: "reciprocal",
315			name:      "apex",
316			roots:     []string{"highest.apex.meta_lic"},
317			expectedOut: []matcher{
318				hr{},
319				library{"Android"},
320				usedBy{"highest.apex"},
321				usedBy{"highest.apex/bin/bin1"},
322				usedBy{"highest.apex/bin/bin2"},
323				usedBy{"highest.apex/lib/libb.so"},
324				firstParty{},
325				hr{},
326				library{"Device"},
327				usedBy{"highest.apex/bin/bin1"},
328				usedBy{"highest.apex/lib/liba.so"},
329				library{"External"},
330				usedBy{"highest.apex/bin/bin1"},
331				reciprocal{},
332			},
333			expectedDeps: []string{
334				"testdata/firstparty/FIRST_PARTY_LICENSE",
335				"testdata/reciprocal/RECIPROCAL_LICENSE",
336			},
337		},
338		{
339			condition: "reciprocal",
340			name:      "container",
341			roots:     []string{"container.zip.meta_lic"},
342			expectedOut: []matcher{
343				hr{},
344				library{"Android"},
345				usedBy{"container.zip"},
346				usedBy{"container.zip/bin1"},
347				usedBy{"container.zip/bin2"},
348				usedBy{"container.zip/libb.so"},
349				firstParty{},
350				hr{},
351				library{"Device"},
352				usedBy{"container.zip/bin1"},
353				usedBy{"container.zip/liba.so"},
354				library{"External"},
355				usedBy{"container.zip/bin1"},
356				reciprocal{},
357			},
358			expectedDeps: []string{
359				"testdata/firstparty/FIRST_PARTY_LICENSE",
360				"testdata/reciprocal/RECIPROCAL_LICENSE",
361			},
362		},
363		{
364			condition: "reciprocal",
365			name:      "application",
366			roots:     []string{"application.meta_lic"},
367			expectedOut: []matcher{
368				hr{},
369				library{"Android"},
370				usedBy{"application"},
371				firstParty{},
372				hr{},
373				library{"Device"},
374				usedBy{"application"},
375				reciprocal{},
376			},
377			expectedDeps: []string{
378				"testdata/firstparty/FIRST_PARTY_LICENSE",
379				"testdata/reciprocal/RECIPROCAL_LICENSE",
380			},
381		},
382		{
383			condition: "reciprocal",
384			name:      "binary",
385			roots:     []string{"bin/bin1.meta_lic"},
386			expectedOut: []matcher{
387				hr{},
388				library{"Android"},
389				usedBy{"bin/bin1"},
390				firstParty{},
391				hr{},
392				library{"Device"},
393				usedBy{"bin/bin1"},
394				library{"External"},
395				usedBy{"bin/bin1"},
396				reciprocal{},
397			},
398			expectedDeps: []string{
399				"testdata/firstparty/FIRST_PARTY_LICENSE",
400				"testdata/reciprocal/RECIPROCAL_LICENSE",
401			},
402		},
403		{
404			condition: "reciprocal",
405			name:      "library",
406			roots:     []string{"lib/libd.so.meta_lic"},
407			expectedOut: []matcher{
408				hr{},
409				library{"External"},
410				usedBy{"lib/libd.so"},
411				notice{},
412			},
413			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
414		},
415		{
416			condition: "restricted",
417			name:      "apex",
418			roots:     []string{"highest.apex.meta_lic"},
419			expectedOut: []matcher{
420				hr{},
421				library{"Android"},
422				usedBy{"highest.apex"},
423				usedBy{"highest.apex/bin/bin1"},
424				usedBy{"highest.apex/bin/bin2"},
425				firstParty{},
426				hr{},
427				library{"Android"},
428				usedBy{"highest.apex/bin/bin2"},
429				usedBy{"highest.apex/lib/libb.so"},
430				library{"Device"},
431				usedBy{"highest.apex/bin/bin1"},
432				usedBy{"highest.apex/lib/liba.so"},
433				restricted{},
434				hr{},
435				library{"External"},
436				usedBy{"highest.apex/bin/bin1"},
437				reciprocal{},
438			},
439			expectedDeps: []string{
440				"testdata/firstparty/FIRST_PARTY_LICENSE",
441				"testdata/reciprocal/RECIPROCAL_LICENSE",
442				"testdata/restricted/RESTRICTED_LICENSE",
443			},
444		},
445		{
446			condition: "restricted",
447			name:      "container",
448			roots:     []string{"container.zip.meta_lic"},
449			expectedOut: []matcher{
450				hr{},
451				library{"Android"},
452				usedBy{"container.zip"},
453				usedBy{"container.zip/bin1"},
454				usedBy{"container.zip/bin2"},
455				firstParty{},
456				hr{},
457				library{"Android"},
458				usedBy{"container.zip/bin2"},
459				usedBy{"container.zip/libb.so"},
460				library{"Device"},
461				usedBy{"container.zip/bin1"},
462				usedBy{"container.zip/liba.so"},
463				restricted{},
464				hr{},
465				library{"External"},
466				usedBy{"container.zip/bin1"},
467				reciprocal{},
468			},
469			expectedDeps: []string{
470				"testdata/firstparty/FIRST_PARTY_LICENSE",
471				"testdata/reciprocal/RECIPROCAL_LICENSE",
472				"testdata/restricted/RESTRICTED_LICENSE",
473			},
474		},
475		{
476			condition: "restricted",
477			name:      "application",
478			roots:     []string{"application.meta_lic"},
479			expectedOut: []matcher{
480				hr{},
481				library{"Android"},
482				usedBy{"application"},
483				firstParty{},
484				hr{},
485				library{"Device"},
486				usedBy{"application"},
487				restricted{},
488			},
489			expectedDeps: []string{
490				"testdata/firstparty/FIRST_PARTY_LICENSE",
491				"testdata/restricted/RESTRICTED_LICENSE",
492			},
493		},
494		{
495			condition: "restricted",
496			name:      "binary",
497			roots:     []string{"bin/bin1.meta_lic"},
498			expectedOut: []matcher{
499				hr{},
500				library{"Android"},
501				usedBy{"bin/bin1"},
502				firstParty{},
503				hr{},
504				library{"Device"},
505				usedBy{"bin/bin1"},
506				restricted{},
507				hr{},
508				library{"External"},
509				usedBy{"bin/bin1"},
510				reciprocal{},
511			},
512			expectedDeps: []string{
513				"testdata/firstparty/FIRST_PARTY_LICENSE",
514				"testdata/reciprocal/RECIPROCAL_LICENSE",
515				"testdata/restricted/RESTRICTED_LICENSE",
516			},
517		},
518		{
519			condition: "restricted",
520			name:      "library",
521			roots:     []string{"lib/libd.so.meta_lic"},
522			expectedOut: []matcher{
523				hr{},
524				library{"External"},
525				usedBy{"lib/libd.so"},
526				notice{},
527			},
528			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
529		},
530		{
531			condition: "proprietary",
532			name:      "apex",
533			roots:     []string{"highest.apex.meta_lic"},
534			expectedOut: []matcher{
535				hr{},
536				library{"Android"},
537				usedBy{"highest.apex/bin/bin2"},
538				usedBy{"highest.apex/lib/libb.so"},
539				restricted{},
540				hr{},
541				library{"Android"},
542				usedBy{"highest.apex"},
543				usedBy{"highest.apex/bin/bin1"},
544				firstParty{},
545				hr{},
546				library{"Android"},
547				usedBy{"highest.apex/bin/bin2"},
548				library{"Device"},
549				usedBy{"highest.apex/bin/bin1"},
550				usedBy{"highest.apex/lib/liba.so"},
551				library{"External"},
552				usedBy{"highest.apex/bin/bin1"},
553				proprietary{},
554			},
555			expectedDeps: []string{
556				"testdata/firstparty/FIRST_PARTY_LICENSE",
557				"testdata/proprietary/PROPRIETARY_LICENSE",
558				"testdata/restricted/RESTRICTED_LICENSE",
559			},
560		},
561		{
562			condition: "proprietary",
563			name:      "container",
564			roots:     []string{"container.zip.meta_lic"},
565			expectedOut: []matcher{
566				hr{},
567				library{"Android"},
568				usedBy{"container.zip/bin2"},
569				usedBy{"container.zip/libb.so"},
570				restricted{},
571				hr{},
572				library{"Android"},
573				usedBy{"container.zip"},
574				usedBy{"container.zip/bin1"},
575				firstParty{},
576				hr{},
577				library{"Android"},
578				usedBy{"container.zip/bin2"},
579				library{"Device"},
580				usedBy{"container.zip/bin1"},
581				usedBy{"container.zip/liba.so"},
582				library{"External"},
583				usedBy{"container.zip/bin1"},
584				proprietary{},
585			},
586			expectedDeps: []string{
587				"testdata/firstparty/FIRST_PARTY_LICENSE",
588				"testdata/proprietary/PROPRIETARY_LICENSE",
589				"testdata/restricted/RESTRICTED_LICENSE",
590			},
591		},
592		{
593			condition: "proprietary",
594			name:      "application",
595			roots:     []string{"application.meta_lic"},
596			expectedOut: []matcher{
597				hr{},
598				library{"Android"},
599				usedBy{"application"},
600				firstParty{},
601				hr{},
602				library{"Device"},
603				usedBy{"application"},
604				proprietary{},
605			},
606			expectedDeps: []string{
607				"testdata/firstparty/FIRST_PARTY_LICENSE",
608				"testdata/proprietary/PROPRIETARY_LICENSE",
609			},
610		},
611		{
612			condition: "proprietary",
613			name:      "binary",
614			roots:     []string{"bin/bin1.meta_lic"},
615			expectedOut: []matcher{
616				hr{},
617				library{"Android"},
618				usedBy{"bin/bin1"},
619				firstParty{},
620				hr{},
621				library{"Device"},
622				usedBy{"bin/bin1"},
623				library{"External"},
624				usedBy{"bin/bin1"},
625				proprietary{},
626			},
627			expectedDeps: []string{
628				"testdata/firstparty/FIRST_PARTY_LICENSE",
629				"testdata/proprietary/PROPRIETARY_LICENSE",
630			},
631		},
632		{
633			condition: "proprietary",
634			name:      "library",
635			roots:     []string{"lib/libd.so.meta_lic"},
636			expectedOut: []matcher{
637				hr{},
638				library{"External"},
639				usedBy{"lib/libd.so"},
640				notice{},
641			},
642			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
643		},
644	}
645	for _, tt := range tests {
646		t.Run(tt.condition+" "+tt.name, func(t *testing.T) {
647			stdout := &bytes.Buffer{}
648			stderr := &bytes.Buffer{}
649
650			rootFiles := make([]string, 0, len(tt.roots))
651			for _, r := range tt.roots {
652				rootFiles = append(rootFiles, "testdata/"+tt.condition+"/"+r)
653			}
654
655			var deps []string
656
657			ctx := context{stdout, stderr, compliance.GetFS(tt.outDir), tt.includeTOC, "", []string{tt.stripPrefix}, tt.title, &deps}
658
659			err := htmlNotice(&ctx, rootFiles...)
660			if err != nil {
661				t.Fatalf("htmlnotice: error = %v, stderr = %v", err, stderr)
662				return
663			}
664			if stderr.Len() > 0 {
665				t.Errorf("htmlnotice: gotStderr = %v, want none", stderr)
666			}
667
668			t.Logf("got stdout: %s", stdout.String())
669
670			t.Logf("want stdout: %s", matcherList(tt.expectedOut).String())
671
672			out := bufio.NewScanner(stdout)
673			lineno := 0
674			inBody := false
675			hasTitle := false
676			ttle, expectTitle := tt.expectedOut[0].(pageTitle)
677			for out.Scan() {
678				line := out.Text()
679				if strings.TrimLeft(line, " ") == "" {
680					continue
681				}
682				if !inBody {
683					if expectTitle {
684						if tl := checkTitle(line); len(tl) > 0 {
685							if tl != ttle.t {
686								t.Errorf("htmlnotice: unexpected title: got %q, want %q", tl, ttle.t)
687							}
688							hasTitle = true
689						}
690					}
691					if bodyTag.MatchString(line) {
692						inBody = true
693						if expectTitle && !hasTitle {
694							t.Errorf("htmlnotice: missing title: got no <title> tag, want <title>%s</title>", ttle.t)
695						}
696					}
697					continue
698				}
699				if boilerPlate.MatchString(line) {
700					continue
701				}
702				if len(tt.expectedOut) <= lineno {
703					t.Errorf("htmlnotice: unexpected output at line %d: got %q, want nothing (wanted %d lines)", lineno+1, line, len(tt.expectedOut))
704				} else if !tt.expectedOut[lineno].isMatch(line) {
705					t.Errorf("htmlnotice: unexpected output at line %d: got %q, want %q", lineno+1, line, tt.expectedOut[lineno].String())
706				}
707				lineno++
708			}
709			if !inBody {
710				t.Errorf("htmlnotice: missing body: got no <body> tag, want <body> tag followed by %s", matcherList(tt.expectedOut).String())
711				return
712			}
713			for ; lineno < len(tt.expectedOut); lineno++ {
714				t.Errorf("htmlnotice: missing output line %d: ended early, want %q", lineno+1, tt.expectedOut[lineno].String())
715			}
716
717			t.Logf("got deps: %q", deps)
718
719			t.Logf("want deps: %q", tt.expectedDeps)
720
721			if g, w := deps, tt.expectedDeps; !reflect.DeepEqual(g, w) {
722				t.Errorf("unexpected deps, wanted:\n%s\ngot:\n%s\n",
723					strings.Join(w, "\n"), strings.Join(g, "\n"))
724			}
725		})
726	}
727}
728
729func checkTitle(line string) string {
730	groups := titleTag.FindStringSubmatch(line)
731	if len(groups) != 2 {
732		return ""
733	}
734	return groups[1]
735}
736
737type matcher interface {
738	isMatch(line string) bool
739	String() string
740}
741
742type pageTitle struct {
743	t string
744}
745
746func (m pageTitle) isMatch(line string) bool {
747	groups := h1Tag.FindStringSubmatch(line)
748	if len(groups) != 2 {
749		return false
750	}
751	return groups[1] == html.EscapeString(m.t)
752}
753
754func (m pageTitle) String() string {
755	return "  <h1>" + html.EscapeString(m.t) + "</h1>"
756}
757
758type toc struct{}
759
760func (m toc) isMatch(line string) bool {
761	return tocTag.MatchString(line)
762}
763
764func (m toc) String() string {
765	return `  <ul class="toc">`
766}
767
768type target struct {
769	name string
770}
771
772func (m target) isMatch(line string) bool {
773	groups := installTarget.FindStringSubmatch(line)
774	if len(groups) != 2 {
775		return false
776	}
777	return strings.HasPrefix(groups[1], "out/") && strings.HasSuffix(groups[1], "/"+html.EscapeString(m.name))
778}
779
780func (m target) String() string {
781	return `  <li id="id#"><strong>` + html.EscapeString(m.name) + `</strong>`
782}
783
784type uses struct {
785	name string
786}
787
788func (m uses) isMatch(line string) bool {
789	groups := libReference.FindStringSubmatch(line)
790	if len(groups) != 2 {
791		return false
792	}
793	return groups[1] == html.EscapeString(m.name)
794}
795
796func (m uses) String() string {
797	return `  <li><a href="#hash">` + html.EscapeString(m.name) + `</a>`
798}
799
800type hr struct{}
801
802func (m hr) isMatch(line string) bool {
803	return horizontalRule.MatchString(line)
804}
805
806func (m hr) String() string {
807	return "  <hr>"
808}
809
810type library struct {
811	name string
812}
813
814func (m library) isMatch(line string) bool {
815	groups := libraryName.FindStringSubmatch(line)
816	if len(groups) != 2 {
817		return false
818	}
819	return groups[1] == html.EscapeString(m.name)
820}
821
822func (m library) String() string {
823	return "  <strong>" + html.EscapeString(m.name) + "</strong> used by:"
824}
825
826type usedBy struct {
827	name string
828}
829
830func (m usedBy) isMatch(line string) bool {
831	groups := usedByTarget.FindStringSubmatch(line)
832	if len(groups) != 2 {
833		return false
834	}
835	return strings.HasPrefix(groups[1], "out/") && strings.HasSuffix(groups[1], "/"+html.EscapeString(m.name))
836}
837
838func (m usedBy) String() string {
839	return "  <li>out/.../" + html.EscapeString(m.name)
840}
841
842func matchesText(line, text string) bool {
843	groups := licenseText.FindStringSubmatch(line)
844	if len(groups) != 2 {
845		return false
846	}
847	return groups[1] == html.EscapeString(text)
848}
849
850func expectedText(text string) string {
851	return `  <a href="#hash"/><pre class="license-text">` + html.EscapeString(text)
852}
853
854type firstParty struct{}
855
856func (m firstParty) isMatch(line string) bool {
857	return matchesText(line, "&&&First Party License&&&")
858}
859
860func (m firstParty) String() string {
861	return expectedText("&&&First Party License&&&")
862}
863
864type notice struct{}
865
866func (m notice) isMatch(line string) bool {
867	return matchesText(line, "%%%Notice License%%%")
868}
869
870func (m notice) String() string {
871	return expectedText("%%%Notice License%%%")
872}
873
874type reciprocal struct{}
875
876func (m reciprocal) isMatch(line string) bool {
877	return matchesText(line, "$$$Reciprocal License$$$")
878}
879
880func (m reciprocal) String() string {
881	return expectedText("$$$Reciprocal License$$$")
882}
883
884type restricted struct{}
885
886func (m restricted) isMatch(line string) bool {
887	return matchesText(line, "###Restricted License###")
888}
889
890func (m restricted) String() string {
891	return expectedText("###Restricted License###")
892}
893
894type proprietary struct{}
895
896func (m proprietary) isMatch(line string) bool {
897	return matchesText(line, "@@@Proprietary License@@@")
898}
899
900func (m proprietary) String() string {
901	return expectedText("@@@Proprietary License@@@")
902}
903
904type matcherList []matcher
905
906func (l matcherList) String() string {
907	var sb strings.Builder
908	for _, m := range l {
909		s := m.String()
910		if s[:3] == s[len(s)-3:] {
911			fmt.Fprintln(&sb)
912		}
913		fmt.Fprintf(&sb, "%s\n", s)
914		if s[:3] == s[len(s)-3:] {
915			fmt.Fprintln(&sb)
916		}
917	}
918	return sb.String()
919}
920