spdx 1.4.4 → 2.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +47 -7
- data/Gemfile +3 -1
- data/Rakefile +5 -3
- data/aliases.json +3 -0
- data/bin/update-license-files +3 -0
- data/exceptions.json +431 -0
- data/lib/exception.rb +14 -0
- data/lib/license.rb +14 -0
- data/lib/spdx.rb +185 -109
- 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 +5136 -0
- data/spdx.gemspec +18 -15
- data/spec/spdx_spec.rb +192 -155
- data/spec/spec_helper.rb +4 -2
- metadata +37 -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,106 +81,177 @@ 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
|
-
|
174
|
-
|
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",
|
175
182
|
}
|
176
183
|
end
|
177
184
|
|
178
185
|
def self.names
|
179
|
-
(
|
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.aliases
|
202
|
+
@aliases = JSON.parse(File.read(File.expand_path("../aliases.json", __dir__))) unless defined?(@aliases)
|
203
|
+
@aliases
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.alias_exists?(string)
|
207
|
+
aliases.key?(string)
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.lookup_alias(string)
|
211
|
+
id = aliases[string]
|
212
|
+
lookup_license(id)
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.license_exists?(id)
|
216
|
+
licenses.key?(id.to_s)
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.lookup_license(id)
|
220
|
+
json = licenses[id.to_s]
|
221
|
+
Spdx::License.new(id.to_s, json["name"], json["isOsiApproved"]) if json
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.lookup_exception(id)
|
225
|
+
json = exceptions[id.to_s]
|
226
|
+
Spdx::Exception.new(id.to_s, json["name"], json["isDeprecatedLicenseId"]) if json
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.exception_exists?(id)
|
230
|
+
exceptions.has_key(id.to_s)
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.licenses
|
234
|
+
unless defined?(@licenses)
|
235
|
+
data = JSON.parse(File.read(File.expand_path("../licenses.json", __dir__)))
|
236
|
+
@licenses = {}
|
237
|
+
data["licenses"].each do |details|
|
238
|
+
id = details.delete("licenseId")
|
239
|
+
@licenses[id] = details
|
240
|
+
end
|
241
|
+
end
|
242
|
+
@licenses
|
243
|
+
end
|
244
|
+
|
245
|
+
def self.valid_spdx?(spdx_string)
|
246
|
+
return false unless spdx_string.is_a?(String)
|
247
|
+
|
248
|
+
SpdxParser.parse(spdx_string)
|
249
|
+
true
|
250
|
+
rescue SpdxGrammar::SpdxParseError
|
251
|
+
false
|
252
|
+
end
|
253
|
+
|
254
|
+
def self.parse_spdx(spdx_string)
|
255
|
+
SpdxParser.parse(spdx_string)
|
180
256
|
end
|
181
257
|
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", ""]
|
12
|
+
@parser = SpdxGrammarParser.new
|
13
|
+
|
14
|
+
def self.parse(data)
|
15
|
+
parse_tree(data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.parse_licenses(data)
|
19
|
+
tree = parse_tree(data)
|
20
|
+
tree.get_licenses
|
21
|
+
end
|
22
|
+
|
23
|
+
private_class_method def self.parse_tree(data)
|
24
|
+
# Couldn't figure out treetop to make parens optional
|
25
|
+
data = "(#{data})" unless data.start_with?("(") || SKIP_PARENS.include?(data)
|
26
|
+
tree = @parser.parse(data)
|
27
|
+
|
28
|
+
raise SpdxGrammar::SpdxParseError, "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
|