use std::fs;
use std::io::ErrorKind;
use std::path::Path;
pub(super) fn install_kind(exe: &Path) -> InstallKind {
let Some(dir) = exe.parent() else {
return InstallKind::Standalone;
};
for candidate in [Some(dir), dir.parent()].into_iter().flatten() {
match cargo_record(candidate) {
CargoRecord::ListsCrate | CargoRecord::Unreadable => return InstallKind::Cargo,
CargoRecord::Absent | CargoRecord::ListsSomethingElse => {}
}
}
InstallKind::Standalone
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CargoRecord {
Absent,
ListsCrate,
ListsSomethingElse,
Unreadable,
}
pub(super) fn cargo_record(dir: &Path) -> CargoRecord {
let json = read_json_record(&dir.join(".crates2.json"));
let toml = read_toml_record(&dir.join(".crates.toml"));
if json == OneRecord::ThisCrate || toml == OneRecord::ThisCrate {
return CargoRecord::ListsCrate;
}
if json == OneRecord::Unreadable || toml == OneRecord::Unreadable {
return CargoRecord::Unreadable;
}
if json == OneRecord::OtherCrates || toml == OneRecord::OtherCrates {
return CargoRecord::ListsSomethingElse;
}
CargoRecord::Absent
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OneRecord {
Missing,
ThisCrate,
OtherCrates,
Unreadable,
}
fn read_json_record(path: &Path) -> OneRecord {
let text = match fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == ErrorKind::NotFound => return OneRecord::Missing,
Err(_) => return OneRecord::Unreadable,
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) else {
return OneRecord::Unreadable;
};
let Some(installs) = json.get("installs").and_then(|value| value.as_object()) else {
return OneRecord::Unreadable;
};
if installs.keys().any(|key| names_this_crate(key)) {
OneRecord::ThisCrate
} else {
OneRecord::OtherCrates
}
}
fn read_toml_record(path: &Path) -> OneRecord {
let text = match fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == ErrorKind::NotFound => return OneRecord::Missing,
Err(_) => return OneRecord::Unreadable,
};
let Ok(table) = toml::from_str::<toml::Table>(&text) else {
return OneRecord::Unreadable;
};
let Some(v1) = table.get("v1").and_then(|value| value.as_table()) else {
return OneRecord::Unreadable;
};
if v1.keys().any(|key| names_this_crate(key)) {
OneRecord::ThisCrate
} else {
OneRecord::OtherCrates
}
}
pub(super) fn names_this_crate(key: &str) -> bool {
key.split_once(' ')
.map(|(name, _)| name == env!("CARGO_PKG_NAME"))
.unwrap_or(false)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum InstallKind {
Cargo,
Standalone,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_record_beside_the_bin_directory_is_what_makes_it_cargos() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(
tmp.path().join(".crates2.json"),
r#"{"installs":{"pmpx 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)":{"bins":["pmpx"]}}}"#,
)
.unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Cargo);
}
#[test]
fn the_older_record_works_too() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(
tmp.path().join(".crates.toml"),
"[v1]\n\"pmpx 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)\" = [\"pmpx\"]\n",
)
.unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Cargo);
}
#[test]
fn a_record_beside_the_binary_itself_counts() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join(".crates.toml"),
"[v1]\n\"pmpx 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)\" = [\"pmpx\"]\n",
)
.unwrap();
assert_eq!(install_kind(&tmp.path().join("pmpx")), InstallKind::Cargo);
}
#[test]
fn another_crates_record_does_not_claim_this_binary() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(
tmp.path().join(".crates2.json"),
r#"{"installs":{"cargo-bumpp 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)":{"bins":["bumpp"]}}}"#,
)
.unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Standalone);
}
#[test]
fn an_unreadable_record_is_treated_as_cargos() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(tmp.path().join(".crates2.json"), "not json at all").unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Cargo);
}
#[test]
fn no_record_at_all_is_standalone() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Standalone);
}
#[test]
fn a_record_that_cannot_be_read_is_treated_as_cargos() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(tmp.path().join(".crates2.json"), [0xff, 0xfe, 0xfd]).unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Cargo);
}
#[test]
fn a_record_with_an_unknown_shape_is_treated_as_cargos() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(
tmp.path().join(".crates2.json"),
r#"{"something_else":{"pmpx 0.1.0":{}}}"#,
)
.unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Cargo);
}
#[test]
fn a_toml_record_without_v1_is_treated_as_cargos() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::fs::write(tmp.path().join(".crates.toml"), "[other]\nx = 1\n").unwrap();
assert_eq!(install_kind(&bin.join("pmpx")), InstallKind::Cargo);
}
}