Skip to main content

keyhog_core/
cache_layout.rs

1//! Unified cache layout classification and eviction policy contracts.
2//!
3//! KeyHog produces persistent artifacts across several sub-systems: Hyperscan
4//! pattern shard databases (`hs-*.db`), pre-parsed detector JSON plans
5//! (`detectors-*.json`), compiled GPU literal set programs (`programs/*`),
6//! and persistent matcher artifact graphs (`*.khm`). Inter-process lock files
7//! (`*.lock`) coordinate atomic writes and cache updates.
8//!
9//! This module provides the single canonical owner of cache artifact classification
10//! and registered eviction policies (count limits, byte caps, and lock age bounds).
11
12use serde::{Deserialize, Serialize};
13use std::path::Path;
14
15/// All distinct cache artifact kinds managed by KeyHog.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub enum CacheKind {
19    /// Hyperscan / Vectorscan compiled regex pattern shard databases (`hs-<key>.db`).
20    HyperscanShards,
21    /// Pre-parsed detector corpus JSON plans (`detectors-<key>.json`).
22    DetectorPlans,
23    /// Compiled GPU literal-set binary matchers (`programs/*`).
24    GpuPrograms,
25    /// Eager compiled matcher artifact graphs (`*.khm`).
26    MatcherArtifacts,
27    /// Inter-process write and synchronization lock files (`*.lock`).
28    LockFiles,
29}
30
31impl CacheKind {
32    /// The complete list of registered cache kinds.
33    pub const ALL: &'static [CacheKind] = &[
34        CacheKind::HyperscanShards,
35        CacheKind::DetectorPlans,
36        CacheKind::GpuPrograms,
37        CacheKind::MatcherArtifacts,
38        CacheKind::LockFiles,
39    ];
40
41    /// Operator-facing label for the cache kind.
42    #[must_use]
43    pub const fn label(self) -> &'static str {
44        match self {
45            Self::HyperscanShards => "hyperscan-shards",
46            Self::DetectorPlans => "detector-plans",
47            Self::GpuPrograms => "gpu-programs",
48            Self::MatcherArtifacts => "matcher-artifacts",
49            Self::LockFiles => "lock-files",
50        }
51    }
52
53    /// Check whether a given filename or path matches this cache kind.
54    #[must_use]
55    pub fn matches_path(self, path: &Path) -> bool {
56        Self::classify_path(path) == Some(self)
57    }
58
59    /// Classify a path into its respective `CacheKind` if recognized.
60    #[must_use]
61    pub fn classify_path(path: &Path) -> Option<Self> {
62        let file_name = path.file_name()?.to_str()?;
63        if file_name.starts_with('.') {
64            return None;
65        }
66        if file_name.ends_with(".lock") {
67            let is_package_lock = file_name.eq_ignore_ascii_case("Cargo.lock")
68                || file_name.eq_ignore_ascii_case("flake.lock")
69                || file_name.eq_ignore_ascii_case("yarn.lock")
70                || file_name.eq_ignore_ascii_case("pnpm-lock.yaml")
71                || file_name.eq_ignore_ascii_case("composer.lock")
72                || file_name.eq_ignore_ascii_case("Gemfile.lock")
73                || file_name.eq_ignore_ascii_case("poetry.lock")
74                || file_name.eq_ignore_ascii_case("Pipfile.lock")
75                || file_name.eq_ignore_ascii_case("package.lock")
76                || file_name.eq_ignore_ascii_case("package-lock.json");
77            if !is_package_lock {
78                return Some(Self::LockFiles);
79            }
80            return None;
81        }
82        if file_name.starts_with(crate::hyperscan_cache::HYPERSCAN_CACHE_PREFIX)
83            && file_name.ends_with(crate::hyperscan_cache::HYPERSCAN_CACHE_SUFFIX)
84        {
85            return Some(Self::HyperscanShards);
86        }
87        if file_name.starts_with("detectors-") && file_name.ends_with(".json") {
88            return Some(Self::DetectorPlans);
89        }
90        if file_name.ends_with(crate::MATCHER_ARTIFACT_SUFFIX) {
91            return Some(Self::MatcherArtifacts);
92        }
93        let in_programs_dir = path
94            .parent()
95            .and_then(|p| p.file_name())
96            .and_then(|n| n.to_str())
97            .is_some_and(|n| n == "programs");
98        let is_gpu_matcher_file = file_name.starts_with("gpu-") && file_name.ends_with(".bin");
99        let is_program_binary = in_programs_dir
100            && (file_name.ends_with(".bin")
101                || (file_name.len() == 64 && file_name.chars().all(|c| c.is_ascii_hexdigit()))
102                || file_name.starts_with("gpu-"));
103        if is_gpu_matcher_file || is_program_binary {
104            return Some(Self::GpuPrograms);
105        }
106        None
107    }
108
109    /// Return the canonical default eviction policy for this cache kind.
110    #[must_use]
111    pub const fn default_policy(self) -> CacheEvictionPolicy {
112        match self {
113            Self::HyperscanShards => CacheEvictionPolicy {
114                max_entries: 128,
115                max_bytes: 512 * 1024 * 1024, // 512 MiB
116                max_lock_age_secs: 600,
117            },
118            Self::DetectorPlans => CacheEvictionPolicy {
119                max_entries: 16,
120                max_bytes: 64 * 1024 * 1024, // 64 MiB
121                max_lock_age_secs: 600,
122            },
123            Self::GpuPrograms => CacheEvictionPolicy {
124                max_entries: 64,
125                max_bytes: 128 * 1024 * 1024, // 128 MiB
126                max_lock_age_secs: 600,
127            },
128            Self::MatcherArtifacts => CacheEvictionPolicy {
129                max_entries: 8,
130                max_bytes: 2 * 1024 * 1024 * 1024, // 2 GiB (allows 8 entries at the 256 MiB per-file limit)
131                max_lock_age_secs: 600,
132            },
133            Self::LockFiles => CacheEvictionPolicy {
134                max_entries: 1024,
135                max_bytes: 10 * 1024 * 1024, // 10 MiB
136                max_lock_age_secs: 600,      // 10 minutes
137            },
138        }
139    }
140}
141
142impl std::fmt::Display for CacheKind {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(f, "{}", self.label())
145    }
146}
147
148/// Eviction policy governing count limits, byte caps, and stale lock reclamation.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub struct CacheEvictionPolicy {
151    /// Maximum number of artifact entries retained.
152    pub max_entries: usize,
153    /// Maximum total bytes occupied by artifacts of this kind.
154    pub max_bytes: u64,
155    /// Maximum allowed age for lock files before they are collected as stale.
156    pub max_lock_age_secs: u64,
157}
158
159impl CacheEvictionPolicy {
160    /// Create a custom policy with explicit entries and byte bounds.
161    #[must_use]
162    pub const fn new(max_entries: usize, max_bytes: u64, max_lock_age_secs: u64) -> Self {
163        Self {
164            max_entries,
165            max_bytes,
166            max_lock_age_secs,
167        }
168    }
169}