use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Instant;
use serde::{Deserialize, Serialize};
use super::languages::{
GoLicenseScanner, JavaLicenseScanner, NodeLicenseScanner, PythonLicenseScanner,
RustLicenseScanner,
};
use super::spdx::SpdxLicense;
pub trait LanguageLicenseScanner {
fn name(&self) -> &str;
fn language(&self) -> &str;
fn detect(&self, path: &Path) -> bool;
fn scan(&self, path: &Path) -> Result<Vec<PackageLicense>, String>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageLicense {
pub name: String,
pub version: String,
pub license: SpdxLicense,
pub license_text: String,
pub ecosystem: String,
pub license_file: Option<PathBuf>,
pub is_direct: bool,
pub url: Option<String>,
pub authors: Vec<String>,
pub repository: Option<String>,
}
impl PackageLicense {
pub fn new(name: &str, version: &str, license: &str, ecosystem: &str) -> Self {
Self {
name: name.to_string(),
version: version.to_string(),
license: SpdxLicense::parse_license(license),
license_text: license.to_string(),
ecosystem: ecosystem.to_string(),
license_file: None,
is_direct: true,
url: None,
authors: Vec::new(),
repository: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ScanOptions {
pub path: PathBuf,
pub include_dev: bool,
pub format: String,
pub generate_sbom: bool,
pub verbose: bool,
}
impl ScanOptions {
pub fn new(path: PathBuf) -> Self {
Self {
path,
format: "human".to_string(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScanResult {
pub packages: Vec<PackageLicense>,
pub by_license: HashMap<String, usize>,
pub by_ecosystem: HashMap<String, usize>,
pub languages_scanned: Vec<String>,
pub duration_ms: u64,
pub errors: Vec<String>,
}
impl ScanResult {
pub fn new() -> Self {
Self::default()
}
pub fn unique_licenses(&self) -> Vec<&SpdxLicense> {
let mut licenses: Vec<_> = self.packages.iter().map(|p| &p.license).collect();
licenses.sort_by(|a, b| a.to_spdx().cmp(b.to_spdx()));
licenses.dedup_by(|a, b| a.to_spdx() == b.to_spdx());
licenses
}
pub fn packages_by_license(&self, license: &SpdxLicense) -> Vec<&PackageLicense> {
self.packages
.iter()
.filter(|p| p.license.to_spdx() == license.to_spdx())
.collect()
}
pub fn permissive_count(&self) -> usize {
self.packages
.iter()
.filter(|p| p.license.is_permissive())
.count()
}
pub fn copyleft_count(&self) -> usize {
self.packages
.iter()
.filter(|p| p.license.is_copyleft())
.count()
}
pub fn weak_copyleft_count(&self) -> usize {
self.packages
.iter()
.filter(|p| p.license.is_weak_copyleft())
.count()
}
pub fn unknown_count(&self) -> usize {
self.packages
.iter()
.filter(|p| matches!(p.license, SpdxLicense::Unknown(_)))
.count()
}
}
pub struct LicenseScanner {
scanners: Vec<Box<dyn LanguageLicenseScanner>>,
}
impl Default for LicenseScanner {
fn default() -> Self {
Self::new()
}
}
impl LicenseScanner {
pub fn new() -> Self {
let scanners: Vec<Box<dyn LanguageLicenseScanner>> = vec![
Box::new(RustLicenseScanner::new()),
Box::new(NodeLicenseScanner::new()),
Box::new(PythonLicenseScanner::new()),
Box::new(GoLicenseScanner::new()),
Box::new(JavaLicenseScanner::new()),
];
Self { scanners }
}
pub fn detect_languages(&self, path: &Path) -> Vec<String> {
self.scanners
.iter()
.filter(|s| s.detect(path))
.map(|s| s.language().to_string())
.collect()
}
pub fn scan(&self, options: &ScanOptions) -> Result<ScanResult, String> {
let start = Instant::now();
let mut result = ScanResult::new();
let path = &options.path;
for scanner in &self.scanners {
if !scanner.detect(path) {
continue;
}
result
.languages_scanned
.push(scanner.language().to_string());
match scanner.scan(path) {
Ok(packages) => {
for pkg in packages {
*result
.by_license
.entry(pkg.license.to_spdx().to_string())
.or_insert(0) += 1;
*result
.by_ecosystem
.entry(pkg.ecosystem.clone())
.or_insert(0) += 1;
result.packages.push(pkg);
}
}
Err(e) => {
result.errors.push(format!("{}: {}", scanner.name(), e));
}
}
}
result.duration_ms = start.elapsed().as_millis() as u64;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_options_default() {
let options = ScanOptions::default();
assert!(!options.include_dev);
assert!(!options.generate_sbom);
}
#[test]
fn test_scan_result_default() {
let result = ScanResult::new();
assert!(result.packages.is_empty());
assert_eq!(result.duration_ms, 0);
}
#[test]
fn test_package_license_new() {
let pkg = PackageLicense::new("test-pkg", "1.0.0", "MIT", "crates.io");
assert_eq!(pkg.name, "test-pkg");
assert_eq!(pkg.version, "1.0.0");
assert_eq!(pkg.license, SpdxLicense::MIT);
}
#[test]
fn test_license_scanner_creation() {
let scanner = LicenseScanner::new();
let temp_dir = tempfile::tempdir().unwrap();
let languages = scanner.detect_languages(temp_dir.path());
assert!(languages.is_empty());
}
}