spdx 1.4.3 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.rubocop.yml +47 -7
- data/Gemfile +3 -1
- data/Rakefile +5 -3
- data/bin/update-license-files +3 -0
- data/exceptions.json +466 -0
- data/lib/exception.rb +14 -0
- data/lib/license.rb +14 -0
- data/lib/spdx.rb +171 -108
- data/lib/spdx/version.rb +3 -1
- data/lib/spdx_grammar.rb +49 -0
- data/lib/spdx_parser.rb +40 -0
- data/lib/spdx_parser.treetop +51 -0
- data/licenses.json +5297 -0
- data/spdx.gemspec +18 -15
- data/spec/spdx_spec.rb +201 -155
- data/spec/spec_helper.rb +4 -2
- metadata +36 -14
data/lib/exception.rb
ADDED
data/lib/license.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spdx
|
4
|
+
class License
|
5
|
+
attr_reader :id, :name, :osi_approved
|
6
|
+
alias osi_approved? osi_approved
|
7
|
+
|
8
|
+
def initialize(id, name, osi_approved)
|
9
|
+
@id = id
|
10
|
+
@name = name
|
11
|
+
@osi_approved = osi_approved
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/spdx.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spdx/version"
|
4
|
+
require "fuzzy_match"
|
5
|
+
require "spdx_parser"
|
6
|
+
require "json"
|
7
|
+
require_relative "exception"
|
8
|
+
require_relative "license"
|
4
9
|
|
5
10
|
# Fuzzy matcher for licenses to SPDX standard licenses
|
6
|
-
module Spdx
|
11
|
+
module Spdx
|
7
12
|
def self.find(name)
|
8
13
|
name = name.strip
|
9
14
|
return nil if commercial?(name)
|
@@ -19,23 +24,23 @@ module Spdx # rubocop:disable Metrics/ModuleLength
|
|
19
24
|
end
|
20
25
|
|
21
26
|
def self.commercial?(name)
|
22
|
-
name.casecmp(
|
27
|
+
name.casecmp("commercial").zero?
|
23
28
|
end
|
24
29
|
|
25
30
|
def self.non_spdx?(name)
|
26
|
-
[
|
31
|
+
["standard pil license"].include? name.downcase
|
27
32
|
end
|
28
33
|
|
29
34
|
def self.lookup(name)
|
30
35
|
return false if name.nil?
|
31
|
-
return
|
36
|
+
return lookup_license(name) if license_exists?(name)
|
32
37
|
|
33
|
-
lowercase =
|
34
|
-
|
38
|
+
lowercase = licenses.keys.sort.find { |k| k.casecmp(name).zero? }
|
39
|
+
lookup_license(lowercase) if lowercase
|
35
40
|
end
|
36
41
|
|
37
42
|
def self.closest(name)
|
38
|
-
name.gsub!(/#{stop_words.join('|')}/i,
|
43
|
+
name.gsub!(/#{stop_words.join('|')}/i, "")
|
39
44
|
name.gsub!(/(\d)/, ' \1 ')
|
40
45
|
best_match = fuzzy_match(name)
|
41
46
|
return nil unless best_match
|
@@ -45,8 +50,8 @@ module Spdx # rubocop:disable Metrics/ModuleLength
|
|
45
50
|
|
46
51
|
def self.matches(name, max_distance = 40)
|
47
52
|
names.map { |key| [key, Text::Levenshtein.distance(name, key)] }
|
48
|
-
|
49
|
-
|
53
|
+
.select { |arr| arr[1] <= max_distance }
|
54
|
+
.sort_by { |arr| arr[1] }
|
50
55
|
end
|
51
56
|
|
52
57
|
def self.fuzzy_match(name)
|
@@ -58,7 +63,7 @@ module Spdx # rubocop:disable Metrics/ModuleLength
|
|
58
63
|
end
|
59
64
|
|
60
65
|
def self.find_by_name(name)
|
61
|
-
match =
|
66
|
+
match = licenses.find { |_k, v| v["name"] == name }
|
62
67
|
lookup(match[0]) if match
|
63
68
|
end
|
64
69
|
|
@@ -76,105 +81,163 @@ module Spdx # rubocop:disable Metrics/ModuleLength
|
|
76
81
|
lookup "#{match[1]}GPL-#{match[2]}.#{match[3] || 0}#{match[4]}"
|
77
82
|
end
|
78
83
|
|
79
|
-
def self.special_cases
|
84
|
+
def self.special_cases
|
80
85
|
{
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
86
|
+
"perl_5" => "Artistic-1.0-Perl",
|
87
|
+
"bsd3" => "BSD-3-Clause",
|
88
|
+
"bsd" => "BSD-3-Clause",
|
89
|
+
"bsd license" => "BSD-3-Clause",
|
90
|
+
"new bsd license" => "BSD-3-Clause",
|
91
|
+
"gnu gpl v2" => "GPL-2.0-only",
|
92
|
+
"gpl" => "GPL-2.0+",
|
93
|
+
"gpl-2 | gpl-3 [expanded from: gpl (≥ 2.0)]" => "GPL-2.0+",
|
94
|
+
"gpl-2 | gpl-3 [expanded from: gpl]" => "GPL-2.0+",
|
95
|
+
"gpl-2 | gpl-3 [expanded from: gpl (≥ 2)]" => "GPL-2.0+",
|
96
|
+
"gpl-2 | gpl-3" => "GPL-2.0+",
|
97
|
+
"gplv2 or later" => "GPL-2.0+",
|
98
|
+
"the gpl v3" => "GPL-3.0",
|
99
|
+
"gpl (≥ 3)" => "GPL-3.0+",
|
100
|
+
"mpl2.0" => "mpl-2.0",
|
101
|
+
"mpl1" => "mpl-1.0",
|
102
|
+
"mpl1.0" => "mpl-1.0",
|
103
|
+
"mpl1.1" => "mpl-1.1",
|
104
|
+
"mpl2" => "mpl-2.0",
|
105
|
+
"gnu lesser general public license" => "LGPL-2.1+",
|
106
|
+
"lgplv2 or later" => "LGPL-2.1+",
|
107
|
+
"gpl2 w/ cpe" => "GPL-2.0-with-classpath-exception",
|
108
|
+
"new bsd license (gpl-compatible)" => "BSD-3-Clause",
|
109
|
+
"public domain" => "Unlicense",
|
110
|
+
"cc0" => "CC0-1.0",
|
111
|
+
"artistic_2" => "Artistic-2.0",
|
112
|
+
"artistic_1" => "Artistic-1.0",
|
113
|
+
"alv2" => "Apache-2.0",
|
114
|
+
"asl" => "Apache-2.0",
|
115
|
+
"asl 2.0" => "Apache-2.0",
|
116
|
+
"mpl 2.0" => "MPL-2.0",
|
117
|
+
"publicdomain" => "Unlicense",
|
118
|
+
"unlicensed" => "Unlicense",
|
119
|
+
"psfl" => "Python-2.0",
|
120
|
+
"psf" => "Python-2.0",
|
121
|
+
"psf 2" => "Python-2.0",
|
122
|
+
"psf 2.0" => "Python-2.0",
|
123
|
+
"asl2" => "Apache-2.0",
|
124
|
+
"al2" => "Apache-2.0",
|
125
|
+
"aslv2" => "Apache-2.0",
|
126
|
+
"apache_2_0" => "Apache-2.0",
|
127
|
+
"apache_v2" => "Apache-2.0",
|
128
|
+
"zpl 1.1" => "ZPL-1.1",
|
129
|
+
"zpl 2.0" => "ZPL-2.0",
|
130
|
+
"zpl 2.1" => "ZPL-2.1",
|
131
|
+
"lgpl_2_1" => "LGPL-2.1",
|
132
|
+
"lgpl_v2_1" => "LGPL-2.1",
|
133
|
+
"lgpl version 3" => "LGPL-3.0",
|
134
|
+
"gnu lgpl v3+" => "LGPL-3.0",
|
135
|
+
"gnu lgpl" => "LGPL-2.1+",
|
136
|
+
"cc by-sa 4.0" => "CC-BY-SA-4.0",
|
137
|
+
"cc by-nc-sa 3.0" => "CC-BY-NC-SA-3.0",
|
138
|
+
"cc by-sa 3.0" => "CC-BY-SA-3.0",
|
139
|
+
"mpl v2.0" => "MPL-2.0",
|
140
|
+
"mplv2.0" => "MPL-2.0",
|
141
|
+
"mplv2" => "MPL-2.0",
|
142
|
+
"cpal v1.0" => "CPAL-1.0",
|
143
|
+
"cddl 1.0" => "CDDL-1.0",
|
144
|
+
"cddl 1.1" => "CDDL-1.1",
|
145
|
+
"epl" => "EPL-1.0",
|
146
|
+
"mit-license" => "MIT",
|
147
|
+
"(mit or x11)" => "MIT",
|
148
|
+
"iscl" => "ISC",
|
149
|
+
"wtf" => "WTFPL",
|
150
|
+
"2-clause bsdl" => "BSD-2-clause",
|
151
|
+
"3-clause bsdl" => "BSD-3-clause",
|
152
|
+
"2-clause bsd" => "BSD-2-clause",
|
153
|
+
"3-clause bsd" => "BSD-3-clause",
|
154
|
+
"bsd 3-clause" => "BSD-3-clause",
|
155
|
+
"bsd 2-clause" => "BSD-2-clause",
|
156
|
+
"two-clause bsd-style license" => "BSD-2-clause",
|
157
|
+
"bsd style" => "BSD-3-clause",
|
158
|
+
"cc0 1.0 universal (cc0 1.0) public domain dedication" => "CC0-1.0",
|
159
|
+
"common development and distribution license 1.0 (cddl-1.0)" => "CDDL-1.0",
|
160
|
+
"european union public licence 1.0 (eupl 1.0)" => "EUPL-1.0",
|
161
|
+
"european union public licence 1.1 (eupl 1.1)" => "EUPL-1.1",
|
162
|
+
"european union public licence 1.2 (eupl 1.2)" => "EUPL-1.2",
|
163
|
+
"vovida software license 1.0" => "VSL-1.0",
|
164
|
+
"w3c license" => "W3C",
|
165
|
+
"zlib/libpng license" => "zlib-acknowledgement",
|
166
|
+
"gnu general public license (gpl)" => "GPL-2.0+",
|
167
|
+
"gnu general public license v2 (gplv2)" => "GPL-2.0",
|
168
|
+
"gnu general public license v2 or later (gplv2+)" => "GPL-2.0+",
|
169
|
+
"gnu general public license v3 (gplv3)" => "GPL-3.0",
|
170
|
+
"gnu general public license v3 or later (gplv3+)" => "GPL-3.0+",
|
171
|
+
"gnu lesser general public license v2 (lgplv2)" => "LGPL-2.0",
|
172
|
+
"gnu lesser general public license v2 or later (lgplv2+)" => "LGPL-2.0+",
|
173
|
+
"gnu lesser general public license v3 (lgplv3)" => "LGPL-3.0",
|
174
|
+
"gnu lesser general public license v3 or later (lgplv3+)" => "LGPL-3.0+",
|
175
|
+
"gnu library or lesser general public license (lgpl)" => "LGPL-2.0+",
|
176
|
+
"netscape public License (npl)" => "NPL-1.1",
|
177
|
+
"apache software license" => "Apache-2.0",
|
178
|
+
"academic free license (afl)" => "AFL-3.0",
|
179
|
+
"gnu free documentation license (fdl)" => "GFDL-1.3",
|
180
|
+
"sun industry standards source license (sissl)" => "SISSL-1.2",
|
181
|
+
"zope public license" => "ZPL-2.1",
|
174
182
|
}
|
175
183
|
end
|
176
184
|
|
177
185
|
def self.names
|
178
|
-
(
|
186
|
+
(licenses.keys + licenses.map { |_k, v| v["name"] }).sort
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.exceptions
|
190
|
+
unless defined?(@exceptions)
|
191
|
+
data = JSON.parse(File.read(File.expand_path("../exceptions.json", __dir__)))
|
192
|
+
@exceptions = {}
|
193
|
+
data["exceptions"].each do |details|
|
194
|
+
id = details.delete("licenseExceptionId")
|
195
|
+
@exceptions[id] = details
|
196
|
+
end
|
197
|
+
end
|
198
|
+
@exceptions
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.license_exists?(id)
|
202
|
+
licenses.key?(id.to_s)
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.lookup_license(id)
|
206
|
+
json = licenses[id.to_s]
|
207
|
+
Spdx::License.new(id.to_s, json["name"], json["isOsiApproved"]) if json
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.lookup_exception(id)
|
211
|
+
json = exceptions[id.to_s]
|
212
|
+
Spdx::Exception.new(id.to_s, json["name"], json["isDeprecatedLicenseId"]) if json
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.exception_exists?(id)
|
216
|
+
exceptions.key?(id.to_s)
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.licenses
|
220
|
+
unless defined?(@licenses)
|
221
|
+
data = JSON.parse(File.read(File.expand_path("../licenses.json", __dir__)))
|
222
|
+
@licenses = {}
|
223
|
+
data["licenses"].each do |details|
|
224
|
+
id = details.delete("licenseId")
|
225
|
+
@licenses[id] = details
|
226
|
+
end
|
227
|
+
end
|
228
|
+
@licenses
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.valid_spdx?(spdx_string)
|
232
|
+
return false unless spdx_string.is_a?(String)
|
233
|
+
|
234
|
+
SpdxParser.parse(spdx_string)
|
235
|
+
true
|
236
|
+
rescue SpdxGrammar::SpdxParseError
|
237
|
+
false
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.parse_spdx(spdx_string)
|
241
|
+
SpdxParser.parse(spdx_string)
|
179
242
|
end
|
180
243
|
end
|
data/lib/spdx/version.rb
CHANGED
data/lib/spdx_grammar.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpdxGrammar
|
4
|
+
class CompoundExpression < Treetop::Runtime::SyntaxNode
|
5
|
+
def licenses
|
6
|
+
elements[0].licenses
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class LogicalOr < Treetop::Runtime::SyntaxNode
|
11
|
+
end
|
12
|
+
|
13
|
+
class LogicalAnd < Treetop::Runtime::SyntaxNode
|
14
|
+
end
|
15
|
+
|
16
|
+
class With < Treetop::Runtime::SyntaxNode
|
17
|
+
end
|
18
|
+
|
19
|
+
class None < Treetop::Runtime::SyntaxNode
|
20
|
+
def licenses
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class NoAssertion < Treetop::Runtime::SyntaxNode
|
26
|
+
def licenses
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class License < Treetop::Runtime::SyntaxNode
|
32
|
+
def licenses
|
33
|
+
text_value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class LicenseException < Treetop::Runtime::SyntaxNode
|
38
|
+
# TODO: actually do license exceptions
|
39
|
+
end
|
40
|
+
|
41
|
+
class Body < Treetop::Runtime::SyntaxNode
|
42
|
+
def licenses
|
43
|
+
elements.map { |node| node.licenses if node.respond_to?(:licenses) }.flatten.uniq.compact
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class SpdxParseError < StandardError
|
48
|
+
end
|
49
|
+
end
|
data/lib/spdx_parser.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "treetop"
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
require_relative "spdx_grammar"
|
7
|
+
|
8
|
+
class SpdxParser
|
9
|
+
Treetop.load(File.expand_path(File.join(File.dirname(__FILE__), "spdx_parser.treetop")))
|
10
|
+
|
11
|
+
SKIP_PARENS = ["NONE", "NOASSERTION", ""].freeze
|
12
|
+
|
13
|
+
def self.parse(data)
|
14
|
+
parse_tree(data)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse_licenses(data)
|
18
|
+
tree = parse_tree(data)
|
19
|
+
tree.get_licenses
|
20
|
+
end
|
21
|
+
|
22
|
+
private_class_method def self.parse_tree(data)
|
23
|
+
parser = SpdxGrammarParser.new # The generated grammar parser is not thread safe
|
24
|
+
# Couldn't figure out treetop to make parens optional
|
25
|
+
data = "(#{data})" unless SKIP_PARENS.include?(data)
|
26
|
+
|
27
|
+
tree = parser.parse(data)
|
28
|
+
raise SpdxGrammar::SpdxParseError, "Unable to parse expression '#{data}'. Parse error at offset: #{parser.index}" if tree.nil?
|
29
|
+
|
30
|
+
clean_tree(tree)
|
31
|
+
tree
|
32
|
+
end
|
33
|
+
|
34
|
+
private_class_method def self.clean_tree(root_node)
|
35
|
+
return if root_node.elements.nil?
|
36
|
+
|
37
|
+
root_node.elements.delete_if { |node| node.class.name == "Treetop::Runtime::SyntaxNode" }
|
38
|
+
root_node.elements.each { |node| clean_tree(node) }
|
39
|
+
end
|
40
|
+
end
|