Skip to main content

codelens_engine/memory/
policy.rs

1use std::path::Path;
2
3use serde::Deserialize;
4
5use super::now_secs;
6
7/// Policy controlling visibility and mutability of memory entries.
8///
9/// Stored as TOML at `.codelens/memories/.policy`.
10#[derive(Debug, Clone, Default, Deserialize)]
11pub struct MemoryPolicy {
12    /// Glob patterns for read-only entries.
13    #[serde(default)]
14    pub read_only: Vec<String>,
15    /// Glob patterns for ignored entries.
16    #[serde(default)]
17    pub ignored: Vec<String>,
18    /// Maximum age in days before an entry is considered stale.
19    #[serde(default)]
20    pub max_age_days: Option<u64>,
21}
22
23pub(crate) const POLICY_FILENAME: &str = "__policy__";
24pub(crate) const POLICY_FILE_BASENAME: &str = ".policy";
25pub(crate) const ARCHIVE_DIRNAME: &str = ".archive";
26
27impl MemoryPolicy {
28    /// Load policy from the `.policy` file inside the memories directory.
29    pub fn load(memories_dir: &Path) -> Self {
30        let policy_path = memories_dir.join(POLICY_FILE_BASENAME);
31        if !policy_path.is_file() {
32            return Self::default();
33        }
34        let content = match std::fs::read_to_string(&policy_path) {
35            Ok(c) => c,
36            Err(_) => return Self::default(),
37        };
38        Self::parse(&content)
39    }
40
41    /// Parse a TOML policy file.
42    pub(crate) fn parse(content: &str) -> Self {
43        toml::from_str(content).unwrap_or_default()
44    }
45
46    pub fn is_read_only(&self, name: &str) -> bool {
47        matches_any_pattern(name, &self.read_only)
48    }
49
50    pub fn is_ignored(&self, name: &str) -> bool {
51        matches_any_pattern(name, &self.ignored)
52    }
53
54    pub fn is_stale(&self, _name: &str, modified_secs: u64) -> bool {
55        let Some(max_days) = self.max_age_days else {
56            return false;
57        };
58        let age_secs = now_secs().saturating_sub(modified_secs);
59        age_secs > max_days.saturating_mul(86400)
60    }
61}
62
63fn matches_any_pattern(name: &str, patterns: &[String]) -> bool {
64    patterns.iter().any(|pattern| glob_match(pattern, name))
65}
66
67pub(crate) fn glob_match(pattern: &str, name: &str) -> bool {
68    let pat_bytes = pattern.as_bytes();
69    let name_bytes = name.as_bytes();
70    let mut pi = 0usize;
71    let mut ni = 0usize;
72    let mut star_pi = usize::MAX;
73    let mut star_ni = usize::MAX;
74
75    while ni < name_bytes.len() {
76        if pi < pat_bytes.len() {
77            let pc = pat_bytes[pi];
78            if pc == b'*' {
79                star_pi = pi;
80                star_ni = ni;
81                pi += 1;
82                continue;
83            }
84            if pc == b'?' || pc == name_bytes[ni] {
85                pi += 1;
86                ni += 1;
87                continue;
88            }
89        }
90        if star_pi != usize::MAX {
91            pi = star_pi + 1;
92            star_ni += 1;
93            ni = star_ni;
94            continue;
95        }
96        return false;
97    }
98
99    while pi < pat_bytes.len() && pat_bytes[pi] == b'*' {
100        pi += 1;
101    }
102    pi == pat_bytes.len()
103}