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

nordisk/nornir

crates/mimir-binary/src/crates.rs

//! StableCrateId → crate identity (`package@version`, origin).
//!
//! A v0 symbol names its crate by NAME + StableCrateId. The StableCrateId is a
//! hash of (crate name, `-C metadata`, is-exe, rustc version): it differs
//! between `cargo check` and `cargo build`, between profiles and between
//! toolchains, so the source dump's ids (nightly `check`) never equal the
//! artifact's (stable `build --release`). The artifact names its own:
//!
//! | source (in order)          | exact? | gives                                             |
//! |----------------------------|--------|---------------------------------------------------|
//! | `cu`      DWARF CU names   | yes    | crate + id + root file (artifact built with debuginfo) |
//! | `rlib`    `--deps-dir`     | yes    | crate + id + root file (cargo's deps dir of the build) |
//! | `driver`  `.stats.json`    | yes    | crate + id + package@version (artifact built BY the driver, `cargo mimir-dump --build`) |
//! | `sysroot` toolchain rlibs  | yes    | crate + id of std, core … and std's private deps (gimli, addr2line …) → `lang` |
//! | `name`    unique name      | by name| the ONE package whose lib crate has that name (cargo metadata), or a toolchain crate |
//! | ambiguous                  | —      | counted in the ledger, version `?`, never guessed |
//!
//! Root file → package@version: the cargo-metadata target whose `src_path` is
//! that file; `/rustc/<commit>/library/…` and `/rust/deps/…` (the sysroot's
//! remapped paths) are toolchain crates (origin `lang`, rust-analyzer's URL
//! version, as the driver writes them).

use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};

use mimir_schema::moniker;
use mimir_schema::Origin;

use crate::elf::{CuCrate, RlibCrate};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrateIdent {
    pub package: String,
    pub version: String,
    pub origin: Origin,
    /// the driver's descriptor prefix for a NON-lib target (`<bin>:`), empty
    /// for a lib — part of every moniker of that crate
    pub prefix: String,
}

/// How a crate id was resolved.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Via {
    Cu,
    Rlib,
    Driver,
    Sysroot,
    Name,
    Ambiguous,
    Unknown,
}

impl Via {
    pub fn as_str(self) -> &'static str {
        match self {
            Via::Cu => "cu",
            Via::Rlib => "rlib",
            Via::Driver => "driver",
            Via::Sysroot => "sysroot",
            Via::Name => "name",
            Via::Ambiguous => "ambiguous",
            Via::Unknown => "unknown",
        }
    }
}

/// One package from cargo metadata.
#[derive(Debug, Clone)]
pub struct MetaPackage {
    pub name: String,
    pub version: String,
    pub local: bool,
    /// (crate name, src_path, kind) of every target — `kind` as cargo names
    /// it, `lib` for the library in any crate type
    pub targets: Vec<(String, PathBuf, String)>,
}

/// The toolchain's own crates (and the sysroot's private deps are recognised
/// by their remapped root path, not by name).
pub const LANG_CRATES: &[&str] = &[
    "std", "core", "alloc", "proc_macro", "test", "compiler_builtins", "panic_unwind", "panic_abort",
    "unwind", "std_detect", "rustc_std_workspace_core", "rustc_std_workspace_alloc", "rustc_std_workspace_std",
    "profiler_builtins", "sysroot",
];

#[derive(Debug, Default)]
pub struct Resolver {
    by_id: HashMap<u64, (CrateIdent, Via)>,
    /// crate name → packages (cargo metadata)
    meta_by_crate: HashMap<String, Vec<MetaPackage>>,
    /// src_path → (package, version, local, target kind)
    meta_by_src: HashMap<PathBuf, (String, String, bool, String)>,
    /// rel-path suffix lookup for relative dep-info paths
    meta_srcs: Vec<(PathBuf, String, String, bool, String)>,
    /// crate name → the non-lib targets of that name: (package, target kind)
    meta_bins: HashMap<String, Vec<(MetaPackage, String)>>,
    /// crate name → ids known exactly (cu / rlib / driver / sysroot)
    known_names: HashMap<String, Vec<u64>>,
    pub conflicts: Vec<String>,
    cache: HashMap<(String, u64), (Option<CrateIdent>, Via)>,
}

