z_mirror/fmm
git clone https://git.raiguard.me/z_mirror/fmm
59fb9e2
rewrite
1use once_cell:: sync:: OnceCell ; 2use regex:: Regex ; 3use semver:: Version ; 4use serde::{ Deserialize , Serialize }; 5use std:: collections:: HashMap ; 6use std:: error:: Error ; 7use std:: ffi:: OsStr ; 8use std:: fs; 9use std:: fs::{ DirEntry , File }; 10use std:: io:: Read ; 11use std:: path::{ Path , PathBuf }; 12use std::{ cmp:: Ordering , collections:: HashSet }; 13use thiserror:: Error ; 14use zip:: ZipArchive ; 15 16use crate :: dependency::{ ModDependency , ModDependencyResult , ModDependencyType }; 17use crate :: input:: InputMod ; 18 19# [ derive ( Deserialize , Serialize )] 20struct ModListJson { 21mods : Vec < ModListJsonMod >, 22} 23 24# [ derive ( Deserialize , Serialize )] 25struct ModListJsonMod { 26name : String , 27# [ serde ( skip_serializing_if = "Option::is_none" )] 28version : Option < Version >, 29enabled : bool , 30} 31 32# [ derive ( Deserialize , Debug )] 33struct InfoJson { 34dependencies : Option < Vec < String >>, 35name : String , 36version : Version , 37} 38 39pub struct ModsSet { 40dir : PathBuf , 41mods : HashMap < String , Mod >, 42} 43 44impl ModsSet { 45// TODO: Better error formatting so the user knows which mod threw the error 46pub fn new ( path : & Path ) ->Result < Self , Box < dyn Error >> { 47// Read mod-list.json to a file 48let mut mlj_path = path. to_owned (); 49 mlj_path. push ( "mod-list.json" ); 50let mlj_contents = std:: fs:: read_to_string ( mlj_path) ?; 51let mut enabled_versions: HashMap < String , ModEnabledType > = 52 serde_json:: from_str ::< ModListJson >( & mlj_contents) ? 53. mods 54. iter () 55. map ( |entry|{ 56( 57 entry. name . clone (), 58match ( entry. enabled , & entry. version ) { 59( true , Some ( version)) =>ModEnabledType :: Version ( version. clone ()), 60( true , None ) =>ModEnabledType :: Latest , 61 _ =>ModEnabledType :: Disabled , 62}, 63) 64}) 65. collect (); 66 67let mut mods: HashMap < String , Mod > =HashMap :: new (); 68 69// Iterate all mods in the directory 70for entryin fs:: read_dir ( path) ?. filter_map ( |entry|{ 71// Exclude mod-list.json and mod-settings.dat 72let entry = entry. ok () ?; 73let file_name = entry. file_name (); 74let file_name = file_name. to_str () ?; 75if file_name !="mod-list.json" && file_name !="mod-settings.dat" { 76Some ( entry) 77} else { 78None 79} 80}) { 81// Determine the mod's structure type 82let mod_structure =ModVersionStructure :: parse ( & entry) ?; 83 84// Extract contents of info.json file 85let info =match mod_structure{ 86ModVersionStructure :: Zip =>{ 87// WORKAROUND: The `zip` crate doesn't have nice iterator methods, so we need to 88// early-return out of a `for` loop, necessitating a separate function 89find_info_json_in_zip ( & entry) ? 90} 91 _ =>{ 92let mut path = entry. path (); 93 path. push ( "info.json" ); 94 fs:: read_to_string ( path) ? 95} 96}; 97 98// Remove all non-UTF8 characters from the string 99let info = info. replace ( |c : char | !c. is_ascii (), "" ); 100 101// Parse info string into a struct 102match serde_json:: from_str ::< InfoJson >( & info) { 103Ok ( info) =>{ 104// Retrive or create mod data 105let mod_data = mods. entry ( info. name . clone ()). or_insert ( Mod { 106name : info. name . clone (), 107versions : vec! [], 108enabled : { 109// Move the enabled status extracted from mod-list.json into the mod object 110let active_version = enabled_versions. remove ( & info. name ); 111match active_version{ 112Some ( enabled_type) => enabled_type, 113None =>ModEnabledType :: Disabled , 114} 115}, 116}); 117 118// TODO: Optimize to not parse dependencies unless we need to insert the version 119let mod_version =ModVersion { 120 entry, 121dependencies : info 122. dependencies 123. unwrap_or_default () 124. iter () 125. map ( |dep|ModDependency :: new ( & dep)) 126. collect ::< ModDependencyResult >() ?, 127structure : mod_structure, 128version : info. version , 129}; 130 131if let Err ( index) = mod_data. versions . binary_search ( & mod_version) { 132 mod_data. versions . insert ( index, mod_version); 133} 134} 135Err ( _) =>{ 136if let Some ( file_name) = entry. file_name (). to_owned (). to_str () { 137println! ( "Could not read mod: {}" , file_name); 138let file_name = file_name. replace ( |c : char | !c. is_ascii (), "" ); 139// Avoid creating the regex object every time 140static FILENAME_REGEX : OnceCell < Regex > =OnceCell :: new (); 141let captures =FILENAME_REGEX 142. get_or_init ( ||{ 143Regex :: new ( r"^(?P<name>.*)_(?P<version>\d*\.\d*\.\d*\.)$" ). unwrap () 144}) 145. captures ( & file_name) 146. ok_or ( ModsSetErr :: ModFilenameUnreadable ); 147// TODO: Keep a list of invalid mods 148// Perhaps we need to re-think the arcitecture to keep active versions and the actual versions separate 149} 150} 151} 152} 153 154Ok ( Self { 155dir : path. to_owned (), 156 mods, 157}) 158} 159 160pub fn dedup ( & mut self ) ->Result <(), ModsSetErr > { 161println! ( "Deduplicating zipped mod versions" ); 162 163for ( _, mod_data) in self . mods . iter_mut () { 164for version_datain mod_data. versions . drain ( ..( mod_data. versions . len () -1 )) { 165if let ModVersionStructure :: Zip = version_data. structure { 166// TODO: Print the removal 167 version_data. remove_from_disk () ?; 168} 169} 170} 171 172Ok (()) 173} 174 175pub fn disable_all ( & mut self , include_base_mod : bool ) { 176println! ( "Disabling all mods" ); 177 178self . mods 179. iter_mut () 180. filter ( |( mod_name, _) | include_base_mod || mod_name. as_str () !="base" ) 181. for_each ( |( _, mod_data) | mod_data. enabled =ModEnabledType :: Disabled ); 182} 183 184pub fn disable ( & mut self , mod_ident : & InputMod ) ->Result <(), ModsSetErr > { 185println! ( "Disabling {}" , mod_ident); 186 187let mod_data =self . get_mod_mut ( & mod_ident. name ) ?; 188 189 mod_data. enabled =ModEnabledType :: Disabled ; 190 191Ok (()) 192} 193 194pub fn enable_all ( & mut self ) { 195println! ( "Enabling latest versions of all mods" ); 196 197self . mods 198. iter_mut () 199. for_each ( |( _, mod_data) | mod_data. enabled =ModEnabledType :: Latest ); 200} 201 202pub fn enable ( & mut self , mod_ident : & InputMod ) ->Result < Vec < InputMod >, ModsSetErr > { 203println! ( "Enabling {}" , mod_ident); 204 205let mod_data =self . get_mod_mut ( & mod_ident. name ) ?; 206 207// Enable this mod 208 mod_data. enabled =match & mod_ident. version { 209ModEnabledType :: Version ( version) =>{ 210if mod_data 211. versions 212. binary_search_by ( |stored_version| stored_version. version . cmp ( version)) 213. is_ok () 214{ 215// TODO: Remove clone? 216Ok ( ModEnabledType :: Version ( version. clone ())) 217} else { 218Err ( ModsSetErr :: ModVersionDoesNotExist ( version. clone ())) 219} 220} 221 _ =>Ok ( ModEnabledType :: Latest ), 222} ?; 223 224// Return a list of dependencies to enable 225let mod_data =self . get_mod ( & mod_ident. name ) ?; 226let active_version = mod_data. get_active_version () ?. unwrap (); 227 active_version 228. dependencies 229. iter () 230. filter ( |dependency_ident|{ 231 dependency_ident. name !="base" 232 &&matches! ( 233 dependency_ident. dep_type , 234ModDependencyType :: NoLoadOrder |ModDependencyType :: Required 235) 236}) 237. map ( |dependency_ident|{ 238let dependency =self 239. get_mod ( & dependency_ident. name ) 240// TODO: More explanative errors 241. map_err ( |_|ModsSetErr :: MatchingDependencyNotFound ) ?; 242let version =if dependency_ident. version_req . is_none () { 243 dependency. versions . last () 244} else { 245let req = dependency_ident. version_req . as_ref (). unwrap (); 246 dependency 247. versions 248. iter () 249. rev () 250. find ( |version| req. matches ( & version. version )) 251}; 252 253if version. is_none () { 254return Err ( ModsSetErr :: MatchingDependencyNotFound ); 255} 256Ok ( InputMod { 257name : dependency. name . clone (), 258version : ModEnabledType :: Version ( version. unwrap (). version . clone ()), 259}) 260}) 261. collect ::< Result < Vec < InputMod >, ModsSetErr >>() 262} 263 264pub fn enable_list ( & mut self , initial_to_enable : Vec < InputMod >) ->Result <(), ModsSetErr > { 265let mut to_enable = initial_to_enable; 266let mut did_enable: HashSet < String > =HashSet :: new (); 267 268while !to_enable. is_empty () { 269let mut to_enable_next =Vec :: new (); 270// Enable all of the mods 271for mod_identin & to_enable{ 272if did_enable. get ( & mod_ident. name ). is_none () { 273 to_enable_next. append ( & mut self . enable ( & mod_ident) ?); 274 did_enable. insert ( mod_ident. name . clone ()); 275} 276} 277// Replace `to_enable` 278 to_enable = to_enable_next; 279} 280Ok (()) 281} 282 283pub fn remove ( & mut self , mod_ident : & InputMod ) ->Result <(), ModsSetErr > { 284println! ( "Removing {}" , mod_ident); 285 286let mod_data =self . get_mod_mut ( & mod_ident. name ) ?; 287 288// Extract the matching version from the versions table 289let version_data = mod_data 290. versions 291. remove ( mod_data. find_version ( & mod_ident. version ) ?); 292 293 version_data. remove_from_disk () ?; 294 295// TODO: Remove mod entry if there are no more versions 296 297Ok (()) 298} 299 300pub fn write_mod_list ( & mut self ) ->Result <(), ModsSetErr > { 301let info =ModListJson { 302mods : self 303. mods 304. iter () 305. map ( |( _, mod_data) |{ 306let ( enabled, version) =match & mod_data. enabled { 307ModEnabledType :: Disabled =>( false , None ), 308ModEnabledType :: Latest =>( true , None ), 309ModEnabledType :: Version ( version) =>( true , Some ( version. clone ())), 310}; 311ModListJsonMod { 312 enabled, 313name : mod_data. name . clone (), 314 version, 315} 316}) 317. collect (), 318}; 319 320let mut path =self . dir . clone (); 321 path. push ( "mod-list.json" ); 322 fs:: write ( 323 path, 324 serde_json:: to_string_pretty ( & info). map_err ( |_|ModsSetErr :: CouldNotWriteChanges ) ?, 325) 326. map_err ( |_|ModsSetErr :: CouldNotWriteChanges ) ?; 327 328Ok (()) 329} 330 331fn get_mod_mut ( & mut self , mod_name : & str ) ->Result < & mut Mod , ModsSetErr > { 332self . mods 333. get_mut ( mod_name) 334. ok_or ( ModsSetErr :: ModDoesNotExist ) 335} 336 337fn get_mod ( & self , mod_name : & str ) ->Result < & Mod , ModsSetErr > { 338self . mods . get ( mod_name). ok_or ( ModsSetErr :: ModDoesNotExist ) 339} 340} 341 342# [ derive ( Debug , Error )] 343pub enum ModsSetErr { 344# [ error ( "Could not remove version {0}" )] 345CouldNotRemoveVersion ( Version ), 346# [ error ( "Could not write changes to mod-list.json" )] 347CouldNotWriteChanges , 348# [ error ( "Filesystem error" )] 349FilesystemError , 350# [ error ( "Invalid mod structure" )] 351InvalidModStructure , 352# [ error ( "Matching dependency not found" )] 353MatchingDependencyNotFound , 354# [ error ( "Mod does not exist" )] 355ModDoesNotExist , 356# [ error ( "Version {0} does not exist" )] 357ModVersionDoesNotExist ( Version ), 358# [ error ( "Could not read mod file name {0}" )] 359ModFilenameUnreadable ( String ), 360} 361 362// The `zip` crate doesn't have proper iterator methods, so we must use a bare `for` loop and early return 363fn find_info_json_in_zip ( entry : & DirEntry ) ->Result < String , Box < dyn Error >> { 364let file =File :: open ( entry. path ()) ?; 365let mut archive =ZipArchive :: new ( file) ?; 366// Thus, we need to use a bare `for` loop and iterate the indices, then act on the file if we find it 367for iin 0 ..archive. len () { 368let mut file = archive. by_index ( i) ?; 369if file. name (). contains ( "info.json" ) { 370let mut contents =String :: new (); 371 file. read_to_string ( & mut contents) ?; 372return Ok ( contents); 373} 374} 375Err ( "Mod ZIP does not contain an info.json file" . into ()) 376} 377 378# [ derive ( Debug )] 379struct Mod { 380name : String , 381versions : Vec < ModVersion >, 382enabled : ModEnabledType , 383} 384 385impl Mod { 386fn find_version ( & self , version_ident : & ModEnabledType ) ->Result < usize , ModsSetErr > { 387match version_ident{ 388ModEnabledType :: Version ( version) =>self 389. versions 390. binary_search_by ( |stored_version| stored_version. version . cmp ( version)) 391. map_err ( |_|ModsSetErr :: ModVersionDoesNotExist ( version. clone ())), 392 _ =>Ok ( self . versions . len () -1 ), 393} 394} 395 396fn get_active_version ( & self ) ->Result < Option < & ModVersion >, ModsSetErr > { 397Ok ( self . versions . get ( self . find_version ( & self . enabled ) ?)) 398} 399} 400 401# [ derive ( Clone , Debug )] 402pub enum ModEnabledType { 403Disabled , 404Latest , 405Version ( Version ), 406} 407 408# [ derive ( Debug )] 409struct ModVersion { 410entry : DirEntry , 411// TODO: Use a HashSet for quick lookup? 412dependencies : Vec < ModDependency >, 413structure : ModVersionStructure , 414version : Version , 415} 416 417impl ModVersion { 418fn remove_from_disk ( & self ) ->Result <(), ModsSetErr > { 419let entry =& self . entry ; 420 421if entry 422. metadata () 423. map_err ( |_|ModsSetErr :: FilesystemError ) ? 424. is_dir () 425{ 426 fs:: remove_dir_all ( entry. path ()) 427} else { 428 fs:: remove_file ( entry. path ()) 429} 430. map_err ( |_|ModsSetErr :: CouldNotRemoveVersion ( self . version . clone ())) 431} 432} 433 434impl PartialOrd for ModVersion { 435fn partial_cmp ( & self , other : & Self ) ->Option < Ordering > { 436self . version . partial_cmp ( & other. version ) 437} 438} 439 440// impl PartialOrd<Version> for ModVersion { 441// fn partial_cmp(&self, other: &Version) -> Option<Ordering> { 442// self.version.partial_cmp(other) 443// } 444// } 445 446impl Ord for ModVersion { 447fn cmp ( & self , other : & Self ) ->Ordering { 448self . version . cmp ( & other. version ) 449} 450} 451 452impl PartialEq for ModVersion { 453fn eq ( & self , other : & Self ) ->bool { 454self . version == other. version 455} 456} 457 458// impl PartialEq<Version> for ModVersion { 459// fn eq(&self, other: &Version) -> bool { 460// self.version == *other 461// } 462// } 463 464impl Eq for ModVersion {} 465 466# [ derive ( Debug )] 467enum ModVersionStructure { 468Directory , 469Invalid , 470Symlink , 471Zip , 472} 473 474impl ModVersionStructure { 475fn parse ( entry : & DirEntry ) ->Result < Self , ModsSetErr > { 476let path = entry. path (); 477let extension = path. extension (); 478 479if extension. is_some () && extension. unwrap () ==OsStr :: new ( "zip" ) { 480return Ok ( ModVersionStructure :: Zip ); 481} else { 482let file_type = entry. file_type (). map_err ( |_|ModsSetErr :: FilesystemError ) ?; 483if file_type. is_symlink () { 484return Ok ( ModVersionStructure :: Symlink ); 485} else { 486let mut path = entry. path (); 487 path. push ( "info.json" ); 488if path. exists () { 489return Ok ( ModVersionStructure :: Directory ); 490} 491} 492}; 493 494Err ( ModsSetErr :: InvalidModStructure ) 495} 496}