use runmat_filesystem as vfs;
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use super::fs::expand_user_path;
use super::path_state::current_path_segments;
pub const MEX_EXTENSIONS: &[&str] = &[
".mexw64",
".mexmaci64",
".mexa64",
".mexglx",
".mexw32",
".mexmaci",
".mex",
];
pub const PCODE_EXTENSIONS: &[&str] = &[".p", ".pp"];
pub const SIMULINK_EXTENSIONS: &[&str] = &[".slx", ".mdl"];
pub const THUNK_EXTENSIONS: &[&str] = &[".thunk"];
pub const LIB_EXTENSIONS: &[&str] = &[".dll", ".so", ".dylib", ".lib", ".a"];
pub const CLASS_M_FILE_EXTENSIONS: &[&str] = &[".m"];
pub const GENERAL_FILE_EXTENSIONS: &[&str] = &[
".m",
".mlx",
".mlapp",
".mltbx",
".mlappinstall",
".mat",
".fig",
".txt",
".csv",
".json",
".xml",
".dat",
".bin",
".h",
".hpp",
".c",
".cc",
".cpp",
".cxx",
".py",
".sh",
".bat",
"",
];
pub const KNOWN_FILE_EXTENSIONS: &[&str] = &[
"m",
"mlx",
"mlapp",
"mat",
"mex",
"mexw64",
"mexmaci64",
"mexa64",
"mexglx",
"mexw32",
"mexmaci",
"p",
"pp",
"slx",
"mdl",
"mltbx",
"mlappinstall",
"fig",
"txt",
"csv",
"json",
"xml",
"dat",
"bin",
"dll",
"so",
"dylib",
"lib",
"a",
"thunk",
"h",
"hpp",
"c",
"cc",
"cpp",
"cxx",
"py",
"sh",
"bat",
];
pub fn search_directories(error_prefix: &str) -> Result<Vec<PathBuf>, String> {
let mut dirs = Vec::new();
let mut seen = HashSet::new();
if let Ok(cwd) = vfs::current_dir() {
push_unique_dir(&mut dirs, &mut seen, cwd);
} else {
return Err(format!(
"{error_prefix}: unable to determine current directory"
));
}
for entry in current_path_segments() {
let expanded = expand_user_path(&entry, error_prefix)?;
push_unique_dir(&mut dirs, &mut seen, PathBuf::from(expanded));
}
Ok(dirs)
}
pub fn split_package_components(name: &str) -> (Vec<String>, String) {
if name.is_empty() {
return (Vec::new(), String::new());
}
let mut parts: Vec<&str> = name.split('.').collect();
if parts.len() == 1 {
return (Vec::new(), parts[0].to_string());
}
let base = parts.pop().unwrap_or_default().to_string();
let packages = parts.into_iter().map(|p| p.to_string()).collect();
(packages, base)
}
pub fn packages_to_path(packages: &[String]) -> PathBuf {
let mut path = PathBuf::new();
for pkg in packages {
path.push(format!("+{}", pkg));
}
path
}
pub fn should_treat_as_path(name: &str) -> bool {
if name.starts_with('~')
|| name.starts_with('@')
|| name.starts_with('+')
|| name.contains('/')
|| name.contains('\\')
{
return true;
}
if cfg!(windows) && has_windows_drive_prefix(name) {
return true;
}
is_probable_filename(name)
}
fn has_windows_drive_prefix(name: &str) -> bool {
let bytes = name.as_bytes();
if bytes.len() < 2 {
return false;
}
bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
fn is_probable_filename(name: &str) -> bool {
if let Some(dot) = name.rfind('.') {
let ext = &name[dot + 1..];
let lowered = ext.to_ascii_lowercase();
KNOWN_FILE_EXTENSIONS.contains(&lowered.as_str())
} else {
false
}
}
pub fn file_candidates(
name: &str,
extensions: &[&str],
error_prefix: &str,
) -> Result<Vec<PathBuf>, String> {
if should_treat_as_path(name) {
collect_direct_file_candidates(name, extensions, error_prefix)
} else {
collect_package_file_candidates(name, extensions, error_prefix)
}
}
fn collect_direct_file_candidates(
name: &str,
extensions: &[&str],
error_prefix: &str,
) -> Result<Vec<PathBuf>, String> {
let expanded = expand_user_path(name, error_prefix)?;
let base = PathBuf::from(&expanded);
let mut candidates = vec![base.clone()];
if base.extension().is_none() {
for &ext in extensions {
if ext.is_empty() {
continue;
}
candidates.push(append_extension(&base, ext));
}
}
Ok(candidates)
}
fn collect_package_file_candidates(
name: &str,
extensions: &[&str],
error_prefix: &str,
) -> Result<Vec<PathBuf>, String> {
let (packages, base_name) = split_package_components(name);
let prefix = packages_to_path(&packages);
let mut candidates = Vec::new();
for dir in search_directories(error_prefix)? {
let mut root = dir.clone();
if !prefix.as_os_str().is_empty() {
root.push(&prefix);
}
let base_path = root.join(&base_name);
push_unique(&mut candidates, base_path.clone());
for &ext in extensions {
if ext.is_empty() {
continue;
}
push_unique(&mut candidates, append_extension(&base_path, ext));
}
}
Ok(candidates)
}
fn append_extension(path: &Path, ext: &str) -> PathBuf {
if ext.is_empty() {
return path.to_path_buf();
}
let mut os: OsString = path.as_os_str().to_os_string();
os.push(ext);
PathBuf::from(os)
}
pub async fn find_file_with_extensions(
name: &str,
extensions: &[&str],
error_prefix: &str,
) -> Result<Option<PathBuf>, String> {
let candidates = file_candidates(name, extensions, error_prefix)?;
for path in candidates {
if path_is_file(&path).await {
return Ok(Some(path));
}
}
Ok(None)
}
pub async fn find_all_files_with_extensions(
name: &str,
extensions: &[&str],
error_prefix: &str,
) -> Result<Vec<PathBuf>, String> {
let mut matches = Vec::new();
let mut seen = HashSet::new();
for path in file_candidates(name, extensions, error_prefix)? {
if path_is_file(&path).await && seen.insert(path.clone()) {
matches.push(path);
}
}
Ok(matches)
}
pub fn directory_candidates(name: &str, error_prefix: &str) -> Result<Vec<PathBuf>, String> {
if should_treat_as_path(name) {
let expanded = expand_user_path(name, error_prefix)?;
return Ok(vec![PathBuf::from(expanded)]);
}
let (packages, base) = split_package_components(name);
let prefix = packages_to_path(&packages);
let mut candidates = Vec::new();
for dir in search_directories(error_prefix)? {
let mut path = dir.clone();
if !prefix.as_os_str().is_empty() {
path.push(&prefix);
}
path.push(&base);
push_unique(&mut candidates, path);
}
Ok(candidates)
}
pub fn class_folder_candidates(name: &str, error_prefix: &str) -> Result<Vec<PathBuf>, String> {
if should_treat_as_path(name) {
let expanded = expand_user_path(name, error_prefix)?;
return Ok(vec![PathBuf::from(expanded)]);
}
let (packages, class_name) = split_package_components(name);
let prefix = packages_to_path(&packages);
let mut candidates = Vec::new();
for dir in search_directories(error_prefix)? {
let mut path = dir.clone();
if !prefix.as_os_str().is_empty() {
path.push(&prefix);
}
path.push(format!("@{}", class_name));
push_unique(&mut candidates, path);
}
Ok(candidates)
}
pub async fn class_file_exists(
name: &str,
class_extensions: &[&str],
keyword: &str,
error_prefix: &str,
) -> Result<bool, String> {
if let Some(path) = find_file_with_extensions(name, class_extensions, error_prefix).await? {
if file_contains_keyword(&path, keyword).await {
return Ok(true);
}
}
Ok(false)
}
pub async fn class_file_paths(
name: &str,
class_extensions: &[&str],
keyword: &str,
error_prefix: &str,
) -> Result<Vec<PathBuf>, String> {
let mut matches = Vec::new();
for path in find_all_files_with_extensions(name, class_extensions, error_prefix).await? {
if file_contains_keyword(&path, keyword).await {
matches.push(path);
}
}
Ok(matches)
}
async fn file_contains_keyword(path: &Path, keyword: &str) -> bool {
const MAX_BYTES: usize = 64 * 1024;
match vfs::read_async(path).await {
Ok(mut buffer) => {
if buffer.len() > MAX_BYTES {
buffer.truncate(MAX_BYTES);
}
let text = String::from_utf8_lossy(&buffer);
text.to_ascii_lowercase()
.contains(&keyword.to_ascii_lowercase())
}
Err(_) => false,
}
}
fn push_unique<T: Eq + std::hash::Hash + Clone>(vec: &mut Vec<T>, value: T) {
if !vec.contains(&value) {
vec.push(value);
}
}
fn push_unique_dir(vec: &mut Vec<PathBuf>, seen: &mut HashSet<PathBuf>, value: PathBuf) {
if seen.insert(value.clone()) {
vec.push(value);
}
}
pub async fn path_is_file(path: &Path) -> bool {
vfs::metadata_async(path)
.await
.map(|meta| meta.is_file())
.unwrap_or(false)
}
pub async fn path_is_directory(path: &Path) -> bool {
vfs::metadata_async(path)
.await
.map(|meta| meta.is_dir())
.unwrap_or(false)
}