blob: 665d32ecbb1a5298011d73204248e5da39fc78fe [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
77 def visit_BinOp(self, node):
78 if isinstance(node.op, ast.BitOr):
79 left = FlattenVisitor(self.choose_licenses)
80 left.visit(node.left)
81
82 right = FlattenVisitor(self.choose_licenses)
83 right.visit(node.right)
84
85 selected = self.choose_licenses(left.licenses, right.licenses)
86 self.licenses.extend(selected)
87 else:
88 self.generic_visit(node)
89
90def flattened_licenses(licensestr, choose_licenses):
91 """Given a license string and choose_licenses function, return a flat list of licenses"""
92 flatten = FlattenVisitor(choose_licenses)
93 try:
94 flatten.visit_string(licensestr)
95 except SyntaxError as exc:
96 raise LicenseSyntaxError(licensestr, exc)
97 return flatten.licenses
98
99def is_included(licensestr, whitelist=None, blacklist=None):
100 """Given a license string and whitelist and blacklist, determine if the
101 license string matches the whitelist and does not match the blacklist.
102
103 Returns a tuple holding the boolean state and a list of the applicable
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500104 licenses that were excluded if state is False, or the licenses that were
105 included if the state is True.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500106 """
107
108 def include_license(license):
109 return any(fnmatch(license, pattern) for pattern in whitelist)
110
111 def exclude_license(license):
112 return any(fnmatch(license, pattern) for pattern in blacklist)
113
114 def choose_licenses(alpha, beta):
115 """Select the option in an OR which is the 'best' (has the most
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500116 included licenses and no excluded licenses)."""
117 # The factor 1000 below is arbitrary, just expected to be much larger
118 # that the number of licenses actually specified. That way the weight
119 # will be negative if the list of licenses contains an excluded license,
120 # but still gives a higher weight to the list with the most included
121 # licenses.
122 alpha_weight = (len(list(filter(include_license, alpha))) -
123 1000 * (len(list(filter(exclude_license, alpha))) > 0))
124 beta_weight = (len(list(filter(include_license, beta))) -
125 1000 * (len(list(filter(exclude_license, beta))) > 0))
126 if alpha_weight >= beta_weight:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500127 return alpha
128 else:
129 return beta
130
131 if not whitelist:
132 whitelist = ['*']
133
134 if not blacklist:
135 blacklist = []
136
137 licenses = flattened_licenses(licensestr, choose_licenses)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600138 excluded = [lic for lic in licenses if exclude_license(lic)]
139 included = [lic for lic in licenses if include_license(lic)]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500140 if excluded:
141 return False, excluded
142 else:
143 return True, included
144
145class ManifestVisitor(LicenseVisitor):
146 """Walk license tree (parsed from a string) removing the incompatible
147 licenses specified"""
148 def __init__(self, dont_want_licenses, canonical_license, d):
149 self._dont_want_licenses = dont_want_licenses
150 self._canonical_license = canonical_license
151 self._d = d
152 self._operators = []
153
154 self.licenses = []
155 self.licensestr = ''
156
157 LicenseVisitor.__init__(self)
158
159 def visit(self, node):
160 if isinstance(node, ast.Str):
161 lic = node.s
162
163 if license_ok(self._canonical_license(self._d, lic),
164 self._dont_want_licenses) == True:
165 if self._operators:
166 ops = []
167 for op in self._operators:
168 if op == '[':
169 ops.append(op)
170 elif op == ']':
171 ops.append(op)
172 else:
173 if not ops:
174 ops.append(op)
175 elif ops[-1] in ['[', ']']:
176 ops.append(op)
177 else:
178 ops[-1] = op
179
180 for op in ops:
181 if op == '[' or op == ']':
182 self.licensestr += op
183 elif self.licenses:
184 self.licensestr += ' ' + op + ' '
185
186 self._operators = []
187
188 self.licensestr += lic
189 self.licenses.append(lic)
190 elif isinstance(node, ast.BitAnd):
191 self._operators.append("&")
192 elif isinstance(node, ast.BitOr):
193 self._operators.append("|")
194 elif isinstance(node, ast.List):
195 self._operators.append("[")
196 elif isinstance(node, ast.Load):
197 self.licensestr += "]"
198
199 self.generic_visit(node)
200
201def manifest_licenses(licensestr, dont_want_licenses, canonical_license, d):
202 """Given a license string and dont_want_licenses list,
203 return license string filtered and a list of licenses"""
204 manifest = ManifestVisitor(dont_want_licenses, canonical_license, d)
205
206 try:
207 elements = manifest.get_elements(licensestr)
208
209 # Replace '()' to '[]' for handle in ast as List and Load types.
210 elements = ['[' if e == '(' else e for e in elements]
211 elements = [']' if e == ')' else e for e in elements]
212
213 manifest.visit_elements(elements)
214 except SyntaxError as exc:
215 raise LicenseSyntaxError(licensestr, exc)
216
217 # Replace '[]' to '()' for output correct license.
218 manifest.licensestr = manifest.licensestr.replace('[', '(').replace(']', ')')
219
220 return (manifest.licensestr, manifest.licenses)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600221
222class ListVisitor(LicenseVisitor):
223 """Record all different licenses found in the license string"""
224 def __init__(self):
225 self.licenses = set()
226
227 def visit_Str(self, node):
228 self.licenses.add(node.s)
229
230def list_licenses(licensestr):
231 """Simply get a list of all licenses mentioned in a license string.
232 Binary operators are not applied or taken into account in any way"""
233 visitor = ListVisitor()
234 try:
235 visitor.visit_string(licensestr)
236 except SyntaxError as exc:
237 raise LicenseSyntaxError(licensestr, exc)
238 return visitor.licenses