From 8768d421b640653d8f56c61d59ed1b8775685c81 Mon Sep 17 00:00:00 2001 From: dark0dave Date: Sun, 25 Aug 2024 17:34:32 +0100 Subject: [PATCH] feat(config): Add config parser file for mod installer Signed-off-by: dark0dave --- .gitignore | 1 - Cargo.lock | 164 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 10 +-- check-version.sh | 30 +++++--- src/args.rs | 4 +- src/main.rs | 15 +++- src/parser_config.rs | 164 +++++++++++++++++++++++++++++++++++++++++++ src/weidu.rs | 22 ++++-- src/weidu_parser.rs | 142 ++----------------------------------- 9 files changed, 394 insertions(+), 158 deletions(-) create mode 100644 src/parser_config.rs diff --git a/.gitignore b/.gitignore index f52a7ba..d0d25b1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ .swp target/ dist/ -Cargo.toml.test diff --git a/Cargo.lock b/Cargo.lock index 317cccd..002719a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.15" @@ -106,12 +118,44 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "confy" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" +dependencies = [ + "directories", + "serde", + "thiserror", + "toml", +] + [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "env_filter" version = "0.1.2" @@ -141,6 +185,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" @@ -159,6 +214,22 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "log" version = "0.4.22" @@ -173,13 +244,16 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mod_installer" -version = "8.1.0" +version = "9.0.0" dependencies = [ "clap", + "confy", "env_logger", "fs_extra", "log", "pretty_assertions", + "serde", + "serde_derive", "walkdir", ] @@ -211,6 +285,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.6" @@ -249,6 +334,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.11.1" @@ -266,6 +371,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -288,6 +422,28 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -297,6 +453,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index cdb4284..1f74698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mod_installer" -version = "8.1.0" +version = "9.0.0" edition = "2021" authors = [ "dark0dave" ] documentation = "https://raw.githubusercontent.com/dark0dave/mod_installer/main/README.md" @@ -19,14 +19,14 @@ keywords = [ ] [dependencies] +confy = "^0.5" +clap = { version = "4.5.9", features = ["derive", "env"] } env_logger = "0.11.1" fs_extra = "1.3.0" log = "0.4.22" +serde = { version = "1.0.152", features = ["derive"] } +serde_derive = "1.0.152" walkdir = "2.3.2" - [dependencies.clap] - version = "4.5.9" - features = [ "derive", "env" ] - [dev-dependencies] pretty_assertions = "1.3.0" diff --git a/check-version.sh b/check-version.sh index ee8a78b..c46b5d0 100755 --- a/check-version.sh +++ b/check-version.sh @@ -1,13 +1,25 @@ #!/usr/bin/env bash set -euo pipefail -release_tag=$(git describe --tags --abbrev=0); -trap "rm -f Cargo.toml.test" SIGINT; -sed "s/^version \= .*/version = \"${release_tag/v/}\"/" Cargo.toml > Cargo.toml.test; -diff -sd Cargo.toml.test Cargo.toml; -if [ $? -eq 0 ]; then - rm -f Cargo.toml.test; - exit 0; -else +function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; } + +function main() { + local tag_version=$(git describe --tags --abbrev=0); + echo "Git tag version: ${tag_version}" + local current_version=$(head -3 Cargo.toml | tail -1 | sed -E "s/^version \= \"(.*)\"/\1/") + echo "Cargo.toml version: v${current_version}" + if [ "${tag_version}" == "v${current_version}" ]; then + echo "Versions are the same" + exit 0; + fi + + if version_gt "v${current_version}" "${tag_version}"; then + echo "Current version greater than tag version" + exit 0; + fi + + echo "Failed, tag version: ${tag_version} is greater than Cargo,toml: v${current_version}" exit 1; -fi +} + +main diff --git a/src/args.rs b/src/args.rs index d438184..6bdb68f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,7 +2,9 @@ use std::path::PathBuf; use clap::{ArgAction, Parser}; -const WEIDU_LOG_MODE_ERROR: &str = r" +pub(crate) const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); + +pub(crate) const WEIDU_LOG_MODE_ERROR: &str = r" Please provide a valid weidu logging setting, options are: --weidu-log-mode log X log output and details to X --weidu-log-mode autolog log output and details to WSETUP.DEBUG diff --git a/src/main.rs b/src/main.rs index 86ac299..cf2fba7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ -use std::{collections::HashMap, process::ExitCode}; +use std::{collections::HashMap, process::ExitCode, sync::Arc}; -use args::Args; +use args::{Args, CARGO_PKG_NAME}; use clap::Parser; use env_logger::Env; +use parser_config::ParserConfig; use utils::find_mods; use crate::{ @@ -13,6 +14,7 @@ use crate::{ mod args; mod component; mod log_file; +mod parser_config; mod state; mod utils; mod weidu; @@ -43,6 +45,14 @@ fn main() -> ExitCode { } }; + let parser_config: Arc = match confy::load(CARGO_PKG_NAME, None) { + Ok(config) => Arc::new(config), + Err(err) => { + log::error!("Internal error with config crate, {:?}", err); + return ExitCode::FAILURE; + } + }; + let mut mod_folder_cache = HashMap::new(); for weidu_mod in &mods_to_be_installed { let mod_folder = mod_folder_cache @@ -63,6 +73,7 @@ fn main() -> ExitCode { match install( &args.weidu_binary, &args.game_directory, + parser_config.clone(), weidu_mod, &args.language, &args.weidu_log_mode, diff --git a/src/parser_config.rs b/src/parser_config.rs new file mode 100644 index 0000000..e9eb7c9 --- /dev/null +++ b/src/parser_config.rs @@ -0,0 +1,164 @@ +use crate::state::State; + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct ParserConfig { + pub(crate) in_progress_words: Vec, + pub(crate) useful_status_words: Vec, + pub(crate) choice_words: Vec, + pub(crate) choice_phrase: Vec, + pub(crate) completed_with_warnings: String, + pub(crate) failed_with_error: String, + pub(crate) finished: String, + pub(crate) eet_finished: String, +} + +impl Default for ParserConfig { + fn default() -> Self { + Self { + in_progress_words: vec!["installing".to_string(), "creating".to_string()], + useful_status_words: vec![ + "copied".to_string(), + "copying".to_string(), + "creating".to_string(), + "installed".to_string(), + "installing".to_string(), + "patched".to_string(), + "patching".to_string(), + "processed".to_string(), + "processing".to_string(), + ], + choice_words: vec![ + "choice".to_string(), + "choose".to_string(), + "select".to_string(), + "enter".to_string(), + ], + choice_phrase: vec!["do you want".to_string(), "would you like".to_string()], + completed_with_warnings: "installed with warnings".to_string(), + failed_with_error: "not installed due to errors".to_string(), + finished: "successfully installed".to_string(), + eet_finished: "process ended".to_string(), + } + } +} + +impl ParserConfig { + pub fn string_looks_like_question(&self, weidu_output: &str) -> bool { + let comparable_output = weidu_output.trim().to_ascii_lowercase(); + // installing|creating + for progress_word in self.in_progress_words.iter() { + if comparable_output.contains(progress_word) { + return false; + } + } + + for question in self.choice_phrase.iter() { + if comparable_output.contains(question) { + return true; + } + } + + for question in self.choice_words.iter() { + for word in comparable_output.split_whitespace() { + if word + .chars() + .filter(|c| c.is_alphabetic()) + .collect::() + == *question + { + return true; + } + } + } + + false + } + + pub fn detect_weidu_finished_state(&self, weidu_output: &str) -> Option { + let comparable_output = weidu_output.trim().to_lowercase(); + if comparable_output.contains(&self.failed_with_error) { + Some(State::CompletedWithErrors { + error_details: comparable_output, + }) + } else if comparable_output.contains(&self.completed_with_warnings) { + Some(State::CompletedWithWarnings) + } else if comparable_output.contains(&self.finished) + || comparable_output.contains(&self.eet_finished) + { + Some(State::Completed) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_exit_warnings() { + let config = ParserConfig::default(); + let test = "INSTALLED WITH WARNINGS Additional equipment for Thieves and Bards"; + assert_eq!(config.string_looks_like_question(test), false); + assert_eq!( + config.detect_weidu_finished_state(test), + Some(State::CompletedWithWarnings) + ) + } + #[test] + fn test_exit_success() { + let config = ParserConfig::default(); + let test = "SUCCESSFULLY INSTALLED Jan's Extended Quest"; + assert_eq!(config.string_looks_like_question(test), false); + assert_eq!( + config.detect_weidu_finished_state(test), + Some(State::Completed) + ) + } + + #[test] + fn is_not_question() { + let config = ParserConfig::default(); + let test = "Creating epilogues. Too many epilogues... Why are there so many options here?"; + assert_eq!(config.string_looks_like_question(test), false); + let test = "Including file(s) spellchoices_defensive/vanilla/ENCHANTER.TPH"; + assert_eq!(config.string_looks_like_question(test), false); + } + + #[test] + fn is_a_question() { + let config = ParserConfig::default(); + let tests = vec!["Enter the full path to your Baldur's Gate installation then press Enter.", "Enter the full path to your BG:EE+SoD installation then press Enter.\ +Example: C:\\Program Files (x86)\\BeamDog\\Games\\00806", "[N]o, [Q]uit or choose one:", "Please enter the chance for items to randomly not be randomised as a integet number (e.g. 10 for 10%)"]; + for question in tests { + assert_eq!( + config.string_looks_like_question(question), + true, + "String {} doesn't look like a question", + question + ); + } + } + + #[test] + fn is_not_a_question() { + let config = ParserConfig::default(); + let tests = vec![ + "FAILURE:", + "NOT INSTALLED DUE TO ERRORS The BG1 NPC Project: Required Modifications", + ]; + for question in tests { + assert_eq!( + config.string_looks_like_question(question), + false, + "String {} does look like a question", + question + ); + } + } +} diff --git a/src/weidu.rs b/src/weidu.rs index f21f147..3de63d3 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -10,7 +10,10 @@ use std::{ thread, }; -use crate::{component::Component, state::State, utils::sleep, weidu_parser::parse_raw_output}; +use crate::{ + component::Component, parser_config::ParserConfig, state::State, utils::sleep, + weidu_parser::parse_raw_output, +}; pub(crate) fn get_user_input() -> String { let stdin = io::stdin(); @@ -42,6 +45,7 @@ pub(crate) enum InstallationResult { pub(crate) fn install( weidu_binary: &PathBuf, game_directory: &PathBuf, + parser_config: Arc, weidu_mod: &Component, language: &str, weidu_log_mode: &str, @@ -58,15 +62,25 @@ pub(crate) fn install( .spawn() .expect("Failed to spawn weidu process"); - handle_io(child, timeout) + handle_io(child, parser_config, timeout) } -pub(crate) fn handle_io(mut child: Child, timeout: usize) -> InstallationResult { +pub(crate) fn handle_io( + mut child: Child, + parser_config: Arc, + timeout: usize, +) -> InstallationResult { let mut weidu_stdin = child.stdin.take().unwrap(); let wait_counter = Arc::new(AtomicUsize::new(0)); let raw_output_receiver = create_output_reader(child.stdout.take().unwrap()); let (sender, parsed_output_receiver) = mpsc::channel::(); - parse_raw_output(sender, raw_output_receiver, wait_counter.clone(), timeout); + parse_raw_output( + sender, + raw_output_receiver, + parser_config, + wait_counter.clone(), + timeout, + ); loop { match parsed_output_receiver.try_recv() { diff --git a/src/weidu_parser.rs b/src/weidu_parser.rs index 9298945..61e2b73 100644 --- a/src/weidu_parser.rs +++ b/src/weidu_parser.rs @@ -7,31 +7,7 @@ use std::{ thread, }; -use crate::{state::State, utils::sleep}; - -const WEIDU_USEFUL_STATUS: [&str; 9] = [ - "copied", - "copying", - "creating", - "installed", - "installing", - "patched", - "patching", - "processed", - "processing", -]; - -const WEIDU_CHOICE_WORDS: [&str; 4] = ["choice", "choose", "select", "enter"]; - -const WEIDU_CHOICE_PHRASE: [&str; 2] = ["do you want", "would you like"]; - -const WEIDU_COMPLETED_WITH_WARNINGS: &str = "installed with warnings"; - -const WEIDU_FAILED_WITH_ERROR: &str = "not installed due to errors"; - -const WEIDU_FINISHED: &str = "successfully installed"; - -const EET_FINISHED: &str = "Process ended"; +use crate::{parser_config::ParserConfig, state::State, utils::sleep}; #[derive(Debug)] enum ParserState { @@ -43,6 +19,7 @@ enum ParserState { pub(crate) fn parse_raw_output( sender: Sender, receiver: Receiver, + parser_config: Arc, wait_count: Arc, timeout: usize, ) { @@ -55,7 +32,7 @@ pub(crate) fn parse_raw_output( match receiver.try_recv() { Ok(string) => match current_state { ParserState::CollectingQuestion | ParserState::WaitingForMoreQuestionContent => { - if WEIDU_USEFUL_STATUS.contains(&string.as_str()) { + if parser_config.useful_status_words.contains(&string) { log::debug!( "Weidu seems to know an answer for the last question, ignoring it" ); @@ -69,12 +46,14 @@ pub(crate) fn parse_raw_output( } ParserState::LookingForInterestingOutput => { log::trace!("{}", string); - if let Some(weidu_finished_state) = detect_weidu_finished_state(&string) { + if let Some(weidu_finished_state) = + parser_config.detect_weidu_finished_state(&string) + { sender .send(weidu_finished_state) .expect("Failed to send process error event"); break; - } else if string_looks_like_question(&string) { + } else if parser_config.string_looks_like_question(&string) { log::debug!( "Changing parser state to '{:?}' due to line {}", ParserState::CollectingQuestion, @@ -125,110 +104,3 @@ pub(crate) fn parse_raw_output( } }); } - -fn string_looks_like_question(weidu_output: &str) -> bool { - let comparable_output = weidu_output.trim().to_ascii_lowercase(); - // installing|creating - if comparable_output.contains(WEIDU_USEFUL_STATUS[2]) - || comparable_output.contains(WEIDU_USEFUL_STATUS[4]) - { - return false; - } - - for question in WEIDU_CHOICE_PHRASE { - if comparable_output.contains(question) { - return true; - } - } - - for question in WEIDU_CHOICE_WORDS { - for word in comparable_output.split_whitespace() { - if word - .chars() - .filter(|c| c.is_alphabetic()) - .collect::() - == question - { - return true; - } - } - } - - false -} - -fn detect_weidu_finished_state(weidu_output: &str) -> Option { - let comparable_output = weidu_output.trim().to_lowercase(); - if comparable_output.contains(WEIDU_FAILED_WITH_ERROR) { - Some(State::CompletedWithErrors { - error_details: comparable_output, - }) - } else if comparable_output.contains(WEIDU_COMPLETED_WITH_WARNINGS) { - Some(State::CompletedWithWarnings) - } else if comparable_output.contains(WEIDU_FINISHED) || comparable_output.contains(EET_FINISHED) - { - Some(State::Completed) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_exit_warnings() { - let test = "INSTALLED WITH WARNINGS Additional equipment for Thieves and Bards"; - assert_eq!(string_looks_like_question(test), false); - assert_eq!( - detect_weidu_finished_state(test), - Some(State::CompletedWithWarnings) - ) - } - #[test] - fn test_exit_success() { - let test = "SUCCESSFULLY INSTALLED Jan's Extended Quest"; - assert_eq!(string_looks_like_question(test), false); - assert_eq!(detect_weidu_finished_state(test), Some(State::Completed)) - } - - #[test] - fn is_not_question() { - let test = "Creating epilogues. Too many epilogues... Why are there so many options here?"; - assert_eq!(string_looks_like_question(test), false); - let test = "Including file(s) spellchoices_defensive/vanilla/ENCHANTER.TPH"; - assert_eq!(string_looks_like_question(test), false); - } - - #[test] - fn is_a_question() { - let tests = vec!["Enter the full path to your Baldur's Gate installation then press Enter.", "Enter the full path to your BG:EE+SoD installation then press Enter.\ -Example: C:\\Program Files (x86)\\BeamDog\\Games\\00806", "[N]o, [Q]uit or choose one:", "Please enter the chance for items to randomly not be randomised as a integet number (e.g. 10 for 10%)"]; - for question in tests { - assert_eq!( - string_looks_like_question(question), - true, - "String {} doesn't look like a question", - question - ); - } - } - - #[test] - fn is_not_a_question() { - let tests = vec![ - "FAILURE:", - "NOT INSTALLED DUE TO ERRORS The BG1 NPC Project: Required Modifications", - ]; - for question in tests { - assert_eq!( - string_looks_like_question(question), - false, - "String {} does look like a question", - question - ); - } - } -}