use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::{debug, warn};
use crate::adapter::overlay::{
validate_overlay, MatchContext, MatchStrength, OverlayDefect, ToolOverlay,
OVERLAY_SCHEMA_VERSION,
};
const BUILTIN_OVERLAYS: &[(&str, &str)] = &[
("cat@bsd", include_str!("../../overlays/cat@bsd.json")),
("cat@gnu", include_str!("../../overlays/cat@gnu.json")),
("chmod@bsd", include_str!("../../overlays/chmod@bsd.json")),
("chmod@gnu", include_str!("../../overlays/chmod@gnu.json")),
("cp@bsd", include_str!("../../overlays/cp@bsd.json")),
("cp@gnu", include_str!("../../overlays/cp@gnu.json")),
("cut@bsd", include_str!("../../overlays/cut@bsd.json")),
("cut@gnu", include_str!("../../overlays/cut@gnu.json")),
("df@bsd", include_str!("../../overlays/df@bsd.json")),
("df@gnu", include_str!("../../overlays/df@gnu.json")),
("diff@bsd", include_str!("../../overlays/diff@bsd.json")),
("diff@gnu", include_str!("../../overlays/diff@gnu.json")),
("du@bsd", include_str!("../../overlays/du@bsd.json")),
("du@gnu", include_str!("../../overlays/du@gnu.json")),
("find@bsd", include_str!("../../overlays/find@bsd.json")),
("find@gnu", include_str!("../../overlays/find@gnu.json")),
("grep@bsd", include_str!("../../overlays/grep@bsd.json")),
("grep@gnu", include_str!("../../overlays/grep@gnu.json")),
("head@bsd", include_str!("../../overlays/head@bsd.json")),
("head@gnu", include_str!("../../overlays/head@gnu.json")),
("ln@bsd", include_str!("../../overlays/ln@bsd.json")),
("ln@gnu", include_str!("../../overlays/ln@gnu.json")),
("ls@bsd", include_str!("../../overlays/ls@bsd.json")),
("ls@gnu", include_str!("../../overlays/ls@gnu.json")),
("mkdir@bsd", include_str!("../../overlays/mkdir@bsd.json")),
("mkdir@gnu", include_str!("../../overlays/mkdir@gnu.json")),
("mv@bsd", include_str!("../../overlays/mv@bsd.json")),
("mv@gnu", include_str!("../../overlays/mv@gnu.json")),
("rm@bsd", include_str!("../../overlays/rm@bsd.json")),
("rm@gnu", include_str!("../../overlays/rm@gnu.json")),
("sort@apple", include_str!("../../overlays/sort@apple.json")),
("sort@gnu", include_str!("../../overlays/sort@gnu.json")),
("tail@bsd", include_str!("../../overlays/tail@bsd.json")),
("tail@gnu", include_str!("../../overlays/tail@gnu.json")),
("touch@bsd", include_str!("../../overlays/touch@bsd.json")),
("touch@gnu", include_str!("../../overlays/touch@gnu.json")),
("uniq@bsd", include_str!("../../overlays/uniq@bsd.json")),
("uniq@gnu", include_str!("../../overlays/uniq@gnu.json")),
("wc@bsd", include_str!("../../overlays/wc@bsd.json")),
("wc@gnu", include_str!("../../overlays/wc@gnu.json")),
("xargs@bsd", include_str!("../../overlays/xargs@bsd.json")),
("xargs@gnu", include_str!("../../overlays/xargs@gnu.json")),
];
#[derive(Debug, Error)]
pub enum OverlayError {
#[error("Failed to read overlay '{path}': {source}")]
Read {
path: String,
#[source]
source: std::io::Error,
},
#[error("Overlay '{path}' is not valid JSON or YAML: {message}")]
Malformed { path: String, message: String },
#[error(
"Overlay '{path}' declares schema_version '{found}', but this build supports '{expected}'"
)]
UnsupportedVersion {
path: String,
found: String,
expected: String,
},
#[error("Overlay '{path}' is invalid:\n{}", format_defects(.defects))]
Invalid {
path: String,
defects: Vec<OverlayDefect>,
},
}
fn format_defects(defects: &[OverlayDefect]) -> String {
defects
.iter()
.map(|defect| format!(" - {defect}"))
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum OverlayOrigin {
Builtin,
UserDir,
Explicit,
}
#[derive(Debug, Clone)]
struct StoredOverlay {
overlay: ToolOverlay,
origin: OverlayOrigin,
}
#[derive(Debug, Clone)]
pub struct OverlaySelection<'a> {
pub overlay: &'a ToolOverlay,
pub strength: MatchStrength,
}
#[derive(Debug, Default)]
pub struct OverlayStore {
entries: Vec<StoredOverlay>,
}
impl OverlayStore {
pub fn empty() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let mut store = Self::default();
for (id, document) in BUILTIN_OVERLAYS {
match parse_overlay(id, document) {
Ok(overlay) => store.entries.push(StoredOverlay {
overlay,
origin: OverlayOrigin::Builtin,
}),
Err(e) => warn!(overlay = id, "Built-in overlay is invalid, skipping: {e}"),
}
}
store
}
pub fn load_dir(&mut self, dir: &Path) -> Result<usize, OverlayError> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Ok(0);
};
let mut loaded = 0;
for entry in entries.flatten() {
let path = entry.path();
if !has_overlay_extension(&path) {
continue;
}
self.push_from_path(&path, OverlayOrigin::UserDir)?;
loaded += 1;
}
Ok(loaded)
}
pub fn load_explicit(&mut self, path: &Path) -> Result<(), OverlayError> {
self.push_from_path(path, OverlayOrigin::Explicit)
}
fn push_from_path(&mut self, path: &Path, origin: OverlayOrigin) -> Result<(), OverlayError> {
let path_text = path.display().to_string();
let document = std::fs::read_to_string(path).map_err(|source| OverlayError::Read {
path: path_text.clone(),
source,
})?;
let overlay = parse_overlay(&path_text, &document)?;
debug!(overlay = %overlay.id(), path = %path_text, "Loaded overlay");
self.entries.push(StoredOverlay { overlay, origin });
Ok(())
}
pub fn probe_arg_sets(&self, command: &str) -> Vec<Vec<String>> {
let mut sets: Vec<Vec<String>> = vec![super::variant::version_probe_args()];
for entry in &self.entries {
if entry.overlay.command != command {
continue;
}
let Some(ref probe) = entry.overlay.match_rules.probe else {
continue;
};
if !sets.contains(&probe.args) {
sets.push(probe.args.clone());
}
}
sets
}
pub fn select(&self, context: &MatchContext) -> Option<OverlaySelection<'_>> {
self.entries
.iter()
.filter_map(|entry| self.evaluate(entry, context))
.max_by_key(|(strength, prefer_user, _)| (*strength, *prefer_user))
.map(|(strength, _, overlay)| OverlaySelection { overlay, strength })
}
fn evaluate<'a>(
&self,
entry: &'a StoredOverlay,
context: &MatchContext,
) -> Option<(MatchStrength, u8, &'a ToolOverlay)> {
let prefer_user = u8::from(entry.origin != OverlayOrigin::Builtin);
if entry.origin == OverlayOrigin::Explicit {
return (entry.overlay.command == context.command).then_some((
MatchStrength::Explicit,
prefer_user,
&entry.overlay,
));
}
entry
.overlay
.evaluate(context)
.map(|strength| (strength, prefer_user, &entry.overlay))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
fn has_overlay_extension(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext, "json" | "yaml" | "yml"))
}
fn parse_overlay(path: &str, document: &str) -> Result<ToolOverlay, OverlayError> {
let overlay: ToolOverlay = if document.trim_start().starts_with('{') {
serde_json::from_str(document).map_err(|e| OverlayError::Malformed {
path: path.to_string(),
message: e.to_string(),
})?
} else {
serde_yaml::from_str(document).map_err(|e| OverlayError::Malformed {
path: path.to_string(),
message: e.to_string(),
})?
};
if !overlay.is_supported_version() {
return Err(OverlayError::UnsupportedVersion {
path: path.to_string(),
found: overlay.schema_version.clone(),
expected: OVERLAY_SCHEMA_VERSION.to_string(),
});
}
let defects = validate_overlay(&overlay);
if !defects.is_empty() {
return Err(OverlayError::Invalid {
path: path.to_string(),
defects,
});
}
Ok(overlay)
}
pub fn user_overlay_dir(config_dir: &Path) -> PathBuf {
config_dir.join("overlays")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::overlay::{Platform, ProbeOutcome};
use crate::models::ToolVariant;
use tempfile::TempDir;
const MINIMAL: &str = r#"{
"schema_version": "1.0",
"command": "widget",
"variant": "bsd",
"match": { "platform": ["macos"] },
"mode": "merge",
"confidence": "verified",
"provenance": {
"platform": "macos",
"tool_version": "test",
"source": "man-page",
"checked_on": "2026-07-27"
},
"flags": []
}"#;
fn context(command: &str, variant: ToolVariant, platform: Platform) -> MatchContext {
MatchContext {
command: command.to_string(),
variant,
platform: Some(platform),
binary_path: format!("/bin/{command}"),
..Default::default()
}
}
#[test]
fn test_apple_variant_overlay_is_selectable() {
let document = MINIMAL
.replace("\"widget\"", "\"sort\"")
.replace("\"variant\": \"bsd\"", "\"variant\": \"apple\"");
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("sort.json"), document).unwrap();
let mut store = OverlayStore::empty();
store.load_dir(tmp.path()).unwrap();
let ctx = context("sort", ToolVariant::Apple, Platform::new("macos"));
let selected = store.select(&ctx).expect("apple overlay must match");
assert_eq!(selected.overlay.id(), "sort@apple");
}
#[test]
fn test_builtin_overlays_all_parse() {
let store = OverlayStore::with_builtins();
assert_eq!(store.len(), BUILTIN_OVERLAYS.len());
assert!(!store.is_empty());
}
#[test]
fn test_builtin_ls_bsd_selected_for_bsd_probe() {
let store = OverlayStore::with_builtins();
let mut ctx = context("ls", ToolVariant::Bsd, Platform::new("macos"));
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: false,
output: "ls: unrecognized option `--version'".to_string(),
}];
let selected = store.select(&ctx).expect("bsd overlay must match");
assert_eq!(selected.overlay.id(), "ls@bsd");
assert_eq!(selected.strength, MatchStrength::Probe);
}
#[test]
fn test_builtin_ls_gnu_selected_on_macos_when_probe_says_gnu() {
let store = OverlayStore::with_builtins();
let mut ctx = context("ls", ToolVariant::Gnu, Platform::new("macos"));
ctx.binary_path = "/opt/homebrew/opt/coreutils/libexec/gnubin/ls".to_string();
ctx.version = Some("9.4".to_string());
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: true,
output: "ls (GNU coreutils) 9.4".to_string(),
}];
let selected = store.select(&ctx).expect("gnu overlay must match");
assert_eq!(selected.overlay.id(), "ls@gnu");
}
#[test]
fn test_builtin_rm_bsd_selected_for_bsd_probe() {
let store = OverlayStore::with_builtins();
let mut ctx = context("rm", ToolVariant::Bsd, Platform::new("macos"));
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: false,
output: "rm: illegal option -- -".to_string(),
}];
let selected = store.select(&ctx).expect("bsd rm overlay must match");
assert_eq!(selected.overlay.id(), "rm@bsd");
assert_eq!(selected.strength, MatchStrength::Probe);
}
#[test]
fn test_builtin_rm_gnu_selected_when_probe_says_gnu() {
let store = OverlayStore::with_builtins();
let mut ctx = context("rm", ToolVariant::Gnu, Platform::new("linux"));
ctx.binary_path = "/usr/bin/rm".to_string();
ctx.version = Some("9.7".to_string());
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: true,
output: "rm (GNU coreutils) 9.7".to_string(),
}];
let selected = store.select(&ctx).expect("gnu rm overlay must match");
assert_eq!(selected.overlay.id(), "rm@gnu");
}
#[test]
fn test_builtin_rm_overlays_assert_destructive_and_require_approval() {
let store = OverlayStore::with_builtins();
let rm_overlays: Vec<_> = store
.entries
.iter()
.filter(|entry| entry.overlay.command == "rm")
.collect();
assert_eq!(rm_overlays.len(), 2, "both rm variants must be registered");
for entry in rm_overlays {
let annotations = &entry.overlay.annotations;
assert_eq!(annotations.destructive, Some(true));
assert_eq!(annotations.requires_approval, Some(true));
assert_eq!(annotations.readonly, Some(false));
}
}
#[test]
fn test_builtin_overlays_are_verified_with_provenance() {
let store = OverlayStore::with_builtins();
for entry in &store.entries {
assert_eq!(
entry.overlay.confidence,
crate::models::Confidence::Verified,
"{} must be verified",
entry.overlay.id()
);
let provenance = entry
.overlay
.provenance
.as_ref()
.unwrap_or_else(|| panic!("{} must carry provenance", entry.overlay.id()));
assert!(!provenance.tool_version.trim().is_empty());
assert_eq!(provenance.checked_on.len(), 10);
}
}
#[test]
fn test_builtin_gnu_overlays_pin_their_own_package() {
let store = OverlayStore::with_builtins();
let gnu: Vec<_> = store
.entries
.iter()
.filter(|entry| entry.overlay.variant == ToolVariant::Gnu)
.collect();
assert!(
gnu.len() >= 14,
"the curated GNU overlays must all be registered, found {}",
gnu.len()
);
for entry in gnu {
let provenance = entry
.overlay
.provenance
.as_ref()
.unwrap_or_else(|| panic!("{} must carry provenance", entry.overlay.id()));
let package = provenance
.package
.as_deref()
.unwrap_or_else(|| panic!("{} must name its upstream package", entry.overlay.id()));
let probe = entry
.overlay
.match_rules
.probe
.as_ref()
.unwrap_or_else(|| panic!("{} must declare a probe", entry.overlay.id()));
assert_eq!(
probe.output_contains.as_deref(),
Some(format!("GNU {package}").as_str()),
"{} must pin its package in the probe",
entry.overlay.id()
);
}
}
#[test]
fn test_builtin_non_coreutils_gnu_overlays_are_selected_by_their_banner() {
let store = OverlayStore::with_builtins();
for (command, banner, expected) in [
("grep", "grep (GNU grep) 3.11", "grep@gnu"),
("find", "find (GNU findutils) 4.10.0", "find@gnu"),
("xargs", "xargs (GNU findutils) 4.10.0", "xargs@gnu"),
("diff", "diff (GNU diffutils) 3.10", "diff@gnu"),
] {
let mut ctx = context(command, ToolVariant::Gnu, Platform::new("linux"));
ctx.binary_path = format!("/usr/bin/{command}");
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: true,
output: banner.to_string(),
}];
let selected = store
.select(&ctx)
.unwrap_or_else(|| panic!("{expected} must match"));
assert_eq!(selected.overlay.id(), expected);
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: true,
output: format!("{command} (GNU coreutils) 9.7"),
}];
assert!(
store.select(&ctx).is_none(),
"{expected} must not match a coreutils banner"
);
}
}
#[test]
fn test_builtin_sort_apple_selected_only_on_the_apple_banner() {
let store = OverlayStore::with_builtins();
let mut ctx = context("sort", ToolVariant::Apple, Platform::new("macos"));
ctx.binary_path = "/usr/bin/sort".to_string();
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: true,
output: "2.3-Apple (197)".to_string(),
}];
let selected = store.select(&ctx).expect("apple overlay must match");
assert_eq!(selected.overlay.id(), "sort@apple");
assert_eq!(selected.strength, MatchStrength::Probe);
ctx.probes = vec![ProbeOutcome {
args: super::super::variant::version_probe_args(),
succeeded: true,
output: "sort (GNU coreutils) 9.7".to_string(),
}];
assert!(store.select(&ctx).is_none());
}
#[test]
fn test_builtin_tail_overlays_mark_follow_long_running() {
let store = OverlayStore::with_builtins();
let tails: Vec<_> = store
.entries
.iter()
.filter(|entry| entry.overlay.command == "tail")
.collect();
assert_eq!(tails.len(), 2, "both tail variants must be registered");
for entry in tails {
let marked: Vec<&str> = entry
.overlay
.flags
.iter()
.filter(|flag| flag.long_running)
.filter_map(|flag| flag.short.as_deref())
.collect();
assert_eq!(
marked,
vec!["-f", "-F"],
"{}: only the following flags may claim it",
entry.overlay.id()
);
}
}
#[test]
fn test_select_returns_none_for_unknown_command() {
let store = OverlayStore::with_builtins();
let ctx = context(
"definitely-not-a-real-tool",
ToolVariant::Bsd,
Platform::new("macos"),
);
assert!(store.select(&ctx).is_none());
}
#[test]
fn test_probe_arg_sets_always_includes_version() {
let store = OverlayStore::with_builtins();
let sets = store.probe_arg_sets("ls");
assert!(sets.contains(&vec!["--version".to_string()]));
}
#[test]
fn test_probe_arg_sets_deduplicates() {
let store = OverlayStore::with_builtins();
let sets = store.probe_arg_sets("ls");
let version_count = sets
.iter()
.filter(|args| *args == &vec!["--version".to_string()])
.count();
assert_eq!(version_count, 1);
}
#[test]
fn test_load_dir_missing_directory_is_not_an_error() {
let mut store = OverlayStore::empty();
let loaded = store
.load_dir(Path::new("/nonexistent/overlay/dir"))
.unwrap();
assert_eq!(loaded, 0);
}
#[test]
fn test_load_dir_reads_json_overlay() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("widget.json"), MINIMAL).unwrap();
std::fs::write(tmp.path().join("notes.txt"), "ignored").unwrap();
let mut store = OverlayStore::empty();
let loaded = store.load_dir(tmp.path()).unwrap();
assert_eq!(loaded, 1);
assert!(store
.select(&context("widget", ToolVariant::Bsd, Platform::new("macos")))
.is_some());
}
#[test]
fn test_load_dir_reads_yaml_overlay() {
let tmp = TempDir::new().unwrap();
let yaml = "schema_version: '1.0'\ncommand: widget\nvariant: bsd\nmode: merge\n";
std::fs::write(tmp.path().join("widget.yaml"), yaml).unwrap();
let mut store = OverlayStore::empty();
assert_eq!(store.load_dir(tmp.path()).unwrap(), 1);
}
#[test]
fn test_load_dir_surfaces_malformed_overlay() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("bad.json"), "{ not json").unwrap();
let mut store = OverlayStore::empty();
let err = store.load_dir(tmp.path()).unwrap_err();
assert!(matches!(err, OverlayError::Malformed { .. }));
}
#[test]
fn test_load_rejects_unsupported_schema_version() {
let tmp = TempDir::new().unwrap();
let document = MINIMAL.replace("\"1.0\"", "\"99.0\"");
std::fs::write(tmp.path().join("widget.json"), document).unwrap();
let mut store = OverlayStore::empty();
let err = store.load_dir(tmp.path()).unwrap_err();
match err {
OverlayError::UnsupportedVersion {
found, expected, ..
} => {
assert_eq!(found, "99.0");
assert_eq!(expected, OVERLAY_SCHEMA_VERSION);
}
other => panic!("expected UnsupportedVersion, got {other:?}"),
}
}
#[test]
fn test_load_explicit_missing_file_errors() {
let mut store = OverlayStore::empty();
let err = store
.load_explicit(Path::new("/nonexistent/overlay.json"))
.unwrap_err();
assert!(matches!(err, OverlayError::Read { .. }));
}
#[test]
fn test_explicit_overlay_outranks_builtin_and_skips_variant_match() {
let tmp = TempDir::new().unwrap();
let document = MINIMAL.replace("\"widget\"", "\"ls\"");
let path = tmp.path().join("mine.json");
std::fs::write(&path, document).unwrap();
let mut store = OverlayStore::with_builtins();
store.load_explicit(&path).unwrap();
let ctx = context("ls", ToolVariant::Unknown, Platform::new("linux"));
let selected = store.select(&ctx).expect("explicit overlay must apply");
assert_eq!(selected.strength, MatchStrength::Explicit);
}
#[test]
fn test_explicit_overlay_still_requires_matching_command() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("mine.json");
std::fs::write(&path, MINIMAL).unwrap();
let mut store = OverlayStore::empty();
store.load_explicit(&path).unwrap();
assert!(store
.select(&context("cat", ToolVariant::Bsd, Platform::new("macos")))
.is_none());
}
#[test]
fn test_user_overlay_shadows_builtin_at_equal_strength() {
let tmp = TempDir::new().unwrap();
let document = MINIMAL
.replace("\"widget\"", "\"ls\"")
.replace("{ \"platform\": [\"macos\"] }", "{}");
std::fs::write(tmp.path().join("ls.json"), document).unwrap();
let mut store = OverlayStore::with_builtins();
store.load_dir(tmp.path()).unwrap();
let ctx = context("ls", ToolVariant::Bsd, Platform::new("macos"));
let selected = store.select(&ctx).unwrap();
assert_eq!(selected.strength, MatchStrength::Platform);
}
#[test]
fn test_user_overlay_dir_is_under_config_dir() {
let dir = user_overlay_dir(Path::new("/home/me/.apexe"));
assert_eq!(dir, PathBuf::from("/home/me/.apexe/overlays"));
}
}