1pub mod error;
3pub mod expression;
4pub mod identifiers;
6pub mod lexer;
8mod licensee;
9#[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#[derive(Copy, Clone, Eq)]
36pub struct LicenseId {
37 pub name: &'static str,
39 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 #[inline]
73 #[must_use]
74 pub fn is_fsf_free_libre(self) -> bool {
75 self.flags & IS_FSF_LIBRE != 0
76 }
77
78 #[inline]
84 #[must_use]
85 pub fn is_osi_approved(self) -> bool {
86 self.flags & IS_OSI_APPROVED != 0
87 }
88
89 #[inline]
95 #[must_use]
96 pub fn is_deprecated(self) -> bool {
97 self.flags & IS_DEPRECATED != 0
98 }
99
100 #[inline]
106 #[must_use]
107 pub fn is_copyleft(self) -> bool {
108 self.flags & IS_COPYLEFT != 0
109 }
110
111 #[inline]
118 #[must_use]
119 pub fn is_gnu(self) -> bool {
120 self.flags & IS_GNU != 0
121 }
122
123 #[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 #[inline]
144 pub fn base(self) -> &'static str {
145 self.name.split_once('-').map_or(self.name, |(n, _)| n)
146 }
147
148 #[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#[derive(Copy, Clone, Eq)]
173pub struct ExceptionId {
174 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 #[inline]
208 #[must_use]
209 pub fn is_deprecated(self) -> bool {
210 self.flags & IS_DEPRECATED != 0
211 }
212
213 #[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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
239pub struct LicenseReq {
240 pub license: LicenseItem,
242 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#[derive(Debug, Clone, Eq)]
276pub enum LicenseItem {
277 Spdx {
279 id: LicenseId,
280 or_later: bool,
283 },
284 Other {
285 doc_ref: Option<String>,
289 lic_ref: String,
293 },
294}
295
296impl LicenseItem {
297 #[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#[derive(Debug, Clone, Eq)]
406pub enum AdditionItem {
407 Spdx(ExceptionId),
409 Other {
410 doc_ref: Option<String>,
414 add_ref: String,
418 },
419}
420
421impl AdditionItem {
422 #[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#[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#[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 return None;
557 };
558 license_id(s)
559 }
560}
561
562#[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#[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#[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}