• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result23     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 
from_str(s: &str) -> Result<Self, Self::Err>31     fn from_str(s: &str) -> Result<Self, Self::Err> {
32         Self::parse(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]
new(license: LicenseItem, exception: Option<ExceptionId>) -> Self41     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     /// ```
parse(original: &str) -> Result<Self, ParseError>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]
satisfies(&self, req: &LicenseReq) -> bool202     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]
into_req(self) -> LicenseReq263     pub fn into_req(self) -> LicenseReq {
264         self.inner
265     }
266 }
267 
268 impl PartialOrd<LicenseReq> for Licensee {
269     #[inline]
partial_cmp(&self, o: &LicenseReq) -> Option<std::cmp::Ordering>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]
eq(&self, o: &LicenseReq) -> bool277     fn eq(&self, o: &LicenseReq) -> bool {
278         self.inner.eq(o)
279     }
280 }
281 
282 impl AsRef<LicenseReq> for Licensee {
283     #[inline]
as_ref(&self) -> &LicenseReq284     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]
handles_or_later()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]
handles_exceptions()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]
handles_license_ref()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]
handles_close()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