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