oficina / estilo Public

Clone
git clone https://amadan.net/oficina/estilo

Plain git — no account needed to clone.

Download

Download this file

1// Package catalog collects notices from the exact packages selected by go list.
2// It reads root and package-ancestor notice files, including vendored notices
3// such as gormlite/LICENSE and libc's third-party licence bundle.
4package catalog
5
6import (
7 "encoding/json"
8 "errors"
9 "io"
10 "os"
11 "path/filepath"
12 "sort"
13 "strings"
14 "unicode/utf8"
15
16 "amadan.net/oficina/estilo.git/credits"
17)
18
19type Module struct {
20 Path, Version, Dir string
21 Main bool
22 Replace *Module
23}
24type Package struct {
25 Dir string
26 Module *Module
27 Error *struct{ Err string }
28}
29type Inventory struct {
30 Components []credits.Component `json:"components"`
31 MissingNotices []string `json:"missing_notices"`
32}
33
34// Collect expects go list -deps -json for one production entry point. Standard
35// library notices are supplied separately by the command from that Go toolchain.
36// First-party module names are excluded only by the caller's exact list.
37func Collect(input io.Reader, exclude map[string]bool) (Inventory, error) {
38 decoder := json.NewDecoder(input)
39 modules := map[string]*Module{}
40 directories := map[string]map[string]bool{}
41 for {
42 var p Package
43 err := decoder.Decode(&p)
44 if errors.Is(err, io.EOF) {
45 break
46 }
47 if err != nil {
48 return Inventory{}, err
49 }
50 if p.Error != nil {
51 return Inventory{}, errors.New("credits: package resolution failed")
52 }
53 m := p.Module
54 if m == nil || m.Main || exclude[m.Path] {
55 continue
56 }
57 if m.Replace != nil {
58 return Inventory{}, errors.New("credits: replaced module requires explicit inventory")
59 }
60 if m.Dir == "" || m.Path == "" || m.Version == "" || p.Dir == "" {
61 return Inventory{}, errors.New("credits: incomplete module provenance")
62 }
63 root := filepath.Clean(m.Dir)
64 rel, err := filepath.Rel(root, p.Dir)
65 if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
66 return Inventory{}, errors.New("credits: package outside module")
67 }
68 if previous := modules[m.Path]; previous != nil && (previous.Version != m.Version || previous.Dir != m.Dir) {
69 return Inventory{}, errors.New("credits: conflicting module versions")
70 }
71 modules[m.Path] = m
72 if directories[m.Path] == nil {
73 directories[m.Path] = map[string]bool{}
74 }
75 for dir := filepath.Clean(p.Dir); ; dir = filepath.Dir(dir) {
76 directories[m.Path][dir] = true
77 if dir == root {
78 break
79 }
80 }
81 }
82 out := Inventory{Components: []credits.Component{}, MissingNotices: []string{}}
83 for name, m := range modules {
84 component := credits.Component{Name: name, Version: m.Version, SourceURL: moduleURL(m), Notices: []credits.Notice{}}
85 for dir := range directories[name] {
86 files, err := os.ReadDir(dir)
87 if err != nil {
88 return Inventory{}, err
89 }
90 for _, entry := range files {
91 if entry.IsDir() || !noticeFilename(entry.Name()) {
92 continue
93 }
94 full := filepath.Join(dir, entry.Name())
95 info, err := os.Lstat(full)
96 if err != nil {
97 return Inventory{}, err
98 }
99 if !info.Mode().IsRegular() || info.Size() > 2<<20 {
100 return Inventory{}, errors.New("credits: unsupported notice file")
101 }
102 raw, err := os.ReadFile(full)
103 if err != nil {
104 return Inventory{}, err
105 }
106 if !utf8.Valid(raw) || strings.TrimSpace(string(raw)) == "" {
107 return Inventory{}, errors.New("credits: empty or non-UTF-8 notice")
108 }
109 relative, err := filepath.Rel(m.Dir, full)
110 if err != nil {
111 return Inventory{}, err
112 }
113 component.Notices = append(component.Notices, credits.Notice{Name: filepath.ToSlash(relative), Text: string(raw)})
114 }
115 }
116 sort.Slice(component.Notices, func(i, j int) bool { return component.Notices[i].Name < component.Notices[j].Name })
117 if len(component.Notices) == 0 {
118 out.MissingNotices = append(out.MissingNotices, name)
119 }
120 out.Components = append(out.Components, component)
121 }
122 sort.Slice(out.Components, func(i, j int) bool { return out.Components[i].Name < out.Components[j].Name })
123 sort.Strings(out.MissingNotices)
124 return out, nil
125}
126
127func noticeFilename(name string) bool {
128 name = strings.ToUpper(name)
129 if strings.HasSuffix(name, ".GO") {
130 return false
131 }
132 for _, prefix := range []string{"LICENSE", "LICENCE", "COPYING", "NOTICE"} {
133 if name == prefix || strings.HasPrefix(name, prefix+".") || strings.HasPrefix(name, prefix+"-") {
134 return true
135 }
136 }
137 return false
138}
139func moduleURL(m *Module) string {
140 if strings.HasPrefix(m.Path, "amadan.net/") {
141 parts := strings.Split(m.Path, "/")
142 if len(parts) >= 3 {
143 return "https://" + parts[0] + "/" + parts[1] + "/" + strings.TrimSuffix(parts[2], ".git")
144 }
145 return ""
146 }
147 return "https://pkg.go.dev/" + m.Path + "@" + m.Version
148}
149