use std::{io::Error, path::PathBuf, str::FromStr}; use just_fmt::fmt_path::fmt_path_str; use crate::globber::constants::{SPLIT_STR, get_base_dir_current}; pub struct Globber { pattern: String, base: PathBuf, names: Vec, } #[allow(dead_code)] impl Globber { pub fn new(pattern: String, base: PathBuf) -> Self { Self { pattern, base, names: Vec::new(), } } pub fn names(&self) -> Vec<&String> { self.names.iter().collect() } pub fn base(&self) -> &PathBuf { &self.base } pub fn into_names(self) -> Vec { self.names } pub fn paths(&self) -> Vec { self.names.iter().map(|n| self.base.join(n)).collect() } pub fn glob(mut self, get_names: F) -> Result where F: Fn(PathBuf) -> Vec, { let full_path = format!("{}{}{}", self.base.display(), SPLIT_STR, self.pattern); let (path, pattern) = if let Some(last_split) = full_path.rfind(SPLIT_STR) { let (path_part, pattern_part) = full_path.split_at(last_split); let mut path = path_part.to_string(); if !path.ends_with(SPLIT_STR) { path.push_str(SPLIT_STR); } let Ok(result) = fmt_path_str(&path) else { return Err(Error::new( std::io::ErrorKind::InvalidInput, format!("Invalid path: \"{}\"", &path), )); }; (result, pattern_part[SPLIT_STR.len()..].to_string()) } else { (String::default(), full_path) }; self.base = match PathBuf::from_str(&path) { Ok(r) => r, Err(_) => { return Err(Error::new( std::io::ErrorKind::InvalidInput, format!("Invalid path: \"{}\"", &path), )); } }; let pattern = if pattern.is_empty() { "*".to_string() } else if pattern == "." { "*".to_string() } else if pattern.ends_with(SPLIT_STR) { format!("{}*", pattern) } else { pattern }; if !pattern.contains('*') && !pattern.contains('?') { self.names = vec![pattern]; return Ok(self); } let mut collected = Vec::new(); collect_files(&path.into(), "./".to_string(), &mut collected, &get_names); fn collect_files( base: &PathBuf, current: String, file_names: &mut Vec, get_names: &F, ) where F: Fn(PathBuf) -> Vec, { let current_path = if current.is_empty() { base.clone() } else { base.join(¤t) }; let items = get_names(current_path); for item in items { match item { GlobItem::File(file_name) => { let relative_path = { fmt_path_str(format!("{}{}{}", current, SPLIT_STR, file_name)) .unwrap_or_default() }; file_names.push(relative_path) } GlobItem::Directory(dir_name) => { let new_current = { fmt_path_str(format!("{}{}{}", current, SPLIT_STR, dir_name)) .unwrap_or_default() }; collect_files(base, new_current, file_names, get_names); } } } } self.names = collected .iter() .filter_map(|name| match_pattern(name, &pattern)) .collect(); Ok(self) } } fn match_pattern(name: &str, pattern: &str) -> Option { if pattern.is_empty() { return None; } let name_chars: Vec = name.chars().collect(); let pattern_chars: Vec = pattern.chars().collect(); let mut name_idx = 0; let mut pattern_idx = 0; let mut star_idx = -1; let mut match_idx = -1; while name_idx < name_chars.len() { if pattern_idx < pattern_chars.len() && (pattern_chars[pattern_idx] == '?' || pattern_chars[pattern_idx] == name_chars[name_idx]) { name_idx += 1; pattern_idx += 1; } else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { star_idx = pattern_idx as i32; match_idx = name_idx as i32; pattern_idx += 1; } else if star_idx != -1 { pattern_idx = (star_idx + 1) as usize; match_idx += 1; name_idx = match_idx as usize; } else { return None; } } while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { pattern_idx += 1; } if pattern_idx == pattern_chars.len() { Some(name.to_string()) } else { None } } impl> From for Globber { fn from(pattern: T) -> Self { let (base_dir, pattern) = get_base_dir_current(pattern.as_ref().to_string()); Self::new(pattern, base_dir) } } #[derive(Debug, Clone, Hash)] pub enum GlobItem { File(String), Directory(String), } impl PartialEq for GlobItem { fn eq(&self, other: &Self) -> bool { match (self, other) { (GlobItem::File(a), GlobItem::File(b)) => a == b, (GlobItem::Directory(a), GlobItem::Directory(b)) => a == b, _ => false, } } } impl std::fmt::Display for GlobItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GlobItem::File(name) => write!(f, "{}", name), GlobItem::Directory(name) => write!(f, "{}", name), } } } impl Eq for GlobItem {} pub mod constants { use std::{env::current_dir, path::PathBuf}; #[cfg(unix)] pub(crate) const CURRENT_DIR_PREFIX: &str = "./"; #[cfg(windows)] pub(crate) const CURRENT_DIR_PREFIX: &str = ".\\"; #[cfg(unix)] pub(crate) const USER_DIR_PREFIX: &str = "~"; #[cfg(windows)] pub(crate) const USER_DIR_PREFIX: &str = "~\\"; #[cfg(unix)] pub(crate) const ROOT_DIR_PREFIX: &str = "/"; #[cfg(windows)] pub(crate) const ROOT_DIR_PREFIX: &str = "\\"; #[cfg(unix)] pub(crate) const SPLIT_STR: &str = "/"; #[cfg(windows)] pub(crate) const SPLIT_STR: &str = "\\"; pub fn get_base_dir_current(input: String) -> (PathBuf, String) { get_base_dir(input, current_dir().unwrap_or_default()) } pub fn get_base_dir(input: String, current_dir: PathBuf) -> (PathBuf, String) { if let Some(remaining) = input.strip_prefix(CURRENT_DIR_PREFIX) { (current_dir, remaining.to_string()) } else if let Some(remaining) = input.strip_prefix(USER_DIR_PREFIX) { (dirs::home_dir().unwrap_or_default(), remaining.to_string()) } else if let Some(remaining) = input.strip_prefix(ROOT_DIR_PREFIX) { { #[cfg(unix)] { (PathBuf::from(ROOT_DIR_PREFIX), remaining.to_string()) } #[cfg(windows)] { let current_drive = current_dir .components() .find_map(|comp| { if let std::path::Component::Prefix(prefix_component) = comp { Some(prefix_component) } else { None } }) .and_then(|prefix_component| match prefix_component.kind() { std::path::Prefix::Disk(drive_letter) | std::path::Prefix::VerbatimDisk(drive_letter) => { Some((drive_letter as char).to_string()) } _ => None, }) .unwrap_or_else(|| "C".to_string()); ( PathBuf::from(format!("{}:{}", current_drive, ROOT_DIR_PREFIX)), remaining.to_string(), ) } } } else { (current_dir, input) } } }