123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- // Copyright (C) 2025 The Syncthing Authors.
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this file,
- // You can obtain one at https://mozilla.org/MPL/2.0/.
- //go:build ignore
- // +build ignore
- // Updates the list of software copyrights in aboutModalView.html based on the
- // output of `go mod graph`.
- package main
- import (
- "bufio"
- "bytes"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "net/url"
- "os"
- "os/exec"
- "regexp"
- "slices"
- "strconv"
- "strings"
- "time"
- "golang.org/x/net/html"
- )
- var copyrightMap = map[string]string{
- // https://github.com/aws/aws-sdk-go/blob/main/NOTICE.txt#L2
- "aws/aws-sdk-go": "Copyright © 2015 Amazon.com, Inc. or its affiliates, Copyright 2014-2015 Stripe, Inc",
- // https://github.com/ccding/go-stun/blob/master/main.go#L1
- "ccding/go-stun": "Copyright © 2016 Cong Ding",
- // https://github.com/search?q=repo%3Acertifi%2Fgocertifi%20copyright&type=code
- // "certifi/gocertifi": "No copyrights found",
- // https://github.com/search?q=repo%3Aebitengine%2Fpurego%20copyright&type=code
- "ebitengine/purego": "Copyright © 2022 The Ebitengine Authors",
- // https://github.com/search?q=repo%3Agoogle%2Fpprof%20copyright&type=code
- "google/pprof": "Copyright © 2016 Google Inc",
- // https://github.com/greatroar/blobloom/blob/master/README.md?plain=1#L74
- "greatroar/blobloom": "Copyright © 2020-2024 the Blobloom authors",
- // https://github.com/jmespath/go-jmespath/blob/master/NOTICE#L2
- "jmespath/go-jmespath": "Copyright © 2015 James Saryerwinnie",
- // https://github.com/maxmind/geoipupdate/blob/main/README.md?plain=1#L140
- "maxmind/geoipupdate": "Copyright © 2018-2024 by MaxMind, Inc",
- // https://github.com/search?q=repo%3Apuzpuzpuz%2Fxsync%20copyright&type=code
- // "puzpuzpuz/xsync": "No copyrights found",
- // https://github.com/search?q=repo%3Atklauser%2Fnumcpus%20copyright&type=code
- "tklauser/numcpus": "Copyright © 2018-2024 Tobias Klauser",
- // https://github.com/search?q=repo%3Auber-go%2Fmock%20copyright&type=code
- "go.uber.org/mock": "Copyright © 2010-2022 Google LLC",
- }
- var urlMap = map[string]string{
- "fontawesome.io": "https://github.com/FortAwesome/Font-Awesome",
- "go.uber.org/automaxprocs": "https://github.com/uber-go/automaxprocs",
- "go.uber.org/mock": "https://github.com/uber-go/mock",
- "google.golang.org/protobuf": "https://github.com/protocolbuffers/protobuf-go",
- "gopkg.in/yaml.v2": "", // ignore, as gopkg.in/yaml.v3 supersedes
- "gopkg.in/yaml.v3": "https://github.com/go-yaml/yaml",
- "sigs.k8s.io/yaml": "https://github.com/kubernetes-sigs/yaml",
- }
- const htmlFile = "gui/default/syncthing/core/aboutModalView.html"
- type Type int
- const (
- // TypeJS defines non-Go copyright notices
- TypeJS Type = iota
- // TypeKeep defines Go copyright notices for packages that are still used.
- TypeKeep
- // TypeToss defines Go copyright notices for packages that are no longer used.
- TypeToss
- // TypeNew defines Go copyright notices for new packages found via `go mod graph`.
- TypeNew
- )
- type CopyrightNotice struct {
- Type Type
- Name string
- HTML string
- Module string
- URL string
- Copyright string
- RepoURL string
- RepoCopyrights []string
- }
- var copyrightRe = regexp.MustCompile(`(?s)id="copyright-notices">(.+?)</ul>`)
- func main() {
- bs := readAll(htmlFile)
- matches := copyrightRe.FindStringSubmatch(string(bs))
- if len(matches) <= 1 {
- log.Fatal("Cannot find id copyright-notices in ", htmlFile)
- }
- modules := getModules()
- notices := parseCopyrightNotices(matches[1])
- old := len(notices)
- // match up modules to notices
- matched := map[string]bool{}
- removes := 0
- for i, notice := range notices {
- if notice.Type == TypeJS {
- continue
- }
- found := ""
- for _, module := range modules {
- if strings.Contains(module, notice.Name) {
- found = module
- break
- }
- }
- if found != "" {
- matched[found] = true
- notices[i].Module = found
- continue
- }
- removes++
- fmt.Printf("Removing: %-40s %-55s %s\n", notice.Name, notice.URL, notice.Copyright)
- notices[i].Type = TypeToss
- }
- // add new modules to notices
- adds := 0
- for _, module := range modules {
- _, ok := matched[module]
- if ok {
- continue
- }
- adds++
- notice := CopyrightNotice{}
- notice.Name = module
- if strings.HasPrefix(notice.Name, "github.com/") {
- notice.Name = strings.ReplaceAll(notice.Name, "github.com/", "")
- }
- notice.Type = TypeNew
- url, ok := urlMap[module]
- if ok {
- notice.URL = url
- notice.RepoURL = url
- } else {
- notice.URL = "https://" + module
- notice.RepoURL = "https://" + module
- }
- notices = append(notices, notice)
- }
- if removes == 0 && adds == 0 {
- // authors.go is quiet, so let's be quiet too.
- // fmt.Printf("No changes detected in %d modules and %d notices\n", len(modules), len(notices))
- os.Exit(0)
- }
- // get copyrights via Github API for new modules
- notfound := 0
- for i, n := range notices {
- if n.Type != TypeNew {
- continue
- }
- copyright, ok := copyrightMap[n.Name]
- if ok {
- notices[i].Copyright = copyright
- continue
- }
- notices[i].Copyright = defaultCopyright(n)
- if strings.Contains(n.URL, "github.com/") {
- notices[i].RepoURL = notices[i].URL
- owner, repo := parseGitHubURL(n.URL)
- licenseText := getLicenseText(owner, repo)
- notices[i].RepoCopyrights = extractCopyrights(licenseText, n)
- if len(notices[i].RepoCopyrights) > 0 {
- notices[i].Copyright = notices[i].RepoCopyrights[0]
- }
- notices[i].HTML = fmt.Sprintf("<li><a href=\"%s\">%s</a>, %s.</li>", n.URL, n.Name, notices[i].Copyright)
- if len(notices[i].RepoCopyrights) > 0 {
- continue
- }
- }
- fmt.Printf("Copyright not found: %-30s : using %q\n", n.Name, notices[i].Copyright)
- notfound++
- }
- replacements := write(notices, bs)
- fmt.Printf("Removed: %3d\n", removes)
- fmt.Printf("Added: %3d\n", adds)
- fmt.Printf("Copyrights not found: %3d\n", notfound)
- fmt.Printf("Old package count: %3d\n", old)
- fmt.Printf("New package count: %3d\n", replacements)
- }
- func write(notices []CopyrightNotice, bs []byte) int {
- keys := make([]string, 0, len(notices))
- noticeMap := make(map[string]CopyrightNotice, 0)
- for _, n := range notices {
- if n.Type != TypeKeep && n.Type != TypeNew {
- continue
- }
- if n.Type == TypeNew {
- fmt.Printf("Adding: %-40s %-55s %s\n", n.Name, n.URL, n.Copyright)
- }
- keys = append(keys, n.Name)
- noticeMap[n.Name] = n
- }
- slices.Sort(keys)
- indent := " "
- replacements := []string{}
- for _, n := range notices {
- if n.Type != TypeJS {
- continue
- }
- replacements = append(replacements, indent+n.HTML)
- }
- for _, k := range keys {
- n := noticeMap[k]
- line := fmt.Sprintf("%s<li><a href=\"%s\">%s</a>, %s.</li>", indent, n.URL, n.Name, n.Copyright)
- replacements = append(replacements, line)
- }
- replacement := strings.Join(replacements, "\n")
- bs = copyrightRe.ReplaceAll(bs, []byte("id=\"copyright-notices\">\n"+replacement+"\n </ul>"))
- writeFile(htmlFile, string(bs))
- return len(replacements)
- }
- func readAll(path string) []byte {
- fd, err := os.Open(path)
- if err != nil {
- log.Fatal(err)
- }
- defer fd.Close()
- bs, err := io.ReadAll(fd)
- if err != nil {
- log.Fatal(err)
- }
- return bs
- }
- func writeFile(path string, data string) {
- err := os.WriteFile(path, []byte(data), 0o644)
- if err != nil {
- log.Fatal(err)
- }
- }
- func getModules() []string {
- cmd := exec.Command("go", "mod", "graph")
- output, err := cmd.Output()
- if err != nil {
- log.Fatal(err)
- }
- seen := make(map[string]struct{})
- scanner := bufio.NewScanner(bytes.NewReader(output))
- for scanner.Scan() {
- line := scanner.Text()
- fields := strings.Fields(line)
- if len(fields) == 0 {
- continue
- }
- if !strings.HasPrefix(fields[0], "github.com/syncthing/syncthing") {
- continue
- }
- // Get left-hand side of dependency pair (before '@')
- mod := strings.SplitN(fields[1], "@", 2)[0]
- // Keep only first 3 path components
- parts := strings.Split(mod, "/")
- if len(parts) == 1 {
- continue
- }
- short := strings.Join(parts[:min(len(parts), 3)], "/")
- if strings.HasPrefix(short, "golang.org/x") ||
- strings.HasPrefix(short, "github.com/prometheus") ||
- short == "go" {
- continue
- }
- seen[short] = struct{}{}
- }
- adds := make([]string, 0)
- for k := range seen {
- adds = append(adds, k)
- }
- slices.Sort(adds)
- return adds
- }
- func parseCopyrightNotices(input string) []CopyrightNotice {
- doc, err := html.Parse(strings.NewReader("<ul>" + input + "</ul>"))
- if err != nil {
- log.Fatal(err)
- }
- var notices []CopyrightNotice
- typ := TypeJS
- var f func(*html.Node)
- f = func(n *html.Node) {
- if n.Type == html.ElementNode && n.Data == "li" {
- var notice CopyrightNotice
- var aFound bool
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- if c.Type == html.ElementNode && c.Data == "a" {
- aFound = true
- for _, attr := range c.Attr {
- if attr.Key == "href" {
- notice.URL = attr.Val
- }
- }
- if c.FirstChild != nil && c.FirstChild.Type == html.TextNode {
- notice.Name = strings.TrimSpace(c.FirstChild.Data)
- }
- } else if c.Type == html.TextNode && aFound {
- // Anything after <a> is considered the copyright
- notice.Copyright = strings.TrimSpace(html.UnescapeString(c.Data))
- notice.Copyright = strings.Trim(notice.Copyright, "., ")
- }
- if typ == TypeJS && strings.Contains(notice.URL, "AudriusButkevicius") {
- typ = TypeKeep
- }
- notice.Type = typ
- var buf strings.Builder
- _ = html.Render(&buf, n)
- notice.HTML = buf.String()
- }
- notice.Copyright = strings.ReplaceAll(notice.Copyright, "©", "©")
- notice.HTML = strings.ReplaceAll(notice.HTML, "©", "©")
- notices = append(notices, notice)
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- f(c)
- }
- }
- f(doc)
- return notices
- }
- func parseGitHubURL(u string) (string, string) {
- parsed, err := url.Parse(u)
- if err != nil {
- log.Fatal(err)
- }
- parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
- if len(parts) < 2 {
- log.Fatal(fmt.Errorf("invalid GitHub URL: %q", parsed.Path))
- }
- return parts[0], parts[1]
- }
- func getLicenseText(owner, repo string) string {
- url := fmt.Sprintf("https://api.github.com/repos/%s/%s/license", owner, repo)
- req, _ := http.NewRequest("GET", url, nil)
- req.Header.Set("Accept", "application/vnd.github.v3+json")
- if token := os.Getenv("GITHUB_TOKEN"); token != "" {
- req.Header.Set("Authorization", "Bearer "+token)
- }
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
- var result struct {
- Content string `json:"content"`
- Encoding string `json:"encoding"`
- }
- body, _ := io.ReadAll(resp.Body)
- err = json.Unmarshal(body, &result)
- if err != nil {
- log.Fatal(err)
- }
- if result.Encoding != "base64" {
- log.Fatal(fmt.Sprintf("unexpected encoding: %s", result.Encoding))
- }
- decoded, err := base64.StdEncoding.DecodeString(result.Content)
- if err != nil {
- log.Fatal(err)
- }
- return string(decoded)
- }
- func extractCopyrights(license string, notice CopyrightNotice) []string {
- lines := strings.Split(license, "\n")
- re := regexp.MustCompile(`(?i)^\s*(copyright\s*(?:©|\(c\)|©|19|20).*)$`)
- copyrights := []string{}
- for _, line := range lines {
- if matches := re.FindStringSubmatch(strings.TrimSpace(line)); len(matches) == 2 {
- copyright := strings.TrimSpace(matches[1])
- re := regexp.MustCompile(`(?i)all rights reserved`)
- copyright = re.ReplaceAllString(copyright, "")
- copyright = strings.ReplaceAll(copyright, "©", "©")
- copyright = strings.ReplaceAll(copyright, "(C)", "©")
- copyright = strings.ReplaceAll(copyright, "(c)", "©")
- copyright = strings.Trim(copyright, "., ")
- copyrights = append(copyrights, copyright)
- }
- }
- if len(copyrights) > 0 {
- return copyrights
- }
- return []string{}
- }
- func defaultCopyright(n CopyrightNotice) string {
- year := time.Now().Format("2006")
- return fmt.Sprintf("Copyright © %v, the %s authors", year, n.Name)
- }
- func writeNotices(path string, notices []CopyrightNotice) {
- s := ""
- for i, n := range notices {
- s += "# : " + strconv.Itoa(i) + "\n" + n.String()
- }
- writeFile(path, s)
- }
- func (n CopyrightNotice) String() string {
- return fmt.Sprintf("Type : %v\nHTML : %v\nName : %v\nModule : %v\nURL : %v\nCopyright: %v\nRepoURL : %v\nRepoCopys: %v\n\n",
- n.Type, n.HTML, n.Name, n.Module, n.URL, n.Copyright, n.RepoURL, strings.Join(n.RepoCopyrights, ","))
- }
- func (t Type) String() string {
- switch t {
- case TypeJS:
- return "TypeJS"
- case TypeKeep:
- return "TypeKeep"
- case TypeToss:
- return "TypeToss"
- case TypeNew:
- return "TypeNew"
- default:
- return "unknown"
- }
- }
|