1#[cfg(feature = "alloc")]
14extern crate alloc;
15
16#[cfg(feature = "alloc")]
17use alloc::{collections::BTreeMap, string::String, vec::Vec};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum License {
26 MIT,
28 Apache2,
30 GPL2,
32 GPL3,
34 LGPL21,
36 BSD2,
38 BSD3,
40 ISC,
42 MPL2,
44 Proprietary,
46 Unknown,
48}
49
50#[cfg(feature = "alloc")]
51impl License {
52 pub fn as_str(self) -> &'static str {
54 match self {
55 Self::MIT => "MIT",
56 Self::Apache2 => "Apache-2.0",
57 Self::GPL2 => "GPL-2.0",
58 Self::GPL3 => "GPL-3.0",
59 Self::LGPL21 => "LGPL-2.1",
60 Self::BSD2 => "BSD-2-Clause",
61 Self::BSD3 => "BSD-3-Clause",
62 Self::ISC => "ISC",
63 Self::MPL2 => "MPL-2.0",
64 Self::Proprietary => "Proprietary",
65 Self::Unknown => "Unknown",
66 }
67 }
68
69 pub fn from_spdx(s: &str) -> Self {
71 match s.trim() {
72 "MIT" => Self::MIT,
73 "Apache-2.0" | "Apache2" => Self::Apache2,
74 "GPL-2.0" | "GPL-2.0-only" | "GPL2" => Self::GPL2,
75 "GPL-3.0" | "GPL-3.0-only" | "GPL3" => Self::GPL3,
76 "LGPL-2.1" | "LGPL-2.1-only" => Self::LGPL21,
77 "BSD-2-Clause" | "BSD2" => Self::BSD2,
78 "BSD-3-Clause" | "BSD3" => Self::BSD3,
79 "ISC" => Self::ISC,
80 "MPL-2.0" | "MPL2" => Self::MPL2,
81 "Proprietary" => Self::Proprietary,
82 _ => Self::Unknown,
83 }
84 }
85
86 pub fn is_copyleft(self) -> bool {
88 matches!(self, Self::GPL2 | Self::GPL3 | Self::LGPL21 | Self::MPL2)
89 }
90
91 pub fn is_permissive(self) -> bool {
93 matches!(
94 self,
95 Self::MIT | Self::Apache2 | Self::BSD2 | Self::BSD3 | Self::ISC
96 )
97 }
98}
99
100#[cfg(feature = "alloc")]
105pub fn detect_license(text: &str) -> License {
106 let lower = text.to_lowercase();
108
109 if lower.contains("gnu general public license") || lower.contains("gpl") {
111 if lower.contains("version 3") || lower.contains("gpl-3") || lower.contains("gplv3") {
112 return License::GPL3;
113 }
114 if lower.contains("version 2") || lower.contains("gpl-2") || lower.contains("gplv2") {
115 return License::GPL2;
116 }
117 if lower.contains("general public license") {
119 return License::GPL3;
120 }
121 }
122
123 if (lower.contains("lesser general public license") || lower.contains("lgpl"))
125 && (lower.contains("2.1") || lower.contains("lgpl-2.1"))
126 {
127 return License::LGPL21;
128 }
129
130 if (lower.contains("apache license") || lower.contains("apache-2"))
132 && (lower.contains("version 2") || lower.contains("2.0") || lower.contains("apache-2"))
133 {
134 return License::Apache2;
135 }
136
137 if lower.contains("mit license")
139 || lower.contains("permission is hereby granted, free of charge")
140 {
141 return License::MIT;
142 }
143
144 if lower.contains("bsd") {
146 if lower.contains("2-clause") || lower.contains("simplified") {
147 return License::BSD2;
148 }
149 if lower.contains("3-clause") || lower.contains("new") || lower.contains("revised") {
150 return License::BSD3;
151 }
152 return License::BSD3;
154 }
155
156 if lower.contains("isc license")
158 || lower.contains("permission to use, copy, modify, and/or distribute")
159 {
160 return License::ISC;
161 }
162
163 if lower.contains("mozilla public license") || lower.contains("mpl-2") {
165 return License::MPL2;
166 }
167
168 if lower.contains("proprietary")
170 || lower.contains("all rights reserved")
171 || lower.contains("no permission")
172 {
173 return License::Proprietary;
174 }
175
176 License::Unknown
177}
178
179#[cfg(feature = "alloc")]
185#[derive(Debug, Clone)]
186pub struct LicenseConflict {
187 pub package_a: String,
189 pub license_a: License,
191 pub package_b: String,
193 pub license_b: License,
195 pub reason: String,
197}
198
199#[cfg(feature = "alloc")]
204pub struct LicenseCompatibility;
205
206#[cfg(feature = "alloc")]
207impl LicenseCompatibility {
208 pub fn is_compatible(a: &License, b: &License) -> bool {
217 if *a == License::Unknown || *b == License::Unknown {
219 return true;
220 }
221
222 if *a == License::Proprietary {
224 return !matches!(b, License::GPL2 | License::GPL3 | License::LGPL21);
225 }
226 if *b == License::Proprietary {
227 return !matches!(a, License::GPL2 | License::GPL3 | License::LGPL21);
228 }
229
230 if (*a == License::GPL3 && *b == License::GPL2)
232 || (*a == License::GPL2 && *b == License::GPL3)
233 {
234 return false;
235 }
236
237 true
239 }
240}
241
242#[cfg(feature = "alloc")]
247pub fn check_compatibility(deps: &[(String, License)]) -> Result<(), Vec<LicenseConflict>> {
248 let mut conflicts = Vec::new();
249
250 for i in 0..deps.len() {
251 for j in (i + 1)..deps.len() {
252 let (ref name_a, ref license_a) = deps[i];
253 let (ref name_b, ref license_b) = deps[j];
254
255 if !LicenseCompatibility::is_compatible(license_a, license_b) {
256 let reason = alloc::format!(
257 "{} ({}) is incompatible with {} ({})",
258 license_a.as_str(),
259 name_a,
260 license_b.as_str(),
261 name_b
262 );
263 conflicts.push(LicenseConflict {
264 package_a: name_a.clone(),
265 license_a: *license_a,
266 package_b: name_b.clone(),
267 license_b: *license_b,
268 reason,
269 });
270 }
271 }
272 }
273
274 if conflicts.is_empty() {
275 Ok(())
276 } else {
277 Err(conflicts)
278 }
279}
280
281#[cfg(feature = "alloc")]
291pub struct DependencyGraph {
292 nodes: BTreeMap<String, Vec<String>>,
294}
295
296#[cfg(feature = "alloc")]
297impl DependencyGraph {
298 pub fn new() -> Self {
300 Self {
301 nodes: BTreeMap::new(),
302 }
303 }
304
305 pub fn add_package(&mut self, name: &str) {
307 self.nodes
308 .entry(String::from(name))
309 .or_insert_with(Vec::new);
310 }
311
312 pub fn add_dependency(&mut self, package: &str, dependency: &str) {
316 self.nodes
317 .entry(String::from(package))
318 .or_insert_with(Vec::new)
319 .push(String::from(dependency));
320 self.nodes
322 .entry(String::from(dependency))
323 .or_insert_with(Vec::new);
324 }
325
326 pub fn find_reverse_deps(&self, package: &str) -> Vec<String> {
329 let mut reverse = Vec::new();
330 for (node, deps) in &self.nodes {
331 if deps.iter().any(|d| d == package) {
332 reverse.push(node.clone());
333 }
334 }
335 reverse
336 }
337
338 pub fn detect_circular_deps(&self) -> Vec<Vec<String>> {
343 let mut cycles = Vec::new();
344 let mut visited: BTreeMap<String, bool> = BTreeMap::new();
345 let mut in_stack: BTreeMap<String, bool> = BTreeMap::new();
346 let mut path: Vec<String> = Vec::new();
347
348 for node in self.nodes.keys() {
349 if !visited.get(node).copied().unwrap_or(false) {
350 self.dfs_detect_cycles(node, &mut visited, &mut in_stack, &mut path, &mut cycles);
351 }
352 }
353
354 cycles
355 }
356
357 fn dfs_detect_cycles(
359 &self,
360 node: &str,
361 visited: &mut BTreeMap<String, bool>,
362 in_stack: &mut BTreeMap<String, bool>,
363 path: &mut Vec<String>,
364 cycles: &mut Vec<Vec<String>>,
365 ) {
366 visited.insert(String::from(node), true);
367 in_stack.insert(String::from(node), true);
368 path.push(String::from(node));
369
370 if let Some(deps) = self.nodes.get(node) {
371 for dep in deps {
372 if !visited.get(dep).copied().unwrap_or(false) {
373 self.dfs_detect_cycles(dep, visited, in_stack, path, cycles);
374 } else if in_stack.get(dep).copied().unwrap_or(false) {
375 let mut cycle = Vec::new();
377 let mut found = false;
378 for p in path.iter() {
379 if p == dep {
380 found = true;
381 }
382 if found {
383 cycle.push(p.clone());
384 }
385 }
386 cycle.push(dep.clone()); cycles.push(cycle);
388 }
389 }
390 }
391
392 path.pop();
393 in_stack.insert(String::from(node), false);
394 }
395
396 pub fn dependency_depth(&self, package: &str) -> usize {
402 if !self.nodes.contains_key(package) {
403 return 0;
404 }
405
406 let roots = self.find_roots();
408 if roots.is_empty() {
409 return 0;
410 }
411
412 let mut max_depth = 0;
413
414 for root in &roots {
415 let depth = self.bfs_depth(root, package);
416 if depth > max_depth {
417 max_depth = depth;
418 }
419 }
420
421 max_depth
422 }
423
424 pub fn find_roots(&self) -> Vec<String> {
426 let mut roots = Vec::new();
427 for node in self.nodes.keys() {
428 let has_reverse = self
429 .nodes
430 .values()
431 .any(|deps| deps.iter().any(|d| d == node));
432 if !has_reverse {
433 roots.push(node.clone());
434 }
435 }
436 roots
437 }
438
439 fn bfs_depth(&self, start: &str, target: &str) -> usize {
443 let mut visited = BTreeMap::new();
445 self.dfs_longest_path(start, target, &mut visited)
446 }
447
448 fn dfs_longest_path(
450 &self,
451 current: &str,
452 target: &str,
453 visited: &mut BTreeMap<String, bool>,
454 ) -> usize {
455 if current == target {
456 return 0;
457 }
458
459 if visited.get(current).copied().unwrap_or(false) {
460 return 0;
461 }
462
463 visited.insert(String::from(current), true);
464
465 let mut max_depth = 0;
466 let mut found = false;
467
468 if let Some(deps) = self.nodes.get(current) {
469 for dep in deps {
470 let depth = self.dfs_longest_path(dep, target, visited);
471 if dep == target || depth > 0 {
472 found = true;
473 let candidate = 1 + depth;
474 if candidate > max_depth {
475 max_depth = candidate;
476 }
477 }
478 }
479 }
480
481 visited.insert(String::from(current), false);
482
483 if found {
484 max_depth
485 } else {
486 0
487 }
488 }
489
490 pub fn package_count(&self) -> usize {
492 self.nodes.len()
493 }
494
495 pub fn edge_count(&self) -> usize {
497 self.nodes.values().map(|deps| deps.len()).sum()
498 }
499
500 pub fn dependencies(&self, package: &str) -> Option<&[String]> {
502 self.nodes.get(package).map(|v| v.as_slice())
503 }
504}
505
506#[cfg(feature = "alloc")]
507impl Default for DependencyGraph {
508 fn default() -> Self {
509 Self::new()
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 #[allow(unused_imports)]
516 use alloc::vec;
517
518 use super::*;
519
520 #[test]
523 fn test_license_as_str() {
524 assert_eq!(License::MIT.as_str(), "MIT");
525 assert_eq!(License::Apache2.as_str(), "Apache-2.0");
526 assert_eq!(License::GPL2.as_str(), "GPL-2.0");
527 assert_eq!(License::GPL3.as_str(), "GPL-3.0");
528 assert_eq!(License::LGPL21.as_str(), "LGPL-2.1");
529 assert_eq!(License::BSD2.as_str(), "BSD-2-Clause");
530 assert_eq!(License::BSD3.as_str(), "BSD-3-Clause");
531 assert_eq!(License::ISC.as_str(), "ISC");
532 assert_eq!(License::MPL2.as_str(), "MPL-2.0");
533 assert_eq!(License::Proprietary.as_str(), "Proprietary");
534 assert_eq!(License::Unknown.as_str(), "Unknown");
535 }
536
537 #[test]
538 fn test_license_from_spdx() {
539 assert_eq!(License::from_spdx("MIT"), License::MIT);
540 assert_eq!(License::from_spdx("Apache-2.0"), License::Apache2);
541 assert_eq!(License::from_spdx("Apache2"), License::Apache2);
542 assert_eq!(License::from_spdx("GPL-2.0"), License::GPL2);
543 assert_eq!(License::from_spdx("GPL-3.0"), License::GPL3);
544 assert_eq!(License::from_spdx("LGPL-2.1"), License::LGPL21);
545 assert_eq!(License::from_spdx("BSD-2-Clause"), License::BSD2);
546 assert_eq!(License::from_spdx("BSD-3-Clause"), License::BSD3);
547 assert_eq!(License::from_spdx("ISC"), License::ISC);
548 assert_eq!(License::from_spdx("MPL-2.0"), License::MPL2);
549 assert_eq!(License::from_spdx("Proprietary"), License::Proprietary);
550 assert_eq!(License::from_spdx("Weird"), License::Unknown);
551 }
552
553 #[test]
554 fn test_license_is_copyleft() {
555 assert!(License::GPL2.is_copyleft());
556 assert!(License::GPL3.is_copyleft());
557 assert!(License::LGPL21.is_copyleft());
558 assert!(License::MPL2.is_copyleft());
559 assert!(!License::MIT.is_copyleft());
560 assert!(!License::Apache2.is_copyleft());
561 assert!(!License::BSD3.is_copyleft());
562 }
563
564 #[test]
565 fn test_license_is_permissive() {
566 assert!(License::MIT.is_permissive());
567 assert!(License::Apache2.is_permissive());
568 assert!(License::BSD2.is_permissive());
569 assert!(License::BSD3.is_permissive());
570 assert!(License::ISC.is_permissive());
571 assert!(!License::GPL2.is_permissive());
572 assert!(!License::GPL3.is_permissive());
573 assert!(!License::Proprietary.is_permissive());
574 }
575
576 #[test]
579 fn test_detect_license_mit() {
580 assert_eq!(detect_license("MIT License\nCopyright..."), License::MIT);
581 assert_eq!(
582 detect_license("Permission is hereby granted, free of charge"),
583 License::MIT
584 );
585 }
586
587 #[test]
588 fn test_detect_license_apache() {
589 assert_eq!(
590 detect_license("Apache License Version 2.0"),
591 License::Apache2
592 );
593 }
594
595 #[test]
596 fn test_detect_license_gpl3() {
597 assert_eq!(
598 detect_license("GNU General Public License version 3"),
599 License::GPL3
600 );
601 }
602
603 #[test]
604 fn test_detect_license_gpl2() {
605 assert_eq!(
606 detect_license("GNU General Public License version 2"),
607 License::GPL2
608 );
609 }
610
611 #[test]
612 fn test_detect_license_bsd2() {
613 assert_eq!(detect_license("BSD 2-Clause License"), License::BSD2);
614 }
615
616 #[test]
617 fn test_detect_license_bsd3() {
618 assert_eq!(detect_license("BSD 3-Clause License"), License::BSD3);
619 }
620
621 #[test]
622 fn test_detect_license_isc() {
623 assert_eq!(detect_license("ISC License"), License::ISC);
624 }
625
626 #[test]
627 fn test_detect_license_mpl() {
628 assert_eq!(detect_license("Mozilla Public License 2.0"), License::MPL2);
629 }
630
631 #[test]
632 fn test_detect_license_proprietary() {
633 assert_eq!(
634 detect_license("All rights reserved. No copying."),
635 License::Proprietary
636 );
637 }
638
639 #[test]
640 fn test_detect_license_unknown() {
641 assert_eq!(detect_license("Some random text"), License::Unknown);
642 }
643
644 #[test]
647 fn test_compatibility_permissive_ok() {
648 assert!(LicenseCompatibility::is_compatible(
649 &License::MIT,
650 &License::Apache2
651 ));
652 assert!(LicenseCompatibility::is_compatible(
653 &License::BSD3,
654 &License::ISC
655 ));
656 }
657
658 #[test]
659 fn test_compatibility_gpl2_gpl3_incompatible() {
660 assert!(!LicenseCompatibility::is_compatible(
661 &License::GPL2,
662 &License::GPL3
663 ));
664 assert!(!LicenseCompatibility::is_compatible(
665 &License::GPL3,
666 &License::GPL2
667 ));
668 }
669
670 #[test]
671 fn test_compatibility_proprietary_gpl_incompatible() {
672 assert!(!LicenseCompatibility::is_compatible(
673 &License::Proprietary,
674 &License::GPL3
675 ));
676 assert!(!LicenseCompatibility::is_compatible(
677 &License::GPL2,
678 &License::Proprietary
679 ));
680 }
681
682 #[test]
683 fn test_compatibility_unknown_always_compatible() {
684 assert!(LicenseCompatibility::is_compatible(
685 &License::Unknown,
686 &License::GPL3
687 ));
688 assert!(LicenseCompatibility::is_compatible(
689 &License::Proprietary,
690 &License::Unknown
691 ));
692 }
693
694 #[test]
695 fn test_compatibility_mit_gpl_ok() {
696 assert!(LicenseCompatibility::is_compatible(
697 &License::MIT,
698 &License::GPL3
699 ));
700 }
701
702 #[test]
705 fn test_check_compatibility_ok() {
706 let deps = vec![
707 (String::from("a"), License::MIT),
708 (String::from("b"), License::Apache2),
709 (String::from("c"), License::BSD3),
710 ];
711 assert!(check_compatibility(&deps).is_ok());
712 }
713
714 #[test]
715 fn test_check_compatibility_conflict() {
716 let deps = vec![
717 (String::from("a"), License::GPL2),
718 (String::from("b"), License::GPL3),
719 ];
720 let result = check_compatibility(&deps);
721 assert!(result.is_err());
722 let conflicts = result.unwrap_err();
723 assert_eq!(conflicts.len(), 1);
724 assert_eq!(conflicts[0].package_a, "a");
725 assert_eq!(conflicts[0].package_b, "b");
726 }
727
728 #[test]
731 fn test_dep_graph_new() {
732 let g = DependencyGraph::new();
733 assert_eq!(g.package_count(), 0);
734 assert_eq!(g.edge_count(), 0);
735 }
736
737 #[test]
738 fn test_dep_graph_add_package() {
739 let mut g = DependencyGraph::new();
740 g.add_package("a");
741 assert_eq!(g.package_count(), 1);
742 assert_eq!(g.edge_count(), 0);
743 }
744
745 #[test]
746 fn test_dep_graph_add_dependency() {
747 let mut g = DependencyGraph::new();
748 g.add_dependency("app", "lib");
749 assert_eq!(g.package_count(), 2);
750 assert_eq!(g.edge_count(), 1);
751 let deps = g.dependencies("app").unwrap();
752 assert_eq!(deps.len(), 1);
753 assert_eq!(deps[0], "lib");
754 }
755
756 #[test]
757 fn test_dep_graph_reverse_deps() {
758 let mut g = DependencyGraph::new();
759 g.add_dependency("app", "lib");
760 g.add_dependency("tool", "lib");
761 let reverse = g.find_reverse_deps("lib");
762 assert_eq!(reverse.len(), 2);
763 }
764
765 #[test]
766 fn test_dep_graph_find_roots() {
767 let mut g = DependencyGraph::new();
768 g.add_dependency("app", "lib");
769 g.add_dependency("lib", "core");
770 let roots = g.find_roots();
771 assert_eq!(roots.len(), 1);
772 assert_eq!(roots[0], "app");
773 }
774
775 #[test]
776 fn test_dep_graph_no_cycles() {
777 let mut g = DependencyGraph::new();
778 g.add_dependency("a", "b");
779 g.add_dependency("b", "c");
780 let cycles = g.detect_circular_deps();
781 assert!(cycles.is_empty());
782 }
783
784 #[test]
785 fn test_dep_graph_with_cycle() {
786 let mut g = DependencyGraph::new();
787 g.add_dependency("a", "b");
788 g.add_dependency("b", "c");
789 g.add_dependency("c", "a");
790 let cycles = g.detect_circular_deps();
791 assert!(!cycles.is_empty());
792 }
793
794 #[test]
795 fn test_dep_graph_depth() {
796 let mut g = DependencyGraph::new();
797 g.add_dependency("app", "lib");
798 g.add_dependency("lib", "core");
799 assert_eq!(g.dependency_depth("core"), 2);
800 assert_eq!(g.dependency_depth("lib"), 1);
801 assert_eq!(g.dependency_depth("app"), 0);
802 }
803
804 #[test]
805 fn test_dep_graph_depth_not_found() {
806 let g = DependencyGraph::new();
807 assert_eq!(g.dependency_depth("nonexistent"), 0);
808 }
809
810 #[test]
811 fn test_dep_graph_dependencies_none() {
812 let g = DependencyGraph::new();
813 assert!(g.dependencies("nonexistent").is_none());
814 }
815}