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>,
}
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,
})
.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())
}
fn parse() -> Option<(Manifest, PathBuf)> {
LOAD_ENV.call_once(|| {
let _ = dotenvy::dotenv();
});
let manifest = manifest_path()?;
let text = std::fs::read_to_string(&manifest).ok()?;
let parsed = toml::from_str::<Manifest>(&text).ok()?;
let root = manifest
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
Some((parsed, root))
}
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)?;
Ok(WorkingCopy { dir, path })
}
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(|s| s.to_string_lossy().replace('.', "_"))
.unwrap_or_else(|| "unknown".into())
}
#[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>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Opens {
Bool(bool),
Word(#[allow(dead_code)] String),
}
impl Default for Opens {
fn default() -> Self {
Opens::Bool(false)
}
}
impl Opens {
fn runnable(&self) -> bool {
matches!(self, Opens::Bool(true))
}
}