copyrights.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright (C) 2025 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. //go:build ignore
  7. // +build ignore
  8. // Updates the list of software copyrights in aboutModalView.html based on the
  9. // output of `go mod graph`.
  10. package main
  11. import (
  12. "encoding/base64"
  13. "encoding/json"
  14. "fmt"
  15. "io"
  16. "log"
  17. "net/http"
  18. "net/url"
  19. "os"
  20. "os/exec"
  21. "regexp"
  22. "slices"
  23. "strconv"
  24. "strings"
  25. "time"
  26. "golang.org/x/net/html"
  27. )
  28. var copyrightMap = map[string]string{
  29. // https://github.com/aws/aws-sdk-go/blob/main/NOTICE.txt#L2
  30. "aws/aws-sdk-go": "Copyright © 2015 Amazon.com, Inc. or its affiliates, Copyright 2014-2015 Stripe, Inc",
  31. // https://github.com/ccding/go-stun/blob/master/main.go#L1
  32. "ccding/go-stun": "Copyright © 2016 Cong Ding",
  33. // https://github.com/search?q=repo%3Acertifi%2Fgocertifi%20copyright&type=code
  34. // "certifi/gocertifi": "No copyrights found",
  35. // https://github.com/search?q=repo%3Aebitengine%2Fpurego%20copyright&type=code
  36. "ebitengine/purego": "Copyright © 2022 The Ebitengine Authors",
  37. // https://github.com/search?q=repo%3Agoogle%2Fpprof%20copyright&type=code
  38. "google/pprof": "Copyright © 2016 Google Inc",
  39. // https://github.com/greatroar/blobloom/blob/master/README.md?plain=1#L74
  40. "greatroar/blobloom": "Copyright © 2020-2024 the Blobloom authors",
  41. // https://github.com/jmespath/go-jmespath/blob/master/NOTICE#L2
  42. "jmespath/go-jmespath": "Copyright © 2015 James Saryerwinnie",
  43. // https://github.com/maxmind/geoipupdate/blob/main/README.md?plain=1#L140
  44. "maxmind/geoipupdate": "Copyright © 2018-2024 by MaxMind, Inc",
  45. // https://github.com/search?q=repo%3Aprometheus%2Fclient_golang%20copyright&type=code
  46. "prometheus/client_golang": "Copyright 2012-2015 The Prometheus Authors",
  47. // https://github.com/search?q=repo%3Apuzpuzpuz%2Fxsync%20copyright&type=code
  48. // "puzpuzpuz/xsync": "No copyrights found",
  49. // https://github.com/search?q=repo%3Atklauser%2Fnumcpus%20copyright&type=code
  50. "tklauser/numcpus": "Copyright © 2018-2024 Tobias Klauser",
  51. // https://github.com/search?q=repo%3Auber-go%2Fmock%20copyright&type=code
  52. "go.uber.org/mock": "Copyright © 2010-2022 Google LLC",
  53. }
  54. var urlMap = map[string]string{
  55. "fontawesome.io": "https://github.com/FortAwesome/Font-Awesome",
  56. "go.uber.org/automaxprocs": "https://github.com/uber-go/automaxprocs",
  57. // "go.uber.org/mock": "https://github.com/uber-go/mock",
  58. "google.golang.org/protobuf": "https://github.com/protocolbuffers/protobuf-go",
  59. // "gopkg.in/yaml.v2": "", // ignore, as gopkg.in/yaml.v3 supersedes
  60. // "gopkg.in/yaml.v3": "https://github.com/go-yaml/yaml",
  61. "sigs.k8s.io/yaml": "https://github.com/kubernetes-sigs/yaml",
  62. }
  63. const htmlFile = "gui/default/syncthing/core/aboutModalView.html"
  64. type Type int
  65. const (
  66. // TypeJS defines non-Go copyright notices
  67. TypeJS Type = iota
  68. // TypeKeep defines Go copyright notices for packages that are still used.
  69. TypeKeep
  70. // TypeToss defines Go copyright notices for packages that are no longer used.
  71. TypeToss
  72. // TypeNew defines Go copyright notices for new packages found via `go mod graph`.
  73. TypeNew
  74. )
  75. type CopyrightNotice struct {
  76. Type Type
  77. Name string
  78. HTML string
  79. Module string
  80. URL string
  81. Copyright string
  82. RepoURL string
  83. RepoCopyrights []string
  84. }
  85. var copyrightRe = regexp.MustCompile(`(?s)id="copyright-notices">(.+?)</ul>`)
  86. func main() {
  87. bs := readAll(htmlFile)
  88. matches := copyrightRe.FindStringSubmatch(string(bs))
  89. if len(matches) <= 1 {
  90. log.Fatal("Cannot find id copyright-notices in ", htmlFile)
  91. }
  92. modules := getModules()
  93. notices := parseCopyrightNotices(matches[1])
  94. old := len(notices)
  95. // match up modules to notices
  96. matched := map[string]bool{}
  97. removes := 0
  98. for i, notice := range notices {
  99. if notice.Type == TypeJS {
  100. continue
  101. }
  102. found := ""
  103. for _, module := range modules {
  104. if strings.Contains(module, notice.Name) {
  105. found = module
  106. break
  107. }
  108. }
  109. if found != "" {
  110. matched[found] = true
  111. notices[i].Module = found
  112. continue
  113. }
  114. removes++
  115. fmt.Printf("Removing: %-40s %-55s %s\n", notice.Name, notice.URL, notice.Copyright)
  116. notices[i].Type = TypeToss
  117. }
  118. // add new modules to notices
  119. adds := 0
  120. for _, module := range modules {
  121. _, ok := matched[module]
  122. if ok {
  123. continue
  124. }
  125. adds++
  126. notice := CopyrightNotice{}
  127. notice.Name = module
  128. if strings.HasPrefix(notice.Name, "github.com/") {
  129. notice.Name = strings.ReplaceAll(notice.Name, "github.com/", "")
  130. }
  131. notice.Type = TypeNew
  132. url, ok := urlMap[module]
  133. if ok {
  134. notice.URL = url
  135. notice.RepoURL = url
  136. } else {
  137. notice.URL = "https://" + module
  138. notice.RepoURL = "https://" + module
  139. }
  140. notices = append(notices, notice)
  141. }
  142. if removes == 0 && adds == 0 {
  143. // authors.go is quiet, so let's be quiet too.
  144. // fmt.Printf("No changes detected in %d modules and %d notices\n", len(modules), len(notices))
  145. os.Exit(0)
  146. }
  147. // get copyrights via Github API for new modules
  148. notfound := 0
  149. for i, n := range notices {
  150. if n.Type != TypeNew {
  151. continue
  152. }
  153. copyright, ok := copyrightMap[n.Name]
  154. if ok {
  155. notices[i].Copyright = copyright
  156. continue
  157. }
  158. notices[i].Copyright = defaultCopyright(n)
  159. if strings.Contains(n.URL, "github.com/") {
  160. notices[i].RepoURL = notices[i].URL
  161. owner, repo := parseGitHubURL(n.URL)
  162. licenseText := getLicenseText(owner, repo)
  163. notices[i].RepoCopyrights = extractCopyrights(licenseText, n)
  164. if len(notices[i].RepoCopyrights) > 0 {
  165. notices[i].Copyright = notices[i].RepoCopyrights[0]
  166. }
  167. notices[i].HTML = fmt.Sprintf("<li><a href=\"%s\">%s</a>, %s.</li>", n.URL, n.Name, notices[i].Copyright)
  168. if len(notices[i].RepoCopyrights) > 0 {
  169. continue
  170. }
  171. }
  172. fmt.Printf("Copyright not found: %-30s : using %q\n", n.Name, notices[i].Copyright)
  173. notfound++
  174. }
  175. replacements := write(notices, bs)
  176. fmt.Printf("Removed: %3d\n", removes)
  177. fmt.Printf("Added: %3d\n", adds)
  178. fmt.Printf("Copyrights not found: %3d\n", notfound)
  179. fmt.Printf("Old package count: %3d\n", old)
  180. fmt.Printf("New package count: %3d\n", replacements)
  181. }
  182. func write(notices []CopyrightNotice, bs []byte) int {
  183. keys := make([]string, 0, len(notices))
  184. noticeMap := make(map[string]CopyrightNotice, 0)
  185. for _, n := range notices {
  186. if n.Type != TypeKeep && n.Type != TypeNew {
  187. continue
  188. }
  189. if n.Type == TypeNew {
  190. fmt.Printf("Adding: %-40s %-55s %s\n", n.Name, n.URL, n.Copyright)
  191. }
  192. keys = append(keys, n.Name)
  193. noticeMap[n.Name] = n
  194. }
  195. slices.Sort(keys)
  196. indent := " "
  197. replacements := []string{}
  198. for _, n := range notices {
  199. if n.Type != TypeJS {
  200. continue
  201. }
  202. replacements = append(replacements, indent+n.HTML)
  203. }
  204. for _, k := range keys {
  205. n := noticeMap[k]
  206. line := fmt.Sprintf("%s<li><a href=\"%s\">%s</a>, %s.</li>", indent, n.URL, n.Name, n.Copyright)
  207. replacements = append(replacements, line)
  208. }
  209. replacement := strings.Join(replacements, "\n")
  210. bs = copyrightRe.ReplaceAll(bs, []byte("id=\"copyright-notices\">\n"+replacement+"\n </ul>"))
  211. writeFile(htmlFile, string(bs))
  212. return len(replacements)
  213. }
  214. func readAll(path string) []byte {
  215. fd, err := os.Open(path)
  216. if err != nil {
  217. log.Fatal(err)
  218. }
  219. defer fd.Close()
  220. bs, err := io.ReadAll(fd)
  221. if err != nil {
  222. log.Fatal(err)
  223. }
  224. return bs
  225. }
  226. func writeFile(path string, data string) {
  227. err := os.WriteFile(path, []byte(data), 0o644)
  228. if err != nil {
  229. log.Fatal(err)
  230. }
  231. }
  232. func getModules() []string {
  233. ignoreRe := regexp.MustCompile(`golang\.org/x/|github\.com/syncthing|^[^.]+(/|$)`)
  234. // List all modules (used for mapping packages to modules)
  235. data, err := exec.Command("go", "list", "-m", "all").Output()
  236. if err != nil {
  237. log.Fatalf("go list -m all: %v", err)
  238. }
  239. modules := strings.Split(string(data), "\n")
  240. for i := range modules {
  241. modules[i], _, _ = strings.Cut(modules[i], " ")
  242. }
  243. modules = slices.DeleteFunc(modules, func(s string) bool { return s == "" })
  244. // List all packages in use by the syncthing binary, map them to modules
  245. data, err = exec.Command("go", "list", "-deps", "./cmd/syncthing").Output()
  246. if err != nil {
  247. log.Fatalf("go list -deps ./cmd/syncthing: %v", err)
  248. }
  249. packages := strings.Split(string(data), "\n")
  250. packages = slices.DeleteFunc(packages, func(s string) bool { return s == "" })
  251. seen := make(map[string]struct{})
  252. for _, pkg := range packages {
  253. if ignoreRe.MatchString(pkg) {
  254. continue
  255. }
  256. // Find module for package
  257. modIdx := slices.IndexFunc(modules, func(mod string) bool {
  258. return strings.HasPrefix(pkg, mod)
  259. })
  260. if modIdx < 0 {
  261. log.Println("no module for", pkg)
  262. continue
  263. }
  264. module := modules[modIdx]
  265. seen[module] = struct{}{}
  266. }
  267. adds := make([]string, 0)
  268. for k := range seen {
  269. adds = append(adds, k)
  270. }
  271. slices.Sort(adds)
  272. return adds
  273. }
  274. func parseCopyrightNotices(input string) []CopyrightNotice {
  275. doc, err := html.Parse(strings.NewReader("<ul>" + input + "</ul>"))
  276. if err != nil {
  277. log.Fatal(err)
  278. }
  279. var notices []CopyrightNotice
  280. typ := TypeJS
  281. var f func(*html.Node)
  282. f = func(n *html.Node) {
  283. if n.Type == html.ElementNode && n.Data == "li" {
  284. var notice CopyrightNotice
  285. var aFound bool
  286. for c := n.FirstChild; c != nil; c = c.NextSibling {
  287. if c.Type == html.ElementNode && c.Data == "a" {
  288. aFound = true
  289. for _, attr := range c.Attr {
  290. if attr.Key == "href" {
  291. notice.URL = attr.Val
  292. }
  293. }
  294. if c.FirstChild != nil && c.FirstChild.Type == html.TextNode {
  295. notice.Name = strings.TrimSpace(c.FirstChild.Data)
  296. }
  297. } else if c.Type == html.TextNode && aFound {
  298. // Anything after <a> is considered the copyright
  299. notice.Copyright = strings.TrimSpace(html.UnescapeString(c.Data))
  300. notice.Copyright = strings.Trim(notice.Copyright, "., ")
  301. }
  302. if typ == TypeJS && strings.Contains(notice.URL, "AudriusButkevicius") {
  303. typ = TypeKeep
  304. }
  305. notice.Type = typ
  306. var buf strings.Builder
  307. _ = html.Render(&buf, n)
  308. notice.HTML = buf.String()
  309. }
  310. notice.Copyright = strings.ReplaceAll(notice.Copyright, "©", "&copy;")
  311. notice.HTML = strings.ReplaceAll(notice.HTML, "©", "&copy;")
  312. notices = append(notices, notice)
  313. }
  314. for c := n.FirstChild; c != nil; c = c.NextSibling {
  315. f(c)
  316. }
  317. }
  318. f(doc)
  319. return notices
  320. }
  321. func parseGitHubURL(u string) (string, string) {
  322. parsed, err := url.Parse(u)
  323. if err != nil {
  324. log.Fatal(err)
  325. }
  326. parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
  327. if len(parts) < 2 {
  328. log.Fatal(fmt.Errorf("invalid GitHub URL: %q", parsed.Path))
  329. }
  330. return parts[0], parts[1]
  331. }
  332. func getLicenseText(owner, repo string) string {
  333. url := fmt.Sprintf("https://api.github.com/repos/%s/%s/license", owner, repo)
  334. req, _ := http.NewRequest("GET", url, nil)
  335. req.Header.Set("Accept", "application/vnd.github.v3+json")
  336. if token := os.Getenv("GITHUB_TOKEN"); token != "" {
  337. req.Header.Set("Authorization", "Bearer "+token)
  338. }
  339. resp, err := http.DefaultClient.Do(req)
  340. if err != nil {
  341. log.Fatal(err)
  342. }
  343. defer resp.Body.Close()
  344. if resp.StatusCode == 404 {
  345. return ""
  346. }
  347. var result struct {
  348. Content string `json:"content"`
  349. Encoding string `json:"encoding"`
  350. }
  351. body, _ := io.ReadAll(resp.Body)
  352. err = json.Unmarshal(body, &result)
  353. if err != nil {
  354. log.Fatal(err)
  355. }
  356. if result.Encoding != "base64" {
  357. log.Fatal(fmt.Sprintf("unexpected encoding: %q", result.Encoding))
  358. }
  359. decoded, err := base64.StdEncoding.DecodeString(result.Content)
  360. if err != nil {
  361. log.Fatal(err)
  362. }
  363. return string(decoded)
  364. }
  365. func extractCopyrights(license string, notice CopyrightNotice) []string {
  366. lines := strings.Split(license, "\n")
  367. re := regexp.MustCompile(`(?i)^\s*(copyright\s*(?:©|\(c\)|&copy;|19|20).*)$`)
  368. copyrights := []string{}
  369. for _, line := range lines {
  370. if matches := re.FindStringSubmatch(strings.TrimSpace(line)); len(matches) == 2 {
  371. copyright := strings.TrimSpace(matches[1])
  372. re := regexp.MustCompile(`(?i)all rights reserved`)
  373. copyright = re.ReplaceAllString(copyright, "")
  374. copyright = strings.ReplaceAll(copyright, "©", "&copy;")
  375. copyright = strings.ReplaceAll(copyright, "(C)", "&copy;")
  376. copyright = strings.ReplaceAll(copyright, "(c)", "&copy;")
  377. copyright = strings.Trim(copyright, "., ")
  378. copyrights = append(copyrights, copyright)
  379. }
  380. }
  381. if len(copyrights) > 0 {
  382. return copyrights
  383. }
  384. return []string{}
  385. }
  386. func defaultCopyright(n CopyrightNotice) string {
  387. year := time.Now().Format("2006")
  388. return fmt.Sprintf("Copyright &copy; %v, the %s authors", year, n.Name)
  389. }
  390. func writeNotices(path string, notices []CopyrightNotice) {
  391. s := ""
  392. for i, n := range notices {
  393. s += "# : " + strconv.Itoa(i) + "\n" + n.String()
  394. }
  395. writeFile(path, s)
  396. }
  397. func (n CopyrightNotice) String() string {
  398. return fmt.Sprintf("Type : %v\nHTML : %v\nName : %v\nModule : %v\nURL : %v\nCopyright: %v\nRepoURL : %v\nRepoCopys: %v\n\n",
  399. n.Type, n.HTML, n.Name, n.Module, n.URL, n.Copyright, n.RepoURL, strings.Join(n.RepoCopyrights, ","))
  400. }
  401. func (t Type) String() string {
  402. switch t {
  403. case TypeJS:
  404. return "TypeJS"
  405. case TypeKeep:
  406. return "TypeKeep"
  407. case TypeToss:
  408. return "TypeToss"
  409. case TypeNew:
  410. return "TypeNew"
  411. default:
  412. return "unknown"
  413. }
  414. }