Skip to main content

runmat_runtime/builtins/common/
path_state.rs

1//! Session-wide MATLAB path state shared between the `path` builtin and
2//! filesystem helpers such as `exist` or `which`.
3//!
4//! The MATLAB search path is represented as a single platform-specific string
5//! using the same separator rules that MathWorks MATLAB applies (`;` on
6//! Windows, `:` everywhere else).  RunMat keeps the current working directory
7//! separate from this list so callers can freely replace or manipulate the path
8//! without losing the implicit `pwd` entry that MATLAB always prioritises.
9
10use once_cell::sync::Lazy;
11use std::sync::RwLock;
12
13use crate::builtins::common::env as runtime_env;
14
15/// Platform-specific separator used when joining MATLAB path entries.
16pub const PATH_LIST_SEPARATOR: char = if cfg!(windows) { ';' } else { ':' };
17
18#[derive(Debug, Clone)]
19struct PathState {
20    /// Current MATLAB path string, excluding the implicit current directory.
21    current: String,
22}
23
24impl PathState {
25    fn initialise() -> Self {
26        Self {
27            current: initial_path_string(),
28        }
29    }
30}
31
32fn initial_path_string() -> String {
33    let mut parts = Vec::<String>::new();
34    for var in ["RUNMAT_PATH", "MATLABPATH"] {
35        if let Ok(value) = runtime_env::var(var) {
36            parts.extend(
37                value
38                    .split(PATH_LIST_SEPARATOR)
39                    .map(|part| part.trim())
40                    .filter(|part| !part.is_empty())
41                    .map(|part| part.to_string()),
42            );
43        }
44    }
45    join_parts(&parts)
46}
47
48fn join_parts(parts: &[String]) -> String {
49    let mut joined = String::new();
50    for (idx, part) in parts.iter().enumerate() {
51        if idx > 0 {
52            joined.push(PATH_LIST_SEPARATOR);
53        }
54        joined.push_str(part);
55    }
56    joined
57}
58
59static PATH_STATE: Lazy<RwLock<PathState>> = Lazy::new(|| RwLock::new(PathState::initialise()));
60
61/// Return the current MATLAB path string (without the implicit current
62/// directory entry).
63pub fn current_path_string() -> String {
64    PATH_STATE
65        .read()
66        .map(|guard| guard.current.clone())
67        .unwrap_or_else(|poison| poison.into_inner().current.clone())
68}
69
70pub fn append_to_path(segments: &[String]) {
71    if segments.is_empty() {
72        return;
73    }
74    let mut guard = PATH_STATE
75        .write()
76        .unwrap_or_else(|poison| poison.into_inner());
77    let mut parts = split_segments(&guard.current);
78    parts.extend(segments.iter().cloned());
79    guard.current = join_parts(&parts);
80}
81
82/// Replace the MATLAB path string for the current session. When `new_path` is
83/// empty the session path becomes empty and the `RUNMAT_PATH` environment
84/// variable is removed.
85pub fn set_path_string(new_path: &str) {
86    if new_path.is_empty() {
87        runtime_env::remove_var("RUNMAT_PATH");
88    } else {
89        runtime_env::set_var("RUNMAT_PATH", new_path);
90    }
91
92    let mut guard = PATH_STATE
93        .write()
94        .unwrap_or_else(|poison| poison.into_inner());
95    guard.current = new_path.to_string();
96}
97
98/// Split the current MATLAB path string into individual entries, omitting
99/// empty segments and trimming surrounding whitespace.
100pub fn current_path_segments() -> Vec<String> {
101    let path = current_path_string();
102    split_segments(&path)
103}
104
105fn split_segments(path: &str) -> Vec<String> {
106    path.split(PATH_LIST_SEPARATOR)
107        .map(|part| part.trim())
108        .filter(|part| !part.is_empty())
109        .map(|part| part.to_string())
110        .collect()
111}
112
113#[cfg(test)]
114pub(crate) mod tests {
115    use super::*;
116
117    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
118    #[test]
119    fn join_and_split_round_trip() {
120        let parts = vec!["/tmp/a".to_string(), "/tmp/b".to_string()];
121        let joined = join_parts(&parts);
122        assert_eq!(split_segments(&joined), parts);
123    }
124}