use crate::DetectorSpec;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectorCorpusMode {
Replace,
Overlay,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DetectorCorpusError {
#[error(
"detector overlay collides with embedded detector id(s): {ids}. \
Overlay mode never shadows shipped detectors; rename the custom detector id(s), \
or select replace mode for a fully custom corpus"
)]
IdCollision {
ids: String,
},
}
pub fn compose_detector_corpus(
mut embedded: Vec<DetectorSpec>,
custom: Vec<DetectorSpec>,
mode: DetectorCorpusMode,
) -> Result<Vec<DetectorSpec>, DetectorCorpusError> {
if mode == DetectorCorpusMode::Replace {
return Ok(custom);
}
let embedded_ids: BTreeSet<&str> = embedded
.iter()
.map(|detector| detector.id.as_str())
.collect();
let collisions: BTreeSet<&str> = custom
.iter()
.map(|detector| detector.id.as_str())
.filter(|id| embedded_ids.contains(id))
.collect();
if !collisions.is_empty() {
return Err(DetectorCorpusError::IdCollision {
ids: collisions.into_iter().collect::<Vec<_>>().join(", "),
});
}
embedded.reserve(custom.len());
embedded.extend(custom);
Ok(embedded)
}
pub fn compute_detector_corpus_digest(
detectors: &[DetectorSpec],
) -> Result<[u8; 32], serde_json::Error> {
compute_detector_corpus_digest_for_schema(detectors, crate::DETECTOR_CORPUS_SCHEMA_VERSION)
}
pub fn compute_detector_corpus_digest_for_schema(
detectors: &[DetectorSpec],
schema_version: u32,
) -> Result<[u8; 32], serde_json::Error> {
let mut canonical: Vec<&DetectorSpec> = detectors.iter().collect();
canonical.sort_by(|left, right| left.id.cmp(&right.id));
let encoded = serde_json::to_vec(&canonical)?;
let mut hasher = blake3::Hasher::new();
hasher.update(b"keyhog-effective-detector-corpus-v2\0");
hasher.update(crate::DETECTOR_CORPUS_MANIFEST_FILE.as_bytes());
hasher.update(&[0]);
hasher.update(&schema_version.to_le_bytes());
hasher.update(&encoded);
Ok(*hasher.finalize().as_bytes())
}