spdx/
lib.rs

1/// Error types
2pub mod error;
3pub mod expression;
4/// Auto-generated lists of license identifiers and exception identifiers
5pub mod identifiers;
6/// Contains types for lexing an SPDX license expression
7pub mod lexer;
8mod licensee;
9/// Auto-generated full canonical text of each license
10#[cfg(feature = "text")]
11pub mod text;
12
13pub use error::ParseError;
14pub use expression::Expression;
15use identifiers::{IS_COPYLEFT, IS_DEPRECATED, IS_FSF_LIBRE, IS_GNU, IS_OSI_APPROVED};
16pub use lexer::ParseMode;
17pub use licensee::Licensee;
18use 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)]
36pub 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
45impl PartialEq for LicenseId {
46    #[inline]
47    fn eq(&self, o: &Self) -> bool {
48        self.index == o.index
49    }
50}
51
52impl Ord for LicenseId {
53    #[inline]
54    fn cmp(&self, o: &Self) -> Ordering {
55        self.index.cmp(&o.index)
56    }
57}
58
59impl PartialOrd for LicenseId {
60    #[inline]
61    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
62        Some(self.cmp(o))
63    }
64}
65
66impl 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]
74    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]
85    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]
96    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]
107    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]
119    pub fn is_gnu(self) -> bool {
120        self.flags & IS_GNU != 0
121    }
122
123    /// Retrieves the version of the license ID, if any
124    ///
125    /// ```
126    /// assert_eq!(spdx::license_id("GPL-2.0-only").unwrap().version().unwrap(), "2.0");
127    /// assert_eq!(spdx::license_id("BSD-3-Clause").unwrap().version().unwrap(), "3");
128    /// assert!(spdx::license_id("Aladdin").unwrap().version().is_none());
129    /// ```
130    #[inline]
131    pub fn version(self) -> Option<&'static str> {
132        self.name
133            .split('-')
134            .find(|comp| comp.chars().all(|c| c == '.' || c.is_ascii_digit()))
135    }
136
137    /// The base name of the license
138    ///
139    /// ```
140    /// assert_eq!(spdx::license_id("GPL-2.0-only").unwrap().base(), "GPL");
141    /// assert_eq!(spdx::license_id("MIT").unwrap().base(), "MIT");
142    /// ```
143    #[inline]
144    pub fn base(self) -> &'static str {
145        self.name.split_once('-').map_or(self.name, |(n, _)| n)
146    }
147
148    /// Attempts to retrieve the license text
149    ///
150    /// ```
151    /// assert!(spdx::license_id("GFDL-1.3-invariants").unwrap().text().contains("Invariant Sections"))
152    /// ```
153    #[cfg(feature = "text")]
154    #[inline]
155    pub fn text(self) -> &'static str {
156        text::LICENSE_TEXTS[self.index].1
157    }
158}
159
160impl fmt::Debug for LicenseId {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "{}", self.name)
163    }
164}
165
166/// Unique identifier for a particular exception
167///
168/// ```
169/// let exception_id = spdx::exception_id("LLVM-exception").unwrap();
170/// assert!(!exception_id.is_deprecated());
171/// ```
172#[derive(Copy, Clone, Eq)]
173pub struct ExceptionId {
174    /// The short identifier for the exception
175    pub name: &'static str,
176    index: usize,
177    flags: u8,
178}
179
180impl PartialEq for ExceptionId {
181    #[inline]
182    fn eq(&self, o: &Self) -> bool {
183        self.index == o.index
184    }
185}
186
187impl Ord for ExceptionId {
188    #[inline]
189    fn cmp(&self, o: &Self) -> Ordering {
190        self.index.cmp(&o.index)
191    }
192}
193
194impl PartialOrd for ExceptionId {
195    #[inline]
196    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
197        Some(self.cmp(o))
198    }
199}
200
201impl ExceptionId {
202    /// Returns true if the exception is deprecated
203    ///
204    /// ```
205    /// assert!(spdx::exception_id("Nokia-Qt-exception-1.1").unwrap().is_deprecated());
206    /// ```
207    #[inline]
208    #[must_use]
209    pub fn is_deprecated(self) -> bool {
210        self.flags & IS_DEPRECATED != 0
211    }
212
213    /// Attempts to retrieve the license exception text
214    ///
215    /// ```
216    /// assert!(spdx::exception_id("LLVM-exception").unwrap().text().contains("LLVM Exceptions to the Apache 2.0 License"));
217    /// ```
218    #[cfg(feature = "text")]
219    #[inline]
220    pub fn text(self) -> &'static str {
221        text::EXCEPTION_TEXTS[self.index].1
222    }
223}
224
225impl fmt::Debug for ExceptionId {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        write!(f, "{}", self.name)
228    }
229}
230
231/// Represents a single license requirement.
232///
233/// The requirement must include a valid [`LicenseItem`], and may allow current
234/// and future versions of the license, and may also allow for a specific exception
235///
236/// While they can be constructed manually, most of the time these will
237/// be parsed and combined in an [Expression]
238#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
239pub struct LicenseReq {
240    /// The license
241    pub license: LicenseItem,
242    /// The additional text for this license, as specified following
243    /// the `WITH` operator
244    pub addition: Option<AdditionItem>,
245}
246
247impl From<LicenseId> for LicenseReq {
248    fn from(id: LicenseId) -> Self {
249        Self {
250            license: LicenseItem::Spdx {
251                id,
252                or_later: false,
253            },
254            addition: None,
255        }
256    }
257}
258
259impl fmt::Display for LicenseReq {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
261        self.license.fmt(f)?;
262
263        if let Some(ref exe) = self.addition {
264            write!(f, " WITH {exe}")?;
265        }
266
267        Ok(())
268    }
269}
270
271/// A single license term in a license expression, according to the SPDX spec.
272///
273/// This can be either an SPDX license, which is mapped to a [`LicenseId`] from
274/// a valid SPDX short identifier, or else a document and/or license ref
275#[derive(Debug, Clone, Eq)]
276pub enum LicenseItem {
277    /// A regular SPDX license id
278    Spdx {
279        id: LicenseId,
280        /// Indicates the license had a `+`, allowing the licensee to license
281        /// the software under either the specific version, or any later versions
282        or_later: bool,
283    },
284    Other {
285        /// Purpose: Identify any external SPDX documents referenced within this SPDX document.
286        /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
287        /// more details.
288        doc_ref: Option<String>,
289        /// Purpose: Provide a locally unique identifier to refer to licenses that are not found on the SPDX License List.
290        /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
291        /// more details.
292        lic_ref: String,
293    },
294}
295
296impl LicenseItem {
297    /// Returns the license identifier, if it is a recognized SPDX license and not
298    /// a license referencer
299    #[must_use]
300    pub fn id(&self) -> Option<LicenseId> {
301        match self {
302            Self::Spdx { id, .. } => Some(*id),
303            Self::Other { .. } => None,
304        }
305    }
306}
307
308impl Ord for LicenseItem {
309    fn cmp(&self, o: &Self) -> Ordering {
310        match (self, o) {
311            (
312                Self::Spdx {
313                    id: a,
314                    or_later: la,
315                },
316                Self::Spdx {
317                    id: b,
318                    or_later: lb,
319                },
320            ) => match a.cmp(b) {
321                Ordering::Equal => la.cmp(lb),
322                o => o,
323            },
324            (
325                Self::Other {
326                    doc_ref: ad,
327                    lic_ref: al,
328                },
329                Self::Other {
330                    doc_ref: bd,
331                    lic_ref: bl,
332                },
333            ) => match ad.cmp(bd) {
334                Ordering::Equal => al.cmp(bl),
335                o => o,
336            },
337            (Self::Spdx { .. }, Self::Other { .. }) => Ordering::Less,
338            (Self::Other { .. }, Self::Spdx { .. }) => Ordering::Greater,
339        }
340    }
341}
342
343impl PartialOrd for LicenseItem {
344    #[allow(clippy::non_canonical_partial_ord_impl)]
345    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
346        match (self, o) {
347            (Self::Spdx { id: a, .. }, Self::Spdx { id: b, .. }) => a.partial_cmp(b),
348            (
349                Self::Other {
350                    doc_ref: ad,
351                    lic_ref: al,
352                },
353                Self::Other {
354                    doc_ref: bd,
355                    lic_ref: bl,
356                },
357            ) => match ad.cmp(bd) {
358                Ordering::Equal => al.partial_cmp(bl),
359                o => Some(o),
360            },
361            (Self::Spdx { .. }, Self::Other { .. }) => Some(cmp::Ordering::Less),
362            (Self::Other { .. }, Self::Spdx { .. }) => Some(cmp::Ordering::Greater),
363        }
364    }
365}
366
367impl PartialEq for LicenseItem {
368    fn eq(&self, o: &Self) -> bool {
369        matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
370    }
371}
372
373impl fmt::Display for LicenseItem {
374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
375        match self {
376            LicenseItem::Spdx { id, or_later } => {
377                id.name.fmt(f)?;
378
379                if *or_later {
380                    if id.is_gnu() && id.is_deprecated() {
381                        f.write_str("-or-later")?;
382                    } else if !id.is_gnu() {
383                        f.write_str("+")?;
384                    }
385                }
386
387                Ok(())
388            }
389            LicenseItem::Other {
390                doc_ref: Some(d),
391                lic_ref: l,
392            } => write!(f, "DocumentRef-{d}:LicenseRef-{l}"),
393            LicenseItem::Other {
394                doc_ref: None,
395                lic_ref: l,
396            } => write!(f, "LicenseRef-{l}"),
397        }
398    }
399}
400
401/// A single addition term in a addition expression, according to the SPDX spec.
402///
403/// This can be either an SPDX license exception, which is mapped to a [`ExceptionId`]
404/// from a valid SPDX short identifier, or else a document and/or addition ref
405#[derive(Debug, Clone, Eq)]
406pub enum AdditionItem {
407    /// A regular SPDX license exception id
408    Spdx(ExceptionId),
409    Other {
410        /// Purpose: Identify any external SPDX documents referenced within this SPDX document.
411        /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
412        /// more details.
413        doc_ref: Option<String>,
414        /// Purpose: Provide a locally unique identifier to refer to additional text that are not found on the SPDX License List.
415        /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
416        /// more details.
417        add_ref: String,
418    },
419}
420
421impl AdditionItem {
422    /// Returns the license exception identifier, if it is a recognized SPDX license exception
423    /// and not a license exception referencer
424    #[must_use]
425    pub fn id(&self) -> Option<ExceptionId> {
426        match self {
427            Self::Spdx(id) => Some(*id),
428            Self::Other { .. } => None,
429        }
430    }
431}
432
433impl Ord for AdditionItem {
434    fn cmp(&self, o: &Self) -> Ordering {
435        match (self, o) {
436            (Self::Spdx(a), Self::Spdx(b)) => match a.cmp(b) {
437                Ordering::Equal => a.cmp(b),
438                o => o,
439            },
440            (
441                Self::Other {
442                    doc_ref: ad,
443                    add_ref: aa,
444                },
445                Self::Other {
446                    doc_ref: bd,
447                    add_ref: ba,
448                },
449            ) => match ad.cmp(bd) {
450                Ordering::Equal => aa.cmp(ba),
451                o => o,
452            },
453            (Self::Spdx(_), Self::Other { .. }) => Ordering::Less,
454            (Self::Other { .. }, Self::Spdx(_)) => Ordering::Greater,
455        }
456    }
457}
458
459impl PartialOrd for AdditionItem {
460    #[allow(clippy::non_canonical_partial_ord_impl)]
461    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
462        match (self, o) {
463            (Self::Spdx(a), Self::Spdx(b)) => a.partial_cmp(b),
464            (
465                Self::Other {
466                    doc_ref: ad,
467                    add_ref: aa,
468                },
469                Self::Other {
470                    doc_ref: bd,
471                    add_ref: ba,
472                },
473            ) => match ad.cmp(bd) {
474                Ordering::Equal => aa.partial_cmp(ba),
475                o => Some(o),
476            },
477            (Self::Spdx(_), Self::Other { .. }) => Some(cmp::Ordering::Less),
478            (Self::Other { .. }, Self::Spdx(_)) => Some(cmp::Ordering::Greater),
479        }
480    }
481}
482
483impl PartialEq for AdditionItem {
484    fn eq(&self, o: &Self) -> bool {
485        matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
486    }
487}
488
489impl fmt::Display for AdditionItem {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
491        match self {
492            AdditionItem::Spdx(id) => id.name.fmt(f),
493            AdditionItem::Other {
494                doc_ref: Some(d),
495                add_ref: a,
496            } => write!(f, "DocumentRef-{d}:AdditionRef-{a}"),
497            AdditionItem::Other {
498                doc_ref: None,
499                add_ref: a,
500            } => write!(f, "AdditionRef-{a}"),
501        }
502    }
503}
504
505/// Attempts to find a [`LicenseId`] for the string.
506///
507/// Note that any `+` at the end is trimmed when searching for a match.
508///
509/// ```
510/// assert!(spdx::license_id("MIT").is_some());
511/// assert!(spdx::license_id("BitTorrent-1.1+").is_some());
512/// ```
513#[inline]
514#[must_use]
515pub fn license_id(name: &str) -> Option<LicenseId> {
516    let name = name.trim_end_matches('+');
517    identifiers::LICENSES
518        .binary_search_by(|lic| lic.0.cmp(name))
519        .map(|index| {
520            let (name, full_name, flags) = identifiers::LICENSES[index];
521            LicenseId {
522                name,
523                full_name,
524                index,
525                flags,
526            }
527        })
528        .ok()
529}
530
531/// Attempts to find a GNU license from its base name.
532///
533/// GNU licenses are "special", unlike every other license in the SPDX list, they
534/// have (in _most_ cases) a bare variant which is deprecated, eg. GPL-2.0, an
535/// `-only` variant which acts like every other license, and an `-or-later`
536/// variant which acts as if `+` was applied.
537#[inline]
538#[must_use]
539pub fn gnu_license_id(base: &str, or_later: bool) -> Option<LicenseId> {
540    if base.ends_with("-only") || base.ends_with("-or-later") {
541        license_id(base)
542    } else {
543        let mut v = smallvec::SmallVec::<[u8; 32]>::new();
544        v.resize(base.len() + if or_later { 9 } else { 5 }, 0);
545
546        v[..base.len()].copy_from_slice(base.as_bytes());
547
548        if or_later {
549            v[base.len()..].copy_from_slice(b"-or-later");
550        } else {
551            v[base.len()..].copy_from_slice(b"-only");
552        }
553
554        let Ok(s) = std::str::from_utf8(v.as_slice()) else {
555            // Unreachable, but whatever
556            return None;
557        };
558        license_id(s)
559    }
560}
561
562/// Find license partially matching the name, e.g. "apache" => "Apache-2.0"
563///
564/// Returns length (in bytes) of the string matched. Garbage at the end is
565/// ignored. See [`crate::identifiers::IMPRECISE_NAMES`] for the list of invalid
566/// names, and the valid license identifiers they are mapped to.
567///
568/// ```
569/// assert_eq!(
570///     spdx::imprecise_license_id("simplified bsd license").unwrap().0,
571///     spdx::license_id("BSD-2-Clause").unwrap()
572/// );
573/// ```
574#[inline]
575#[must_use]
576pub fn imprecise_license_id(name: &str) -> Option<(LicenseId, usize)> {
577    for (prefix, correct_name) in identifiers::IMPRECISE_NAMES {
578        if let Some(name_prefix) = name.as_bytes().get(0..prefix.len()) {
579            if prefix.as_bytes().eq_ignore_ascii_case(name_prefix) {
580                return license_id(correct_name).map(|lic| (lic, prefix.len()));
581            }
582        }
583    }
584    None
585}
586
587/// Attempts to find an [`ExceptionId`] for the string
588///
589/// ```
590/// assert!(spdx::exception_id("LLVM-exception").is_some());
591/// ```
592#[inline]
593#[must_use]
594pub fn exception_id(name: &str) -> Option<ExceptionId> {
595    identifiers::EXCEPTIONS
596        .binary_search_by(|exc| exc.0.cmp(name))
597        .map(|index| {
598            let (name, flags) = identifiers::EXCEPTIONS[index];
599            ExceptionId { name, index, flags }
600        })
601        .ok()
602}
603
604/// Returns the version number of the SPDX list from which
605/// the license and exception identifiers are sourced from
606#[inline]
607#[must_use]
608pub fn license_version() -> &'static str {
609    identifiers::VERSION
610}
611
612#[cfg(test)]
613mod test {
614    use super::LicenseItem;
615
616    use crate::{Expression, license_id};
617
618    #[test]
619    fn gnu_or_later_display() {
620        let gpl_or_later = LicenseItem::Spdx {
621            id: license_id("GPL-3.0").unwrap(),
622            or_later: true,
623        };
624
625        let gpl_or_later_in_id = LicenseItem::Spdx {
626            id: license_id("GPL-3.0-or-later").unwrap(),
627            or_later: true,
628        };
629
630        let gpl_or_later_parsed = Expression::parse("GPL-3.0-or-later").unwrap();
631
632        let non_gnu_or_later = LicenseItem::Spdx {
633            id: license_id("Apache-2.0").unwrap(),
634            or_later: true,
635        };
636
637        assert_eq!(gpl_or_later.to_string(), "GPL-3.0-or-later");
638        assert_eq!(gpl_or_later_parsed.to_string(), "GPL-3.0-or-later");
639        assert_eq!(gpl_or_later_in_id.to_string(), "GPL-3.0-or-later");
640        assert_eq!(non_gnu_or_later.to_string(), "Apache-2.0+");
641    }
642}