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 ] |
47 | fn eq(&self, o: &Self) -> bool { |
48 | self.index == o.index |
49 | } |
50 | } |
51 | |
52 | impl Ord for LicenseId { |
53 | #[inline ] |
54 | fn cmp(&self, o: &Self) -> Ordering { |
55 | self.index.cmp(&o.index) |
56 | } |
57 | } |
58 | |
59 | impl PartialOrd for LicenseId { |
60 | #[inline ] |
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 ] |
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 | |
135 | impl 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)] |
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 ] |
157 | fn eq(&self, o: &Self) -> bool { |
158 | self.index == o.index |
159 | } |
160 | } |
161 | |
162 | impl Ord for ExceptionId { |
163 | #[inline ] |
164 | fn cmp(&self, o: &Self) -> Ordering { |
165 | self.index.cmp(&o.index) |
166 | } |
167 | } |
168 | |
169 | impl PartialOrd for ExceptionId { |
170 | #[inline ] |
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 ] |
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 | |
200 | impl 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)] |
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 { |
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 | |
251 | impl 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)] |
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 ] |
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 | |
299 | impl 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 | |
334 | impl 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 | |
358 | impl PartialEq for LicenseItem { |
359 | 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 { |
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 ] |
401 | pub 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 ] |
430 | pub 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 ] |
448 | pub 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 ] |
466 | 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 ] |
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 | |