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