z_mirror/fmm

git clone https://git.raiguard.me/z_mirror/fmm

raiguardRead mod-settings.dat (causing Rust ICE)216c7c7

rust-ice
9.9 KiB294 linesraw
1use anyhow::Result;
2use console::style;
3use std::collections::HashMap;
4use std::ffi::OsString;
5use std::fs;
6use std::fs::{DirEntry, File};
7use std::io::{Cursor, Read, Seek, SeekFrom};
8use std::path::PathBuf;
9
10use semver::{Version, VersionReq};
11use zip::ZipArchive;
12
13use crate::dependency::ModDependencyType;
14use crate::get_mod_version;
15use crate::read::PropertyTree;
16use crate::types::*;
17
18pub struct Directory {
19    pub mods: HashMap<String, Vec<ModEntry>>,
20    pub mod_list: Vec<ModListJsonMod>,
21    pub mod_list_path: PathBuf,
22    pub startup_settings: PropertyTree,
23}
24
25impl Directory {
26    pub fn new(path: PathBuf) -> Result<Self> {
27        // Get all mods in the directory
28        let mod_entries = fs::read_dir(&path)?
29            .filter_map(|entry| {
30                let entry = entry.ok()?;
31
32                if let Some((mod_name, version)) = parse_file_name(&entry.file_name()) {
33                    Some((mod_name, ModEntry { entry, version }))
34                } else {
35                    let info_json = read_info_json(&entry)?;
36                    Some((
37                        info_json.name,
38                        ModEntry {
39                            entry,
40                            version: info_json.version,
41                        },
42                    ))
43                }
44            })
45            .fold(HashMap::new(), |mut directory_mods, (mod_name, version)| {
46                let versions = directory_mods.entry(mod_name).or_insert_with(Vec::new);
47
48                let index = versions
49                    .binary_search(&version)
50                    .unwrap_or_else(|index| index);
51                versions.insert(index, version);
52
53                directory_mods
54            });
55
56        // Parse mod-list.json
57        let mut mlj_path = path.clone();
58        mlj_path.push("mod-list.json");
59        let enabled_versions = fs::read_to_string(&mlj_path)?;
60        let mod_list_json: ModListJson = serde_json::from_str(&enabled_versions)?;
61
62        // Parse mod-settings.dat
63        let mut settings_path = path;
64        settings_path.push("mod-settings.dat");
65        let mut settings_cursor = Cursor::new(fs::read(&mlj_path)?);
66        settings_cursor.seek(SeekFrom::Current(8))?;
67        let startup_settings = PropertyTree::load(&mut settings_cursor)?;
68
69        println!("{:#?}", startup_settings);
70
71        Ok(Self {
72            mods: mod_entries,
73            mod_list: mod_list_json.mods,
74            mod_list_path: mlj_path,
75            startup_settings,
76        })
77    }
78
79    /// Adds the mod, but keeps it disabled
80    pub fn add(&mut self, (mod_name, mod_entry): (String, ModEntry)) {
81        // Add or disable mod in mod-list.json
82        if let Some(mod_state) = self
83            .mod_list
84            .iter_mut()
85            .find(|mod_state| mod_name == mod_state.name)
86        {
87            mod_state.enabled = false;
88            mod_state.version = None;
89        } else {
90            self.mod_list.push(ModListJsonMod {
91                name: mod_name.clone(),
92                enabled: false,
93                version: None,
94            });
95        }
96
97        let entries = self.mods.entry(mod_name).or_default();
98        if let Err(index) = entries.binary_search(&mod_entry) {
99            entries.insert(index, mod_entry);
100        }
101    }
102
103    pub fn disable(&mut self, mod_ident: &ModIdent) {
104        if mod_ident.name == "base" || self.mods.contains_key(&mod_ident.name) {
105            let mod_state = self
106                .mod_list
107                .iter_mut()
108                .find(|mod_state| mod_ident.name == mod_state.name);
109
110            println!("{} {}", style("Disabled").yellow().bold(), &mod_ident);
111
112            if let Some(mod_state) = mod_state {
113                mod_state.enabled = false;
114                mod_state.version = None;
115            }
116        } else {
117            println!("Could not find {}", &mod_ident);
118        }
119    }
120
121    pub fn disable_all(&mut self) {
122        println!("{}", style("Disabled all mods").yellow().bold());
123
124        for mod_data in self
125            .mod_list
126            .iter_mut()
127            .filter(|mod_state| mod_state.name != "base")
128        {
129            mod_data.enabled = false;
130            mod_data.version = None;
131        }
132    }
133
134    pub fn enable(&mut self, mod_ident: &ModIdent) -> Option<Vec<ManageOrder>> {
135        if let Some(mod_entry) = self
136            .mods
137            .get(&mod_ident.name)
138            .and_then(|mod_entries| get_mod_version(mod_entries, mod_ident))
139        {
140            let mod_state = self
141                .mod_list
142                .iter_mut()
143                .find(|mod_state| mod_ident.name == mod_state.name);
144
145            let enabled = mod_state.is_some() && mod_state.as_ref().unwrap().enabled;
146
147            if !enabled {
148                println!(
149                    "{} {} v{}",
150                    style("Enabled").green().bold(),
151                    mod_ident.name,
152                    mod_entry.version
153                );
154
155                let version = mod_ident
156                    .version_req
157                    .as_ref()
158                    .map(|_| mod_entry.version.clone());
159
160                if let Some(mod_state) = mod_state {
161                    mod_state.enabled = true;
162                    mod_state.version = version;
163                } else {
164                    self.mod_list.push(ModListJsonMod {
165                        name: mod_ident.name.to_string(),
166                        enabled: true,
167                        version,
168                    });
169                }
170
171                return Some(
172                    read_info_json(&mod_entry.entry)
173                        .and_then(|info_json| info_json.dependencies)
174                        .unwrap_or_default()
175                        .iter()
176                        .filter(|dependency| {
177                            dependency.name != "base"
178                                && matches!(
179                                    dependency.dep_type,
180                                    ModDependencyType::NoLoadOrder | ModDependencyType::Required
181                                )
182                        })
183                        .map(|dependency| ModIdent {
184                            name: dependency.name.clone(),
185                            version_req: dependency.version_req.clone(),
186                        })
187                        .map(|dependency_ident| {
188                            self.mods
189                                .get(&dependency_ident.name)
190                                .and_then(|dependency_entries| {
191                                    get_mod_version(dependency_entries, &dependency_ident)
192                                })
193                                .map(|_| ManageOrder::Enable(dependency_ident.clone()))
194                                .unwrap_or_else(|| ManageOrder::Download(dependency_ident.clone()))
195                        })
196                        .collect(),
197                );
198            }
199        } else {
200            return Some(vec![ManageOrder::Download(mod_ident.clone())]);
201        }
202
203        None
204    }
205
206    pub fn enable_all(&mut self) {
207        println!(
208            "{}",
209            style("Enabled latest versions of all mods").green().bold()
210        );
211        for mod_data in self.mod_list.iter_mut() {
212            mod_data.enabled = true;
213            mod_data.version = None;
214        }
215    }
216
217    pub fn remove(&mut self, mod_ident: &ModIdent) {
218        let version_req = mod_ident
219            .version_req
220            .as_ref()
221            .cloned()
222            .unwrap_or_else(VersionReq::any);
223        if let Some(mod_versions) = self.mods.get(&mod_ident.name) {
224            mod_versions
225                .iter()
226                .filter(|version| version_req.matches(&version.version))
227                .for_each(|version| {
228                    let result = version.entry.metadata().and_then(|metadata| {
229                        if metadata.is_dir() {
230                            fs::remove_dir_all(version.entry.path())
231                        } else {
232                            fs::remove_file(version.entry.path())
233                        }
234                    });
235                    if result.is_ok() {
236                        println!(
237                            "{} {} v{}",
238                            style("Removed").red().bold(),
239                            &mod_ident.name,
240                            version.version
241                        );
242                    } else {
243                        println!("Could not remove {} v{}", &mod_ident.name, version.version);
244                    }
245                });
246            self.mods.remove(&mod_ident.name);
247        }
248
249        if let Some((index, _)) = self
250            .mod_list
251            .iter()
252            .enumerate()
253            .find(|(_, mod_state)| mod_ident.name == mod_state.name)
254        {
255            self.mod_list.remove(index);
256        }
257    }
258}
259
260fn parse_file_name(file_name: &OsString) -> Option<(String, Version)> {
261    let (name, version) = file_name
262        .to_str()?
263        .trim_end_matches(".zip")
264        .rsplit_once('_')?;
265
266    if let Ok(version) = Version::parse(version) {
267        Some((name.to_string(), version))
268    } else {
269        None
270    }
271}
272
273fn read_info_json(entry: &DirEntry) -> Option<InfoJson> {
274    let contents = match ModEntryStructure::parse(entry)? {
275        ModEntryStructure::Directory | ModEntryStructure::Symlink => {
276            let mut path = entry.path();
277            path.push("info.json");
278            fs::read_to_string(path).ok()?
279        }
280        ModEntryStructure::Zip => {
281            let mut archive = ZipArchive::new(File::open(entry.path()).ok()?).ok()?;
282            let filename = archive
283                .file_names()
284                .find(|name| name.contains("info.json"))
285                .map(ToString::to_string)?;
286            let mut file = archive.by_name(&filename).ok()?;
287            let mut contents = String::new();
288            file.read_to_string(&mut contents).ok()?;
289            contents
290        }
291    };
292
293    serde_json::from_str::<InfoJson>(&contents).ok()
294}