use std::path::Path;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Verdict {
SomeCloud {
cloud: Vec<String>,
local: Vec<String>,
},
NoCloud,
}
pub(crate) fn inspect(home: &Path) -> Verdict {
let Ok(entries) = std::fs::read_dir(home.join("plugins")) else {
return Verdict::NoCloud;
};
let (mut cloud, mut local) = (Vec::new(), Vec::new());
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
let Some(stem) = name.split('.').next() else {
continue;
};
if !stem.starts_with("hex") {
continue;
}
if stem.starts_with("hexc") {
cloud.push(name);
} else {
local.push(name);
}
}
if cloud.is_empty() {
return Verdict::NoCloud;
}
cloud.sort();
local.sort();
Verdict::SomeCloud { cloud, local }
}
pub(crate) fn warning(home: &Path) -> Option<String> {
let Verdict::SomeCloud { cloud, local } = inspect(home) else {
return None;
};
let mut message = format!(
"This IDA installation has cloud decompilers ({}). Microcode cannot be \
produced without a decompiler, and every function a cloud one handles \
is sent to Hex-Rays' servers. Nothing has been sent yet.",
cloud.join(", ")
);
if !local.is_empty() {
message.push_str(&format!(
" It also has local decompilers ({}); which of them handles this \
binary depends on its architecture.",
local.join(", ")
));
}
Some(message)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn install(plugins: &[&str]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("plugins")).unwrap();
for p in plugins {
fs::write(dir.path().join("plugins").join(p), b"").unwrap();
}
dir
}
#[test]
fn test_only_cloud_plugins_warns() {
let dir = install(&["hexcx64.dylib", "hexcarm.dylib", "dbg.dylib"]);
assert_eq!(
inspect(dir.path()),
Verdict::SomeCloud {
cloud: vec!["hexcarm.dylib".into(), "hexcx64.dylib".into()],
local: vec![],
}
);
let warning = warning(dir.path()).expect("no warning for a cloud-only install");
assert!(warning.contains("hexcx64.dylib"), "{warning}");
assert!(warning.contains("Hex-Rays"), "{warning}");
assert!(
!warning.contains("local decompilers"),
"claims a local decompiler that is not there: {warning}"
);
}
#[test]
fn test_a_local_plugin_for_another_architecture_does_not_silence_the_warning() {
let dir = install(&["hexcx64.dylib", "hexarm64.dylib"]);
assert_eq!(
inspect(dir.path()),
Verdict::SomeCloud {
cloud: vec!["hexcx64.dylib".into()],
local: vec!["hexarm64.dylib".into()],
}
);
let warning = warning(dir.path()).expect("no warning where a cloud plugin exists");
assert!(warning.contains("hexcx64.dylib"), "{warning}");
assert!(
warning.contains("hexarm64.dylib") && warning.contains("architecture"),
"does not say the answer depends on the architecture: {warning}"
);
}
#[test]
fn test_only_local_plugins_says_nothing() {
let dir = install(&["hexx64.dylib", "hexarm64.dylib"]);
assert_eq!(inspect(dir.path()), Verdict::NoCloud);
assert_eq!(warning(dir.path()), None);
}
#[test]
fn test_a_cloud_plugin_is_not_mistaken_for_a_local_one() {
let dir = install(&["hexcarm.dylib"]);
assert!(matches!(inspect(dir.path()), Verdict::SomeCloud { .. }));
}
#[test]
fn test_no_decompiler_is_not_a_warning() {
let dir = install(&["dbg.dylib", "pdb.dylib"]);
assert_eq!(inspect(dir.path()), Verdict::NoCloud);
assert_eq!(warning(dir.path()), None);
}
#[test]
fn test_an_unreadable_installation_says_nothing() {
assert_eq!(inspect(Path::new("/oinkie-no-such-ida")), Verdict::NoCloud);
assert_eq!(warning(Path::new("/oinkie-no-such-ida")), None);
}
}