Skip to main content

archiver_engine/
policy.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use archiver_core::registry::SampleMode;
6use archiver_core::types::ArchDbType;
7
8/// Per-PV archiving policy.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PvPolicy {
11    /// The PV name pattern (supports glob).
12    pub pv: String,
13    /// Stable identifier persisted onto the matched PV's typeinfo so
14    /// audit / metrics paths know which policy governed the archive
15    /// (Java parity b30f1a6). Defaults to the pattern when omitted.
16    #[serde(default)]
17    pub name: Option<String>,
18    /// Sampling mode.
19    #[serde(default = "default_sample_mode")]
20    pub sample_mode: PolicySampleMode,
21    /// Expected DBR type (auto-detected if None).
22    pub dbr_type: Option<ArchDbType>,
23    /// Sampling period in seconds (only for Scan mode).
24    pub sampling_period: Option<f64>,
25}
26
27impl PvPolicy {
28    /// Return the policy's stable name, falling back to the pattern.
29    pub fn policy_name(&self) -> &str {
30        self.name.as_deref().unwrap_or(&self.pv)
31    }
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum PolicySampleMode {
37    #[default]
38    Monitor,
39    Scan,
40}
41
42fn default_sample_mode() -> PolicySampleMode {
43    PolicySampleMode::Monitor
44}
45
46impl PvPolicy {
47    pub fn to_sample_mode(&self) -> SampleMode {
48        match self.sample_mode {
49            PolicySampleMode::Monitor => SampleMode::Monitor,
50            PolicySampleMode::Scan => SampleMode::Scan {
51                period_secs: self.sampling_period.unwrap_or(1.0),
52            },
53        }
54    }
55}
56
57/// Collection of PV policies loaded from a TOML file.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct PolicyConfig {
60    #[serde(rename = "policy")]
61    pub policies: Vec<PvPolicy>,
62}
63
64impl PolicyConfig {
65    pub fn load(path: &Path) -> anyhow::Result<Self> {
66        let content = std::fs::read_to_string(path)?;
67        let config: PolicyConfig = toml::from_str(&content)?;
68        Ok(config)
69    }
70
71    /// Find the best matching policy for a PV name.
72    pub fn find_policy(&self, pv_name: &str) -> Option<&PvPolicy> {
73        // Exact match first.
74        if let Some(p) = self.policies.iter().find(|p| p.pv == pv_name) {
75            return Some(p);
76        }
77        // Glob match.
78        self.policies.iter().find(|p| glob_match(&p.pv, pv_name))
79    }
80}
81
82/// Iterative glob matching (supports `*` and `?`).
83/// Uses a two-pointer backtracking approach — O(n*m) worst-case, no recursion.
84fn glob_match(pattern: &str, text: &str) -> bool {
85    let p: Vec<char> = pattern.chars().collect();
86    let t: Vec<char> = text.chars().collect();
87    let (mut pi, mut ti) = (0usize, 0usize);
88    let (mut star_pi, mut star_ti) = (usize::MAX, 0usize);
89
90    while ti < t.len() {
91        if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
92            pi += 1;
93            ti += 1;
94        } else if pi < p.len() && p[pi] == '*' {
95            star_pi = pi;
96            star_ti = ti;
97            pi += 1;
98        } else if star_pi != usize::MAX {
99            pi = star_pi + 1;
100            star_ti += 1;
101            ti = star_ti;
102        } else {
103            return false;
104        }
105    }
106
107    while pi < p.len() && p[pi] == '*' {
108        pi += 1;
109    }
110
111    pi == p.len()
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_glob_match() {
120        assert!(glob_match("SIM:*", "SIM:Sine"));
121        assert!(glob_match("SIM:*", "SIM:Cosine:Value"));
122        assert!(!glob_match("SIM:*", "OTHER:Sine"));
123        assert!(glob_match("SIM:?ine", "SIM:Sine"));
124        assert!(glob_match("*", "anything"));
125        assert!(glob_match("", ""));
126        assert!(!glob_match("", "x"));
127        assert!(!glob_match("x", ""));
128        assert!(glob_match("**", "abc"));
129        assert!(glob_match("a*b*c", "aXXbYYc"));
130        assert!(!glob_match("a*b*c", "aXXbYY"));
131    }
132
133    #[test]
134    fn test_glob_no_exponential_backtracking() {
135        // Pattern that would cause exponential backtracking in a naive recursive impl.
136        let pattern = "*a*a*a*a*b";
137        let text = "aaaaaaaaaaaaaaaaaaaaaaaa"; // no 'b' → must fail fast
138        assert!(!glob_match(pattern, text));
139    }
140}