crates/dwarves-slsa/src/lock.rs
//! `Cargo.lock`: every locked package, for the vendor check and the provenance.
use std::collections::BTreeMap;
use serde::Deserialize;
use crate::statement::ResourceDescriptor;
use crate::Result;
/// One `[[package]]` of a lock.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct LockedPackage {
pub name: String,
pub version: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub checksum: Option<String>,
/// `name`, `name version` or `name version (source)`.
#[serde(default)]
pub dependencies: Vec<String>,
}
impl LockedPackage {
/// From a registry (crates.io or another).
pub fn is_registry(&self) -> bool {
self.source
.as_deref()
.is_some_and(|s| s.starts_with("registry+") || s.starts_with("sparse+"))
}
/// `(url, rev)` of a git source (`git+URL?query#REV`).
pub fn git_rev(&self) -> Option<(String, String)> {
let s = self.source.as_deref()?.strip_prefix("git+")?;
let (url, rev) = s.split_once('#')?;
let url = url.split_once('?').map(|(u, _)| u).unwrap_or(url);
Some((url.to_string(), rev.to_string()))
}
}
#[derive(Deserialize)]
struct LockFile {
#[serde(default)]
package: Vec<LockedPackage>,
}
/// Every package of a lock (version 3 or 4).
pub fn parse_lock(text: &str) -> Result<Vec<LockedPackage>> {
let f: LockFile = toml::from_str(text).map_err(|e| crate::Error(format!("Cargo.lock: {e}")))?;
Ok(f.package)
}
/// The resolved dependencies the locks name, deduplicated and sorted: a registry
/// crate as `pkg:cargo/<name>@<version>` with its sha256 checksum, a git crate as
/// `git+<url>` with its commit. Path packages (the release's own) are left out.
pub fn dependencies_of(locks: &[Vec<LockedPackage>]) -> Vec<ResourceDescriptor> {
let mut by_uri: BTreeMap<String, ResourceDescriptor> = BTreeMap::new();
for p in locks.iter().flatten() {
if p.is_registry() {
let Some(sum) = &p.checksum else { continue };
let uri = format!("pkg:cargo/{}@{}", p.name, p.version);
by_uri
.entry(uri.clone())
.or_insert_with(|| ResourceDescriptor::new(None, &uri, "sha256", sum));
} else if let Some((url, rev)) = p.git_rev() {
let uri = format!("git+{url}");
by_uri
.entry(format!("{uri}#{rev}"))
.or_insert_with(|| ResourceDescriptor::new(None, &uri, "gitCommit", &rev));
}
}
by_uri.into_values().collect()
}
/// ★ The path packages (no source) that the release's crates reach through the lock
/// but that are not release crates themselves: what a `[patch.crates-io]` lays in by
/// path. Packaged, a crate names them from crates.io, so the runner vendors those.
pub fn path_externals(
locks: &[Vec<LockedPackage>],
steps: &std::collections::BTreeSet<String>,
) -> std::collections::BTreeSet<(String, String)> {
let mut out = std::collections::BTreeSet::new();
for lock in locks {
let mut by_name: BTreeMap<&str, Vec<&LockedPackage>> = BTreeMap::new();
for p in lock {
by_name.entry(p.name.as_str()).or_default().push(p);
}
let mut seen = std::collections::BTreeSet::new();
let mut todo: Vec<&LockedPackage> = lock
.iter()
.filter(|p| p.source.is_none() && steps.contains(&p.name))
.collect();
while let Some(p) = todo.pop() {
if !seen.insert((p.name.as_str(), p.version.as_str(), p.source.as_deref())) {
continue;
}
if p.source.is_none() && !steps.contains(&p.name) {
out.insert((p.name.clone(), p.version.clone()));
}
for d in &p.dependencies {
let mut f = d.split_whitespace();
let name = f.next().unwrap_or("");
let ver = f.next();
let cands: Vec<&&LockedPackage> = by_name
.get(name)
.map(|v| {
v.iter()
.filter(|c| ver.is_none_or(|x| c.version == x))
.collect()
})
.unwrap_or_default();
if let [one] = cands.as_slice() {
todo.push(one);
} else {
// `name version (source)` where two sources share a version: take them all.
todo.extend(cands.into_iter().copied());
}
}
}
}
out
}
/// The newest non-pre-release version semver-compatible with `v` (same major, or
/// same 0.minor, or the same 0.0.patch), as cargo picks for `^v`.
pub fn newest_compatible(versions: &[String], v: &str) -> Option<String> {
fn parse(s: &str) -> Option<(u64, u64, u64)> {
if s.contains('-') {
return None;
}
let core = s.split('+').next()?;
let mut it = core.split('.').map(|x| x.parse::<u64>().ok());
Some((it.next()??, it.next()??, it.next()??))
}
let base = parse(v)?;
let compatible = |c: (u64, u64, u64)| {
c >= base
&& match base {
(0, 0, p) => c == (0, 0, p),
(0, m, _) => c.0 == 0 && c.1 == m,
(maj, _, _) => c.0 == maj,
}
};
versions
.iter()
.filter_map(|s| parse(s).map(|p| (p, s)))
.filter(|(p, _)| compatible(*p))
.max_by_key(|(p, _)| *p)
.map(|(_, s)| s.clone())
}