1 | use crate::{ |
2 | error::{ParseError, Reason}, |
3 | lexer::{Lexer, Token}, |
4 | ExceptionId, LicenseItem, LicenseReq, |
5 | }; |
6 | use std::fmt; |
7 | |
8 | /// A convenience wrapper for a license and optional exception that can be |
9 | /// checked against a license requirement to see if it satisfies the requirement |
10 | /// placed by a license holder |
11 | /// |
12 | /// ``` |
13 | /// let licensee = spdx::Licensee::parse("GPL-2.0" ).unwrap(); |
14 | /// |
15 | /// assert!(licensee.satisfies(&spdx::LicenseReq::from(spdx::license_id("GPL-2.0-only" ).unwrap()))); |
16 | /// ``` |
17 | #[derive (PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] |
18 | pub struct Licensee { |
19 | inner: LicenseReq, |
20 | } |
21 | |
22 | impl fmt::Display for Licensee { |
23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
24 | self.inner.fmt(f) |
25 | } |
26 | } |
27 | |
28 | impl std::str::FromStr for Licensee { |
29 | type Err = ParseError; |
30 | |
31 | fn from_str(s: &str) -> Result<Self, Self::Err> { |
32 | Self::parse(original:s) |
33 | } |
34 | } |
35 | |
36 | impl Licensee { |
37 | /// Creates a licensee from its component parts. Note that use of SPDX's |
38 | /// `or_later` is completely ignored for licensees as it only applies |
39 | /// to the license holder(s), not the licensee |
40 | #[must_use ] |
41 | pub fn new(license: LicenseItem, exception: Option<ExceptionId>) -> Self { |
42 | if let LicenseItem::Spdx { or_later, .. } = &license { |
43 | debug_assert!(!or_later); |
44 | } |
45 | |
46 | Self { |
47 | inner: LicenseReq { license, exception }, |
48 | } |
49 | } |
50 | |
51 | /// Parses an simplified version of an SPDX license expression that can |
52 | /// contain at most 1 valid SDPX license with an optional exception joined |
53 | /// by a `WITH`. |
54 | /// |
55 | /// ``` |
56 | /// use spdx::Licensee; |
57 | /// |
58 | /// // Normal single license |
59 | /// Licensee::parse("MIT" ).unwrap(); |
60 | /// |
61 | /// // SPDX allows license identifiers outside of the official license list |
62 | /// // via the LicenseRef- prefix |
63 | /// Licensee::parse("LicenseRef-My-Super-Extra-Special-License" ).unwrap(); |
64 | /// |
65 | /// // License and exception |
66 | /// Licensee::parse("Apache-2.0 WITH LLVM-exception" ).unwrap(); |
67 | /// |
68 | /// // `+` is only allowed to be used by license requirements from the license holder |
69 | /// Licensee::parse("Apache-2.0+" ).unwrap_err(); |
70 | /// |
71 | /// Licensee::parse("GPL-2.0" ).unwrap(); |
72 | /// |
73 | /// // GNU suffix license (GPL, AGPL, LGPL, GFDL) must not contain the suffix |
74 | /// Licensee::parse("GPL-3.0-or-later" ).unwrap_err(); |
75 | /// |
76 | /// // GFDL licenses are only allowed to contain the `invariants` suffix |
77 | /// Licensee::parse("GFDL-1.3-invariants" ).unwrap(); |
78 | /// ``` |
79 | pub fn parse(original: &str) -> Result<Self, ParseError> { |
80 | let mut lexer = Lexer::new(original); |
81 | |
82 | let license = { |
83 | let lt = lexer.next().ok_or_else(|| ParseError { |
84 | original: original.to_owned(), |
85 | span: 0..original.len(), |
86 | reason: Reason::Empty, |
87 | })??; |
88 | |
89 | match lt.token { |
90 | Token::Spdx(id) => { |
91 | // If we have one of the GNU licenses which use the `-only` |
92 | // or `-or-later` suffixes return an error rather than |
93 | // silently truncating, the `-only` and `-or-later` suffixes |
94 | // are for the license holder(s) to specify what license(s) |
95 | // they can be licensed under, not for the licensee, |
96 | // similarly to the `+` |
97 | if id.is_gnu() { |
98 | let is_only = original.ends_with("-only" ); |
99 | let or_later = original.ends_with("-or-later" ); |
100 | |
101 | if is_only || or_later { |
102 | return Err(ParseError { |
103 | original: original.to_owned(), |
104 | span: if is_only { |
105 | original.len() - 5..original.len() |
106 | } else { |
107 | original.len() - 9..original.len() |
108 | }, |
109 | reason: Reason::Unexpected(&["<bare-gnu-license>" ]), |
110 | }); |
111 | } |
112 | |
113 | // GFDL has `no-invariants` and `invariants` variants, we |
114 | // treat `no-invariants` as invalid, just the same as |
115 | // only, it would be the same as a bare GFDL-<version>. |
116 | // However, the `invariants`...variant we do allow since |
117 | // it is a modifier on the license...and should therefore |
118 | // by a WITH exception but GNU licenses are the worst |
119 | if original.starts_with("GFDL" ) && original.contains("-no-invariants" ) { |
120 | return Err(ParseError { |
121 | original: original.to_owned(), |
122 | span: 8..original.len(), |
123 | reason: Reason::Unexpected(&["<bare-gfdl-license>" ]), |
124 | }); |
125 | } |
126 | } |
127 | |
128 | LicenseItem::Spdx { |
129 | id, |
130 | or_later: false, |
131 | } |
132 | } |
133 | Token::LicenseRef { doc_ref, lic_ref } => LicenseItem::Other { |
134 | doc_ref: doc_ref.map(String::from), |
135 | lic_ref: lic_ref.to_owned(), |
136 | }, |
137 | _ => { |
138 | return Err(ParseError { |
139 | original: original.to_owned(), |
140 | span: lt.span, |
141 | reason: Reason::Unexpected(&["<license>" ]), |
142 | }) |
143 | } |
144 | } |
145 | }; |
146 | |
147 | let exception = match lexer.next() { |
148 | None => None, |
149 | Some(lt) => { |
150 | let lt = lt?; |
151 | match lt.token { |
152 | Token::With => { |
153 | let lt = lexer.next().ok_or(ParseError { |
154 | original: original.to_owned(), |
155 | span: lt.span, |
156 | reason: Reason::Empty, |
157 | })??; |
158 | |
159 | match lt.token { |
160 | Token::Exception(exc) => Some(exc), |
161 | _ => { |
162 | return Err(ParseError { |
163 | original: original.to_owned(), |
164 | span: lt.span, |
165 | reason: Reason::Unexpected(&["<exception>" ]), |
166 | }) |
167 | } |
168 | } |
169 | } |
170 | _ => { |
171 | return Err(ParseError { |
172 | original: original.to_owned(), |
173 | span: lt.span, |
174 | reason: Reason::Unexpected(&["WITH" ]), |
175 | }) |
176 | } |
177 | } |
178 | } |
179 | }; |
180 | |
181 | Ok(Licensee { |
182 | inner: LicenseReq { license, exception }, |
183 | }) |
184 | } |
185 | |
186 | /// Determines whether the specified license requirement is satisfied by |
187 | /// this license (+exception) |
188 | /// |
189 | /// ``` |
190 | /// let licensee = spdx::Licensee::parse("Apache-2.0 WITH LLVM-exception" ).unwrap(); |
191 | /// |
192 | /// assert!(licensee.satisfies(&spdx::LicenseReq { |
193 | /// license: spdx::LicenseItem::Spdx { |
194 | /// id: spdx::license_id("Apache-2.0" ).unwrap(), |
195 | /// // Means the license holder is fine with Apache-2.0 or higher |
196 | /// or_later: true, |
197 | /// }, |
198 | /// exception: spdx::exception_id("LLVM-exception" ), |
199 | /// })); |
200 | /// ``` |
201 | #[must_use ] |
202 | pub fn satisfies(&self, req: &LicenseReq) -> bool { |
203 | match (&self.inner.license, &req.license) { |
204 | (LicenseItem::Spdx { id: a, .. }, LicenseItem::Spdx { id: b, or_later }) => { |
205 | if a.index != b.index { |
206 | if *or_later { |
207 | let (a_name, a_gfdl_invariants) = if a.name.starts_with("GFDL" ) { |
208 | a.name |
209 | .strip_suffix("-invariants" ) |
210 | .map_or((a.name, false), |name| (name, true)) |
211 | } else { |
212 | (a.name, false) |
213 | }; |
214 | |
215 | let (b_name, b_gfdl_invariants) = if b.name.starts_with("GFDL" ) { |
216 | b.name |
217 | .strip_suffix("-invariants" ) |
218 | .map_or((b.name, false), |name| (name, true)) |
219 | } else { |
220 | (b.name, false) |
221 | }; |
222 | |
223 | if a_gfdl_invariants != b_gfdl_invariants { |
224 | return false; |
225 | } |
226 | |
227 | // Many of the SPDX identifiers end with `-<version number>`, |
228 | // so chop that off and ensure the base strings match, and if so, |
229 | // just a do a lexical compare, if this "allowed license" is >, |
230 | // then we satisfed the license requirement |
231 | let a_test_name = &a_name[..a_name.rfind('-' ).unwrap_or(a_name.len())]; |
232 | let b_test_name = &b_name[..b_name.rfind('-' ).unwrap_or(b_name.len())]; |
233 | |
234 | if a_test_name != b_test_name || a_name < b_name { |
235 | return false; |
236 | } |
237 | } else { |
238 | return false; |
239 | } |
240 | } |
241 | } |
242 | ( |
243 | LicenseItem::Other { |
244 | doc_ref: doc_a, |
245 | lic_ref: lic_a, |
246 | }, |
247 | LicenseItem::Other { |
248 | doc_ref: doc_b, |
249 | lic_ref: lic_b, |
250 | }, |
251 | ) => { |
252 | if doc_a != doc_b || lic_a != lic_b { |
253 | return false; |
254 | } |
255 | } |
256 | _ => return false, |
257 | } |
258 | |
259 | req.exception == self.inner.exception |
260 | } |
261 | |
262 | #[must_use ] |
263 | pub fn into_req(self) -> LicenseReq { |
264 | self.inner |
265 | } |
266 | } |
267 | |
268 | impl PartialOrd<LicenseReq> for Licensee { |
269 | #[inline ] |
270 | fn partial_cmp(&self, o: &LicenseReq) -> Option<std::cmp::Ordering> { |
271 | self.inner.partial_cmp(o) |
272 | } |
273 | } |
274 | |
275 | impl PartialEq<LicenseReq> for Licensee { |
276 | #[inline ] |
277 | fn eq(&self, o: &LicenseReq) -> bool { |
278 | self.inner.eq(o) |
279 | } |
280 | } |
281 | |
282 | impl AsRef<LicenseReq> for Licensee { |
283 | #[inline ] |
284 | fn as_ref(&self) -> &LicenseReq { |
285 | &self.inner |
286 | } |
287 | } |
288 | |
289 | #[cfg (test)] |
290 | mod test { |
291 | use crate::{exception_id, license_id, LicenseItem, LicenseReq, Licensee}; |
292 | |
293 | const LICENSEES: &[&str] = &[ |
294 | "LicenseRef-Embark-Proprietary" , |
295 | "BSD-2-Clause" , |
296 | "Apache-2.0 WITH LLVM-exception" , |
297 | "BSD-2-Clause-FreeBSD" , |
298 | "BSL-1.0" , |
299 | "Zlib" , |
300 | "CC0-1.0" , |
301 | "FTL" , |
302 | "ISC" , |
303 | "MIT" , |
304 | "MPL-2.0" , |
305 | "BSD-3-Clause" , |
306 | "Unicode-DFS-2016" , |
307 | "Unlicense" , |
308 | "Apache-2.0" , |
309 | ]; |
310 | |
311 | #[test ] |
312 | fn handles_or_later() { |
313 | let mut licensees: Vec<_> = LICENSEES |
314 | .iter() |
315 | .map(|l| Licensee::parse(l).unwrap()) |
316 | .collect(); |
317 | licensees.sort(); |
318 | |
319 | let mpl_id = license_id("MPL-2.0" ).unwrap(); |
320 | let req = LicenseReq { |
321 | license: LicenseItem::Spdx { |
322 | id: mpl_id, |
323 | or_later: true, |
324 | }, |
325 | exception: None, |
326 | }; |
327 | |
328 | // Licensees can't have the `or_later` |
329 | assert!(licensees.binary_search_by(|l| l.inner.cmp(&req)).is_err()); |
330 | |
331 | match &licensees[licensees |
332 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
333 | .unwrap()] |
334 | .inner |
335 | .license |
336 | { |
337 | LicenseItem::Spdx { id, .. } => assert_eq!(*id, mpl_id), |
338 | o @ LicenseItem::Other { .. } => panic!("unexpected {:?}" , o), |
339 | } |
340 | } |
341 | |
342 | #[test ] |
343 | fn handles_exceptions() { |
344 | let mut licensees: Vec<_> = LICENSEES |
345 | .iter() |
346 | .map(|l| Licensee::parse(l).unwrap()) |
347 | .collect(); |
348 | licensees.sort(); |
349 | |
350 | let apache_id = license_id("Apache-2.0" ).unwrap(); |
351 | let llvm_exc = exception_id("LLVM-exception" ).unwrap(); |
352 | let req = LicenseReq { |
353 | license: LicenseItem::Spdx { |
354 | id: apache_id, |
355 | or_later: false, |
356 | }, |
357 | exception: Some(llvm_exc), |
358 | }; |
359 | |
360 | assert_eq!( |
361 | &req, |
362 | &licensees[licensees |
363 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
364 | .unwrap()] |
365 | .inner |
366 | ); |
367 | } |
368 | |
369 | #[test ] |
370 | fn handles_license_ref() { |
371 | let mut licensees: Vec<_> = LICENSEES |
372 | .iter() |
373 | .map(|l| Licensee::parse(l).unwrap()) |
374 | .collect(); |
375 | licensees.sort(); |
376 | |
377 | let req = LicenseReq { |
378 | license: LicenseItem::Other { |
379 | doc_ref: None, |
380 | lic_ref: "Embark-Proprietary" .to_owned(), |
381 | }, |
382 | exception: None, |
383 | }; |
384 | |
385 | assert_eq!( |
386 | &req, |
387 | &licensees[licensees |
388 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
389 | .unwrap()] |
390 | .inner |
391 | ); |
392 | } |
393 | |
394 | #[test ] |
395 | fn handles_close() { |
396 | let mut licensees: Vec<_> = LICENSEES |
397 | .iter() |
398 | .map(|l| Licensee::parse(l).unwrap()) |
399 | .collect(); |
400 | licensees.sort(); |
401 | |
402 | for id in &["BSD-2-Clause" , "BSD-2-Clause-FreeBSD" ] { |
403 | let lic_id = license_id(id).unwrap(); |
404 | let req = LicenseReq { |
405 | license: LicenseItem::Spdx { |
406 | id: lic_id, |
407 | or_later: true, |
408 | }, |
409 | exception: None, |
410 | }; |
411 | |
412 | // Licensees can't have the `or_later` |
413 | assert!(licensees.binary_search_by(|l| l.inner.cmp(&req)).is_err()); |
414 | |
415 | match &licensees[licensees |
416 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
417 | .unwrap()] |
418 | .inner |
419 | .license |
420 | { |
421 | LicenseItem::Spdx { id, .. } => assert_eq!(*id, lic_id), |
422 | o @ LicenseItem::Other { .. } => panic!("unexpected {:?}" , o), |
423 | } |
424 | } |
425 | } |
426 | } |
427 | |