impl Resolver {
    pub fn new(meta: &[MetaPackage]) -> Resolver {
        let mut r = Resolver::default();
        for p in meta {
            for (cname, src, kind) in &p.targets {
                let src = canon(src);
                r.meta_by_src.insert(src.clone(), (p.name.clone(), p.version.clone(), p.local, kind.clone()));
                r.meta_srcs.push((src, p.name.clone(), p.version.clone(), p.local, kind.clone()));
                if moniker::LIB_TARGET_KINDS.contains(&kind.as_str()) {
                    r.meta_by_crate.entry(cname.clone()).or_default().push(p.clone());
                } else {
                    r.meta_bins.entry(cname.clone()).or_default().push((p.clone(), kind.clone()));
                }
            }
        }
        r
    }

    fn insert(&mut self, crate_name: &str, id: u64, ident: CrateIdent, via: Via) {
        let ids = self.known_names.entry(crate_name.to_string()).or_default();
        if !ids.contains(&id) {
            ids.push(id);
        }
        match self.by_id.get(&id) {
            Some((old, _)) if *old != ident => {
                self.conflicts.push(format!("{id:016x}: {}@{} vs {}@{} ({})", old.package, old.version, ident.package, ident.version, via.as_str()));
            }
            Some(_) => {}
            None => {
                self.by_id.insert(id, (ident, via));
            }
        }
    }

    /// A crate root file → its identity.
    pub fn ident_of_root(&self, crate_name: &str, root: &Path) -> Option<CrateIdent> {
        let s = root.to_string_lossy();
        if s.starts_with("/rustc/") || s.starts_with("/rust/deps/") || s.contains("/lib/rustlib/src/rust/") {
            return Some(lang(crate_name));
        }
        let mk = |p: &String, v: &String, local: bool, kind: &str| CrateIdent {
            package: p.clone(),
            version: v.clone(),
            origin: if local { Origin::Local } else { Origin::Library },
            prefix: moniker::target_prefix(kind, crate_name),
        };
        let c = canon(root);
        if let Some((p, v, local, kind)) = self.meta_by_src.get(&c) {
            return Some(mk(p, v, *local, kind));
        }
        if root.is_relative() {
            let hits: Vec<_> = self.meta_srcs.iter().filter(|(src, ..)| src.ends_with(root)).collect();
            if hits.len() == 1 {
                let (_, p, v, local, kind) = hits[0];
                return Some(mk(p, v, *local, kind));
            }
        }
        // cargo's registry layout: …/registry/src/<index>/<name>-<version>/…
        registry_dir_ident(root)
    }

    pub fn add_cus(&mut self, cus: &[CuCrate]) {
        for cu in cus {
            if let Some(id) = self.ident_of_root(&cu.crate_name, &cu.root_file) {
                self.insert(&cu.crate_name, cu.stable_crate_id, id, Via::Cu);
            }
        }
    }

    pub fn add_rlibs(&mut self, rlibs: &[RlibCrate]) {
        for r in rlibs {
            let Some(root) = &r.root_file else { continue };
            let Some(ident) = self.ident_of_root(&r.crate_name, root) else { continue };
            for id in &r.ids {
                self.insert(&r.crate_name, *id, ident.clone(), Via::Rlib);
            }
        }
    }

    /// Sysroot rlibs: toolchain crates (std's private deps included), origin
    /// `lang` like the driver writes every crate under the sysroot.
    pub fn add_sysroot(&mut self, rlibs: &[RlibCrate]) {
        for r in rlibs {
            for id in &r.ids {
                self.insert(&r.crate_name, *id, lang(&r.crate_name), Via::Sysroot);
            }
        }
    }

    /// Driver records: `(id, crate name, package@version + prefix)`. The
    /// driver knows which target of the package it compiled and writes the
    /// prefix of a non-lib one (`moniker::target_prefix`).
    pub fn add_driver(&mut self, ids: &[(u64, String, CrateIdent)]) {
        for (id, crate_name, ident) in ids {
            self.insert(crate_name, *id, ident.clone(), Via::Driver);
        }
    }

    /// Resolve one `(crate name, StableCrateId)` from a symbol.
    pub fn resolve(&mut self, crate_name: &str, id: u64) -> (Option<CrateIdent>, Via) {
        if let Some(r) = self.cache.get(&(crate_name.to_string(), id)) {
            return r.clone();
        }
        let r = self.compute(crate_name, id);
        self.cache.insert((crate_name.to_string(), id), r.clone());
        r
    }

