blob: b5d378a549be8348ac48ca96cb36aea5de76fe62 [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
2# SPDX-License-Identifier: GPL-2.0-only
3#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004"""Code for parsing OpenEmbedded license strings"""
5
6import ast
7import re
8from fnmatch import fnmatchcase as fnmatch
9
10def license_ok(license, dont_want_licenses):
11 """ Return False if License exist in dont_want_licenses else True """
12 for dwl in dont_want_licenses:
Andrew Geissler90fd73c2021-03-05 15:25:55 -060013 if fnmatch(license, dwl):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050014 return False
15 return True
16
17class LicenseError(Exception):
18 pass
19
20class LicenseSyntaxError(LicenseError):
21 def __init__(self, licensestr, exc):
22 self.licensestr = licensestr
23 self.exc = exc
24 LicenseError.__init__(self)
25
26 def __str__(self):
27 return "error in '%s': %s" % (self.licensestr, self.exc)
28
29class InvalidLicense(LicenseError):
30 def __init__(self, license):
31 self.license = license
32 LicenseError.__init__(self)
33
34 def __str__(self):
35 return "invalid characters in license '%s'" % self.license
36
37license_operator_chars = '&|() '
Brad Bishop19323692019-04-05 15:28:33 -040038license_operator = re.compile(r'([' + license_operator_chars + '])')
39license_pattern = re.compile(r'[a-zA-Z0-9.+_\-]+$')
Patrick Williamsc124f4f2015-09-15 14:41:29 -050040
41class LicenseVisitor(ast.NodeVisitor):
42 """Get elements based on OpenEmbedded license strings"""
43 def get_elements(self, licensestr):
44 new_elements = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -060045 elements = list([x for x in license_operator.split(licensestr) if x.strip()])
Patrick Williamsc124f4f2015-09-15 14:41:29 -050046 for pos, element in enumerate(elements):
47 if license_pattern.match(element):
48 if pos > 0 and license_pattern.match(elements[pos-1]):
49 new_elements.append('&')
50 element = '"' + element + '"'
51 elif not license_operator.match(element):
52 raise InvalidLicense(element)
53 new_elements.append(element)
54
55 return new_elements
56
57 """Syntax tree visitor which can accept elements previously generated with
58 OpenEmbedded license string"""
59 def visit_elements(self, elements):
60 self.visit(ast.parse(' '.join(elements)))
61
62 """Syntax tree visitor which can accept OpenEmbedded license strings"""
63 def visit_string(self, licensestr):
64 self.visit_elements(self.get_elements(licensestr))
65
66class FlattenVisitor(LicenseVisitor):
67 """Flatten a license tree (parsed from a string) by selecting one of each
68 set of OR options, in the way the user specifies"""
69 def __init__(self, choose_licenses):
70 self.choose_licenses = choose_licenses
71 self.licenses = []
72 LicenseVisitor.__init__(self)
73
74 def visit_Str(self, node):
75 self.licenses.append(node.s)
76
Andrew Geisslereff27472021-10-29 15:35:00 -050077 def visit_Constant(self, node):
78 self.licenses.append(node.value)
79
Patrick Williamsc124f4f2015-09-15 14:41:29 -050080 def visit_BinOp(self, node):
81 if isinstance(node.op, ast.BitOr):
82 left = FlattenVisitor(self.choose_licenses)
83 left.visit(node.left)
84
85 right = FlattenVisitor(self.choose_licenses)
86 right.visit(node.right)
87
88 selected = self.choose_licenses(left.licenses, right.licenses)
89 self.licenses.extend(selected)
90 else:
91 self.generic_visit(node)
92
93def flattened_licenses(licensestr, choose_licenses):
94 """Given a license string and choose_licenses function, return a flat list of licenses"""
95 flatten = FlattenVisitor(choose_licenses)
96 try:
97 flatten.visit_string(licensestr)
98 except SyntaxError as exc:
99 raise LicenseSyntaxError(licensestr, exc)
100 return flatten.licenses
101
102def is_included(licensestr, whitelist=None, blacklist=None):
103 """Given a license string and whitelist and blacklist, determine if the
104 license string matches the whitelist and does not match the blacklist.
105
106 Returns a tuple holding the boolean state and a list of the applicable
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500107 licenses that were excluded if state is False, or the licenses that were
108 included if the state is True.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500109 """
110
111 def include_license(license):
112 return any(fnmatch(license, pattern) for pattern in whitelist)
113
114 def exclude_license(license):
115 return any(fnmatch(license, pattern) for pattern in blacklist)
116
117 def choose_licenses(alpha, beta):
118 """Select the option in an OR which is the 'best' (has the most
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500119 included licenses and no excluded licenses)."""
120 # The factor 1000 below is arbitrary, just expected to be much larger
121 # that the number of licenses actually specified. That way the weight
122 # will be negative if the list of licenses contains an excluded license,
123 # but still gives a higher weight to the list with the most included
124 # licenses.
125 alpha_weight = (len(list(filter(include_license, alpha))) -
126 1000 * (len(list(filter(exclude_license, alpha))) > 0))
127 beta_weight = (len(list(filter(include_license, beta))) -
128 1000 * (len(list(filter(exclude_license, beta))) > 0))
129 if alpha_weight >= beta_weight:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500130 return alpha
131 else:
132 return beta
133
134 if not whitelist:
135 whitelist = ['*']
136
137 if not blacklist:
138 blacklist = []
139
140 licenses = flattened_licenses(licensestr, choose_licenses)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600141 excluded = [lic for lic in licenses if exclude_license(lic)]
142 included = [lic for lic in licenses if include_license(lic)]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143 if excluded:
144 return False, excluded
145 else:
146 return True, included
147
148class ManifestVisitor(LicenseVisitor):
149 """Walk license tree (parsed from a string) removing the incompatible
150 licenses specified"""
151 def __init__(self, dont_want_licenses, canonical_license, d):
152 self._dont_want_licenses = dont_want_licenses
153 self._canonical_license = canonical_license
154 self._d = d
155 self._operators = []
156
157 self.licenses = []
158 self.licensestr = ''
159
160 LicenseVisitor.__init__(self)
161
162 def visit(self, node):
163 if isinstance(node, ast.Str):
164 lic = node.s
165
166 if license_ok(self._canonical_license(self._d, lic),
167 self._dont_want_licenses) == True:
168 if self._operators:
169 ops = []
170 for op in self._operators:
171 if op == '[':
172 ops.append(op)
173 elif op == ']':
174 ops.append(op)
175 else:
176 if not ops:
177 ops.append(op)
178 elif ops[-1] in ['[', ']']:
179 ops.append(op)
180 else:
181 ops[-1] = op
182
183 for op in ops:
184 if op == '[' or op == ']':
185 self.licensestr += op
186 elif self.licenses:
187 self.licensestr += ' ' + op + ' '
188
189 self._operators = []
190
191 self.licensestr += lic
192 self.licenses.append(lic)
193 elif isinstance(node, ast.BitAnd):
194 self._operators.append("&")
195 elif isinstance(node, ast.BitOr):
196 self._operators.append("|")
197 elif isinstance(node, ast.List):
198 self._operators.append("[")
199 elif isinstance(node, ast.Load):
200 self.licensestr += "]"
201
202 self.generic_visit(node)
203
204def manifest_licenses(licensestr, dont_want_licenses, canonical_license, d):
205 """Given a license string and dont_want_licenses list,
206 return license string filtered and a list of licenses"""
207 manifest = ManifestVisitor(dont_want_licenses, canonical_license, d)
208
209 try:
210 elements = manifest.get_elements(licensestr)
211
212 # Replace '()' to '[]' for handle in ast as List and Load types.
213 elements = ['[' if e == '(' else e for e in elements]
214 elements = [']' if e == ')' else e for e in elements]
215
216 manifest.visit_elements(elements)
217 except SyntaxError as exc:
218 raise LicenseSyntaxError(licensestr, exc)
219
220 # Replace '[]' to '()' for output correct license.
221 manifest.licensestr = manifest.licensestr.replace('[', '(').replace(']', ')')
222
223 return (manifest.licensestr, manifest.licenses)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600224
225class ListVisitor(LicenseVisitor):
226 """Record all different licenses found in the license string"""
227 def __init__(self):
228 self.licenses = set()
229
230 def visit_Str(self, node):
231 self.licenses.add(node.s)
232
Andrew Geisslereff27472021-10-29 15:35:00 -0500233 def visit_Constant(self, node):
234 self.licenses.add(node.value)
235
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600236def list_licenses(licensestr):
237 """Simply get a list of all licenses mentioned in a license string.
238 Binary operators are not applied or taken into account in any way"""
239 visitor = ListVisitor()
240 try:
241 visitor.visit_string(licensestr)
242 except SyntaxError as exc:
243 raise LicenseSyntaxError(licensestr, exc)
244 return visitor.licenses