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