use crate::{
Krate,
licenses::{KrateLicense, LicenseInfo, config},
};
use spdx::{Expression, LicenseReq, Licensee};
use std::fmt;
type Label = codespan_reporting::diagnostic::Label<codespan::FileId>;
use codespan_reporting::diagnostic::LabelStyle;
pub use codespan_reporting::diagnostic::Severity;
pub type Diagnostic = codespan_reporting::diagnostic::Diagnostic<codespan::FileId>;
pub type Files = codespan::Files<String>;
struct Accepted<'acc> {
global: &'acc [Licensee],
krate: Option<&'acc [Licensee]>,
}
impl<'acc> Accepted<'acc> {
#[inline]
fn satisfies(&self, req: &spdx::LicenseReq) -> bool {
self.iter().any(|licensee| licensee.satisfies(req))
}
#[inline]
fn iter(&'acc self) -> impl Iterator<Item = &'acc Licensee> {
self.global
.iter()
.chain(self.krate.iter().flat_map(|o| o.iter()))
}
}
impl fmt::Display for Accepted<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "global: [")?;
for (id, val) in self.global.iter().enumerate() {
write!(f, "{val}")?;
if id + 1 < self.global.len() {
write!(f, ", ")?;
}
}
write!(f, "]")?;
if let Some(krate) = self.krate {
write!(f, "\ncrate: [")?;
for (id, val) in krate.iter().enumerate() {
write!(f, "{val}")?;
if id + 1 < krate.len() {
write!(f, ", ")?;
}
}
write!(f, "]")?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct Resolved {
pub licenses: Vec<LicenseReq>,
pub diagnostics: Vec<Diagnostic>,
}
fn synthesize_manifest(
krate: &Krate,
existing: Option<toml_edit::DocumentMut>,
expression: &spdx::Expression,
) -> (String, usize) {
let mut doc = if let Some(existing) = existing {
existing
} else {
let mut doc = toml_edit::DocumentMut::new();
let package = &mut doc["package"];
package["name"] = toml_edit::value(krate.name.clone());
package["version"] = toml_edit::value(krate.version.to_string());
package["authors"] =
toml_edit::value(krate.authors.iter().cloned().collect::<toml_edit::Array>());
doc
};
doc["package"]["license"] = toml_edit::value(expression.as_ref().to_owned());
let serialized = doc.to_string();
let offset = serialized
.find(expression.as_ref())
.expect("we literally just serialized this");
(serialized, offset)
}
pub fn resolve(
licenses: &[KrateLicense<'_>],
accepted: &[Licensee],
krate_cfg: &std::collections::BTreeMap<String, config::KrateConfig>,
fail_on_missing: bool,
) -> (Files, Vec<Option<Resolved>>) {
let mut files = codespan::Files::new();
let resolved = licenses
.iter()
.map(|kl| {
let mut resolved = Resolved {
licenses: Vec::new(),
diagnostics: Vec::new(),
};
let manifest = std::fs::read_to_string(&kl.krate.manifest_path)
.map_err(|e| {
log::error!(
"failed to read manifest path {} for crate '{}': {e}",
kl.krate.manifest_path,
kl.krate,
);
e
})
.ok();
let expr = match &kl.lic_info {
LicenseInfo::Expr(expr) => std::borrow::Cow::Borrowed(expr),
LicenseInfo::Ignore => {
return None;
}
LicenseInfo::Unknown => {
let mut unique_exprs = Vec::new();
if kl.license_files.is_empty() {
let msg = format!("unable to synthesize license expression for '{}': no `license` specified, and no license files were found", kl.krate);
if fail_on_missing {
resolved.diagnostics.push(Diagnostic::new(Severity::Error).with_message(msg));
} else {
log::warn!("{msg}");
}
return Some(resolved);
}
for file in &kl.license_files {
if let Err(i) = unique_exprs.binary_search_by(|expr: &String| {
expr.as_str().cmp(file.license_expr.as_ref())
}) {
unique_exprs.insert(i, file.license_expr.as_ref().to_owned());
}
}
let mut concat_expr = String::new();
for (i, expr) in unique_exprs.into_iter().enumerate() {
if i > 0 {
concat_expr.push_str(" AND ");
}
concat_expr.push('(');
concat_expr.push_str(&expr);
concat_expr.push(')');
}
match Expression::parse(&concat_expr) {
Ok(expr) => std::borrow::Cow::Owned(expr),
Err(e) => {
let span = e.span;
let reason = e.reason;
let failed_expr_id =
files.add(format!("{}.license", kl.krate), concat_expr);
resolved.diagnostics.push(
Diagnostic::new(Severity::Error)
.with_message("failed to parse synthesized license expression")
.with_labels(vec![Label::new(
LabelStyle::Primary,
failed_expr_id,
span,
)
.with_message(reason.to_string())]),
);
return Some(resolved);
}
}
}
};
let expr_offset =
if let (LicenseInfo::Expr(expr), Some(manifest)) = (&kl.lic_info, &manifest) {
manifest.find(expr.as_ref())
} else {
None
};
let (manifest, expr_offset) = match (manifest, expr_offset) {
(Some(manifest), Some(expr_offset)) => (manifest, expr_offset),
(Some(manifest), None) => {
let doc: Option<toml_edit::DocumentMut> = manifest
.parse()
.map_err(|e| {
log::error!(
"failed to parse manifest at '{}' for crate '{}': {e}",
kl.krate.manifest_path,
kl.krate
);
e
})
.ok();
synthesize_manifest(kl.krate, doc, &expr)
}
_ => synthesize_manifest(kl.krate, None, &expr),
};
let accepted = match krate_cfg.get(&kl.krate.name) {
Some(kcfg) => {
if kcfg.accepted.is_empty() {
Accepted {
global: accepted,
krate: None,
}
} else {
Accepted {
global: accepted,
krate: Some(&kcfg.accepted),
}
}
}
None => Accepted {
global: accepted,
krate: None,
},
};
let manifest_file_id = files.add(kl.krate.manifest_path.clone(), manifest);
if let Err(failed) = expr.evaluate_with_failures(|req| accepted.satisfies(req)) {
resolved.diagnostics.push(
Diagnostic::new(Severity::Error)
.with_message("failed to satisfy license requirements")
.with_labels(
failed
.into_iter()
.map(|fr| {
let span = fr.span.start as usize + expr_offset
..fr.span.end as usize + expr_offset;
Label::new(LabelStyle::Secondary, manifest_file_id, span)
})
.collect(),
),
);
return Some(resolved);
}
match expr.minimized_requirements(accepted.iter()) {
Ok(min_reqs) => {
resolved.licenses = min_reqs;
}
Err(e) => {
log::warn!("failed to minimize license requirements: {e}");
}
}
Some(resolved)
})
.collect();
(files, resolved)
}