blob: 79800c2b8f867f921899380d24e9ec94d891d9d5 [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
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000102def is_included(licensestr, include_licenses=None, exclude_licenses=None):
103 """Given a license a list of list to include and a list of
104 licenses to exclude, determine if the license string
105 matches the an include list and does not match the
106 exclude list.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500107
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000108 Returns a tuple holding the boolean state and a list of
109 the applicable licenses that were excluded if state is
110 False, or the licenses that were included if the state
111 is True.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500112 """
113
114 def include_license(license):
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000115 return any(fnmatch(license, pattern) for pattern in include_licenses)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500116
117 def exclude_license(license):
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000118 return any(fnmatch(license, pattern) for pattern in exclude_licenses)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500119
120 def choose_licenses(alpha, beta):
121 """Select the option in an OR which is the 'best' (has the most
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500122 included licenses and no excluded licenses)."""
123 # The factor 1000 below is arbitrary, just expected to be much larger
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000124 # than the number of licenses actually specified. That way the weight
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500125 # will be negative if the list of licenses contains an excluded license,
126 # but still gives a higher weight to the list with the most included
127 # licenses.
128 alpha_weight = (len(list(filter(include_license, alpha))) -
129 1000 * (len(list(filter(exclude_license, alpha))) > 0))
130 beta_weight = (len(list(filter(include_license, beta))) -
131 1000 * (len(list(filter(exclude_license, beta))) > 0))
132 if alpha_weight >= beta_weight:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500133 return alpha
134 else:
135 return beta
136
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000137 if not include_licenses:
138 include_licenses = ['*']
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500139
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000140 if not exclude_licenses:
141 exclude_licenses = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500142
143 licenses = flattened_licenses(licensestr, choose_licenses)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600144 excluded = [lic for lic in licenses if exclude_license(lic)]
145 included = [lic for lic in licenses if include_license(lic)]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500146 if excluded:
147 return False, excluded
148 else:
149 return True, included
150
151class ManifestVisitor(LicenseVisitor):
152 """Walk license tree (parsed from a string) removing the incompatible
153 licenses specified"""
154 def __init__(self, dont_want_licenses, canonical_license, d):
155 self._dont_want_licenses = dont_want_licenses
156 self._canonical_license = canonical_license
157 self._d = d
158 self._operators = []
159
160 self.licenses = []
161 self.licensestr = ''
162
163 LicenseVisitor.__init__(self)
164
165 def visit(self, node):
166 if isinstance(node, ast.Str):
167 lic = node.s
168
169 if license_ok(self._canonical_license(self._d, lic),
170 self._dont_want_licenses) == True:
171 if self._operators:
172 ops = []
173 for op in self._operators:
174 if op == '[':
175 ops.append(op)
176 elif op == ']':
177 ops.append(op)
178 else:
179 if not ops:
180 ops.append(op)
181 elif ops[-1] in ['[', ']']:
182 ops.append(op)
183 else:
184 ops[-1] = op
185
186 for op in ops:
187 if op == '[' or op == ']':
188 self.licensestr += op
189 elif self.licenses:
190 self.licensestr += ' ' + op + ' '
191
192 self._operators = []
193
194 self.licensestr += lic
195 self.licenses.append(lic)
196 elif isinstance(node, ast.BitAnd):
197 self._operators.append("&")
198 elif isinstance(node, ast.BitOr):
199 self._operators.append("|")
200 elif isinstance(node, ast.List):
201 self._operators.append("[")
202 elif isinstance(node, ast.Load):
203 self.licensestr += "]"
204
205 self.generic_visit(node)
206
207def manifest_licenses(licensestr, dont_want_licenses, canonical_license, d):
208 """Given a license string and dont_want_licenses list,
209 return license string filtered and a list of licenses"""
210 manifest = ManifestVisitor(dont_want_licenses, canonical_license, d)
211
212 try:
213 elements = manifest.get_elements(licensestr)
214
215 # Replace '()' to '[]' for handle in ast as List and Load types.
216 elements = ['[' if e == '(' else e for e in elements]
217 elements = [']' if e == ')' else e for e in elements]
218
219 manifest.visit_elements(elements)
220 except SyntaxError as exc:
221 raise LicenseSyntaxError(licensestr, exc)
222
223 # Replace '[]' to '()' for output correct license.
224 manifest.licensestr = manifest.licensestr.replace('[', '(').replace(']', ')')
225
226 return (manifest.licensestr, manifest.licenses)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600227
228class ListVisitor(LicenseVisitor):
229 """Record all different licenses found in the license string"""
230 def __init__(self):
231 self.licenses = set()
232
233 def visit_Str(self, node):
234 self.licenses.add(node.s)
235
Andrew Geisslereff27472021-10-29 15:35:00 -0500236 def visit_Constant(self, node):
237 self.licenses.add(node.value)
238
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600239def list_licenses(licensestr):
240 """Simply get a list of all licenses mentioned in a license string.
241 Binary operators are not applied or taken into account in any way"""
242 visitor = ListVisitor()
243 try:
244 visitor.visit_string(licensestr)
245 except SyntaxError as exc:
246 raise LicenseSyntaxError(licensestr, exc)
247 return visitor.licenses