runmat_runtime/builtins/common/
path_state.rs1use once_cell::sync::Lazy;
14use std::sync::{
15 atomic::{AtomicU64, Ordering},
16 Arc, RwLock,
17};
18
19use crate::builtins::common::env as runtime_env;
20
21pub const PATH_LIST_SEPARATOR: char = if cfg!(windows) { ';' } else { ':' };
23
24#[derive(Debug, Clone)]
25struct PathState {
26 current: String,
28}
29
30impl PathState {
31 fn initialise() -> Self {
32 Self {
33 current: initial_path_string(),
34 }
35 }
36}
37
38#[derive(Debug)]
41pub struct SearchPath {
42 current: RwLock<String>,
43 generation: AtomicU64,
44}
45
46impl SearchPath {
47 pub fn new(current: String) -> Self {
48 Self {
49 current: RwLock::new(current),
50 generation: AtomicU64::new(0),
51 }
52 }
53
54 pub fn current_string(&self) -> String {
55 self.current
56 .read()
57 .map(|guard| guard.clone())
58 .unwrap_or_else(|poison| poison.into_inner().clone())
59 }
60
61 pub fn generation(&self) -> u64 {
62 self.generation.load(Ordering::Acquire)
63 }
64
65 fn replace(&self, new_path: &str) {
66 let mut guard = self
67 .current
68 .write()
69 .unwrap_or_else(|poison| poison.into_inner());
70 if *guard != new_path {
71 *guard = new_path.to_string();
72 self.generation.fetch_add(1, Ordering::AcqRel);
73 }
74 }
75
76 fn append(&self, segments: &[String]) {
77 if segments.is_empty() {
78 return;
79 }
80 let mut guard = self
81 .current
82 .write()
83 .unwrap_or_else(|poison| poison.into_inner());
84 let mut parts = split_segments(&guard);
85 parts.extend(segments.iter().cloned());
86 let next = join_parts(&parts);
87 if *guard != next {
88 *guard = next;
89 self.generation.fetch_add(1, Ordering::AcqRel);
90 }
91 }
92}
93
94fn initial_path_string() -> String {
95 let mut parts = Vec::<String>::new();
96 for var in ["RUNMAT_PATH", "MATLABPATH"] {
97 if let Ok(value) = runtime_env::var(var) {
98 parts.extend(
99 value
100 .split(PATH_LIST_SEPARATOR)
101 .map(|part| part.trim())
102 .filter(|part| !part.is_empty())
103 .map(|part| part.to_string()),
104 );
105 }
106 }
107 join_parts(&parts)
108}
109
110fn join_parts(parts: &[String]) -> String {
111 let mut joined = String::new();
112 for (idx, part) in parts.iter().enumerate() {
113 if idx > 0 {
114 joined.push(PATH_LIST_SEPARATOR);
115 }
116 joined.push_str(part);
117 }
118 joined
119}
120
121static PATH_STATE: Lazy<RwLock<PathState>> = Lazy::new(|| RwLock::new(PathState::initialise()));
122
123pub fn current_path_string() -> String {
126 if let Some(search_path) = active_search_path() {
127 return search_path.current_string();
128 }
129 PATH_STATE
130 .read()
131 .map(|guard| guard.current.clone())
132 .unwrap_or_else(|poison| poison.into_inner().current.clone())
133}
134
135pub fn append_to_path(segments: &[String]) {
136 if let Some(search_path) = active_search_path() {
137 search_path.append(segments);
138 return;
139 }
140 if segments.is_empty() {
141 return;
142 }
143 let mut guard = PATH_STATE
144 .write()
145 .unwrap_or_else(|poison| poison.into_inner());
146 let mut parts = split_segments(&guard.current);
147 parts.extend(segments.iter().cloned());
148 guard.current = join_parts(&parts);
149}
150
151pub fn set_path_string(new_path: &str) {
155 if let Some(search_path) = active_search_path() {
156 search_path.replace(new_path);
157 return;
158 }
159 if new_path.is_empty() {
160 runtime_env::remove_var("RUNMAT_PATH");
161 } else {
162 runtime_env::set_var("RUNMAT_PATH", new_path);
163 }
164
165 let mut guard = PATH_STATE
166 .write()
167 .unwrap_or_else(|poison| poison.into_inner());
168 guard.current = new_path.to_string();
169}
170
171fn active_search_path() -> Option<Arc<SearchPath>> {
172 crate::user_functions::active_runtime_context().map(|context| Arc::clone(context.search_path()))
173}
174
175pub fn current_path_segments() -> Vec<String> {
178 let path = current_path_string();
179 split_segments(&path)
180}
181
182fn split_segments(path: &str) -> Vec<String> {
183 path.split(PATH_LIST_SEPARATOR)
184 .map(|part| part.trim())
185 .filter(|part| !part.is_empty())
186 .map(|part| part.to_string())
187 .collect()
188}
189
190#[cfg(test)]
191pub(crate) mod tests {
192 use super::*;
193
194 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
195 #[test]
196 fn join_and_split_round_trip() {
197 let parts = vec!["/tmp/a".to_string(), "/tmp/b".to_string()];
198 let joined = join_parts(&parts);
199 assert_eq!(split_segments(&joined), parts);
200 }
201
202 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
203 #[test]
204 fn active_runtime_context_owns_path_and_generation() {
205 let first = Arc::new(SearchPath::new("first".to_string()));
206 let _first_context = crate::user_functions::install_runtime_context(Arc::new(
207 crate::user_functions::RuntimeContext::new(Arc::clone(&first)),
208 ));
209 assert_eq!(current_path_string(), "first");
210 set_path_string("first");
211 assert_eq!(first.generation(), 0, "no-op replacement is not a mutation");
212 let updated = format!("first{PATH_LIST_SEPARATOR}added");
213 set_path_string(&updated);
214 assert_eq!(current_path_string(), updated);
215 assert_eq!(first.generation(), 1);
216
217 let second = Arc::new(SearchPath::new("second".to_string()));
218 {
219 let _second_context = crate::user_functions::install_runtime_context(Arc::new(
220 crate::user_functions::RuntimeContext::new(second),
221 ));
222 assert_eq!(current_path_string(), "second");
223 }
224 assert_eq!(current_path_string(), updated);
225 }
226}