Skip to main content

runmat_runtime/builtins/io/repl_fs/
tempdir.rs

1//! MATLAB-compatible `tempdir` builtin for RunMat.
2
3use crate::builtins::common::env as runtime_env;
4use std::convert::TryFrom;
5use std::path::Path;
6
7use runmat_builtins::{CharArray, Value};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::spec::{
11    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12    ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::{build_runtime_error, RuntimeError};
15
16const ERR_TOO_MANY_INPUTS: &str = "tempdir: too many input arguments";
17const ERR_UNABLE_TO_DETERMINE: &str =
18    "tempdir: unable to determine temporary directory (OS returned empty path)";
19
20#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::tempdir")]
21pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
22    name: "tempdir",
23    op_kind: GpuOpKind::Custom("io"),
24    supported_precisions: &[],
25    broadcast: BroadcastSemantics::None,
26    provider_hooks: &[],
27    constant_strategy: ConstantStrategy::InlineLiteral,
28    residency: ResidencyPolicy::GatherImmediately,
29    nan_mode: ReductionNaN::Include,
30    two_pass_threshold: None,
31    workgroup_size: None,
32    accepts_nan_mode: false,
33    notes: "Host-only operation that queries the environment for the temporary folder. No provider hooks are required.",
34};
35
36#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::tempdir")]
37pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
38    name: "tempdir",
39    shape: ShapeRequirements::Any,
40    constant_strategy: ConstantStrategy::InlineLiteral,
41    elementwise: None,
42    reduction: None,
43    emits_nan: false,
44    notes: "I/O builtin that always executes on the host; fusion metadata is present for introspection completeness.",
45};
46
47const BUILTIN_NAME: &str = "tempdir";
48
49fn tempdir_error(message: impl Into<String>) -> RuntimeError {
50    build_runtime_error(message)
51        .with_builtin(BUILTIN_NAME)
52        .build()
53}
54
55#[runtime_builtin(
56    name = "tempdir",
57    category = "io/repl_fs",
58    summary = "Return the absolute path to the system temporary folder.",
59    keywords = "tempdir,temporary folder,temp directory,system temp",
60    accel = "cpu",
61    type_resolver(crate::builtins::io::type_resolvers::tempdir_type),
62    builtin_path = "crate::builtins::io::repl_fs::tempdir"
63)]
64async fn tempdir_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
65    if !args.is_empty() {
66        return Err(tempdir_error(ERR_TOO_MANY_INPUTS));
67    }
68    let path = runtime_env::temp_dir();
69    if path.as_os_str().is_empty() {
70        return Err(tempdir_error(ERR_UNABLE_TO_DETERMINE));
71    }
72    let value = path_to_char_array(&path);
73    if let Ok(text) = String::try_from(&value) {
74        if text.is_empty() {
75            return Err(tempdir_error(ERR_UNABLE_TO_DETERMINE));
76        }
77    }
78    Ok(value)
79}
80
81fn path_to_char_array(path: &Path) -> Value {
82    let mut text = path.to_string_lossy().into_owned();
83    if !text.is_empty() && !ends_with_separator(&text) {
84        text.push(std::path::MAIN_SEPARATOR);
85    }
86    Value::CharArray(CharArray::new_row(&text))
87}
88
89fn ends_with_separator(text: &str) -> bool {
90    let sep = std::path::MAIN_SEPARATOR;
91    text.chars()
92        .next_back()
93        .is_some_and(|ch| ch == sep || (cfg!(windows) && (ch == '/' || ch == '\\')))
94}
95
96#[cfg(test)]
97pub(crate) mod tests {
98    use super::*;
99    use crate::BuiltinResult;
100    use std::convert::TryFrom;
101    use std::path::Path;
102
103    fn tempdir_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
104        futures::executor::block_on(super::tempdir_builtin(args))
105    }
106
107    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
108    #[test]
109    fn tempdir_points_to_existing_directory() {
110        let value = tempdir_builtin(Vec::new()).expect("tempdir");
111        let path_string = String::try_from(&value).expect("convert to string");
112        let _path = Path::new(&path_string);
113    }
114
115    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
116    #[test]
117    fn tempdir_returns_char_array_row_vector() {
118        let value = tempdir_builtin(Vec::new()).expect("tempdir");
119        match value {
120            Value::CharArray(CharArray { rows, cols, .. }) => {
121                assert_eq!(rows, 1);
122                assert!(
123                    cols >= 1,
124                    "expected tempdir to return at least one character"
125                );
126            }
127            other => panic!("expected CharArray result, got {other:?}"),
128        }
129    }
130
131    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
132    #[test]
133    fn tempdir_appends_trailing_separator() {
134        let value = tempdir_builtin(Vec::new()).expect("tempdir");
135        let path_string = String::try_from(&value).expect("convert to string");
136        let expected_sep = std::path::MAIN_SEPARATOR;
137        let last = path_string
138            .chars()
139            .last()
140            .expect("tempdir should return non-empty path");
141        if cfg!(windows) {
142            assert!(
143                last == '/' || last == '\\',
144                "expected trailing separator, got {:?}",
145                path_string
146            );
147        } else {
148            assert_eq!(
149                last, expected_sep,
150                "expected trailing separator {}, got {}",
151                expected_sep, path_string
152            );
153        }
154    }
155
156    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
157    #[test]
158    fn tempdir_returns_consistent_result() {
159        let first = tempdir_builtin(Vec::new()).expect("tempdir");
160        let second = tempdir_builtin(Vec::new()).expect("tempdir");
161        let first_str = String::try_from(&first).expect("first string");
162        let second_str = String::try_from(&second).expect("second string");
163        assert_eq!(
164            first_str, second_str,
165            "tempdir should be stable within a process"
166        );
167    }
168
169    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
170    #[test]
171    fn tempdir_errors_when_arguments_provided() {
172        let err = tempdir_builtin(vec![Value::Num(1.0)]).expect_err("expected error");
173        assert_eq!(err.message(), ERR_TOO_MANY_INPUTS);
174    }
175
176    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
177    #[test]
178    fn path_to_char_array_appends_separator_when_missing() {
179        let path = Path::new("runmat_tempdir_unit_path");
180        let value = path_to_char_array(path);
181        let text = String::try_from(&value).expect("string conversion");
182        assert!(
183            text.ends_with(std::path::MAIN_SEPARATOR),
184            "expected trailing separator in {text:?}"
185        );
186        let trimmed = text.trim_end_matches(std::path::MAIN_SEPARATOR);
187        assert_eq!(trimmed, path.to_string_lossy());
188    }
189
190    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
191    #[test]
192    fn path_to_char_array_preserves_existing_separator() {
193        let sep = std::path::MAIN_SEPARATOR;
194        let input = format!("runmat_tempdir_existing{sep}");
195        let path = Path::new(&input);
196        let value = path_to_char_array(path);
197        let text = String::try_from(&value).expect("string conversion");
198        assert_eq!(text, input);
199    }
200
201    #[cfg(windows)]
202    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
203    #[test]
204    fn ends_with_separator_accepts_forward_slash() {
205        assert!(ends_with_separator("C:/Temp/"));
206        assert!(ends_with_separator("temp/"));
207    }
208}