keyhog_core/
detector_corpus.rs1use crate::DetectorSpec;
4use std::collections::BTreeSet;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DetectorCorpusMode {
9 Replace,
11 Overlay,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
17pub enum DetectorCorpusError {
18 #[error(
20 "detector overlay collides with embedded detector id(s): {ids}. \
21 Overlay mode never shadows shipped detectors; rename the custom detector id(s), \
22 or select replace mode for a fully custom corpus"
23 )]
24 IdCollision {
25 ids: String,
27 },
28}
29
30pub fn compose_detector_corpus(
37 mut embedded: Vec<DetectorSpec>,
38 custom: Vec<DetectorSpec>,
39 mode: DetectorCorpusMode,
40) -> Result<Vec<DetectorSpec>, DetectorCorpusError> {
41 if mode == DetectorCorpusMode::Replace {
42 return Ok(custom);
43 }
44
45 let embedded_ids: BTreeSet<&str> = embedded
46 .iter()
47 .map(|detector| detector.id.as_str())
48 .collect();
49 let collisions: BTreeSet<&str> = custom
50 .iter()
51 .map(|detector| detector.id.as_str())
52 .filter(|id| embedded_ids.contains(id))
53 .collect();
54 if !collisions.is_empty() {
55 return Err(DetectorCorpusError::IdCollision {
56 ids: collisions.into_iter().collect::<Vec<_>>().join(", "),
57 });
58 }
59
60 embedded.reserve(custom.len());
61 embedded.extend(custom);
62 Ok(embedded)
63}
64pub fn compute_detector_corpus_digest(
70 detectors: &[DetectorSpec],
71) -> Result<[u8; 32], serde_json::Error> {
72 compute_detector_corpus_digest_for_schema(detectors, crate::DETECTOR_CORPUS_SCHEMA_VERSION)
73}
74
75pub fn compute_detector_corpus_digest_for_schema(
85 detectors: &[DetectorSpec],
86 schema_version: u32,
87) -> Result<[u8; 32], serde_json::Error> {
88 let mut canonical: Vec<&DetectorSpec> = detectors.iter().collect();
89 canonical.sort_by(|left, right| left.id.cmp(&right.id));
90 let encoded = serde_json::to_vec(&canonical)?;
91 let mut hasher = blake3::Hasher::new();
92 hasher.update(b"keyhog-effective-detector-corpus-v2\0");
93 hasher.update(crate::DETECTOR_CORPUS_MANIFEST_FILE.as_bytes());
94 hasher.update(&[0]);
95 hasher.update(&schema_version.to_le_bytes());
96 hasher.update(&encoded);
97 Ok(*hasher.finalize().as_bytes())
98}