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 /// 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]
130 pub fn text(self) -> &'static str {
131 text::LICENSE_TEXTS[self.index].1
132 }
133}
134
135impl fmt::Debug for LicenseId {
136 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)]
148pub struct ExceptionId {
149 /// The short identifier for the exception
150 pub name: &'static str,
151 index: usize,
152 flags: u8,
153}
154
155impl PartialEq for ExceptionId {
156 #[inline]
157 fn eq(&self, o: &Self) -> bool {
158 self.index == o.index
159 }
160}
161
162impl Ord for ExceptionId {
163 #[inline]
164 fn cmp(&self, o: &Self) -> Ordering {
165 self.index.cmp(&o.index)
166 }
167}
168
169impl PartialOrd for ExceptionId {
170 #[inline]
171 fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
172 Some(self.cmp(o))
173 }
174}
175
176impl 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]
184 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]
195 pub fn text(self) -> &'static str {
196 text::EXCEPTION_TEXTS[self.index].1
197 }
198}
199
200impl fmt::Debug for ExceptionId {
201 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)]
213pub 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
221impl From<LicenseId> for LicenseReq {
222 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
251impl fmt::Display for LicenseReq {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
253 self.license.fmt(f)?;
254
255 if let Some(ref exe: &ExceptionId) = 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)]
267pub 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
287impl LicenseItem {
288 /// Returns the license identifier, if it is a recognized SPDX license and not
289 /// a license referencer
290 #[must_use]
291 pub fn id(&self) -> Option<LicenseId> {
292 match self {
293 Self::Spdx { id: &LicenseId, .. } => Some(*id),
294 Self::Other { .. } => None,
295 }
296 }
297}
298
299impl Ord for LicenseItem {
300 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
334impl PartialOrd for LicenseItem {
335 #[allow(clippy::non_canonical_partial_ord_impl)]
336 fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
337 match (self, o) {
338 (Self::Spdx { id: a: &LicenseId, .. }, Self::Spdx { id: b: &LicenseId, .. }) => a.partial_cmp(b),
339 (
340 Self::Other {
341 doc_ref: ad: &Option,
342 lic_ref: al: &String,
343 },
344 Self::Other {
345 doc_ref: bd: &Option,
346 lic_ref: bl: &String,
347 },
348 ) => match ad.cmp(bd) {
349 Ordering::Equal => al.partial_cmp(bl),
350 o: Ordering => 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
358impl PartialEq for LicenseItem {
359 fn eq(&self, o: &Self) -> bool {
360 matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
361 }
362}
363
364impl fmt::Display for LicenseItem {
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]
401pub fn license_id(name: &str) -> Option<LicenseId> {
402 let name: &str = name.trim_end_matches('+');
403 identifiersResult::LICENSES
404 .binary_search_by(|lic| lic.0.cmp(name))
405 .map(|index: usize| {
406 let (name: &str, full_name: &str, flags: u8) = 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]
430pub fn imprecise_license_id(name: &str) -> Option<(LicenseId, usize)> {
431 for (prefix: &&str, correct_name: &&str) in identifiers::IMPRECISE_NAMES {
432 if let Some(name_prefix: &[u8]) = name.as_bytes().get(index:0..prefix.len()) {
433 if prefix.as_bytes().eq_ignore_ascii_case(name_prefix) {
434 return license_id(correct_name).map(|lic: LicenseId| (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]
448pub fn exception_id(name: &str) -> Option<ExceptionId> {
449 identifiersResult::EXCEPTIONS
450 .binary_search_by(|exc| exc.0.cmp(name))
451 .map(|index: usize| {
452 let (name: &str, flags: u8) = 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]
466pub fn license_version() -> &'static str {
467 identifiers::VERSION
468}
469
470#[cfg(test)]
471mod test {
472 use super::LicenseItem;
473
474 use crate::{license_id, Expression};
475
476 #[test]
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