use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::core::doctor::CheckStatus;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstallSource {
Registry,
Git(String),
Path(PathBuf),
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoInstall {
pub package: String,
pub version: String,
pub source: InstallSource,
pub bins: Vec<String>,
}
#[derive(Deserialize)]
struct Crates2Entry {
#[serde(default)]
bins: Vec<String>,
}
#[derive(Deserialize)]
struct Crates2Doc {
#[serde(default)]
installs: std::collections::BTreeMap<String, Crates2Entry>,
}
pub fn parse_cargo_installs(json: &str) -> Option<Vec<CargoInstall>> {
let doc: Crates2Doc = serde_json::from_str(json).ok()?;
let mut out = Vec::new();
for (key, entry) in doc.installs {
let Some((head, source)) = key.rsplit_once(" (") else {
continue;
};
let source = source.strip_suffix(')').unwrap_or(source);
let Some((package, version)) = head.rsplit_once(' ') else {
continue;
};
out.push(CargoInstall {
package: package.to_string(),
version: version.to_string(),
source: classify_source(source),
bins: entry.bins,
});
}
out.sort_by(|a, b| a.package.cmp(&b.package).then(a.version.cmp(&b.version)));
Some(out)
}
fn classify_source(fragment: &str) -> InstallSource {
if fragment.starts_with("registry+") {
InstallSource::Registry
} else if let Some(rest) = fragment.strip_prefix("git+") {
InstallSource::Git(rest.to_string())
} else if let Some(rest) = fragment.strip_prefix("path+file://") {
InstallSource::Path(PathBuf::from(rest))
} else {
InstallSource::Other(fragment.to_string())
}
}
pub fn provenance_report(
bin_name: &str,
running_version: &str,
running_exe: &Path,
cargo_bin_dir: &Path,
ledger: Option<&[CargoInstall]>,
source_exists: &dyn Fn(&Path) -> bool,
) -> (CheckStatus, String) {
let where_from = format!(" (running `{}`)", running_exe.display());
let Some(ledger) = ledger else {
return (
CheckStatus::Unknown,
format!(
"cargo's install ledger (`$CARGO_HOME/.crates2.json`) is missing or unreadable \
— cannot verify how `{bin_name}` was installed{where_from}"
),
);
};
let providers: Vec<&CargoInstall> = ledger
.iter()
.filter(|i| i.bins.iter().any(|b| b == bin_name))
.collect();
if providers.is_empty() {
return (
CheckStatus::Unknown,
format!(
"`{bin_name}` is not recorded in cargo's install ledger{where_from} — it was \
placed by the prebuilt installer, a package manager, or built in place, so its \
provenance and upgrade path cannot be verified from here (issue #4033)"
),
);
}
if providers.len() > 1 {
let sources: Vec<String> = providers
.iter()
.map(|i| format!("{} {} from {}", i.package, i.version, describe(&i.source)))
.collect();
return (
CheckStatus::Fail,
format!(
"`{bin_name}` is provided by {} separate installs ({}) — whichever wrote last \
wins and the others are silently shadowed, so what runs is not determined by \
version{where_from} (issue #4033)",
providers.len(),
sources.join("; ")
),
);
}
let record = providers[0];
let installed = cargo_bin_dir.join(bin_name);
if !same_file(running_exe, &installed) {
return (
CheckStatus::Unknown,
format!(
"the running `{bin_name}` is `{}`, but cargo's ledger describes a DIFFERENT file — `{}` ({} {} from {}). The running binary was not installed by cargo, so its provenance and upgrade path cannot be verified from here, and upgrading the cargo install would not replace it. Check `which -a {bin_name}` for a shadowing copy (issue #4033, ADR-0021)",
running_exe.display(),
installed.display(),
record.package,
record.version,
describe(&record.source),
),
);
}
if record.version != running_version {
return version_skew_verdict(bin_name, running_version, record, &installed, &where_from);
}
match &record.source {
InstallSource::Registry => (
CheckStatus::Ok,
format!(
"`{bin_name}` {running_version} installed from crates.io; cargo's ledger \
agrees with the running version{where_from}"
),
),
InstallSource::Path(dir) if !source_exists(dir) => (
CheckStatus::Fail,
format!(
"`{bin_name}` {running_version} was installed with `cargo install --path` from \
`{}`, and that directory NO LONGER EXISTS — the binary has no provenance and \
no upgrade path; reinstall from crates.io (`cargo install {} --locked`) \
{where_from} (issue #4033)",
dir.display(),
record.package,
),
),
InstallSource::Path(dir) => (
CheckStatus::Warn,
format!(
"`{bin_name}` {running_version} was installed with `cargo install --path` from \
`{}` — a path install is invisible to registry update detection, so a newer \
published version will never be offered; reinstall with \
`cargo install {} --locked` once the change is published{where_from} \
(issue #4033, ADR-0021)",
dir.display(),
record.package,
),
),
InstallSource::Git(rev) => (
CheckStatus::Warn,
format!(
"`{bin_name}` {running_version} was installed from git ({rev}) — pinned to a \
commit, so registry update detection will never offer a newer published \
version{where_from} (issue #4033)"
),
),
InstallSource::Other(raw) => (
CheckStatus::Warn,
format!(
"`{bin_name}` {running_version} was installed from an unrecognised source \
(`{raw}`) — its upgrade path cannot be classified{where_from} (issue #4033)"
),
),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VersionSkew {
LedgerStale,
RunningStale,
Unordered,
}
pub(crate) fn classify_version_skew(ledger_version: &str, running_version: &str) -> VersionSkew {
let (Ok(ledger), Ok(running)) = (
semver::Version::parse(ledger_version),
semver::Version::parse(running_version),
) else {
return VersionSkew::Unordered;
};
match running.cmp(&ledger) {
Ordering::Greater => VersionSkew::LedgerStale,
Ordering::Less => VersionSkew::RunningStale,
Ordering::Equal => VersionSkew::Unordered,
}
}
fn version_skew_verdict(
bin_name: &str,
running_version: &str,
record: &CargoInstall,
installed: &Path,
where_from: &str,
) -> (CheckStatus, String) {
let recorded = format!(
"{} {} from {}",
record.package,
record.version,
describe(&record.source)
);
match classify_version_skew(&record.version, running_version) {
VersionSkew::LedgerStale => (
CheckStatus::Unknown,
format!(
"the running `{bin_name}` is version {running_version}, NEWER than the {recorded} \
cargo's ledger records for that same file `{}` — cargo's record does not \
describe the binary on disk. Usually that means a writer keeping no cargo \
metadata placed it (the prebuilt installer `tctl install` / `tctl upgrade`, a \
package manager, or a manual copy), but `cargo install --no-track --force` and \
an install interrupted before it saved its tracker leave the same state. This \
comparison orders two RELEASE labels and reads no source tree, so it says \
nothing about whether the binary's compiled-in assets lag the repo — the \
`bundled_asset_lag` row answers that where it applies (#8482). What cannot be \
verified from here is where the binary came from, and `cargo install --list` \
will keep reporting {}{where_from} (issues #4033, #4964, #8482, ADR-0021)",
installed.display(),
record.version,
),
),
VersionSkew::RunningStale => (
CheckStatus::Fail,
format!(
"the running `{bin_name}` is version {running_version}, OLDER than the {recorded} \
cargo's ledger records for that same file `{}` — an older binary is sitting on \
top of a newer install, so an upgrade the ledger says landed is not what is \
executing{where_from} (issue #4033)",
installed.display(),
),
),
VersionSkew::Unordered => (
CheckStatus::Fail,
format!(
"the running `{bin_name}` reports version {running_version}, but cargo's ledger \
records {recorded} for that same file `{}`, and the two cannot be ordered as \
semantic versions — the binary on disk is not the one cargo tracks and which of \
them is newer cannot be determined{where_from} (issue #4033)",
installed.display(),
),
),
}
}
pub(crate) fn same_file(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
pub(crate) fn describe(source: &InstallSource) -> String {
match source {
InstallSource::Registry => "crates.io".to_string(),
InstallSource::Git(rev) => format!("git ({rev})"),
InstallSource::Path(p) => format!("path `{}`", p.display()),
InstallSource::Other(raw) => format!("`{raw}`"),
}
}
#[cfg(test)]
#[path = "binary_provenance_tests.rs"]
mod tests;