#![cfg_attr(coverage_nightly, coverage(off))]
use std::path::{Path, PathBuf};
use std::sync::Once;
use std::sync::atomic::{AtomicU32, Ordering};
use serde::Deserialize;
static LOAD_ENV: Once = Once::new();
static NEXT_COPY: AtomicU32 = AtomicU32::new(0);
pub struct Fixture {
pub name: String,
pub path: PathBuf,
pub skip_checks: Vec<String>,
pub decompiler: bool,
}
impl Fixture {
#[must_use]
pub fn skips(&self, check: &str) -> bool {
self.skip_checks.iter().any(|c| c == check)
}
}
#[must_use]
pub fn fixtures() -> Vec<Fixture> {
let Some((parsed, root)) = parse() else {
return Vec::new();
};
let mut out: Vec<Fixture> = parsed
.fixture
.into_iter()
.filter(|e| e.opens.runnable())
.map(|e| Fixture {
name: display_name(&e.path),
path: root.join(&e.path),
skip_checks: e.skip_checks,
decompiler: e.decompiler,
})
.filter(|f| f.path.is_file())
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
#[must_use]
pub fn canonical() -> Option<PathBuf> {
let (parsed, root) = parse()?;
let target = parsed.corpus?.canonical?;
parsed
.fixture
.into_iter()
.find(|e| e.path == target && e.opens.runnable())
.map(|e| root.join(&e.path))
.filter(|p| p.is_file())
}
#[must_use]
pub fn parked_fixture() -> Option<PathBuf> {
let (parsed, root) = parse()?;
parsed
.fixture
.into_iter()
.find(|e| e.opens.parked())
.map(|e| root.join(&e.path))
.filter(|p| p.is_file())
}
enum Loaded {
NotConfigured,
Malformed(String),
Parsed(Manifest, PathBuf),
}
fn load() -> Loaded {
LOAD_ENV.call_once(|| {
let _ = dotenvy::dotenv();
});
let raw = match std::env::var("IDAKIT_CORPUS_MANIFEST") {
Ok(raw) if !raw.is_empty() => raw,
_ => return Loaded::NotConfigured,
};
let manifest = PathBuf::from(raw);
if !manifest.is_file() {
return Loaded::Malformed(format!(
"IDAKIT_CORPUS_MANIFEST={manifest:?} does not point to a file"
));
}
let text = match std::fs::read_to_string(&manifest) {
Ok(text) => text,
Err(e) => return Loaded::Malformed(format!("failed to read {manifest:?}: {e}")),
};
let parsed = match toml::from_str::<Manifest>(&text) {
Ok(parsed) => parsed,
Err(e) => return Loaded::Malformed(format!("failed to parse {manifest:?}: {e}")),
};
let root = manifest
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
Loaded::Parsed(parsed, root)
}
fn parse() -> Option<(Manifest, PathBuf)> {
match load() {
Loaded::Parsed(parsed, root) => Some((parsed, root)),
Loaded::NotConfigured | Loaded::Malformed(_) => None,
}
}
pub fn validate() -> Result<(), String> {
let (parsed, root) = match load() {
Loaded::NotConfigured => return Ok(()),
Loaded::Malformed(reason) => return Err(reason),
Loaded::Parsed(parsed, root) => (parsed, root),
};
let openable: Vec<&Entry> = parsed
.fixture
.iter()
.filter(|e| e.opens.runnable())
.collect();
if openable.is_empty() {
return Err(format!(
"corpus manifest at {root:?} declares {} fixture(s) but none have opens = true",
parsed.fixture.len()
));
}
let missing: Vec<PathBuf> = openable
.iter()
.map(|e| root.join(&e.path))
.filter(|p| !p.is_file())
.collect();
if !missing.is_empty() {
return Err(format!(
"corpus manifest declares {} openable fixture(s) whose file does not exist: {missing:?}",
missing.len()
));
}
if let Some(canonical) = parsed.corpus.as_ref().and_then(|c| c.canonical.as_ref())
&& !openable.iter().any(|e| e.path == *canonical)
{
return Err(format!(
"corpus manifest's [corpus].canonical = {canonical:?} names no opens = true fixture"
));
}
Ok(())
}
fn manifest_path() -> Option<PathBuf> {
let raw = std::env::var("IDAKIT_CORPUS_MANIFEST").ok()?;
let path = PathBuf::from(raw);
(!path.as_os_str().is_empty() && path.is_file()).then_some(path)
}
pub struct WorkingCopy {
dir: PathBuf,
path: PathBuf,
}
impl WorkingCopy {
#[must_use]
pub fn path(&self) -> &str {
self.path.to_str().expect("scratch path is valid UTF-8")
}
}
impl Drop for WorkingCopy {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
pub fn working_copy(src: &Path) -> std::io::Result<WorkingCopy> {
let unique = format!(
"{}-{}",
std::process::id(),
NEXT_COPY.fetch_add(1, Ordering::Relaxed)
);
let dir = scratch_root().join(unique);
std::fs::create_dir_all(&dir)?;
let file_name = src.file_name().expect("fixture has a file name");
let path = dir.join(file_name);
std::fs::copy(src, &path)?;
make_writable(&path)?;
Ok(WorkingCopy { dir, path })
}
pub fn make_writable(path: &Path) -> std::io::Result<()> {
let mut perms = std::fs::metadata(path)?.permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = perms.mode();
perms.set_mode(mode | 0o600);
}
#[cfg(not(unix))]
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
std::fs::set_permissions(path, perms)
}
fn scratch_root() -> PathBuf {
manifest_path()
.and_then(|m| m.parent().map(|p| p.join(".scratch")))
.unwrap_or_else(std::env::temp_dir)
}
#[must_use]
pub fn display_name(rel: &str) -> String {
Path::new(rel).file_stem().map_or_else(
|| "unknown".into(),
|s| s.to_string_lossy().replace('.', "_"),
)
}
#[derive(Deserialize)]
struct Manifest {
#[serde(default)]
corpus: Option<Corpus>,
#[serde(default)]
fixture: Vec<Entry>,
}
#[derive(Deserialize)]
struct Corpus {
canonical: Option<String>,
}
#[derive(Deserialize)]
struct Entry {
path: String,
#[serde(default)]
opens: Opens,
#[serde(default)]
skip_checks: Vec<String>,
#[serde(default = "default_decompiler")]
decompiler: bool,
}
fn default_decompiler() -> bool {
true
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Opens {
Bool(bool),
Word(String),
}
impl Default for Opens {
fn default() -> Self {
Self::Bool(false)
}
}
impl Opens {
fn runnable(&self) -> bool {
matches!(self, Self::Bool(true))
}
fn parked(&self) -> bool {
matches!(self, Self::Word(w) if w == "parked")
}
}