1use crate::{
2 error::{ParseError, Reason},
3 lexer::{Lexer, Token},
4 ExceptionId, LicenseItem, LicenseReq,
5};
6use 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)]
18pub struct Licensee {
19 inner: LicenseReq,
20}
21
22impl fmt::Display for Licensee {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 self.inner.fmt(f)
25 }
26}
27
28impl 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
36impl 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
268impl 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
275impl PartialEq<LicenseReq> for Licensee {
276 #[inline]
277 fn eq(&self, o: &LicenseReq) -> bool {
278 self.inner.eq(o)
279 }
280}
281
282impl AsRef<LicenseReq> for Licensee {
283 #[inline]
284 fn as_ref(&self) -> &LicenseReq {
285 &self.inner
286 }
287}
288
289#[cfg(test)]
290mod 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