use anyhow::Context;
pub fn write_install_record(
cargo_home: &std::path::Path,
binary_name: &str,
new_version: &str,
) -> anyhow::Result<()> {
let path = cargo_home.join(".crates.toml");
let body = std::fs::read_to_string(&path).context("read .crates.toml")?;
let new_body = update_record(&body, binary_name, new_version)?;
std::fs::write(&path, new_body).context("write .crates.toml")?;
Ok(())
}
pub fn update_record(body: &str, binary_name: &str, new_version: &str) -> anyhow::Result<String> {
let lines: Vec<&str> = body.lines().collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut matched = false;
for line in lines {
if matched {
out.push(line.to_string());
continue;
}
if let Some(new_line) = try_rewrite_line(line, binary_name, new_version) {
out.push(new_line);
matched = true;
} else {
out.push(line.to_string());
}
}
let mut joined = out.join("\n");
if body.ends_with('\n') {
joined.push('\n');
}
Ok(joined)
}
fn try_rewrite_line(line: &str, binary_name: &str, new_version: &str) -> Option<String> {
let trimmed = line.trim_start();
if !trimmed.starts_with('"') {
return None;
}
let after_open_quote = &trimmed[1..];
let close_quote_idx = after_open_quote.find('"')?;
let key = &after_open_quote[..close_quote_idx];
let mut parts = key.splitn(3, ' ');
let pkg_name = parts.next()?;
let version = parts.next()?;
let rest = parts.next()?;
let bracket_part = &after_open_quote[close_quote_idx + 1..];
let is_match = pkg_name == binary_name || bracket_contains_binary(bracket_part, binary_name);
if !is_match {
return None;
}
let indent_len = line.len() - trimmed.len();
let indent = &line[..indent_len];
let _ = version; let _ = rest;
let new_key = format!("\"{pkg_name} {new_version} {rest}\"");
Some(format!("{indent}{new_key}{bracket_part}"))
}
fn bracket_contains_binary(bracket_part: &str, binary_name: &str) -> bool {
let needle = format!("\"{binary_name}\"");
bracket_part.contains(&needle)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"[v1]
"cargo-deny 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-deny"]
"ripgrep 14.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["rg"]
"mdbook 0.4.40 (registry+https://github.com/rust-lang/crates.io-index)" = ["mdbook"]
"#;
#[test]
fn updates_version_for_matching_package_name() {
let out = update_record(SAMPLE, "cargo-deny", "0.19.7").unwrap();
assert!(out.contains("\"cargo-deny 0.19.7 (registry+"));
assert!(!out.contains("\"cargo-deny 0.16.0"));
}
#[test]
fn updates_version_via_binary_name_lookup() {
let out = update_record(SAMPLE, "rg", "15.1.0").unwrap();
assert!(out.contains("\"ripgrep 15.1.0 (registry+"));
assert!(!out.contains("\"ripgrep 14.1.1"));
}
#[test]
fn unmatched_binary_leaves_body_unchanged() {
let out = update_record(SAMPLE, "nonexistent", "1.0.0").unwrap();
assert_eq!(out, SAMPLE);
}
#[test]
fn preserves_trailing_newline() {
let out = update_record(SAMPLE, "cargo-deny", "0.19.7").unwrap();
assert!(out.ends_with('\n'));
}
#[test]
fn preserves_v1_header() {
let out = update_record(SAMPLE, "cargo-deny", "0.19.7").unwrap();
assert!(out.starts_with("[v1]\n"));
}
#[test]
fn only_updates_first_matching_line() {
let body = r#"[v1]
"foo 1.0.0 (registry+x)" = ["foo"]
"foo 2.0.0 (registry+x)" = ["foo"]
"#;
let out = update_record(body, "foo", "9.9.9").unwrap();
assert!(out.contains("\"foo 9.9.9 (registry+x)\" = [\"foo\"]"));
assert!(out.contains("\"foo 2.0.0 (registry+x)\" = [\"foo\"]"));
}
#[test]
fn preserves_multi_binary_list() {
let body = r#"[v1]
"multi 1.0.0 (registry+x)" = ["bin-a", "bin-b"]
"#;
let out = update_record(body, "multi", "2.0.0").unwrap();
assert!(out.contains("\"multi 2.0.0 (registry+x)\" = [\"bin-a\", \"bin-b\"]"));
}
}