diff --git a/modda-cli/src/subcommands/reset.rs b/modda-cli/src/subcommands/reset.rs index cc1172c..30aa35f 100644 --- a/modda-cli/src/subcommands/reset.rs +++ b/modda-cli/src/subcommands/reset.rs @@ -14,7 +14,7 @@ use modda_lib::tp2::find_tp2_str; pub fn reset(args: &Reset, weidu_context: &WeiduContext) -> Result<()> { - let installed = extract_bare_mods()?; + let installed = extract_bare_mods(weidu_context.current_dir)?; let manifest = Manifest::read_path(&args.manifest_path,)?; let reset_index = args.to_index; diff --git a/modda-lib/Cargo.toml b/modda-lib/Cargo.toml index 4474962..9fe6761 100644 --- a/modda-lib/Cargo.toml +++ b/modda-lib/Cargo.toml @@ -47,6 +47,9 @@ tempfile = "3.10.1" url = "2.5.0" zip = "4.3.0" void = "1.0.2" +ignore = "0.4.25" +walkdir = "2.5.0" +globset = "0.4.18" [dev-dependencies] faux = "0.1.10" diff --git a/modda-lib/resources/test/file_lookup/MiXed/Chitin.Key b/modda-lib/resources/test/file_lookup/MiXed/Chitin.Key new file mode 100644 index 0000000..283ca5f Binary files /dev/null and b/modda-lib/resources/test/file_lookup/MiXed/Chitin.Key differ diff --git a/modda-lib/resources/test/file_lookup/MiXed/Mod1/File1.txt b/modda-lib/resources/test/file_lookup/MiXed/Mod1/File1.txt new file mode 100644 index 0000000..01b51e6 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/MiXed/Mod1/File1.txt @@ -0,0 +1 @@ +Some text diff --git a/modda-lib/resources/test/file_lookup/MiXed/WeiDu.Log b/modda-lib/resources/test/file_lookup/MiXed/WeiDu.Log new file mode 100644 index 0000000..5038ec1 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/MiXed/WeiDu.Log @@ -0,0 +1,5 @@ +// Log of Currently Installed WeiDU Mods +// The top of the file is the 'oldest' mod +// ~TP2_File~ #language_number #component_number // [Subcomponent Name -> ] Component Name [ : Version] +// Recently Uninstalled: ~DLCMERGER/DLCMERGER.TP2~ #0 #1 // Merge DLC into game -> Merge "Siege of Dragonspear" DLC: 1.7 +~DLCMERGER/DLCMERGER.TP2~ #0 #1 // Merge DLC into game -> Merge "Siege of Dragonspear" DLC: 1.7 diff --git a/modda-lib/resources/test/file_lookup/MiXed/WeiDu.conf b/modda-lib/resources/test/file_lookup/MiXed/WeiDu.conf new file mode 100644 index 0000000..a2f9ea5 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/MiXed/WeiDu.conf @@ -0,0 +1 @@ +lang_dir = fr_fr diff --git a/modda-lib/resources/test/file_lookup/UPPER/CHITIN.KEY b/modda-lib/resources/test/file_lookup/UPPER/CHITIN.KEY new file mode 100644 index 0000000..283ca5f Binary files /dev/null and b/modda-lib/resources/test/file_lookup/UPPER/CHITIN.KEY differ diff --git a/modda-lib/resources/test/file_lookup/UPPER/MOD1/FILE1.TXT b/modda-lib/resources/test/file_lookup/UPPER/MOD1/FILE1.TXT new file mode 100644 index 0000000..01b51e6 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/UPPER/MOD1/FILE1.TXT @@ -0,0 +1 @@ +Some text diff --git a/modda-lib/resources/test/file_lookup/UPPER/WEIDU.CONF b/modda-lib/resources/test/file_lookup/UPPER/WEIDU.CONF new file mode 100644 index 0000000..a2f9ea5 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/UPPER/WEIDU.CONF @@ -0,0 +1 @@ +lang_dir = fr_fr diff --git a/modda-lib/resources/test/file_lookup/UPPER/WEIDU.LOG b/modda-lib/resources/test/file_lookup/UPPER/WEIDU.LOG new file mode 100644 index 0000000..5038ec1 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/UPPER/WEIDU.LOG @@ -0,0 +1,5 @@ +// Log of Currently Installed WeiDU Mods +// The top of the file is the 'oldest' mod +// ~TP2_File~ #language_number #component_number // [Subcomponent Name -> ] Component Name [ : Version] +// Recently Uninstalled: ~DLCMERGER/DLCMERGER.TP2~ #0 #1 // Merge DLC into game -> Merge "Siege of Dragonspear" DLC: 1.7 +~DLCMERGER/DLCMERGER.TP2~ #0 #1 // Merge DLC into game -> Merge "Siege of Dragonspear" DLC: 1.7 diff --git a/modda-lib/resources/test/file_lookup/lower/chitin.key b/modda-lib/resources/test/file_lookup/lower/chitin.key new file mode 100644 index 0000000..283ca5f Binary files /dev/null and b/modda-lib/resources/test/file_lookup/lower/chitin.key differ diff --git a/modda-lib/resources/test/file_lookup/lower/mod1/file1.txt b/modda-lib/resources/test/file_lookup/lower/mod1/file1.txt new file mode 100644 index 0000000..01b51e6 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/lower/mod1/file1.txt @@ -0,0 +1 @@ +Some text diff --git a/modda-lib/resources/test/file_lookup/lower/weidu.conf b/modda-lib/resources/test/file_lookup/lower/weidu.conf new file mode 100644 index 0000000..a2f9ea5 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/lower/weidu.conf @@ -0,0 +1 @@ +lang_dir = fr_fr diff --git a/modda-lib/resources/test/file_lookup/lower/weidu.log b/modda-lib/resources/test/file_lookup/lower/weidu.log new file mode 100644 index 0000000..5038ec1 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/lower/weidu.log @@ -0,0 +1,5 @@ +// Log of Currently Installed WeiDU Mods +// The top of the file is the 'oldest' mod +// ~TP2_File~ #language_number #component_number // [Subcomponent Name -> ] Component Name [ : Version] +// Recently Uninstalled: ~DLCMERGER/DLCMERGER.TP2~ #0 #1 // Merge DLC into game -> Merge "Siege of Dragonspear" DLC: 1.7 +~DLCMERGER/DLCMERGER.TP2~ #0 #1 // Merge DLC into game -> Merge "Siege of Dragonspear" DLC: 1.7 diff --git a/modda-lib/resources/test/file_lookup/multiple/File2.Txt b/modda-lib/resources/test/file_lookup/multiple/File2.Txt new file mode 100644 index 0000000..01b51e6 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/multiple/File2.Txt @@ -0,0 +1 @@ +Some text diff --git a/modda-lib/resources/test/file_lookup/multiple/File2.txt b/modda-lib/resources/test/file_lookup/multiple/File2.txt new file mode 100644 index 0000000..01b51e6 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/multiple/File2.txt @@ -0,0 +1 @@ +Some text diff --git a/modda-lib/resources/test/file_lookup/multiple/Mod1/File1 b/modda-lib/resources/test/file_lookup/multiple/Mod1/File1 new file mode 100644 index 0000000..24f4f69 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/multiple/Mod1/File1 @@ -0,0 +1 @@ +Some data diff --git a/modda-lib/resources/test/file_lookup/multiple/mod1/file1 b/modda-lib/resources/test/file_lookup/multiple/mod1/file1 new file mode 100644 index 0000000..24f4f69 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/multiple/mod1/file1 @@ -0,0 +1 @@ +Some data diff --git a/modda-lib/resources/test/file_lookup/multiple/mod2/File/data b/modda-lib/resources/test/file_lookup/multiple/mod2/File/data new file mode 100644 index 0000000..24f4f69 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/multiple/mod2/File/data @@ -0,0 +1 @@ +Some data diff --git a/modda-lib/resources/test/file_lookup/multiple/mod2/file b/modda-lib/resources/test/file_lookup/multiple/mod2/file new file mode 100644 index 0000000..4268632 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/multiple/mod2/file @@ -0,0 +1 @@ +some data diff --git a/modda-lib/src/log_parser.rs b/modda-lib/src/log_parser.rs index 9bd3b65..3f8549e 100644 --- a/modda-lib/src/log_parser.rs +++ b/modda-lib/src/log_parser.rs @@ -10,6 +10,7 @@ use lazy_static::lazy_static; use log::{info, warn}; use regex::{Regex, RegexBuilder}; +use crate::canon_path::CanonPath; use crate::lowercase::LwcString; use crate::module::components::Components; use crate::module::module::Module; @@ -32,8 +33,8 @@ lazy_static! { .case_insensitive(true).build().unwrap(); } -pub fn parse_weidu_log(mod_filter: Option<&LwcString>) -> Result> { - let weidu_log_path = match find_insensitive(".", "weidu.log") { +pub fn parse_weidu_log(game_dir: &CanonPath,mod_filter: Option<&LwcString>) -> Result> { + let weidu_log_path = match find_insensitive(game_dir, "weidu.log") { Ok(None) =>return Ok(vec![]), Ok(Some(path)) => path, Err(error) => bail!("could not find weidu.log file\n {:?}", error) @@ -91,15 +92,15 @@ pub fn parse_weidu_log(mod_filter: Option<&LwcString>) -> Result> { result } -pub fn check_install_complete(module: &Module) -> Result<()> { +pub fn check_install_complete(game_dir: &CanonPath, module: &Module) -> Result<()> { match module { - Module::Mod { weidu_mod } => check_install_weidu_mod(weidu_mod), - Module::Generated { generated } => check_install_weidu_mod(&generated.as_weidu()), + Module::Mod { weidu_mod } => check_install_weidu_mod(game_dir, weidu_mod), + Module::Generated { generated } => check_install_weidu_mod(game_dir, &generated.as_weidu()), } } -fn check_install_weidu_mod(weidu_mod: &WeiduMod) -> Result<()> { - match check_installed_components(weidu_mod) { +fn check_install_weidu_mod(game_dir: &CanonPath, weidu_mod: &WeiduMod) -> Result<()> { + match check_installed_components(game_dir, weidu_mod) { Err(err) => return Err(err), Ok(missing) => if !missing.is_empty() { bail!("All requested components for mod {} could not be installed.\nMissing: {:?}", weidu_mod.name , missing); @@ -107,13 +108,13 @@ fn check_install_weidu_mod(weidu_mod: &WeiduMod) -> Result<()> { } } -fn check_installed_components(module: &WeiduMod) -> Result> { +fn check_installed_components(game_dir: &CanonPath, module: &WeiduMod) -> Result> { match &module.components { Components::None => Ok(vec![]), Components::Ask => Ok(vec![]), Components::All => Ok(vec![]), Components::List(components) => { - let log_rows = match parse_weidu_log(Some(&module.name)) { + let log_rows = match parse_weidu_log(game_dir, Some(&module.name)) { Ok(log_rows) => log_rows, Err(err) => bail!("Could not check installed components\n -> {:?}", err), }; diff --git a/modda-lib/src/obtain/get_module.rs b/modda-lib/src/obtain/get_module.rs index bbcc08f..706f141 100644 --- a/modda-lib/src/obtain/get_module.rs +++ b/modda-lib/src/obtain/get_module.rs @@ -173,7 +173,7 @@ mod test_retrieve_location { use crate::module::location::location::{ConcreteLocation, Location}; use crate::module::location::source::Source; use crate::module::weidu_mod::WeiduMod; - use crate:: config::{Config, DefaultOptions}; + use crate:: config::Config; use crate::canon_path::CanonPath; use crate::cache::Cache; use crate::obtain::get_module::ModuleDownload; diff --git a/modda-lib/src/process_weidu_mod.rs b/modda-lib/src/process_weidu_mod.rs index b4aa3b5..5df1a7e 100644 --- a/modda-lib/src/process_weidu_mod.rs +++ b/modda-lib/src/process_weidu_mod.rs @@ -1,7 +1,6 @@ use std::io::BufWriter; use std::io::Write; -use std::path::Path; use nu_ansi_term::Color; @@ -26,6 +25,7 @@ use crate::tp2::find_tp2; use crate::tp2_template::create_tp2; use crate::run_weidu::run_weidu_install; use crate::modda_context::ModdaContext; +use crate::utils::insensitive::find_insensitive; pub struct ProcessResult { pub was_disabled: bool, @@ -63,7 +63,7 @@ pub fn process_weidu_mod(weidu_mod: &WeiduMod, modda_context: &ModdaContext, man return Err(error) } Ok(setup_log) => { - configure_module(weidu_mod)?; + configure_module(weidu_mod, modda_context)?; SetupTimeline { configured: Some(Local::now()), ..setup_log @@ -185,9 +185,14 @@ fn fail_warnings(module: &WeiduMod, index: usize, total: usize) -> (String, Colo (message, Red) } -fn configure_module(module: &WeiduMod) -> Result<()> { +fn configure_module(module: &WeiduMod, modda_context: &ModdaContext) -> Result<()> { if let Some(conf) = &module.add_conf { - let conf_path = Path::new(module.name.as_ref()).join(&conf.file_name); + let mod_dir = match find_insensitive(modda_context.current_dir, module.name.as_ref()) { + Ok(Some(path)) => path, + Ok(None) => bail!("Could not find mod directory {}", module.name), + Err(err) => bail!("Error looking for mod directory {} - {err:?}", module.name), + }; + let conf_path = mod_dir.join(&conf.file_name); let file = match std::fs::OpenOptions::new() .create(true).write(true).truncate(true) .open(&conf_path) { diff --git a/modda-lib/src/sub/extract_manifest.rs b/modda-lib/src/sub/extract_manifest.rs index a7107ca..122b214 100644 --- a/modda-lib/src/sub/extract_manifest.rs +++ b/modda-lib/src/sub/extract_manifest.rs @@ -19,8 +19,8 @@ use crate::module::weidu_mod::BareMod; use crate::unique_component::UniqueComponent; use crate::weidu_conf::read_weidu_conf_lang_dir; -pub fn extract_bare_mods() -> Result> { - let log_rows = parse_weidu_log(None)?; +pub fn extract_bare_mods(game_dir: &CanonPath) -> Result> { + let log_rows = parse_weidu_log(game_dir, None)?; let init: Vec = vec![]; let mod_fragments = log_rows.iter().fold(init, |mut accumulator, row| { let current_mod = row.module.to_lowercase(); @@ -47,7 +47,7 @@ fn format_modules(bare_mods: Vec, export_component_name: Option, } pub fn extract_manifest(args: &Reverse, game_dir: &CanonPath) -> Result<()> { - let mods = extract_bare_mods()?; + let mods = extract_bare_mods(game_dir)?; let mods = format_modules(mods, args.export_component_name, args.export_language); let manifest = generate_manifest(game_dir, mods)?; @@ -98,8 +98,8 @@ fn bare_mod_from_log_row(row: &LogRow) -> BareMod { } } -pub fn extract_unique_components() -> Result> { - let log_rows = parse_weidu_log(None)?; +pub fn extract_unique_components(game_dir: &CanonPath) -> Result> { + let log_rows = parse_weidu_log(game_dir, None)?; log_rows.iter().try_fold(HashSet::new(), |mut set, row| { let unique_component = UniqueComponent { mod_key: lwc!(&row.module), diff --git a/modda-lib/src/sub/install.rs b/modda-lib/src/sub/install.rs index e98e6fd..d4a62f1 100644 --- a/modda-lib/src/sub/install.rs +++ b/modda-lib/src/sub/install.rs @@ -73,7 +73,7 @@ pub fn install(opts: &Install, settings: &Config, game_dir: &CanonPath, cache: & info!("module {} - {}", real_index, module.describe()); debug!("{:?}", module); - match check_safely_installable(module)? { + match check_safely_installable(&game_dir, module)? { SafetyResult::Abort => bail!("Aborted"), SafetyResult::Safe => {} SafetyResult::Conflicts(matches) if matches.is_empty() => {} @@ -91,7 +91,7 @@ pub fn install(opts: &Install, settings: &Config, game_dir: &CanonPath, cache: & } match module { Module::Mod { weidu_mod } => - install_weidu(weidu_mod, &modda_context, &manifest, opts, index, real_index)?, + install_weidu(weidu_mod, &modda_context, &manifest, &modda_context, index, real_index)?, Module::Generated { generated } => process_generated_mod(generated, &modda_context, &manifest, real_index)?, } @@ -134,7 +134,7 @@ pub fn install(opts: &Install, settings: &Config, game_dir: &CanonPath, cache: & // Now check we actually installed all requested components // if dry_run, nothing will have been installed at all so don't check if !opts.dry_run && !was_disabled { - check_install_complete(&module)? + check_install_complete(game_dir, &module)? } } if !opts.dry_run { @@ -148,12 +148,12 @@ pub fn install(opts: &Install, settings: &Config, game_dir: &CanonPath, cache: & } fn install_weidu(weidu_mod: &WeiduMod, modda_context: &ModdaContext, manifest: &Manifest, - opts: &Install, index: usize, real_index: usize) -> Result { + context: &ModdaContext, index: usize, real_index: usize) -> Result { let result = process_weidu_mod(weidu_mod, &modda_context, &manifest, real_index)?; if weidu_mod.components.is_ask() { - if let Some(output_path) = &opts.record { - let manifest_path = PathBuf::from(&opts.manifest_path); - record_selection(index, weidu_mod, &output_path, &manifest_path, opts)?; + if let Some(output_path) = &context.opts.record { + let manifest_path = PathBuf::from(&context.opts.manifest_path); + record_selection(index, weidu_mod, &output_path, &manifest_path, context)?; } } Ok(result) @@ -189,8 +189,8 @@ fn get_modules_range<'a>(modules: &'a[Module], opts: &Install) -> Result<&'a [M Ok(result) } -fn check_safely_installable(module: &Module) -> Result { - let installed = extract_unique_components()?; +fn check_safely_installable(game_dir: &CanonPath, module: &Module) -> Result { + let installed = extract_unique_components(game_dir)?; match module.get_components() { Components::None => Ok(SafetyResult::Safe), Components::Ask | Components::All => { @@ -266,8 +266,8 @@ pub enum SafetyResult { Abort, } -fn record_selection(index: usize, module: &WeiduMod, output_file: &str, original_manifest_path: &Path, opts: &Install) -> Result<()> { - let log_rows = parse_weidu_log(None)?; +fn record_selection(index: usize, module: &WeiduMod, output_file: &str, original_manifest_path: &Path, context: &ModdaContext) -> Result<()> { + let log_rows = parse_weidu_log(&context.current_dir, None)?; let output_path = PathBuf::from(output_file); let mut record_manifest = if output_path.exists() { Manifest::read_path_convert_comments(&output_path)? @@ -311,7 +311,7 @@ fn record_selection(index: usize, module: &WeiduMod, output_file: &str, original Component::Full(FullComponent { index: row.component_index, component_name: row.component_name.to_owned() }) ).collect_vec(); - if confirm_record(opts.record_no_confirm, &selection_rows, &module.name)? { + if confirm_record(context.opts.record_no_confirm, &selection_rows, &module.name)? { // update manifest with new component selection let components = if selection.is_empty() { Components::None @@ -325,7 +325,7 @@ fn record_selection(index: usize, module: &WeiduMod, output_file: &str, original } }; // write updated manifest to new file - record_manifest.write(&output_path, opts.record_with_comment_as_field)?; + record_manifest.write(&output_path, context.opts.record_with_comment_as_field)?; } diff --git a/modda-lib/src/utils/insensitive.rs b/modda-lib/src/utils/insensitive.rs index 82c028b..371c73b 100644 --- a/modda-lib/src/utils/insensitive.rs +++ b/modda-lib/src/utils/insensitive.rs @@ -1,34 +1,278 @@ + +use std::io::ErrorKind; use std::path::{Path, PathBuf}; use anyhow::{Result, bail}; -use globwalk::GlobWalkerBuilder; use log::debug; - -pub fn find_insensitive (base: P, pattern: S) -> Result> +/// Looks for a file matching the argument, allowing case-insensitive matches. +/// +/// - `base` is the path where we're looking for. It's an exact path. +/// - `searched` is a path from the `base`, potentially cas insensitive. +pub fn find_insensitive

(base: P, searched: &str) -> Result> where - P: AsRef + std::fmt::Debug, - S: AsRef + std::fmt::Debug { - debug!("Looking for file matching pattern {:?} in {:?}", pattern, base); - let glob_builder = GlobWalkerBuilder::new(&base, &pattern) - .case_insensitive(true) - .max_depth(1); - let glob = match glob_builder.build() { - Err(error) => bail!("Could not look up files matching {:?} in {:?}\n -> {:?}", pattern, base, error), - Ok(glob) => glob, - }; - let candidates = glob.into_iter().filter_map(Result::ok) - .map(|entry| { - debug!("Found file matching pattern {:?} (in {:?}): '{}'", pattern, base, entry.file_name().to_string_lossy()); - entry.file_name().to_owned() - }) - .collect::>(); + P: AsRef + std::fmt::Debug { + let candidates = find_all_insensitive(&base, searched)?; match candidates[..] { [] => { - debug!("Found no matches for {pattern:?} in {base:?}"); + debug!("Found no matches for {searched} in {base:?}"); Ok(None) }, [ref name] => Ok(Some(PathBuf::from(name))), - _ => bail!("More than one candidate for pattern {:?} in {:?}", pattern, base), + _ => { + let msg = format!("More than one candidate ({count}) for lookup of {searched:?} in {base:?}", + count = candidates.len()); + debug!("{msg}"); + bail!(msg) + }, + } +} + +pub fn find_all_insensitive

(base: P, searched: &str) -> Result> + where + P: AsRef + std::fmt::Debug { + + match &base.as_ref().metadata() { + Err(err) => match err.kind() { + ErrorKind::NotFound => bail!("Base directory {base:?} doesn't exist"), + _ => bail!("Could not obtain fs metadata for {base:?} - {err:?}") + } + Ok(meta) if !meta.is_dir() => bail!("Base {base:?} is not a directory"), + _ => {} + } + let as_pathbuf: PathBuf = PathBuf::from(searched); + let searched_components = as_pathbuf.components().collect::>(); + let partial_paths = searched_components.iter().take(searched_components.len() - 1) + .try_fold(vec![], |mut acc: Vec, curr| { + let part = match curr { + std::path::Component::Normal(part) => part, + _ => bail!("Path component is not allowed: `{curr:?}` (in `{searched:?}`") + }; + let next = match acc.last() { + None => PathBuf::from(part), + Some(last) => last.join(part), + }; + acc.push(next); + Ok(acc) + })?; + + let partial_paths_as_str= partial_paths.iter().map(|path| { + match path.as_os_str().to_str() { + None => bail!("Unsupported character in searched path: `{path:?}` (in `{searched:?}`"), + Some(path_string) => Ok(format!("^{}$", regex::escape(path_string))), + } + }) + .collect::>>()?; + + let partial_path_regexes: Vec = partial_paths_as_str.iter().map(|escaped| { + Ok(regex::bytes::RegexBuilder::new(&escaped).case_insensitive(true).build()?) + }).collect::>>()?; + + let full_regex_as_str = format!("^{}$", regex::escape(searched)); + debug!("search (insensitive) for {searched} in {base:?}\n escaped pattern is {full_regex_as_str}\n parent patterns are\n - {parents}", + parents = partial_paths_as_str.join("\n - ")); + let full_regex = regex::bytes::RegexBuilder::new(&full_regex_as_str).case_insensitive(true).build()?; + + let result = walkdir::WalkDir::new(&base) + .min_depth(1) // root is depth 0 + .into_iter() + .filter_entry(|dir_entry| { + let path = dir_entry.path(); + match path.strip_prefix(&base) { + Err(_) => false, + Ok(stripped_path) => { + let path_as_bytes = stripped_path.as_os_str().as_encoded_bytes(); + if path.is_dir() { + // allow descending in this directory if it matches partially + let accepted = partial_path_regexes.iter().any(|regex| regex.is_match(path_as_bytes)) + || full_regex.is_match(path_as_bytes); + debug!("dir is [{stripped_path:?}] => {accepted} (descending)"); + accepted + } else { + let accepted = full_regex.is_match(path_as_bytes); + debug!("path is [{stripped_path:?}] => {accepted}"); + accepted + } + } + } + }) + .filter_map(|entry| entry.ok()) + // remove parent directories + .filter(|candidate| { + let path = candidate.path(); + match path.strip_prefix(&base) { + Err(_) => false, + Ok(stripped_path) => full_regex.is_match(stripped_path.as_os_str().as_encoded_bytes()) + } + }) + .map(|dir_entry| base.as_ref().join(dir_entry.into_path())) + .collect::>(); + Ok(result) +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + use std::path::{Path, PathBuf}; + + use crate::utils::insensitive::{find_all_insensitive, find_insensitive}; + + #[test] + fn find_chitin_key_base_doesnt_exist_in_same_case() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/LOWER"); + println!("{:?}", find_insensitive(&base, "chitin.key")); + assert!(find_insensitive(&base, "chitin.key").is_err()); + } + + #[test] + fn find_chitin_key_lowercase_param_lower() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/lower"); + assert_eq!(find_insensitive(&base, "chitin.key").unwrap(), Some(PathBuf::from(base).join("chitin.key"))); + } + + #[test] + fn find_chitin_key_lowercase_param_upper() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/lower"); + assert_eq!(find_insensitive(&base, "CHITIN.KEY").unwrap(), Some(PathBuf::from(base).join("chitin.key"))); + } + + #[test] + fn find_chitin_key_lowercase_param_mixed() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/lower"); + assert_eq!(find_insensitive(&base, "Chitin.Key").unwrap(), Some(PathBuf::from(base).join("chitin.key"))); + } + + #[test] + fn find_chitin_key_uppercase_param_lower() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/UPPER"); + assert_eq!(find_insensitive(&base, "chitin.key").unwrap(), Some(PathBuf::from(base).join("CHITIN.KEY"))); + } + + #[test] + fn find_chitin_key_uppercase_param_upper() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/UPPER"); + assert_eq!(find_insensitive(&base, "CHITIN.KEY").unwrap(), Some(PathBuf::from(base).join("CHITIN.KEY"))); + } + + #[test] + fn find_chitin_key_uppercase_param_mixed() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/UPPER"); + assert_eq!(find_insensitive(&base, "Chitin.Key").unwrap(), Some(PathBuf::from(base).join("CHITIN.KEY"))); + } + + #[test] + fn find_chitin_key_mixed_case_param_lower() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/MiXed"); + assert_eq!(find_insensitive(&base, "chitin.key").unwrap(), Some(PathBuf::from(base).join("Chitin.Key"))); + } + + #[test] + fn find_chitin_key_mixed_case_param_upper() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/MiXed"); + assert_eq!(find_insensitive(&base, "CHITIN.KEY").unwrap(), Some(PathBuf::from(base).join("Chitin.Key"))); + } + + #[test] + fn find_chitin_key_mixed_case_param_mixed() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/MiXed"); + assert_eq!(find_insensitive(&base, "Chitin.Key").unwrap(), Some(PathBuf::from(base).join("Chitin.Key"))); + } + + #[test] + fn find_with_subdir_lower_param_lower() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/lower"); + assert_eq!(find_insensitive(&base, "mod1/file1.txt").unwrap(), Some(PathBuf::from(base).join("mod1").join("file1.txt"))); + } + + #[test] + fn find_with_subdir_lower_param_upper() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/lower"); + assert_eq!(find_insensitive(&base, "MOD1/FILE1.TXT").unwrap(), Some(PathBuf::from(base).join("mod1").join("file1.txt"))); + } + + #[test] + fn find_with_subdir_lower_param_mixed() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/lower"); + assert_eq!(find_insensitive(&base, "Mod1/File1.Txt").unwrap(), Some(PathBuf::from(base).join("mod1").join("file1.txt"))); + } + + #[test] + fn find_with_subdir_upper_param_lower() { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/UPPER"); + assert_eq!(find_insensitive(&base, "mod1/file1.txt").unwrap(), Some(PathBuf::from(base).join("MOD1").join("FILE1.TXT"))); + } + + #[test] + fn find_with_subdir_mixed_param_lower() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/MiXed"); + assert_eq!(find_insensitive(&base, "mod1/file1.txt").unwrap(), Some(PathBuf::from(base).join("Mod1").join("File1.txt"))); + } + + #[test] + fn find_all_multiple_with_different_case() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/multiple"); + assert_eq!( + find_all_insensitive(&base, "file2.txt").unwrap(), + vec![ + PathBuf::from(&base).join("File2.txt"), + PathBuf::from(&base).join("File2.Txt"), + ], + ); + } + + #[test] + fn find_multiple_with_different_case_and_folders() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/multiple"); + assert!(find_insensitive(&base, "mod1/file1").is_err()); + } + + #[test] + fn find_all_multiple_with_different_case_and_folders() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/multiple"); + assert_eq!( + find_all_insensitive(&base, "mod1/file1").unwrap().iter().collect::>(), + vec![ + PathBuf::from(&base).join("Mod1").join("File1"), + PathBuf::from(&base).join("mod1").join("file1"), + ].iter().collect::>(), + ); + } + + #[test] + fn find_directory_with_mismatched_case() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/UPPER"); + assert_eq!(find_insensitive(&base, "mod1").unwrap(), Some(PathBuf::from(base).join("MOD1"))); + } + + #[test] + fn find_multiple_directories() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/multiple"); + assert_eq!( + find_all_insensitive(&base, "mod1").unwrap().iter().collect::>(), + vec![ + PathBuf::from(&base).join("Mod1"), + PathBuf::from(&base).join("mod1"), + ].iter().collect::>(), + ); + } + + #[test] + fn find_file_and_dir_with_same_name_except_case() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/multiple"); + assert_eq!( + find_all_insensitive(&base, "mod2/file").unwrap().iter().collect::>(), + vec![ + PathBuf::from(&base).join("mod2").join("file"), + PathBuf::from(&base).join("mod2").join("File"), + ].iter().collect::>(), + ); } } diff --git a/modda-lib/src/utils/pathext.rs b/modda-lib/src/utils/pathext.rs index 28cf94d..0b45020 100644 --- a/modda-lib/src/utils/pathext.rs +++ b/modda-lib/src/utils/pathext.rs @@ -1,6 +1,6 @@ use std::ffi::{OsStr, OsString}; -use std::path::{PathBuf, Path}; +use std::path::{Path, PathBuf}; /// Returns a path with a new dotted extension component appended to the end. /// Note: does not check if the path is a file or directory; you should do that.