#![allow(dead_code)]
use std::path::PathBuf;
use std::sync::OnceLock;
use keyhog_core::{Chunk, ChunkMetadata, RawMatch};
use keyhog_scanner::{CompiledScanner, ScannerConfig};
use serde::Deserialize;
mod support;
pub use support::paths::detector_dir;
pub fn contracts_dir() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("tests");
d.push("contracts");
d
}
#[derive(Debug, Deserialize)]
pub struct Contract {
pub detector_id: String,
#[serde(default)]
pub positive: Vec<Positive>,
}
#[derive(Debug, Deserialize)]
pub struct Positive {
pub text: String,
pub credential: String,
#[serde(default)]
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct Canonical {
pub detector_id: String,
pub credential: String,
pub canonical_text: String,
}
pub fn load_canonicals() -> Vec<Canonical> {
let dir = contracts_dir();
let mut out = Vec::new();
let entries = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read tests/contracts dir {}: {e}", dir.display()));
for entry in entries {
let path = entry.expect("contracts dir entry readable").path();
if path.is_dir() {
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read contract {}: {e}", path.display()));
let contract = toml::from_str::<Contract>(&text)
.unwrap_or_else(|e| panic!("parse contract {}: {e}", path.display()));
if let Some(pos) = contract.positive.first() {
out.push(Canonical {
detector_id: contract.detector_id,
credential: pos.credential.clone(),
canonical_text: pos.text.clone(),
});
}
}
assert!(
!out.is_empty(),
"tests/contracts/ yielded no canonical positives, the capability suite has nothing to drive"
);
out.sort_by(|a, b| a.detector_id.cmp(&b.detector_id));
out
}
pub fn scanner() -> &'static CompiledScanner {
static SCANNER: OnceLock<CompiledScanner> = OnceLock::new();
SCANNER.get_or_init(|| {
let detectors = keyhog_core::load_detectors(&detector_dir()).expect("detectors/ must load");
let mut config = ScannerConfig::default();
config.unicode_normalization = true;
config.min_confidence = 0.0;
config.penalize_test_paths = false;
CompiledScanner::compile(detectors)
.expect("on-disk corpus must compile into one scanner")
.with_config(config)
})
}
pub fn scan(text: &str, path: &str) -> Vec<RawMatch> {
let chunk = Chunk {
data: text.into(),
metadata: ChunkMetadata {
source_type: "capability-target-spec".into(),
path: Some(path.into()),
base_offset: 0,
..Default::default()
},
};
scanner().clear_fragment_cache();
scanner()
.scan(&chunk)
.expect("capability target-spec scan should succeed")
}
pub fn surfaces(matches: &[RawMatch], credential: &str) -> bool {
matches
.iter()
.any(|m| m.credential.as_ref().contains(credential))
}
pub fn surfaces_as(matches: &[RawMatch], credential: &str, detector_id: &str) -> bool {
matches.iter().any(|m| {
m.detector_id.as_ref() == detector_id && m.credential.as_ref().contains(credential)
})
}
pub fn sufficient_canonicals(all: &[Canonical]) -> Vec<Canonical> {
all.iter()
.filter(|c| {
let m = scan(&c.credential, "sufficiency-probe.txt");
surfaces(&m, &c.credential)
})
.cloned()
.collect()
}
pub fn join_capped(failures: &[String], cap: usize) -> String {
if failures.len() <= cap {
failures.join("\n - ")
} else {
let head = failures[..cap].join("\n - ");
format!("{head}\n - … and {} more", failures.len() - cap)
}
}