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::env;
12use std::sync::RwLock;
13
14/// Platform-specific separator used when joining MATLAB path entries.
15pub const PATH_LIST_SEPARATOR: char = if cfg!(windows) { ';' } else { ':' };
16
17#[derive(Debug, Clone)]
18struct PathState {
19    /// Current MATLAB path string, excluding the implicit current directory.
20    current: String,
21}
22
23impl PathState {
24    fn initialise() -> Self {
25        Self {
26            current: initial_path_string(),
27        }
28    }
29}
30
31fn initial_path_string() -> String {
32    let mut parts = Vec::<String>::new();
33    for var in ["RUNMAT_PATH", "MATLABPATH"] {
34        if let Ok(value) = env::var(var) {
35            parts.extend(
36                value
37                    .split(PATH_LIST_SEPARATOR)
38                    .map(|part| part.trim())
39                    .filter(|part| !part.is_empty())
40                    .map(|part| part.to_string()),
41            );
42        }
43    }
44    join_parts(&parts)
45}
46
47fn join_parts(parts: &[String]) -> String {
48    let mut joined = String::new();
49    for (idx, part) in parts.iter().enumerate() {
50        if idx > 0 {
51            joined.push(PATH_LIST_SEPARATOR);
52        }
53        joined.push_str(part);
54    }
55    joined
56}
57
58static PATH_STATE: Lazy<RwLock<PathState>> = Lazy::new(|| RwLock::new(PathState::initialise()));
59
60/// Return the current MATLAB path string (without the implicit current
61/// directory entry).
62pub fn current_path_string() -> String {
63    PATH_STATE
64        .read()
65        .map(|guard| guard.current.clone())
66        .unwrap_or_else(|poison| poison.into_inner().current.clone())
67}
68
69/// Replace the MATLAB path string for the current session. When `new_path` is
70/// empty the session path becomes empty and the `RUNMAT_PATH` environment
71/// variable is removed.
72pub fn set_path_string(new_path: &str) {
73    if new_path.is_empty() {
74        env::remove_var("RUNMAT_PATH");
75    } else {
76        env::set_var("RUNMAT_PATH", new_path);
77    }
78
79    let mut guard = PATH_STATE
80        .write()
81        .unwrap_or_else(|poison| poison.into_inner());
82    guard.current = new_path.to_string();
83}
84
85/// Split the current MATLAB path string into individual entries, omitting
86/// empty segments and trimming surrounding whitespace.
87pub fn current_path_segments() -> Vec<String> {
88    let path = current_path_string();
89    split_segments(&path)
90}
91
92fn split_segments(path: &str) -> Vec<String> {
93    path.split(PATH_LIST_SEPARATOR)
94        .map(|part| part.trim())
95        .filter(|part| !part.is_empty())
96        .map(|part| part.to_string())
97        .collect()
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn join_and_split_round_trip() {
106        let parts = vec!["/tmp/a".to_string(), "/tmp/b".to_string()];
107        let joined = join_parts(&parts);
108        assert_eq!(split_segments(&joined), parts);
109    }
110}