• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2024 The BoringSSL Authors
2//
3// Permission to use, copy, modify, and/or distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
10// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
12// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
13// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15//go:build ignore
16
17package main
18
19import (
20	"bufio"
21	"cmp"
22	_ "embed"
23	"encoding/json"
24	"flag"
25	"fmt"
26	"iter"
27	"maps"
28	"os"
29	"regexp"
30	"slices"
31	"strconv"
32)
33
34var (
35	outPath     = flag.String("out", "", "The path to write the results in JSON format")
36	comparePath = flag.String("compare", "", "The path to a JSON file to compare against")
37)
38
39func sortedKeyValuePairs[K cmp.Ordered, V any](m map[K]V) iter.Seq2[K, V] {
40	return func(yield func(K, V) bool) {
41		for _, k := range slices.Sorted(maps.Keys(m)) {
42			if !yield(k, m[k]) {
43				return
44			}
45		}
46	}
47}
48
49var copyrightRE = regexp.MustCompile(
50	`Copyright ` +
51		// Ignore (c) and (C)
52		`(?:\([cC]\) )?` +
53		// Capture the starting copyright year.
54		`([0-9]+)` +
55		// Ignore ending copyright year. OpenSSL's "copyright consolidation"
56		// tool rewrites it anyway. We're just interested in looking for which
57		// start years changed, to manually double-check.
58		`(?:[-,][0-9]+)?` +
59		// Some files have a comma after the years.
60		`,?` +
61		// Skip spaces.
62		` *` +
63		// Capture the name. Stop at punctuation and don't pick up trailing
64		// spaces. We don't want to pick up things like "All Rights Reserved".
65		// This does drop things like ", Inc", but this is good enough for a
66		// summary to double-check an otherwise mostly automated process.
67		`([-a-zA-Z ]*[-a-zA-Z])`)
68
69type CopyrightInfo struct {
70	Name      string
71	StartYear int
72}
73
74type FileInfo struct {
75	CopyrightInfos []CopyrightInfo
76}
77
78func (f *FileInfo) MergeFrom(other FileInfo) {
79	f.CopyrightInfos = append(f.CopyrightInfos, other.CopyrightInfos...)
80}
81
82func summarize(info FileInfo) map[string]int {
83	ret := map[string]int{}
84	for _, c := range info.CopyrightInfos {
85		name := c.Name
86		// Apply the same mapping as OpenSSL's "copyright consolidation" script.
87		if name == "The OpenSSL Project" || name == "Eric Young" {
88			name = "The OpenSSL Project Authors"
89		}
90		if old, ok := ret[name]; !ok || old > c.StartYear {
91			ret[name] = c.StartYear
92		}
93	}
94	return ret
95}
96
97func process(path string) (info FileInfo, err error) {
98	f, err := os.Open(path)
99	if err != nil {
100		return
101	}
102	defer f.Close()
103
104	scanner := bufio.NewScanner(f)
105	for scanner.Scan() {
106		m := copyrightRE.FindStringSubmatch(scanner.Text())
107		if m == nil {
108			continue
109		}
110		var year int
111		year, err = strconv.Atoi(m[1])
112		if err != nil {
113			err = fmt.Errorf("error parsing year %q: %s", m[1], err)
114			return
115		}
116		info.CopyrightInfos = append(info.CopyrightInfos, CopyrightInfo{Name: m[2], StartYear: year})
117	}
118	err = scanner.Err()
119	return
120}
121
122func main() {
123	flag.Parse()
124
125	infos := map[string]FileInfo{}
126	for _, path := range flag.Args() {
127		info, err := process(path)
128		if err != nil {
129			fmt.Fprintf(os.Stderr, "Error processing %q: %s\n", path, err)
130			os.Exit(1)
131		}
132		infos[path] = info
133	}
134
135	if len(*outPath) != 0 {
136		data, err := json.Marshal(infos)
137		if err != nil {
138			fmt.Fprintf(os.Stderr, "Error serializing results: %s\n", err)
139			os.Exit(1)
140		}
141		if err := os.WriteFile(*outPath, data, 0666); err != nil {
142			fmt.Fprintf(os.Stderr, "Error writing results: %s\n", err)
143			os.Exit(1)
144		}
145	}
146
147	if len(*comparePath) == 0 {
148		// Print what we have and return.
149		for path, info := range sortedKeyValuePairs(infos) {
150			for _, c := range info.CopyrightInfos {
151				fmt.Printf("%s: %d %s\n", path, c.StartYear, c.Name)
152			}
153		}
154		return
155	}
156
157	oldData, err := os.ReadFile(*comparePath)
158	if err != nil {
159		fmt.Fprintf(os.Stderr, "Error reading file: %s\n", err)
160		os.Exit(1)
161	}
162	var oldInfos map[string]FileInfo
163	if err := json.Unmarshal(oldData, &oldInfos); err != nil {
164		fmt.Fprintf(os.Stderr, "Error decoding %q: %s\n", *comparePath, err)
165		os.Exit(1)
166	}
167	// Output in CSV, so it is easy to paste into a spreadsheet.
168	fmt.Printf("Path,Name,Old Start Year,New Start Year\n")
169	for path, info := range sortedKeyValuePairs(infos) {
170		oldInfo, ok := oldInfos[path]
171		if !ok {
172			fmt.Printf("%s: file not previously present\n", path)
173			continue
174		}
175
176		summary := summarize(info)
177		oldSummary := summarize(oldInfo)
178		for name, year := range sortedKeyValuePairs(summary) {
179			oldYear, ok := oldSummary[name]
180			if !ok {
181				fmt.Printf("%s,%s,-1,%d\n", path, name, year)
182			} else if year != oldYear {
183				fmt.Printf("%s,%s,%d,%d\n", path, name, oldYear, year)
184			}
185		}
186		for oldName, oldYear := range sortedKeyValuePairs(oldSummary) {
187			if _, ok := summary[oldName]; !ok {
188				fmt.Printf("%s,%s,%d,-1\n", path, oldName, oldYear)
189			}
190		}
191	}
192}
193