Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

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())
}