runmat_runtime/builtins/common/
path_search.rs1use runmat_filesystem as vfs;
10use std::collections::HashSet;
11use std::ffi::OsString;
12use std::path::{Path, PathBuf};
13
14use super::fs::expand_user_path;
15use super::path_state::current_path_segments;
16
17pub const MEX_EXTENSIONS: &[&str] = &[
19 ".mexw64",
20 ".mexmaci64",
21 ".mexa64",
22 ".mexglx",
23 ".mexw32",
24 ".mexmaci",
25 ".mex",
26];
27
28pub const PCODE_EXTENSIONS: &[&str] = &[".p", ".pp"];
30
31pub const SIMULINK_EXTENSIONS: &[&str] = &[".slx", ".mdl"];
33
34pub const THUNK_EXTENSIONS: &[&str] = &[".thunk"];
36
37pub const LIB_EXTENSIONS: &[&str] = &[".dll", ".so", ".dylib", ".lib", ".a"];
39
40pub const CLASS_M_FILE_EXTENSIONS: &[&str] = &[".m"];
43
44pub const GENERAL_FILE_EXTENSIONS: &[&str] = &[
47 ".m",
48 ".mlx",
49 ".mlapp",
50 ".mltbx",
51 ".mlappinstall",
52 ".mat",
53 ".fig",
54 ".txt",
55 ".csv",
56 ".json",
57 ".xml",
58 ".dat",
59 ".bin",
60 ".h",
61 ".hpp",
62 ".c",
63 ".cc",
64 ".cpp",
65 ".cxx",
66 ".py",
67 ".sh",
68 ".bat",
69 "",
70];
71
72pub const KNOWN_FILE_EXTENSIONS: &[&str] = &[
75 "m",
76 "mlx",
77 "mlapp",
78 "mat",
79 "mex",
80 "mexw64",
81 "mexmaci64",
82 "mexa64",
83 "mexglx",
84 "mexw32",
85 "mexmaci",
86 "p",
87 "pp",
88 "slx",
89 "mdl",
90 "mltbx",
91 "mlappinstall",
92 "fig",
93 "txt",
94 "csv",
95 "json",
96 "xml",
97 "dat",
98 "bin",
99 "dll",
100 "so",
101 "dylib",
102 "lib",
103 "a",
104 "thunk",
105 "h",
106 "hpp",
107 "c",
108 "cc",
109 "cpp",
110 "cxx",
111 "py",
112 "sh",
113 "bat",
114];
115
116pub fn search_directories(error_prefix: &str) -> Result<Vec<PathBuf>, String> {
121 let mut dirs = Vec::new();
122 let mut seen = HashSet::new();
123
124 if let Ok(cwd) = vfs::current_dir() {
125 push_unique_dir(&mut dirs, &mut seen, cwd);
126 } else {
127 return Err(format!(
128 "{error_prefix}: unable to determine current directory"
129 ));
130 }
131
132 for entry in current_path_segments() {
133 let expanded = expand_user_path(&entry, error_prefix)?;
134 push_unique_dir(&mut dirs, &mut seen, PathBuf::from(expanded));
135 }
136
137 Ok(dirs)
138}
139
140pub fn split_package_components(name: &str) -> (Vec<String>, String) {
143 if name.is_empty() {
144 return (Vec::new(), String::new());
145 }
146 let mut parts: Vec<&str> = name.split('.').collect();
147 if parts.len() == 1 {
148 return (Vec::new(), parts[0].to_string());
149 }
150 let base = parts.pop().unwrap_or_default().to_string();
151 let packages = parts.into_iter().map(|p| p.to_string()).collect();
152 (packages, base)
153}
154
155pub fn packages_to_path(packages: &[String]) -> PathBuf {
157 let mut path = PathBuf::new();
158 for pkg in packages {
159 path.push(format!("+{}", pkg));
160 }
161 path
162}
163
164pub fn should_treat_as_path(name: &str) -> bool {
167 if name.starts_with('~')
168 || name.starts_with('@')
169 || name.starts_with('+')
170 || name.contains('/')
171 || name.contains('\\')
172 {
173 return true;
174 }
175 if cfg!(windows) && has_windows_drive_prefix(name) {
176 return true;
177 }
178 is_probable_filename(name)
179}
180
181fn has_windows_drive_prefix(name: &str) -> bool {
182 let bytes = name.as_bytes();
183 if bytes.len() < 2 {
184 return false;
185 }
186 bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
187}
188
189fn is_probable_filename(name: &str) -> bool {
190 if let Some(dot) = name.rfind('.') {
191 let ext = &name[dot + 1..];
192 let lowered = ext.to_ascii_lowercase();
193 KNOWN_FILE_EXTENSIONS.contains(&lowered.as_str())
194 } else {
195 false
196 }
197}
198
199pub fn file_candidates(
202 name: &str,
203 extensions: &[&str],
204 error_prefix: &str,
205) -> Result<Vec<PathBuf>, String> {
206 if should_treat_as_path(name) {
207 collect_direct_file_candidates(name, extensions, error_prefix)
208 } else {
209 collect_package_file_candidates(name, extensions, error_prefix)
210 }
211}
212
213fn collect_direct_file_candidates(
214 name: &str,
215 extensions: &[&str],
216 error_prefix: &str,
217) -> Result<Vec<PathBuf>, String> {
218 let expanded = expand_user_path(name, error_prefix)?;
219 let base = PathBuf::from(&expanded);
220 let mut candidates = vec![base.clone()];
221 if base.extension().is_none() {
222 for &ext in extensions {
223 if ext.is_empty() {
224 continue;
225 }
226 candidates.push(append_extension(&base, ext));
227 }
228 }
229 Ok(candidates)
230}
231
232fn collect_package_file_candidates(
233 name: &str,
234 extensions: &[&str],
235 error_prefix: &str,
236) -> Result<Vec<PathBuf>, String> {
237 let (packages, base_name) = split_package_components(name);
238 let prefix = packages_to_path(&packages);
239 let mut candidates = Vec::new();
240
241 for dir in search_directories(error_prefix)? {
242 let mut root = dir.clone();
243 if !prefix.as_os_str().is_empty() {
244 root.push(&prefix);
245 }
246 let base_path = root.join(&base_name);
247 push_unique(&mut candidates, base_path.clone());
248 for &ext in extensions {
249 if ext.is_empty() {
250 continue;
251 }
252 push_unique(&mut candidates, append_extension(&base_path, ext));
253 }
254 }
255
256 Ok(candidates)
257}
258
259fn append_extension(path: &Path, ext: &str) -> PathBuf {
260 if ext.is_empty() {
261 return path.to_path_buf();
262 }
263 let mut os: OsString = path.as_os_str().to_os_string();
264 os.push(ext);
265 PathBuf::from(os)
266}
267
268pub async fn find_file_with_extensions(
271 name: &str,
272 extensions: &[&str],
273 error_prefix: &str,
274) -> Result<Option<PathBuf>, String> {
275 let candidates = file_candidates(name, extensions, error_prefix)?;
276 for path in candidates {
277 if path_is_file(&path).await {
278 return Ok(Some(path));
279 }
280 }
281 Ok(None)
282}
283
284pub async fn find_all_files_with_extensions(
287 name: &str,
288 extensions: &[&str],
289 error_prefix: &str,
290) -> Result<Vec<PathBuf>, String> {
291 let mut matches = Vec::new();
292 let mut seen = HashSet::new();
293 for path in file_candidates(name, extensions, error_prefix)? {
294 if path_is_file(&path).await && seen.insert(path.clone()) {
295 matches.push(path);
296 }
297 }
298 Ok(matches)
299}
300
301pub fn directory_candidates(name: &str, error_prefix: &str) -> Result<Vec<PathBuf>, String> {
303 if should_treat_as_path(name) {
304 let expanded = expand_user_path(name, error_prefix)?;
305 return Ok(vec![PathBuf::from(expanded)]);
306 }
307 let (packages, base) = split_package_components(name);
308 let prefix = packages_to_path(&packages);
309 let mut candidates = Vec::new();
310 for dir in search_directories(error_prefix)? {
311 let mut path = dir.clone();
312 if !prefix.as_os_str().is_empty() {
313 path.push(&prefix);
314 }
315 path.push(&base);
316 push_unique(&mut candidates, path);
317 }
318 Ok(candidates)
319}
320
321pub fn class_folder_candidates(name: &str, error_prefix: &str) -> Result<Vec<PathBuf>, String> {
323 if should_treat_as_path(name) {
324 let expanded = expand_user_path(name, error_prefix)?;
325 return Ok(vec![PathBuf::from(expanded)]);
326 }
327 let (packages, class_name) = split_package_components(name);
328 let prefix = packages_to_path(&packages);
329 let mut candidates = Vec::new();
330 for dir in search_directories(error_prefix)? {
331 let mut path = dir.clone();
332 if !prefix.as_os_str().is_empty() {
333 path.push(&prefix);
334 }
335 path.push(format!("@{}", class_name));
336 push_unique(&mut candidates, path);
337 }
338 Ok(candidates)
339}
340
341pub async fn class_file_exists(
344 name: &str,
345 class_extensions: &[&str],
346 keyword: &str,
347 error_prefix: &str,
348) -> Result<bool, String> {
349 if let Some(path) = find_file_with_extensions(name, class_extensions, error_prefix).await? {
350 if file_contains_keyword(&path, keyword).await {
351 return Ok(true);
352 }
353 }
354 Ok(false)
355}
356
357pub async fn class_file_paths(
359 name: &str,
360 class_extensions: &[&str],
361 keyword: &str,
362 error_prefix: &str,
363) -> Result<Vec<PathBuf>, String> {
364 let mut matches = Vec::new();
365 for path in find_all_files_with_extensions(name, class_extensions, error_prefix).await? {
366 if file_contains_keyword(&path, keyword).await {
367 matches.push(path);
368 }
369 }
370 Ok(matches)
371}
372
373async fn file_contains_keyword(path: &Path, keyword: &str) -> bool {
374 const MAX_BYTES: usize = 64 * 1024;
375 match vfs::read_async(path).await {
376 Ok(mut buffer) => {
377 if buffer.len() > MAX_BYTES {
378 buffer.truncate(MAX_BYTES);
379 }
380 let text = String::from_utf8_lossy(&buffer);
381 text.to_ascii_lowercase()
382 .contains(&keyword.to_ascii_lowercase())
383 }
384 Err(_) => false,
385 }
386}
387
388fn push_unique<T: Eq + std::hash::Hash + Clone>(vec: &mut Vec<T>, value: T) {
389 if !vec.contains(&value) {
390 vec.push(value);
391 }
392}
393
394fn push_unique_dir(vec: &mut Vec<PathBuf>, seen: &mut HashSet<PathBuf>, value: PathBuf) {
395 if seen.insert(value.clone()) {
396 vec.push(value);
397 }
398}
399
400pub async fn path_is_file(path: &Path) -> bool {
401 vfs::metadata_async(path)
402 .await
403 .map(|meta| meta.is_file())
404 .unwrap_or(false)
405}
406
407pub async fn path_is_directory(path: &Path) -> bool {
408 vfs::metadata_async(path)
409 .await
410 .map(|meta| meta.is_dir())
411 .unwrap_or(false)
412}