use std::path::{Path, PathBuf};
pub const KNOWN_METHODS: &[&str] = &["install.sh", "deb", "brew"];
pub fn parse_method(json: &str) -> Option<&'static str> {
let marker: serde_json::Value = serde_json::from_str(json).ok()?;
let method = marker.as_object()?.get("method")?.as_str()?;
KNOWN_METHODS.iter().copied().find(|known| *known == method)
}
pub fn marker_paths(bin_dir: &Path) -> Vec<PathBuf> {
let mut paths = Vec::with_capacity(2);
if let Some(prefix) = bin_dir.parent() {
paths.push(prefix.join("share").join("cerulion").join("install.json"));
}
paths.push(bin_dir.join(".cerulion-provenance.json"));
paths
}
pub fn method_at(paths: &[PathBuf]) -> Option<&'static str> {
let mut newest: Option<(&'static str, (i64, i64))> = None;
for path in paths {
let Some((method, meta)) = read_marker(path) else {
continue;
};
let changed = changed_at(&meta);
if newest.is_none_or(|(_, at)| changed > at) {
newest = Some((method, changed));
}
}
newest.map(|(method, _)| method)
}
const MAX_MARKER_BYTES: u64 = 4096;
fn read_marker(path: &Path) -> Option<(&'static str, std::fs::Metadata)> {
if !std::fs::symlink_metadata(path).ok()?.is_file() {
return None;
}
let mut file = std::fs::File::open(path).ok()?;
let meta = file.metadata().ok()?;
if !meta.is_file() {
return None;
}
let mut json = String::new();
std::io::Read::read_to_string(
&mut std::io::Read::take(&mut file, MAX_MARKER_BYTES + 1),
&mut json,
)
.ok()?;
if json.len() as u64 > MAX_MARKER_BYTES {
return None;
}
Some((parse_method(&json)?, meta))
}
#[cfg(unix)]
fn changed_at(meta: &std::fs::Metadata) -> (i64, i64) {
use std::os::unix::fs::MetadataExt;
(meta.ctime(), meta.ctime_nsec())
}
#[cfg(not(unix))]
fn changed_at(_: &std::fs::Metadata) -> (i64, i64) {
(0, 0)
}
pub fn current_method() -> Option<&'static str> {
let exe = std::env::current_exe().ok()?.canonicalize().ok()?;
method_at(&marker_paths(exe.parent()?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_known_methods_parse() {
assert_eq!(
parse_method(r#"{"method":"install.sh","version":"1.0.0"}"#),
Some("install.sh")
);
assert_eq!(parse_method(r#"{"method":"deb"}"#), Some("deb"));
assert_eq!(parse_method(r#"{"method":"brew"}"#), Some("brew"));
assert_eq!(parse_method(r#"{"method":"../elsewhere"}"#), None);
assert_eq!(parse_method(r#"{"version":"1.0.0"}"#), None);
assert_eq!(parse_method("not json"), None);
assert_eq!(parse_method(r#"{"method":"deb2"}"#), None);
assert_eq!(parse_method(r#"{"method":"d eb"}"#), None);
assert_eq!(parse_method(r#"{ "method" : "deb" }"#), Some("deb"));
assert_eq!(parse_method(r#"{"method":["deb"]}"#), None);
assert_eq!(parse_method(r#"["deb"]"#), None);
}
#[test]
fn the_package_marker_wins_over_a_stale_script_marker() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("bin");
let share = dir.path().join("share").join("cerulion");
std::fs::create_dir_all(&bin).unwrap();
std::fs::create_dir_all(&share).unwrap();
let paths = marker_paths(&bin);
assert_eq!(method_at(&paths), None, "no marker is a source build");
std::fs::write(
bin.join(".cerulion-provenance.json"),
r#"{"method":"install.sh"}"#,
)
.unwrap();
assert_eq!(method_at(&paths), Some("install.sh"));
wait_for_a_later_change_time(&bin.join(".cerulion-provenance.json"));
std::fs::write(share.join("install.json"), r#"{"method":"deb"}"#).unwrap();
assert_eq!(method_at(&paths), Some("deb"));
}
#[cfg(unix)]
#[test]
fn a_script_install_over_a_package_wins_over_the_package_marker() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("bin");
let share = dir.path().join("share").join("cerulion");
std::fs::create_dir_all(&bin).unwrap();
std::fs::create_dir_all(&share).unwrap();
let package = share.join("install.json");
std::fs::write(&package, r#"{"method":"deb"}"#).unwrap();
let paths = marker_paths(&bin);
assert_eq!(method_at(&paths), Some("deb"));
wait_for_a_later_change_time(&package);
std::fs::write(
bin.join(".cerulion-provenance.json"),
r#"{"method":"install.sh"}"#,
)
.unwrap();
assert_eq!(method_at(&paths), Some("install.sh"));
wait_for_a_later_change_time(&bin.join(".cerulion-provenance.json"));
std::fs::write(&package, r#"{"method":"deb"}"#).unwrap();
assert_eq!(
method_at(&paths),
Some("deb"),
"a package upgrade wins back"
);
}
fn wait_for_a_later_change_time(path: &Path) {
let before = changed_at(&std::fs::symlink_metadata(path).unwrap());
let probe = path.with_extension("probe");
loop {
std::fs::write(&probe, b"").unwrap();
let now = changed_at(&std::fs::symlink_metadata(&probe).unwrap());
std::fs::remove_file(&probe).unwrap();
if now > before || cfg!(not(unix)) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
#[test]
fn a_directory_or_unknown_marker_is_skipped() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("bin");
std::fs::create_dir_all(bin.join(".cerulion-provenance.json")).unwrap();
let share = dir.path().join("share").join("cerulion");
std::fs::create_dir_all(&share).unwrap();
std::fs::write(share.join("install.json"), r#"{"method":"other"}"#).unwrap();
assert_eq!(method_at(&marker_paths(&bin)), None);
}
#[test]
fn an_oversized_marker_is_skipped() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
let padding = " ".repeat(MAX_MARKER_BYTES as usize);
std::fs::write(
bin.join(".cerulion-provenance.json"),
format!(r#"{{"method":"install.sh"}}{padding}"#),
)
.unwrap();
assert_eq!(method_at(&marker_paths(&bin)), None);
}
#[cfg(unix)]
#[test]
fn a_symlinked_marker_is_skipped() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.json");
std::fs::write(&target, r#"{"method":"brew"}"#).unwrap();
let bin = dir.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
std::os::unix::fs::symlink(&target, bin.join(".cerulion-provenance.json")).unwrap();
assert_eq!(method_at(&marker_paths(&bin)), None);
}
}