    fn compute(&self, crate_name: &str, id: u64) -> (Option<CrateIdent>, Via) {
        if let Some((ident, via)) = self.by_id.get(&id) {
            return (Some(ident.clone()), *via);
        }
        let libs = self.meta_by_crate.get(crate_name).map(Vec::as_slice).unwrap_or(&[]);
        let bins = self.meta_bins.get(crate_name).map(Vec::as_slice).unwrap_or(&[]);
        let bin_ident = |(p, kind): &(MetaPackage, String)| CrateIdent {
            package: p.name.clone(),
            version: p.version.clone(),
            origin: if p.local { Origin::Local } else { Origin::Library },
            prefix: moniker::target_prefix(kind, crate_name),
        };
        // Other ids of this name are known exactly, this one is not: it is a
        // DIFFERENT compile of that name — a bin target named like its
        // package's lib (`lgz` bin + `lgz` lib), or another version.
        if self.known_names.contains_key(crate_name) {
            return if bins.len() == 1 { (Some(bin_ident(&bins[0])), Via::Name) } else { (None, Via::Ambiguous) };
        }
        match (libs.len(), bins.len(), LANG_CRATES.contains(&crate_name)) {
            (0, 0, true) => (Some(lang(crate_name)), Via::Name),
            (1, 0, false) => {
                let p = &libs[0];
                (
                    Some(CrateIdent {
                        package: p.name.clone(),
                        version: p.version.clone(),
                        origin: if p.local { Origin::Local } else { Origin::Library },
                        prefix: String::new(),
                    }),
                    Via::Name,
                )
            }
            (0, 1, false) => (Some(bin_ident(&bins[0])), Via::Name),
            (0, 0, false) => (None, Via::Unknown),
            // a lib and a bin of one name, or several packages: never guessed
            _ => (None, Via::Ambiguous),
        }
    }

    pub fn known_ids(&self) -> BTreeMap<String, usize> {
        let mut m = BTreeMap::new();
        for (_, via) in self.by_id.values() {
            *m.entry(via.as_str().to_string()).or_default() += 1;
        }
        m
    }
}

fn lang(crate_name: &str) -> CrateIdent {
    CrateIdent {
        package: crate_name.to_string(),
        version: moniker::lang_version(crate_name),
        origin: Origin::Lang,
        prefix: String::new(),
    }
}

fn canon(p: &Path) -> PathBuf {
    std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
}

fn registry_dir_ident(root: &Path) -> Option<CrateIdent> {
    let comps: Vec<String> = root.components().map(|c| c.as_os_str().to_string_lossy().into_owned()).collect();
    let i = comps.iter().position(|c| c == "registry")?;
    if comps.get(i + 1).map(String::as_str) != Some("src") {
        return None;
    }
    let dir = comps.get(i + 3)?;
    // `<name>-<semver>`: the version starts after the last `-` followed by a digit
    let cut = dir.char_indices().filter(|(j, c)| *c == '-' && dir[j + 1..].starts_with(|d: char| d.is_ascii_digit())).map(|(j, _)| j).next_back()?;
    Some(CrateIdent { package: dir[..cut].to_string(), version: dir[cut + 1..].to_string(), origin: Origin::Library, prefix: String::new() })
}

/// `cargo metadata --format-version 1` JSON → packages.
pub fn packages_from_metadata(meta: &serde_json::Value) -> Vec<MetaPackage> {
    let members: Vec<&str> = meta["workspace_members"].as_array().map(|a| a.iter().filter_map(|v| v.as_str()).collect()).unwrap_or_default();
    let mut out = Vec::new();
    for p in meta["packages"].as_array().into_iter().flatten() {
        let id = p["id"].as_str().unwrap_or_default();
        let targets = p["targets"]
            .as_array()
            .into_iter()
            .flatten()
            .filter_map(|t| {
                let kinds: Vec<&str> = t["kind"].as_array().into_iter().flatten().filter_map(|k| k.as_str()).collect();
                let kind = if kinds.iter().any(|k| moniker::LIB_TARGET_KINDS.contains(k)) { "lib" } else { kinds.first().copied()? };
                Some((t["name"].as_str()?.replace('-', "_"), PathBuf::from(t["src_path"].as_str()?), kind.to_string()))
            })
            .collect();
        out.push(MetaPackage {
            name: p["name"].as_str().unwrap_or_default().to_string(),
            version: p["version"].as_str().unwrap_or_default().to_string(),
            local: members.contains(&id),
            targets,
        });
    }
    out
}

