• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /// Error types
2 pub mod error;
3 pub mod expression;
4 /// Auto-generated lists of license identifiers and exception identifiers
5 pub mod identifiers;
6 /// Contains types for lexing an SPDX license expression
7 pub mod lexer;
8 mod licensee;
9 /// Auto-generated full canonical text of each license
10 #[cfg(feature = "text")]
11 pub mod text;
12 
13 pub use error::ParseError;
14 pub use expression::Expression;
15 use identifiers::{IS_COPYLEFT, IS_DEPRECATED, IS_FSF_LIBRE, IS_GNU, IS_OSI_APPROVED};
16 pub use lexer::ParseMode;
17 pub use licensee::Licensee;
18 use std::{
19     cmp::{self, Ordering},
20     fmt,
21 };
22 
23 /// Unique identifier for a particular license
24 ///
25 /// ```
26 /// let bsd = spdx::license_id("BSD-3-Clause").unwrap();
27 ///
28 /// assert!(
29 ///     bsd.is_fsf_free_libre()
30 ///     && bsd.is_osi_approved()
31 ///     && !bsd.is_deprecated()
32 ///     && !bsd.is_copyleft()
33 /// );
34 /// ```
35 #[derive(Copy, Clone, Eq)]
36 pub struct LicenseId {
37     /// The short identifier for the license
38     pub name: &'static str,
39     /// The full name of the license
40     pub full_name: &'static str,
41     index: usize,
42     flags: u8,
43 }
44 
45 impl PartialEq for LicenseId {
46     #[inline]
eq(&self, o: &Self) -> bool47     fn eq(&self, o: &Self) -> bool {
48         self.index == o.index
49     }
50 }
51 
52 impl Ord for LicenseId {
53     #[inline]
cmp(&self, o: &Self) -> Ordering54     fn cmp(&self, o: &Self) -> Ordering {
55         self.index.cmp(&o.index)
56     }
57 }
58 
59 impl PartialOrd for LicenseId {
60     #[inline]
partial_cmp(&self, o: &Self) -> Option<Ordering>61     fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
62         Some(self.cmp(o))
63     }
64 }
65 
66 impl LicenseId {
67     /// Returns true if the license is [considered free by the FSF](https://www.gnu.org/licenses/license-list.en.html)
68     ///
69     /// ```
70     /// assert!(spdx::license_id("GPL-2.0-only").unwrap().is_fsf_free_libre());
71     /// ```
72     #[inline]
73     #[must_use]
is_fsf_free_libre(self) -> bool74     pub fn is_fsf_free_libre(self) -> bool {
75         self.flags & IS_FSF_LIBRE != 0
76     }
77 
78     /// Returns true if the license is [OSI approved](https://opensource.org/licenses)
79     ///
80     /// ```
81     /// assert!(spdx::license_id("MIT").unwrap().is_osi_approved());
82     /// ```
83     #[inline]
84     #[must_use]
is_osi_approved(self) -> bool85     pub fn is_osi_approved(self) -> bool {
86         self.flags & IS_OSI_APPROVED != 0
87     }
88 
89     /// Returns true if the license is deprecated
90     ///
91     /// ```
92     /// assert!(spdx::license_id("wxWindows").unwrap().is_deprecated());
93     /// ```
94     #[inline]
95     #[must_use]
is_deprecated(self) -> bool96     pub fn is_deprecated(self) -> bool {
97         self.flags & IS_DEPRECATED != 0
98     }
99 
100     /// Returns true if the license is [copyleft](https://en.wikipedia.org/wiki/Copyleft)
101     ///
102     /// ```
103     /// assert!(spdx::license_id("LGPL-3.0-or-later").unwrap().is_copyleft());
104     /// ```
105     #[inline]
106     #[must_use]
is_copyleft(self) -> bool107     pub fn is_copyleft(self) -> bool {
108         self.flags & IS_COPYLEFT != 0
109     }
110 
111     /// Returns true if the license is a [GNU license](https://www.gnu.org/licenses/identify-licenses-clearly.html),
112     /// which operate differently than all other SPDX license identifiers
113     ///
114     /// ```
115     /// assert!(spdx::license_id("AGPL-3.0-only").unwrap().is_gnu());
116     /// ```
117     #[inline]
118     #[must_use]
is_gnu(self) -> bool119     pub fn is_gnu(self) -> bool {
120         self.flags & IS_GNU != 0
121     }
122 
123     /// Attempts to retrieve the license text
124     ///
125     /// ```
126     /// assert!(spdx::license_id("GFDL-1.3-invariants").unwrap().text().contains("Invariant Sections"))
127     /// ```
128     #[cfg(feature = "text")]
129     #[inline]
text(self) -> &'static str130     pub fn text(self) -> &'static str {
131         text::LICENSE_TEXTS[self.index].1
132     }
133 }
134 
135 impl fmt::Debug for LicenseId {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result136     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137         write!(f, "{}", self.name)
138     }
139 }
140 
141 /// Unique identifier for a particular exception
142 ///
143 /// ```
144 /// let exception_id = spdx::exception_id("LLVM-exception").unwrap();
145 /// assert!(!exception_id.is_deprecated());
146 /// ```
147 #[derive(Copy, Clone, Eq)]
148 pub struct ExceptionId {
149     /// The short identifier for the exception
150     pub name: &'static str,
151     index: usize,
152     flags: u8,
153 }
154 
155 impl PartialEq for ExceptionId {
156     #[inline]
eq(&self, o: &Self) -> bool157     fn eq(&self, o: &Self) -> bool {
158         self.index == o.index
159     }
160 }
161 
162 impl Ord for ExceptionId {
163     #[inline]
cmp(&self, o: &Self) -> Ordering164     fn cmp(&self, o: &Self) -> Ordering {
165         self.index.cmp(&o.index)
166     }
167 }
168 
169 impl PartialOrd for ExceptionId {
170     #[inline]
partial_cmp(&self, o: &Self) -> Option<Ordering>171     fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
172         Some(self.cmp(o))
173     }
174 }
175 
176 impl ExceptionId {
177     /// Returns true if the exception is deprecated
178     ///
179     /// ```
180     /// assert!(spdx::exception_id("Nokia-Qt-exception-1.1").unwrap().is_deprecated());
181     /// ```
182     #[inline]
183     #[must_use]
is_deprecated(self) -> bool184     pub fn is_deprecated(self) -> bool {
185         self.flags & IS_DEPRECATED != 0
186     }
187 
188     /// Attempts to retrieve the license exception text
189     ///
190     /// ```
191     /// assert!(spdx::exception_id("LLVM-exception").unwrap().text().contains("LLVM Exceptions to the Apache 2.0 License"));
192     /// ```
193     #[cfg(feature = "text")]
194     #[inline]
text(self) -> &'static str195     pub fn text(self) -> &'static str {
196         text::EXCEPTION_TEXTS[self.index].1
197     }
198 }
199 
200 impl fmt::Debug for ExceptionId {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result201     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202         write!(f, "{}", self.name)
203     }
204 }
205 
206 /// Represents a single license requirement, which must include a valid
207 /// [`LicenseItem`], and may allow current and future versions of the license,
208 /// and may also allow for a specific exception
209 ///
210 /// While they can be constructed manually, most of the time these will
211 /// be parsed and combined in an `Expression`
212 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
213 pub struct LicenseReq {
214     /// The license
215     pub license: LicenseItem,
216     /// The exception allowed for this license, as specified following
217     /// the `WITH` operator
218     pub exception: Option<ExceptionId>,
219 }
220 
221 impl From<LicenseId> for LicenseReq {
from(id: LicenseId) -> Self222     fn from(id: LicenseId) -> Self {
223         // We need to special case GNU licenses because reasons
224         let (id, or_later) = if id.is_gnu() {
225             let (or_later, name) = id
226                 .name
227                 .strip_suffix("-or-later")
228                 .map_or((false, id.name), |name| (true, name));
229 
230             let root = name.strip_suffix("-only").unwrap_or(name);
231 
232             // If the root, eg GPL-2.0 licenses, which are currently deprecated,
233             // are actually removed we will need to add them manually, but that
234             // should only occur on a major revision of the SPDX license list,
235             // so for now we should be fine with this
236             (
237                 license_id(root).expect("Unable to find root GNU license"),
238                 or_later,
239             )
240         } else {
241             (id, false)
242         };
243 
244         Self {
245             license: LicenseItem::Spdx { id, or_later },
246             exception: None,
247         }
248     }
249 }
250 
251 impl fmt::Display for LicenseReq {
fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>252     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
253         self.license.fmt(f)?;
254 
255         if let Some(ref exe) = self.exception {
256             write!(f, " WITH {}", exe.name)?;
257         }
258 
259         Ok(())
260     }
261 }
262 
263 /// A single license term in a license expression, according to the SPDX spec.
264 /// This can be either an SPDX license, which is mapped to a [`LicenseId`] from
265 /// a valid SPDX short identifier, or else a document AND/OR license ref
266 #[derive(Debug, Clone, Eq)]
267 pub enum LicenseItem {
268     /// A regular SPDX license id
269     Spdx {
270         id: LicenseId,
271         /// Indicates the license had a `+`, allowing the licensee to license
272         /// the software under either the specific version, or any later versions
273         or_later: bool,
274     },
275     Other {
276         /// Purpose: Identify any external SPDX documents referenced within this SPDX document.
277         /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
278         /// more details.
279         doc_ref: Option<String>,
280         /// Purpose: Provide a locally unique identifier to refer to licenses that are not found on the SPDX License List.
281         /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
282         /// more details.
283         lic_ref: String,
284     },
285 }
286 
287 impl LicenseItem {
288     /// Returns the license identifier, if it is a recognized SPDX license and not
289     /// a license referencer
290     #[must_use]
id(&self) -> Option<LicenseId>291     pub fn id(&self) -> Option<LicenseId> {
292         match self {
293             Self::Spdx { id, .. } => Some(*id),
294             Self::Other { .. } => None,
295         }
296     }
297 }
298 
299 impl Ord for LicenseItem {
cmp(&self, o: &Self) -> Ordering300     fn cmp(&self, o: &Self) -> Ordering {
301         match (self, o) {
302             (
303                 Self::Spdx {
304                     id: a,
305                     or_later: la,
306                 },
307                 Self::Spdx {
308                     id: b,
309                     or_later: lb,
310                 },
311             ) => match a.cmp(b) {
312                 Ordering::Equal => la.cmp(lb),
313                 o => o,
314             },
315             (
316                 Self::Other {
317                     doc_ref: ad,
318                     lic_ref: al,
319                 },
320                 Self::Other {
321                     doc_ref: bd,
322                     lic_ref: bl,
323                 },
324             ) => match ad.cmp(bd) {
325                 Ordering::Equal => al.cmp(bl),
326                 o => o,
327             },
328             (Self::Spdx { .. }, Self::Other { .. }) => Ordering::Less,
329             (Self::Other { .. }, Self::Spdx { .. }) => Ordering::Greater,
330         }
331     }
332 }
333 
334 impl PartialOrd for LicenseItem {
335     #[allow(clippy::non_canonical_partial_ord_impl)]
partial_cmp(&self, o: &Self) -> Option<Ordering>336     fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
337         match (self, o) {
338             (Self::Spdx { id: a, .. }, Self::Spdx { id: b, .. }) => a.partial_cmp(b),
339             (
340                 Self::Other {
341                     doc_ref: ad,
342                     lic_ref: al,
343                 },
344                 Self::Other {
345                     doc_ref: bd,
346                     lic_ref: bl,
347                 },
348             ) => match ad.cmp(bd) {
349                 Ordering::Equal => al.partial_cmp(bl),
350                 o => Some(o),
351             },
352             (Self::Spdx { .. }, Self::Other { .. }) => Some(cmp::Ordering::Less),
353             (Self::Other { .. }, Self::Spdx { .. }) => Some(cmp::Ordering::Greater),
354         }
355     }
356 }
357 
358 impl PartialEq for LicenseItem {
eq(&self, o: &Self) -> bool359     fn eq(&self, o: &Self) -> bool {
360         matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
361     }
362 }
363 
364 impl fmt::Display for LicenseItem {
fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>365     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
366         match self {
367             LicenseItem::Spdx { id, or_later } => {
368                 id.name.fmt(f)?;
369 
370                 if *or_later {
371                     if id.is_gnu() && id.is_deprecated() {
372                         f.write_str("-or-later")?;
373                     } else if !id.is_gnu() {
374                         f.write_str("+")?;
375                     }
376                 }
377 
378                 Ok(())
379             }
380             LicenseItem::Other {
381                 doc_ref: Some(d),
382                 lic_ref: l,
383             } => write!(f, "DocumentRef-{}:LicenseRef-{}", d, l),
384             LicenseItem::Other {
385                 doc_ref: None,
386                 lic_ref: l,
387             } => write!(f, "LicenseRef-{}", l),
388         }
389     }
390 }
391 
392 /// Attempts to find a [`LicenseId`] for the string. Note that any `+` at the
393 /// end is trimmed when searching for a match.
394 ///
395 /// ```
396 /// assert!(spdx::license_id("MIT").is_some());
397 /// assert!(spdx::license_id("BitTorrent-1.1+").is_some());
398 /// ```
399 #[inline]
400 #[must_use]
license_id(name: &str) -> Option<LicenseId>401 pub fn license_id(name: &str) -> Option<LicenseId> {
402     let name = name.trim_end_matches('+');
403     identifiers::LICENSES
404         .binary_search_by(|lic| lic.0.cmp(name))
405         .map(|index| {
406             let (name, full_name, flags) = identifiers::LICENSES[index];
407             LicenseId {
408                 name,
409                 full_name,
410                 index,
411                 flags,
412             }
413         })
414         .ok()
415 }
416 
417 /// Find license partially matching the name, e.g. "apache" => "Apache-2.0"
418 ///
419 /// Returns length (in bytes) of the string matched. Garbage at the end is
420 /// ignored. See
421 /// [`identifiers::IMPRECISE_NAMES`](identifiers/constant.IMPRECISE_NAMES.html)
422 /// for the list of invalid names, and the valid license identifiers they are
423 /// paired with.
424 ///
425 /// ```
426 /// assert!(spdx::imprecise_license_id("simplified bsd license").unwrap().0 == spdx::license_id("BSD-2-Clause").unwrap());
427 /// ```
428 #[inline]
429 #[must_use]
imprecise_license_id(name: &str) -> Option<(LicenseId, usize)>430 pub fn imprecise_license_id(name: &str) -> Option<(LicenseId, usize)> {
431     for (prefix, correct_name) in identifiers::IMPRECISE_NAMES {
432         if let Some(name_prefix) = name.as_bytes().get(0..prefix.len()) {
433             if prefix.as_bytes().eq_ignore_ascii_case(name_prefix) {
434                 return license_id(correct_name).map(|lic| (lic, prefix.len()));
435             }
436         }
437     }
438     None
439 }
440 
441 /// Attempts to find an [`ExceptionId`] for the string
442 ///
443 /// ```
444 /// assert!(spdx::exception_id("LLVM-exception").is_some());
445 /// ```
446 #[inline]
447 #[must_use]
exception_id(name: &str) -> Option<ExceptionId>448 pub fn exception_id(name: &str) -> Option<ExceptionId> {
449     identifiers::EXCEPTIONS
450         .binary_search_by(|exc| exc.0.cmp(name))
451         .map(|index| {
452             let (name, flags) = identifiers::EXCEPTIONS[index];
453             ExceptionId { name, index, flags }
454         })
455         .ok()
456 }
457 
458 /// Returns the version number of the SPDX list from which
459 /// the license and exception identifiers are sourced from
460 ///
461 /// ```
462 /// assert_eq!(spdx::license_version(), "3.26.0");
463 /// ```
464 #[inline]
465 #[must_use]
license_version() -> &'static str466 pub fn license_version() -> &'static str {
467     identifiers::VERSION
468 }
469 
470 #[cfg(test)]
471 mod test {
472     use super::LicenseItem;
473 
474     use crate::{license_id, Expression};
475 
476     #[test]
gnu_or_later_display()477     fn gnu_or_later_display() {
478         let gpl_or_later = LicenseItem::Spdx {
479             id: license_id("GPL-3.0").unwrap(),
480             or_later: true,
481         };
482 
483         let gpl_or_later_in_id = LicenseItem::Spdx {
484             id: license_id("GPL-3.0-or-later").unwrap(),
485             or_later: true,
486         };
487 
488         let gpl_or_later_parsed = Expression::parse("GPL-3.0-or-later").unwrap();
489 
490         let non_gnu_or_later = LicenseItem::Spdx {
491             id: license_id("Apache-2.0").unwrap(),
492             or_later: true,
493         };
494 
495         assert_eq!(gpl_or_later.to_string(), "GPL-3.0-or-later");
496         assert_eq!(gpl_or_later_parsed.to_string(), "GPL-3.0-or-later");
497         assert_eq!(gpl_or_later_in_id.to_string(), "GPL-3.0-or-later");
498         assert_eq!(non_gnu_or_later.to_string(), "Apache-2.0+");
499     }
500 }
501