Skip to main content

mabi_opcua/modeling/
cache.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6
7use crate::error::{OpcUaError, OpcUaResult};
8
9use super::{CompiledOpcUaSession, ImportedNodeSet, NodeSetSource, OpcUaSimulatorConfig};
10
11#[derive(Debug, Clone, serde::Serialize)]
12pub struct CompilationCacheReport {
13    pub compilation_hit: bool,
14    pub import_hits: usize,
15    pub import_misses: usize,
16    pub cache_dir: String,
17}
18
19#[derive(Debug, Default, Clone, Copy)]
20pub(crate) struct ImportCacheCounters {
21    pub(crate) hits: usize,
22    pub(crate) misses: usize,
23}
24
25impl ImportCacheCounters {
26    pub(crate) fn record_hit(&mut self) {
27        self.hits += 1;
28    }
29
30    pub(crate) fn record_miss(&mut self) {
31        self.misses += 1;
32    }
33}
34
35pub(crate) struct ModelingCache {
36    root: PathBuf,
37}
38
39impl ModelingCache {
40    pub(crate) fn new() -> Self {
41        Self {
42            root: default_cache_root(),
43        }
44    }
45
46    pub(crate) fn root_display(&self) -> String {
47        self.root.display().to_string()
48    }
49
50    pub(crate) fn load_imported_nodeset(&self, key: &str) -> Option<ImportedNodeSet> {
51        self.load_json("imports", key)
52    }
53
54    pub(crate) fn save_imported_nodeset(
55        &self,
56        key: &str,
57        imported: &ImportedNodeSet,
58    ) -> OpcUaResult<()> {
59        self.save_json("imports", key, imported)
60    }
61
62    pub(crate) fn load_compiled_session(&self, key: &str) -> Option<CompiledOpcUaSession> {
63        self.load_json("sessions", key)
64    }
65
66    pub(crate) fn save_compiled_session(
67        &self,
68        key: &str,
69        compiled: &CompiledOpcUaSession,
70    ) -> OpcUaResult<()> {
71        self.save_json("sessions", key, compiled)
72    }
73
74    fn load_json<T: DeserializeOwned>(&self, kind: &str, key: &str) -> Option<T> {
75        let path = self.root.join(kind).join(format!("{}.json", key));
76        let content = fs::read_to_string(path).ok()?;
77        serde_json::from_str(&content).ok()
78    }
79
80    fn save_json<T: Serialize>(&self, kind: &str, key: &str, value: &T) -> OpcUaResult<()> {
81        let dir = self.root.join(kind);
82        fs::create_dir_all(&dir)?;
83        let path = dir.join(format!("{}.json", key));
84        let temp_path = path.with_extension("json.tmp");
85        let content = serde_json::to_vec_pretty(value)
86            .map_err(|error| OpcUaError::Config(error.to_string()))?;
87        fs::write(&temp_path, content)?;
88        fs::rename(&temp_path, &path)?;
89        Ok(())
90    }
91}
92
93pub(crate) fn build_import_cache_key(
94    source: &NodeSetSource,
95    base_path: Option<&Path>,
96) -> OpcUaResult<String> {
97    let mut bytes = serde_json::to_vec(source).map_err(|error| {
98        OpcUaError::Config(format!(
99            "failed to serialize NodeSet source for cache key: {}",
100            error
101        ))
102    })?;
103    match source {
104        NodeSetSource::File { path, .. } => {
105            let resolved = resolve_path(base_path, path);
106            let content = fs::read(&resolved).map_err(|error| {
107                OpcUaError::Config(format!(
108                    "failed to read NodeSet '{}' for cache key: {}",
109                    resolved.display(),
110                    error
111                ))
112            })?;
113            bytes.extend_from_slice(&content);
114        }
115        NodeSetSource::Embedded { alias, .. } => {
116            bytes.extend_from_slice(alias.as_bytes());
117        }
118    }
119    Ok(stable_hash_bytes(&bytes))
120}
121
122pub(crate) fn build_compilation_cache_key(
123    config: &OpcUaSimulatorConfig,
124    session_name: &str,
125    base_path: Option<&Path>,
126) -> OpcUaResult<String> {
127    let mut bytes = serde_json::to_vec(config).map_err(|error| {
128        OpcUaError::Config(format!(
129            "failed to serialize OPC UA simulator config for cache key: {}",
130            error
131        ))
132    })?;
133    bytes.extend_from_slice(session_name.as_bytes());
134    for source in config.nodesets.values() {
135        let import_key = build_import_cache_key(source, base_path)?;
136        bytes.extend_from_slice(import_key.as_bytes());
137    }
138    Ok(stable_hash_bytes(&bytes))
139}
140
141fn default_cache_root() -> PathBuf {
142    if let Some(home) = std::env::var_os("HOME") {
143        let home = PathBuf::from(home);
144        if cfg!(target_os = "macos") {
145            return home
146                .join("Library")
147                .join("Caches")
148                .join("mabinogion")
149                .join("mabi-opcua");
150        }
151        return home.join(".cache").join("mabinogion").join("mabi-opcua");
152    }
153
154    std::env::temp_dir().join("mabinogion").join("mabi-opcua")
155}
156
157fn resolve_path(base_path: Option<&Path>, path: &Path) -> PathBuf {
158    if path.is_absolute() {
159        path.to_path_buf()
160    } else if let Some(base_path) = base_path {
161        base_path.parent().unwrap_or(base_path).join(path)
162    } else {
163        path.to_path_buf()
164    }
165}
166
167fn stable_hash_bytes(bytes: &[u8]) -> String {
168    let mut hash = 0xcbf29ce484222325u64;
169    for byte in bytes {
170        hash ^= u64::from(*byte);
171        hash = hash.wrapping_mul(0x100000001b3);
172    }
173    format!("{:016x}", hash)
174}