/// Run `cargo metadata` for a manifest.
pub fn cargo_metadata(manifest: &Path) -> Result<Vec<MetaPackage>, String> {
    let o = std::process::Command::new("cargo")
        .args(["metadata", "--format-version", "1", "--manifest-path"])
        .arg(manifest)
        .output()
        .map_err(|e| format!("cargo metadata: {e}"))?;
    if !o.status.success() {
        return Err(format!("cargo metadata failed: {}", String::from_utf8_lossy(&o.stderr)));
    }
    let v: serde_json::Value = serde_json::from_slice(&o.stdout).map_err(|e| e.to_string())?;
    Ok(packages_from_metadata(&v))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_layout() {
        let i = registry_dir_ident(Path::new("/home/x/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-json-1.0.228/src/lib.rs")).unwrap();
        assert_eq!((i.package.as_str(), i.version.as_str()), ("serde-json", "1.0.228"));
        assert!(registry_dir_ident(Path::new("/w/a/src/lib.rs")).is_none());
    }

    #[test]
    fn sysroot_paths_are_lang() {
        let r = Resolver::new(&[]);
        let i = r.ident_of_root("memchr", Path::new("/rust/deps/memchr-2.7.6/src/lib.rs")).unwrap();
        assert_eq!(i.origin, Origin::Lang);
        assert_eq!(i.version, moniker::lang_version("memchr"));
    }

    #[test]
    fn ambiguous_names_are_not_guessed() {
        let pk = |v: &str| MetaPackage { name: "a".into(), version: v.into(), local: false, targets: vec![("a".into(), PathBuf::from(format!("/x/a-{v}/src/lib.rs")), "lib".into())] };
        let mut r = Resolver::new(&[pk("0.1.0"), pk("0.0.9")]);
        assert_eq!(r.resolve("a", 42), (None, Via::Ambiguous));
        r.add_driver(&[(42, "a".into(), CrateIdent { package: "a".into(), version: "0.0.9".into(), origin: Origin::Library, prefix: String::new() })]);
        // cached answer is per (name, id); a fresh id resolves through the driver map
        assert_eq!(r.resolve("a", 43).1, Via::Ambiguous);
        let mut r2 = Resolver::new(&[pk("0.1.0"), pk("0.0.9")]);
        r2.add_driver(&[(42, "a".into(), CrateIdent { package: "a".into(), version: "0.0.9".into(), origin: Origin::Library, prefix: String::new() })]);
        assert_eq!(r2.resolve("a", 42).1, Via::Driver);
        assert_eq!(r2.resolve("a", 42).0.unwrap().prefix, "");
        // an unknown id of a name whose other ids are known: never the lib's
        assert_eq!(r2.resolve("a", 44), (None, Via::Ambiguous));
        // a bin named like its package's lib resolves to the bin target (prefixed)
        let lgz = MetaPackage {
            name: "lgz".into(),
            version: "0.3.14".into(),
            local: true,
            targets: vec![("lgz".into(), PathBuf::from("/w/lgz/src/lib.rs"), "lib".into()), ("lgz".into(), PathBuf::from("/w/lgz/src/bin/lgz.rs"), "bin".into())],
        };
        let mut r3 = Resolver::new(&[lgz]);
        assert_eq!(r3.resolve("lgz", 1), (None, Via::Ambiguous), "lib or bin: not guessed by name alone");
        let mut r4 = Resolver::new(&[r3_pkg()]);
        r4.add_rlibs(&[RlibCrate { crate_name: "lgz".into(), ids: vec![5], root_file: Some(PathBuf::from("/w/lgz/src/lib.rs")), path: PathBuf::new() }]);
        let (bin, via) = r4.resolve("lgz", 6);
        assert_eq!((bin.unwrap().prefix.as_str(), via), ("`bin=lgz`:", Via::Name));
        assert_eq!(r4.resolve("lgz", 5).0.unwrap().prefix, "");
    }

    fn r3_pkg() -> MetaPackage {
        MetaPackage {
            name: "lgz".into(),
            version: "0.3.14".into(),
            local: true,
            targets: vec![("lgz".into(), PathBuf::from("/w/lgz/src/lib.rs"), "lib".into()), ("lgz".into(), PathBuf::from("/w/lgz/src/bin/lgz.rs"), "bin".into())],
        }
    }
}