diff --git a/Cargo.lock b/Cargo.lock index cac7319..80f71d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -28,33 +28,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys", @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.9" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" dependencies = [ "clap_builder", "clap_derive", @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" dependencies = [ "anstream", "anstyle", @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ "heck", "proc-macro2", @@ -96,15 +96,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "diff" @@ -114,9 +114,9 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", @@ -155,9 +155,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "log" @@ -257,9 +257,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.68" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", diff --git a/src/args.rs b/src/args.rs index c4ea7e1..d01088a 100644 --- a/src/args.rs +++ b/src/args.rs @@ -14,7 +14,7 @@ Please provide a valid weidu logging setting, options are: #[clap(author, version, about, long_about = None)] pub struct Args { /// Full path to target log - #[clap(env, long, short = 'f', required = true)] + #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)] pub log_file: PathBuf, /// Full path to game directory @@ -84,14 +84,21 @@ fn parse_weidu_log_mode(arg: &str) -> Result { Ok(output.join(" ")) } +fn path_must_exist(arg: &str) -> Result { + let path = PathBuf::from(arg); + path.try_exists()?; + Ok(path) +} + fn parse_absolute_path(arg: &str) -> Result { - let path = Path::new(arg); + let path = path_must_exist(arg).map_err(|err| err.to_string())?; if path.is_absolute() { - Ok(path.to_path_buf()) + Ok(path) } else { - Err("Please provide the absolute path".to_string()) + Err("Please provide an absolute path".to_string()) } } + #[cfg(test)] mod tests { use super::*; diff --git a/src/component.rs b/src/component.rs index c8496a2..fe2ddc7 100644 --- a/src/component.rs +++ b/src/component.rs @@ -1,20 +1,14 @@ -use std::{ - fs::File, - io::{BufRead, BufReader}, - path::PathBuf, -}; - // This should mirror the weidu component // https://github.com/WeiDUorg/weidu/blob/devel/src/tp.ml#L98 #[derive(Debug, PartialEq, PartialOrd, Clone)] -pub struct Component { - pub tp_file: String, - pub name: String, - pub lang: String, - pub component: String, - pub component_name: String, - pub sub_component: String, - pub version: String, +pub(crate) struct Component { + pub(crate) tp_file: String, + pub(crate) name: String, + pub(crate) lang: String, + pub(crate) component: String, + pub(crate) component_name: String, + pub(crate) sub_component: String, + pub(crate) version: String, } impl From for Component { @@ -116,88 +110,10 @@ impl From for Component { } } -pub fn parse_weidu_log(weidu_log_path: PathBuf) -> Vec { - let file = File::open(weidu_log_path).expect("Could not open weidu log exiting"); - let reader = BufReader::new(file); - - reader - .lines() - .flat_map(|line| match line { - // Ignore comments and empty lines - Ok(component) - if !component.is_empty() - && !component.starts_with('\n') - && !component.starts_with("//") => - { - Some(Component::from(component)) - } - _ => None, - }) - .collect() -} - #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; - use std::path::Path; - - #[test] - fn test_parse_weidu_log() { - let test_log = Path::new("fixtures/test.log"); - let logs = parse_weidu_log(test_log.to_path_buf()); - assert_eq!( - logs, - vec![ - Component { - tp_file: "TEST.TP2".to_string(), - name: "test_mod_name_1".to_string(), - lang: "0".to_string(), - component: "0".to_string(), - component_name: "test mod one".to_string(), - sub_component: "".to_string(), - version: "".to_string() - }, - Component { - tp_file: "TEST.TP2".to_string(), - name: "test_mod_name_1".to_string(), - lang: "0".to_string(), - component: "1".to_string(), - component_name: "test mod two".to_string(), - sub_component: "".to_string(), - version: "".to_string() - }, - Component { - tp_file: "END.TP2".to_string(), - name: "test_mod_name_2".to_string(), - lang: "0".to_string(), - component: "0".to_string(), - component_name: "test mod with subcomponent information".to_string(), - sub_component: "Standard installation".to_string(), - version: "".to_string() - }, - Component { - tp_file: "END.TP2".to_string(), - name: "test_mod_name_3".to_string(), - lang: "0".to_string(), - component: "0".to_string(), - component_name: "test mod with version".to_string(), - sub_component: "".to_string(), - version: "1.02".to_string() - }, - Component { - tp_file: "TWEAKS.TP2".to_string(), - name: "test_mod_name_4".to_string(), - lang: "0".to_string(), - component: "3346".to_string(), - component_name: "test mod with both subcomponent information and version" - .to_string(), - sub_component: "Casting speed only".to_string(), - version: "v16".to_string() - } - ] - ); - } #[test] fn test_parse_windows() { diff --git a/src/log_file.rs b/src/log_file.rs new file mode 100644 index 0000000..860dc3e --- /dev/null +++ b/src/log_file.rs @@ -0,0 +1,121 @@ +use std::{ + fs::File, + io::{self, BufRead, BufReader}, + path::PathBuf, + slice::Iter, +}; + +use crate::component::Component; + +#[derive(Debug, PartialEq, PartialOrd)] +pub(crate) struct LogFile(Vec); + +impl LogFile { + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + pub(crate) fn retain(&mut self, mut f: F) + where + F: FnMut(&Component) -> bool, + { + self.0.retain_mut(|elem| f(elem)); + } +} + +impl<'a> IntoIterator for &'a LogFile { + type Item = &'a Component; + type IntoIter = Iter<'a, Component>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl TryFrom for LogFile { + type Error = io::Error; + + fn try_from(value: PathBuf) -> Result { + let file = File::open(value)?; + let reader = BufReader::new(file); + + Ok(LogFile( + reader + .lines() + .flat_map(|line| match line { + // Ignore comments and empty lines + Ok(component) + if !component.is_empty() + && !component.starts_with('\n') + && !component.starts_with("//") => + { + Some(Component::from(component)) + } + _ => None, + }) + .collect(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use std::path::PathBuf; + + #[test] + fn test_parse_weidu_log() { + let test_log = PathBuf::from("fixtures/test.log"); + let result = LogFile::try_from(test_log).unwrap(); + let expected = LogFile(vec![ + Component { + tp_file: "TEST.TP2".to_string(), + name: "test_mod_name_1".to_string(), + lang: "0".to_string(), + component: "0".to_string(), + component_name: "test mod one".to_string(), + sub_component: "".to_string(), + version: "".to_string(), + }, + Component { + tp_file: "TEST.TP2".to_string(), + name: "test_mod_name_1".to_string(), + lang: "0".to_string(), + component: "1".to_string(), + component_name: "test mod two".to_string(), + sub_component: "".to_string(), + version: "".to_string(), + }, + Component { + tp_file: "END.TP2".to_string(), + name: "test_mod_name_2".to_string(), + lang: "0".to_string(), + component: "0".to_string(), + component_name: "test mod with subcomponent information".to_string(), + sub_component: "Standard installation".to_string(), + version: "".to_string(), + }, + Component { + tp_file: "END.TP2".to_string(), + name: "test_mod_name_3".to_string(), + lang: "0".to_string(), + component: "0".to_string(), + component_name: "test mod with version".to_string(), + sub_component: "".to_string(), + version: "1.02".to_string(), + }, + Component { + tp_file: "TWEAKS.TP2".to_string(), + name: "test_mod_name_4".to_string(), + lang: "0".to_string(), + component: "3346".to_string(), + component_name: "test mod with both subcomponent information and version" + .to_string(), + sub_component: "Casting speed only".to_string(), + version: "v16".to_string(), + }, + ]); + assert_eq!(expected, result) + } +} diff --git a/src/main.rs b/src/main.rs index dc3a663..5624756 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,18 +3,16 @@ use std::collections::HashMap; use args::Args; use clap::Parser; use env_logger::Env; +use log_file::LogFile; use crate::{ - component::parse_weidu_log, - utils::{ - copy_mod_folder, create_weidu_log_if_not_exists, mod_folder_present_in_game_directory, - search_mod_folders, - }, + utils::{copy_mod_folder, mod_folder_present_in_game_directory, search_mod_folders}, weidu::{install, InstallationResult}, }; mod args; mod component; +mod log_file; mod state; mod utils; mod weidu; @@ -32,22 +30,16 @@ fn main() { ); let args = Args::parse(); - let installed_log_path = create_weidu_log_if_not_exists(&args.game_directory); - - let mods = parse_weidu_log(args.log_file); + let mut mods = LogFile::try_from(args.log_file).expect("Could not open log file"); let number_of_mods_found = mods.len(); let mods_to_be_installed = if args.skip_installed { - let installed_mods = parse_weidu_log(installed_log_path); - mods.iter() - .filter_map(|weidu_mod| { - if !installed_mods.contains(weidu_mod) { - log::debug!("Mod to be installed {:?}", weidu_mod); - Some(weidu_mod.clone()) - } else { - None - } - }) - .collect() + let existing_weidu_log_file_path = args.game_directory.join("weidu").with_extension("log"); + if let Ok(installed_mods) = LogFile::try_from(existing_weidu_log_file_path) { + for installed_mod in &installed_mods { + mods.retain(|mod_to_install| installed_mod != mod_to_install); + } + } + mods } else { mods }; @@ -59,12 +51,10 @@ fn main() { ); let mut mod_folder_cache = HashMap::new(); - for weidu_mod in mods_to_be_installed { + for weidu_mod in &mods_to_be_installed { let mod_folder = mod_folder_cache .entry(weidu_mod.tp_file.clone()) - .or_insert_with(|| { - search_mod_folders(&args.mod_directories, &weidu_mod.clone(), args.depth) - }); + .or_insert_with(|| search_mod_folders(&args.mod_directories, weidu_mod, args.depth)); log::debug!("Found mod folder {:?}, for mod {:?}", mod_folder, weidu_mod); @@ -80,7 +70,7 @@ fn main() { match install( &args.weidu_binary, &args.game_directory, - &weidu_mod, + weidu_mod, &args.language, &args.weidu_log_mode, args.timeout, @@ -96,10 +86,10 @@ fn main() { } InstallationResult::Warnings => { if args.abort_on_warnings { - log::error!("Installed mod {:?} with warnings, stopping", &weidu_mod); + log::error!("Installed mod {:?} with warnings, stopping", weidu_mod); break; } else { - log::warn!("Installed mod {:?} with warnings, keep going", &weidu_mod); + log::warn!("Installed mod {:?} with warnings, keep going", weidu_mod); } } } diff --git a/src/utils.rs b/src/utils.rs index 46c642e..320c7c5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,6 @@ use core::time; use fs_extra::dir::{copy, CopyOptions}; use std::{ - fs::File, path::{Path, PathBuf}, thread, }; @@ -9,14 +8,6 @@ use walkdir::WalkDir; use crate::component::Component; -pub fn create_weidu_log_if_not_exists(game_directory: &Path) -> PathBuf { - let weidu_log_file = game_directory.join("weidu").with_extension("log"); - if !weidu_log_file.exists() { - File::create(weidu_log_file.clone()).unwrap(); - } - weidu_log_file -} - pub fn mod_folder_present_in_game_directory(game_directory: &Path, mod_name: &str) -> bool { game_directory.join(mod_name).is_dir() } diff --git a/src/weidu.rs b/src/weidu.rs index 26f49d3..f041bd9 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -13,7 +13,7 @@ use std::{ use crate::{component::Component, state::State, utils::sleep, weidu_parser::parse_raw_output}; -pub fn get_user_input() -> String { +pub(crate) fn get_user_input() -> String { let stdin = io::stdin(); let mut input = String::new(); stdin.read_line(&mut input).unwrap_or_default(); @@ -34,13 +34,13 @@ fn generate_args(weidu_mod: &Component, weidu_log_mode: &str, language: &str) -> .collect() } -pub enum InstallationResult { +pub(crate) enum InstallationResult { Success, Warnings, Fail(String), } -pub fn install( +pub(crate) fn install( weidu_binary: &PathBuf, game_directory: &PathBuf, weidu_mod: &Component, @@ -62,7 +62,7 @@ pub fn install( handle_io(child, timeout) } -pub fn handle_io(mut child: Child, timeout: usize) -> InstallationResult { +pub(crate) fn handle_io(mut child: Child, 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()); diff --git a/src/weidu_parser.rs b/src/weidu_parser.rs index b4db7a6..3165b7f 100644 --- a/src/weidu_parser.rs +++ b/src/weidu_parser.rs @@ -43,7 +43,7 @@ enum ParserState { LookingForInterestingOutput, } -pub fn parse_raw_output( +pub(crate) fn parse_raw_output( sender: Sender, receiver: Receiver, wait_count: Arc,