blob: f0f661c3ba96c8b40ee5f7dfa14438865e9eec71 [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
16 if not re.search('\+$', dwl):
17 lic = re.sub('\+', '', license)
18 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 = '&|() '
43license_operator = re.compile('([' + license_operator_chars + '])')
44license_pattern = re.compile('[a-zA-Z0-9.+_\-]+$')
45
46class LicenseVisitor(ast.NodeVisitor):
47 """Get elements based on OpenEmbedded license strings"""
48 def get_elements(self, licensestr):
49 new_elements = []
50 elements = filter(lambda x: x.strip(), license_operator.split(licensestr))
51 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
109 licenses which were excluded (or None, if the state is True)
110 """
111
112 def include_license(license):
113 return any(fnmatch(license, pattern) for pattern in whitelist)
114
115 def exclude_license(license):
116 return any(fnmatch(license, pattern) for pattern in blacklist)
117
118 def choose_licenses(alpha, beta):
119 """Select the option in an OR which is the 'best' (has the most
120 included licenses)."""
121 alpha_weight = len(filter(include_license, alpha))
122 beta_weight = len(filter(include_license, beta))
123 if alpha_weight > beta_weight:
124 return alpha
125 else:
126 return beta
127
128 if not whitelist:
129 whitelist = ['*']
130
131 if not blacklist:
132 blacklist = []
133
134 licenses = flattened_licenses(licensestr, choose_licenses)
135 excluded = filter(lambda lic: exclude_license(lic), licenses)
136 included = filter(lambda lic: include_license(lic), licenses)
137 if excluded:
138 return False, excluded
139 else:
140 return True, included
141
142class ManifestVisitor(LicenseVisitor):
143 """Walk license tree (parsed from a string) removing the incompatible
144 licenses specified"""
145 def __init__(self, dont_want_licenses, canonical_license, d):
146 self._dont_want_licenses = dont_want_licenses
147 self._canonical_license = canonical_license
148 self._d = d
149 self._operators = []
150
151 self.licenses = []
152 self.licensestr = ''
153
154 LicenseVisitor.__init__(self)
155
156 def visit(self, node):
157 if isinstance(node, ast.Str):
158 lic = node.s
159
160 if license_ok(self._canonical_license(self._d, lic),
161 self._dont_want_licenses) == True:
162 if self._operators:
163 ops = []
164 for op in self._operators:
165 if op == '[':
166 ops.append(op)
167 elif op == ']':
168 ops.append(op)
169 else:
170 if not ops:
171 ops.append(op)
172 elif ops[-1] in ['[', ']']:
173 ops.append(op)
174 else:
175 ops[-1] = op
176
177 for op in ops:
178 if op == '[' or op == ']':
179 self.licensestr += op
180 elif self.licenses:
181 self.licensestr += ' ' + op + ' '
182
183 self._operators = []
184
185 self.licensestr += lic
186 self.licenses.append(lic)
187 elif isinstance(node, ast.BitAnd):
188 self._operators.append("&")
189 elif isinstance(node, ast.BitOr):
190 self._operators.append("|")
191 elif isinstance(node, ast.List):
192 self._operators.append("[")
193 elif isinstance(node, ast.Load):
194 self.licensestr += "]"
195
196 self.generic_visit(node)
197
198def manifest_licenses(licensestr, dont_want_licenses, canonical_license, d):
199 """Given a license string and dont_want_licenses list,
200 return license string filtered and a list of licenses"""
201 manifest = ManifestVisitor(dont_want_licenses, canonical_license, d)
202
203 try:
204 elements = manifest.get_elements(licensestr)
205
206 # Replace '()' to '[]' for handle in ast as List and Load types.
207 elements = ['[' if e == '(' else e for e in elements]
208 elements = [']' if e == ')' else e for e in elements]
209
210 manifest.visit_elements(elements)
211 except SyntaxError as exc:
212 raise LicenseSyntaxError(licensestr, exc)
213
214 # Replace '[]' to '()' for output correct license.
215 manifest.licensestr = manifest.licensestr.replace('[', '(').replace(']', ')')
216
217 return (manifest.licensestr, manifest.licenses)