mabi_opcua/modeling/
cache.rs1use 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}