use std::fmt;
#[path = "../fingerprint.rs"]
mod fingerprint;
pub const ABI_FINGERPRINT_SYMBOL: &[u8] = b"tellur_abi_fingerprint_v1\0";
pub const ABI_FINGERPRINT: &str = env!("TELLUR_ABI_FINGERPRINT");
#[doc(hidden)]
pub static ABI_FINGERPRINT_C: &[u8] = concat!(env!("TELLUR_ABI_FINGERPRINT"), "\0").as_bytes();
pub type AbiFingerprintFn = unsafe extern "C" fn() -> *const std::os::raw::c_char;
#[derive(Clone, PartialEq, Eq)]
pub struct AbiMismatchError {
pub host: String,
pub plugin: String,
pub hint: String,
}
const SIDE_LABEL_WIDTH: usize = 7;
impl fmt::Display for AbiMismatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
writeln!(f, "AbiMismatch: The plugin ABI is incompatible with the host.")?;
writeln!(f)?;
let diffs = fingerprint::fingerprint_diffs(&self.host, &self.plugin);
if diffs.is_empty() {
writeln!(
f,
"{:>width$} {}",
"Host",
self.host,
width = SIDE_LABEL_WIDTH
)?;
writeln!(
f,
"{:>width$} {}",
"Plugin",
self.plugin,
width = SIDE_LABEL_WIDTH
)?;
} else {
for (field, host_val, plugin_val) in diffs {
writeln!(
f,
"{:>width$} {field}={host_val}",
"Host",
width = SIDE_LABEL_WIDTH
)?;
writeln!(
f,
"{:>width$} {field}={plugin_val}",
"Plugin",
width = SIDE_LABEL_WIDTH
)?;
}
}
writeln!(f)?;
write!(f, "hint: {}", self.hint)
}
}
impl fmt::Debug for AbiMismatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl std::error::Error for AbiMismatchError {}
pub fn validate_plugin_fingerprint(plugin: &str) -> Result<(), AbiMismatchError> {
if plugin == ABI_FINGERPRINT {
return Ok(());
}
Err(AbiMismatchError {
host: ABI_FINGERPRINT.to_owned(),
plugin: plugin.to_owned(),
hint: fingerprint::remediation_hint(ABI_FINGERPRINT, plugin),
})
}
#[cfg(test)]
mod tests {
use super::*;
use fingerprint::{find_cargo_lock, fingerprint_diffs, parse_crate_versions, remediation_hint};
use std::path::Path;
#[test]
fn validate_accepts_host_fingerprint() {
validate_plugin_fingerprint(ABI_FINGERPRINT).expect("host fingerprint matches");
}
#[test]
fn validate_rejects_mismatched_fingerprint() {
let err =
validate_plugin_fingerprint("rustc=0.0.0/000 target=unknown tellur-plugin=0.0.0 lock=unknown bytes=unknown")
.expect_err("mismatch");
assert_ne!(err.host, err.plugin);
assert!(!err.hint.is_empty());
assert!(err.to_string().contains("AbiMismatch:"));
}
#[test]
fn find_cargo_lock_walks_up_from_out_dir() {
let lock = find_cargo_lock(Path::new(env!("CARGO_MANIFEST_DIR")));
assert!(lock.is_some(), "workspace Cargo.lock should be discoverable");
}
#[test]
fn remediation_suggests_precise_update_for_bytes() {
let host = "rustc=1.95.0/abc target=x86_64-unknown-linux-gnu tellur-plugin=0.1.0 lock=found bytes=1.11.1";
let plugin = "rustc=1.95.0/abc target=x86_64-unknown-linux-gnu tellur-plugin=0.1.0 lock=found bytes=1.12.0";
let hint = remediation_hint(host, plugin);
assert!(hint.contains(
"On the plugin side (your video editing project),\n please run `cargo update -p bytes --precise 1.11.1`."
));
}
#[test]
fn display_shows_only_diffing_fields() {
let host = "rustc=1.95.0/abc target=x86_64-unknown-linux-gnu tellur-plugin=0.1.0 lock=found bytes=1.11.1";
let plugin = "rustc=1.95.0/abc target=x86_64-unknown-linux-gnu tellur-plugin=0.1.0 lock=found bytes=1.12.0";
let err = AbiMismatchError {
host: host.to_owned(),
plugin: plugin.to_owned(),
hint: remediation_hint(host, plugin),
};
let expected = "\nAbiMismatch: The plugin ABI is incompatible with the host.\n\n Host bytes=1.11.1\n Plugin bytes=1.12.0\n\nhint: On the plugin side (your video editing project),\n please run `cargo update -p bytes --precise 1.11.1`.";
assert_eq!(err.to_string(), expected);
assert_eq!(format!("{err:?}"), expected);
assert_eq!(fingerprint_diffs(host, plugin).len(), 1);
}
#[test]
fn parse_crate_versions_reads_lock_entries() {
let lock = r#"
[[package]]
name = "bytes"
version = "1.11.1"
"#;
let versions = parse_crate_versions(lock, fingerprint::BOUNDARY_CRATES);
assert_eq!(versions.get("bytes"), Some(&"1.11.1".to_owned()));
}
}