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