1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
// Package license classifies a repository's LICENSE text into a known license
// for display (badge, category color, canonical link). Detection is heuristic —
// it keys off the distinctive phrases each license uses — and degrades to an
// "unknown" license rather than guessing.
package license
import "strings"
// License is a classified license for display.
type License struct {
SPDX string // e.g. "CC-BY-NC-SA-4.0", "MIT" ("" if unknown)
Short string // badge text, e.g. "CC BY-NC-SA", "MIT", "AGPL-3.0"
Name string // full name
URL string // canonical reference
Category string // permissive | weak-copyleft | copyleft | cc | public-domain | unknown
CC []string // CC clause chips: BY, NC, SA, ND (empty for non-CC)
Path string // repo path of the source LICENSE file (set by caller)
}
// Detect classifies LICENSE file contents.
func Detect(b []byte) License {
s := strings.ToLower(string(b))
switch {
case has(s, "creative commons") || strings.Contains(s, "creativecommons.org/licenses") || strings.Contains(s, "creativecommons.org/publicdomain"):
return detectCC(s)
case has(s, "gnu affero general public license"):
return mk("AGPL-3.0", "AGPL-3.0", "GNU Affero General Public License v3.0", "https://www.gnu.org/licenses/agpl-3.0", "copyleft")
case has(s, "gnu lesser general public license"):
return mk("LGPL-3.0", "LGPL-3.0", "GNU Lesser General Public License v3.0", "https://www.gnu.org/licenses/lgpl-3.0", "weak-copyleft")
case has(s, "gnu general public license"):
if has(s, "version 2") {
return mk("GPL-2.0", "GPL-2.0", "GNU General Public License v2.0", "https://www.gnu.org/licenses/gpl-2.0", "copyleft")
}
return mk("GPL-3.0", "GPL-3.0", "GNU General Public License v3.0", "https://www.gnu.org/licenses/gpl-3.0", "copyleft")
case has(s, "apache license") && has(s, "version 2.0"):
return mk("Apache-2.0", "Apache-2.0", "Apache License 2.0", "https://www.apache.org/licenses/LICENSE-2.0", "permissive")
case has(s, "mozilla public license") && has(s, "version 2.0"):
return mk("MPL-2.0", "MPL-2.0", "Mozilla Public License 2.0", "https://www.mozilla.org/MPL/2.0/", "weak-copyleft")
case has(s, "mit license") || (has(s, "permission is hereby granted, free of charge") && !has(s, "without restriction. ")):
return mk("MIT", "MIT", "MIT License", "https://opensource.org/license/mit", "permissive")
case has(s, "redistribution and use in source and binary forms"):
if has(s, "neither the name") {
return mk("BSD-3-Clause", "BSD-3-Clause", "BSD 3-Clause License", "https://opensource.org/license/bsd-3-clause", "permissive")
}
return mk("BSD-2-Clause", "BSD-2-Clause", "BSD 2-Clause License", "https://opensource.org/license/bsd-2-clause", "permissive")
case has(s, "isc license") || has(s, "internet systems consortium"):
return mk("ISC", "ISC", "ISC License", "https://opensource.org/license/isc-license-txt", "permissive")
case has(s, "this is free and unencumbered software released into the public domain"):
return mk("Unlicense", "Unlicense", "The Unlicense", "https://unlicense.org/", "public-domain")
}
return License{Category: "unknown", Short: "License"}
}
// detectCC resolves a Creative Commons license, including clause set + version.
func detectCC(s string) License {
if has(s, "cc0") || has(s, "public domain dedication") {
l := mk("CC0-1.0", "CC0", "Creative Commons Zero v1.0 Universal", "https://creativecommons.org/publicdomain/zero/1.0/", "public-domain")
return l
}
// Clauses, from URL token (by-nc-sa) or the human title.
clauses := []string{"BY"} // every non-CC0 CC license is at least Attribution
if has(s, "noncommercial") || hasToken(s, "nc") {
clauses = append(clauses, "NC")
}
if has(s, "sharealike") || hasToken(s, "sa") {
clauses = append(clauses, "SA")
}
nd := has(s, "noderivatives") || has(s, "no derivative") || hasToken(s, "nd")
if nd {
clauses = append(clauses, "ND")
}
ver := ccVersion(s)
slug := ccSlug(clauses) // by-nc-sa
return License{
SPDX: "CC-" + strings.ToUpper(slug) + "-" + ver,
Short: "CC " + ccShort(clauses),
Name: "Creative Commons " + ccShort(clauses) + " " + ver,
URL: "https://creativecommons.org/licenses/" + slug + "/" + ver + "/",
Category: "cc",
CC: clauses,
}
}
func ccSlug(clauses []string) string {
parts := make([]string, 0, len(clauses))
for _, c := range clauses {
parts = append(parts, strings.ToLower(c))
}
return strings.Join(parts, "-")
}
func ccShort(clauses []string) string { return strings.Join(clauses, "-") }
func ccVersion(s string) string {
for _, v := range []string{"4.0", "3.0", "2.5", "2.0", "1.0"} {
if strings.Contains(s, v) {
return v
}
}
return "4.0"
}
func has(s, sub string) bool { return strings.Contains(s, sub) }
// hasToken matches a standalone token like "nc" inside a license URL slug
// (by-nc-sa) without matching arbitrary substrings.
func hasToken(s, tok string) bool {
return strings.Contains(s, "-"+tok+"-") || strings.Contains(s, "-"+tok+"/") || strings.Contains(s, "/"+tok+"-")
}
func mk(spdx, short, name, url, cat string) License {
return License{SPDX: spdx, Short: short, Name: name, URL: url, Category: cat}
}
|