diff --git a/src/lib.rs b/src/lib.rs index 1ea454d..09522d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,9 @@ use filetime::set_file_mtime; use ini::Ini; use log::{error, info, warn}; use openmw_cfg::config_path; +use regex::Regex; use rules::*; +use semver::Version; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, ValueEnum, Default)] @@ -302,47 +304,7 @@ where P: AsRef, { let files = get_plugins_sorted(&path.as_ref().join("Data Files"), false); - let names = files - .iter() - .filter_map(|f| { - if let Some(file_name) = f.file_name().and_then(|n| n.to_str()) { - let mut data = PluginData { - name: file_name.to_owned(), - size: f.metadata().unwrap().len(), - description: None, - version: None, - masters: None, - }; - - // skip if extension is omwscripts - if !file_name.to_ascii_lowercase().ends_with("omwscripts") { - match parse_header(f) { - Ok(header) => { - data.description = Some(header.description); - data.masters = header.masters; - - // parse semver - let version = header.version.to_string(); - match lenient_semver::parse(&version) { - Ok(v) => { - data.version = Some(v); - } - Err(e) => { - log::debug!("Error parsing version: {}", e); - } - } - } - Err(e) => { - log::debug!("Error parsing header: {}, {}", e, f.display()); - } - } - } - - return Some(data); - } - None - }) - .collect::>(); + let names = files.iter().filter_map(|f| map_data(f)).collect::>(); // check against mw ini let morrowind_ini_path = PathBuf::from("Morrowind.ini"); @@ -390,42 +352,7 @@ where if let Ok(cfg) = openmw_cfg::Ini::load_from_file_noescape(path) { if let Ok(files) = openmw_cfg::get_plugins(&cfg) { - let names = files - .iter() - .filter_map(|f| { - if let Some(file_name) = f.file_name().and_then(|n| n.to_str()) { - let mut data = PluginData { - name: file_name.to_owned(), - size: f.metadata().unwrap().len(), - description: None, - version: None, - masters: None, - }; - match parse_header(f) { - Ok(header) => { - data.description = Some(header.description); - data.masters = header.masters; - - // parse semver - let version = header.version.to_string(); - match lenient_semver::parse(&version) { - Ok(v) => { - data.version = Some(v); - } - Err(e) => { - log::debug!("Error parsing version: {}", e); - } - } - } - Err(e) => { - log::debug!("Error parsing header: {}, {}", e, f.display()); - } - } - return Some(data); - } - None - }) - .collect::>(); + let names = files.iter().filter_map(|f| map_data(f)).collect::>(); return names; } } else { @@ -435,6 +362,110 @@ where vec![] } +fn map_data(f: &Path) -> Option { + if let Some(file_name) = f.file_name().and_then(|n| n.to_str()) { + let mut data = PluginData { + name: file_name.to_owned(), + size: f.metadata().unwrap().len(), + description: None, + version: None, + masters: None, + }; + + match parse_header(f) { + Ok(header) => { + data.description = Some(header.description); + data.masters = header.masters; + } + Err(e) => { + log::debug!("Error parsing header: {}, {}", e, f.display()); + } + }; + + // parse semver + if let Some(version) = get_version(file_name, &data.description) { + data.version = Some(version); + } + + return Some(data); + } + None +} + +const VERSION_REGEX: &str = r"(\d+(?:[_.-]?\d+)*[a-zA-Z]?)"; + +/// Get version from filename or description +fn get_version(file_name: &str, description: &Option) -> Option { + let mut final_version_str = None; + + // try to get version from description first + if let Some(desc) = description { + if let Some(value) = match_desc_version(desc) { + final_version_str = Some(value); + } + } + + // try to get version from filename + if let Some(value) = match_filename_version(file_name) { + final_version_str = Some(value); + } + + if let Some(version) = final_version_str { + if let Some(value) = get_semver(version.as_str()) { + return Some(value); + } + } + + None +} + +/// Get semver from string +fn get_semver(version: &str) -> Option { + // replace _ and - with . + let formatted_version = version.replace(['_', '-'], "."); + // TODO limit to major minor and patch version + + match lenient_semver::parse(&formatted_version) { + Ok(v) => return Some(v), + Err(e) => { + log::debug!("Error parsing version: {}", e); + } + } + None +} + +/// Get version from filename +/// +/// # Panics +/// +/// Panics if the regex pattern is invalid +fn match_filename_version(file_name: &str) -> Option { + let filename_version_regex = format!(r"\D{}\D*", VERSION_REGEX); + let pattern: Regex = Regex::new(filename_version_regex.as_str()).unwrap(); + if let Some(captures) = pattern.captures(file_name) { + if let Some(version) = captures.get(1) { + return Some(version.as_str().to_string()); + } + } + None +} + +/// Get version from description +/// +/// # Panics +/// +/// Panics if the regex pattern is invalid +fn match_desc_version(desc: &str) -> Option { + let header_version_regex = format!(r"\b(?:version\b\D+|v(?:er)?\.?\s*){}", VERSION_REGEX); + let pattern: Regex = Regex::new(header_version_regex.as_str()).unwrap(); + if let Some(captures) = pattern.captures(desc) { + if let Some(version) = captures.get(1) { + return Some(version.as_str().to_string()); + } + } + None +} + pub fn gather_cp77_mods

(root: &P) -> Vec where P: AsRef, @@ -604,7 +635,6 @@ pub fn check_order(result: &[String], order_rules: &[EOrderRule]) -> bool { //////////////////////////////////////////////////////////////////////// #[derive(Debug, Clone, Default)] pub struct Tes3Header { - pub version: f32, pub description: String, pub masters: Option>, } @@ -653,7 +683,7 @@ fn parse_hedr(reader: &mut R, stream_size: u64) -> std::io::Resu let _header_size = reader.read_u32::()?; // next 4 bytes is the version - header.version = reader.read_f32::()?; + let _ = reader.read_f32::()?; // next 4 bytes is unused let _ = reader.read_u32::()?; @@ -915,7 +945,8 @@ pub fn wild_contains(list: &[String], str: &String) -> Option> { } /// Checks if the list contains the str -pub fn wild_contains_data(list: &[PluginData], str: &String) -> Option> { +pub fn wild_contains_data(list: &[PluginData], str: &str) -> Option> { + let str = str.to_lowercase(); if str.contains('*') || str.contains('?') || str.contains("") { let mut results = vec![]; // Replace * with .* to match any sequence of characters @@ -947,7 +978,7 @@ pub fn wild_contains_data(list: &[PluginData], str: &String) -> Option Option { mod tests { //use std::fs::create_dir_all; + use semver::{BuildMetadata, Prerelease}; + // Note this useful idiom: importing names from outer (for mod tests) scope. use super::*; @@ -1219,4 +1252,101 @@ mod tests { // fs::remove_file(path).expect("remove failed"); // } // } + + #[test] + fn test_match_filename_version() { + let inputs = [ + ("a.esp", None), + ("a_2.0.esp", Some("2.0".to_owned())), + ("a_3.0_comment.esp", Some("3.0".to_owned())), + ("a_4.0a_comment.archive", Some("4.0a".to_owned())), + ("a_5-0-3abc_comment.omwaddon", Some("5-0-3a".to_owned())), + ("a_6_0_3abc_comment.omwaddon", Some("6_0_3a".to_owned())), + ("a_7.0_3abc_comment.omwaddon", Some("7.0_3a".to_owned())), + ("a_7.0-3abc_comment.omwaddon", Some("7.0-3a".to_owned())), + // TODO this is a valid version number but we don't support it + ("a_1.1-nightly_comment.esp", Some("1.1".to_owned())), + ]; + + for (input, expected) in &inputs { + let got = match_filename_version(input); + assert_eq!(got, *expected); + } + } + + #[test] + fn test_match_desc_version() { + let inputs = [ + ("a version 1.0", Some("1.0".to_owned())), + ("a version 1.0_comment", Some("1.0".to_owned())), + ("a version 1.0", Some("1.0".to_owned())), + ("some comments", None), + ( + "some comment about a plugin with version 2.0 and some other stuff", + Some("2.0".to_owned()), + ), + ("do we support v2.4 here? yes", Some("2.4".to_owned())), + ("many v3.0 anf v2.0 in the header", Some("3.0".to_owned())), + ("we also match ver6.8 etc", Some("6.8".to_owned())), + ( + "and other delims v3-5-7-8-8 are supported", + Some("3-5-7-8-8".to_owned()), + ), + ( + "and other delims v1_5_5 are supported", + Some("1_5_5".to_owned()), + ), + ("and mixed ver3.6_7-8 versions", Some("3.6_7-8".to_owned())), + ( + "and with chars version 9.3a at the end", + Some("9.3a".to_owned()), + ), + ( + "but not actually v3.5.6-nightly semver", + Some("3.5.6".to_owned()), + ), + ("we only have v1.1-5n this stuff", Some("1.1-5n".to_owned())), + ]; + + for (input, expected) in &inputs { + let got = match_desc_version(input); + assert_eq!(got, *expected); + } + } + + #[test] + fn test_get_semver() { + let inputs = [ + ("nothing", None), + ("1.0", Some(Version::new(1, 0, 0))), + ("1_5_5", Some(Version::new(1, 5, 5))), + ("3.6_7", Some(Version::new(3, 6, 7))), + ("3-6_7", Some(Version::new(3, 6, 7))), + ( + "9.3a", + Some(Version { + major: 9, + minor: 0, + patch: 0, + pre: Prerelease::new("3a").unwrap(), + build: BuildMetadata::EMPTY, + }), + ), + ( + "1.1-5n", + Some(Version { + major: 1, + minor: 1, + patch: 0, + pre: Prerelease::new("5n").unwrap(), + build: BuildMetadata::EMPTY, + }), + ), + ]; + + for (input, expected) in &inputs { + let got = get_semver(input.to_owned()); + assert_eq!(got, *expected); + } + } } diff --git a/src/rules.rs b/src/rules.rs index d8d0277..20fdc14 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -506,6 +506,7 @@ impl TWarningRule for Conflict { fn set_comment(&mut self, comment: String) { self.comment = comment; } + /// Conflicts evaluate as true if both expressions evaluate as true fn eval(&mut self, items: &[PluginData]) -> bool { let mut i = 0; @@ -525,10 +526,6 @@ impl TParser for Conflict { reader: R, parser: &parser::Parser, ) -> Result<()> { - // if let Ok(Some(comment)) = read_comment(&mut reader) { - // this.set_comment(comment); - // } - // add all parsed expressions this.expressions = parser.parse_expressions(reader)?; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 61747df..198f872 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -415,7 +415,6 @@ mod integration_tests { let header = parse_header(&plugin_test_path).expect("failed to parse header"); // check some things - assert_eq!(header.version, 1.2_f32); assert_eq!(header.description, "The main data file For Morrowind"); // check master files assert!(header.masters.is_none()); @@ -426,7 +425,6 @@ mod integration_tests { let header = parse_header(&plugin_test_path).expect("failed to parse header"); // check some things - assert_eq!(header.version, 1.3_f32); assert_eq!( header.description, "The main data file for BloodMoon.\r\n(requires Morrowind.esm to run)" diff --git a/tests/unit_expression_tests.rs b/tests/unit_expression_tests.rs index 5009153..3111d52 100644 --- a/tests/unit_expression_tests.rs +++ b/tests/unit_expression_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod unit_tests { - use plox::{expressions::*, PluginData}; + use plox::{expressions::*, rules::TWarningRule, PluginData}; fn init() { let _ = env_logger::builder().is_test(true).try_init(); @@ -244,73 +244,42 @@ mod unit_tests { } } - #[allow(dead_code)] - //TODO add plugin filename version parsing #[test] - fn evaluate_ver_filename() { - init(); + #[test] + fn test_ver_problem() { + // check specific problem + // [Conflict] + // Texture Fix 2.0.esm + // [VER < 2.0 Texture Fix .esm] - let mods = ["a.esp", "b.esp"] + let mods = ["Texture Fix 2.0.esm"] .iter() .map(|e| PluginData { - name: e.to_string(), + name: e.to_lowercase().to_string(), size: 0_u64, description: None, - version: None, masters: None, + version: None, }) .collect::>(); - // Check equals - // [VER] equals is true if the plugin version matches the given version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Equal, "1.0.0".to_string()); - assert!(expr.eval(&mods).is_some()); - } - - // [VER] equals is false if the plugin version does not matches the given version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Equal, "1.1.0".to_string()); - assert!(expr.eval(&mods).is_none()); - } - - // Check greater - // [Note this is a newer version, it's broken] [VER > 0.1 foo.esp] - // [VER] greater is true if the plugin version is greater than the rule version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Greater, "0.1.0".to_string()); - assert!(expr.eval(&mods).is_some()); - } - - // [VER] greater is false if the plugin version is less than the given version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Greater, "1.2.0".to_string()); - assert!(expr.eval(&mods).is_none()); - } - - // [VER] greater is false if the plugin version is equal to the given version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Greater, "1.0.0".to_string()); - assert!(expr.eval(&mods).is_none()); - } - - // Check less - // [Note this is an old version, please upgrade] [VER < 1.2 foo.esp] - // [VER] less is true if the plugin version is less than the rule version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Less, "1.2.0".to_string()); - assert!(expr.eval(&mods).is_some()); - } - - // [VER] less is false if the plugin version is greater than the given version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Less, "0.1.0".to_string()); - assert!(expr.eval(&mods).is_none()); - } - - // [VER] less is false if the plugin version is equal to the given version - { - let expr = VER::new(Atomic::from(A), EVerOperator::Less, "1.0.0".to_string()); - assert!(expr.eval(&mods).is_none()); + // { + // let expr = VER::new( + // Atomic::from("Texture Fix .esm"), + // EVerOperator::Less, + // "3.0.0".to_string(), + // ); + // assert!(expr.eval(&mods).is_some()); + // } + + { + let expr1 = Atomic::from("Texture Fix 2.0.esm"); + let expr2 = VER::new( + Atomic::from("Texture Fix .esm"), + EVerOperator::Less, + "2.0.0".to_string(), + ); + let mut rule = plox::rules::Conflict::new("".into(), &[expr1.into(), expr2.into()]); + assert!(!rule.eval(&mods)); } }