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