z_mirror/fmm

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

raiguardBroken half-implementation of dependencies7007399

dependencies
13.2 KiB422 linesraw
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 {
19    mods: Vec<ModListJsonMod>,
20}
21
22#[derive(Deserialize, Serialize)]
23struct ModListJsonMod {
24    name: String,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    version: Option<Version>,
27    enabled: bool,
28}
29
30#[derive(Deserialize, Debug)]
31struct InfoJson {
32    dependencies: Option<Vec<String>>,
33    name: String,
34    version: Version,
35}
36
37pub struct ModsSet {
38    #[allow(unused)]
39    dir: PathBuf,
40    mods: HashMap<String, Mod>,
41}
42
43impl ModsSet {
44    // TODO: Better error formatting so the user knows which mod threw the error
45    pub fn new(path: &PathBuf) -> Result<Self, Box<dyn Error>> {
46        // Read mod-list.json to a file
47        let mut mlj_path = path.clone();
48        mlj_path.push("mod-list.json");
49        let mlj_contents = std::fs::read_to_string(mlj_path)?;
50        let mut enabled_versions: HashMap<String, ModEnabledType> =
51            serde_json::from_str::<ModListJson>(&mlj_contents)?
52                .mods
53                .iter()
54                .filter_map(|entry| {
55                    Some((
56                        entry.name.clone(),
57                        match (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
66        let mut mods: HashMap<String, Mod> = HashMap::new();
67
68        // Iterate all mods in the directory
69        for entry in fs::read_dir(path)?.filter_map(|entry| {
70            // Exclude mod-list.json and mod-settings.dat
71            let entry = entry.ok()?;
72            let file_name = entry.file_name();
73            let file_name = file_name.to_str()?;
74            if file_name != "mod-list.json" && file_name != "mod-settings.dat" {
75                Some(entry)
76            } else {
77                None
78            }
79        }) {
80            // Determine the mod's structure type
81            let mod_structure = ModVersionStructure::parse(&entry)?;
82
83            // Extract contents of info.json file
84            let info: InfoJson = match mod_structure {
85                ModVersionStructure::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?
89                    find_info_json_in_zip(&entry)?
90                }
91                _ => {
92                    let mut path = entry.path();
93                    path.push("info.json");
94                    let contents = fs::read_to_string(path)?;
95                    let json: InfoJson = serde_json::from_str(&contents)?;
96                    json
97                }
98            };
99
100            // Retrive or create mod data
101            let mod_data = mods.entry(info.name.clone()).or_insert(Mod {
102                name: info.name.clone(),
103                versions: vec![],
104                enabled: {
105                    // Move the enabled status extracted from mod-list.json into the mod object
106                    let active_version = enabled_versions.remove(&info.name);
107                    match active_version {
108                        Some(enabled_type) => enabled_type,
109                        None => ModEnabledType::Disabled,
110                    }
111                },
112            });
113
114            // TODO: Optimize to not parse dependencies unless we need to insert the version
115            let mod_version = ModVersion {
116                entry,
117                dependencies: info
118                    .dependencies
119                    .unwrap_or(vec![])
120                    .iter()
121                    .map(ModDependency::new)
122                    .collect::<ModDependencyResult>()?,
123                structure: mod_structure,
124                version: info.version,
125            };
126
127            if let Err(index) = mod_data.versions.binary_search(&mod_version) {
128                mod_data.versions.insert(index, mod_version);
129            }
130        }
131
132        Ok(Self {
133            dir: path.clone(),
134            mods,
135        })
136    }
137
138    pub fn dedup(&mut self) -> Result<(), ModsSetErr> {
139        println!("Deduplicating zipped mod versions");
140
141        for (_, mod_data) in self.mods.iter_mut() {
142            for version_data in mod_data.versions.drain(..(mod_data.versions.len() - 1)) {
143                if let ModVersionStructure::Zip = version_data.structure {
144                    // TODO: Print the removal
145                    version_data.remove_from_disk()?;
146                }
147            }
148        }
149
150        Ok(())
151    }
152
153    pub fn disable_all(&mut self, include_base_mod: bool) {
154        println!("Disabling all mods");
155
156        self.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
162    pub fn disable(&mut self, mod_ident: &InputMod) -> Result<(), ModsSetErr> {
163        println!("Disabling {}", mod_ident);
164
165        let mod_data = self.get_mod(&mod_ident.name)?;
166
167        mod_data.enabled = ModEnabledType::Disabled;
168
169        Ok(())
170    }
171
172    pub fn enable_all(&mut self) {
173        println!("Enabling latest versions of all mods");
174
175        self.mods
176            .iter_mut()
177            .for_each(|(_, mod_data)| mod_data.enabled = ModEnabledType::Latest);
178    }
179
180    pub fn enable(&mut self, mod_ident: &InputMod) -> Result<Vec<InputMod>, ModsSetErr> {
181        println!("Enabling {}", mod_ident);
182
183        let 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.
188        let version_index = mod_data.find_version(&mod_ident.version)?;
189        // PANIC: If `find_version` returns an index, then the version is guaranteed to exist
190        let dependencies = &mod_data.versions.get(version_index).unwrap().dependencies;
191
192        let to_enable: Result<Vec<InputMod>, ModsSetErr> = dependencies
193            .iter()
194            .map(|dependency| {
195                let dependency_data = self.get_mod(&dependency.name)?;
196                let version_req = &dependency.version_req;
197
198                // Iterate from newest to oldest
199                for version in dependency_data.versions.iter().rev() {
200                    if version_req.is_none()
201                        || version_req.as_ref().unwrap().matches(&version.version)
202                    {
203                        return Ok(InputMod {
204                            name: dependency_data.name.clone(),
205                            version: ModEnabledType::Version(version.version.clone()),
206                        });
207                    }
208                }
209
210                Err(ModsSetErr::NoCompatibleDependencyVersionsFound)
211            })
212            .collect();
213        let to_enable = to_enable?;
214
215        Ok(to_enable)
216    }
217
218    pub fn remove(&mut self, mod_ident: &InputMod) -> Result<(), ModsSetErr> {
219        println!("Removing {}", mod_ident);
220
221        let mod_data = self.get_mod(&mod_ident.name)?;
222
223        // Extract the matching version from the versions table
224        let 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
232        Ok(())
233    }
234
235    pub fn write_mod_list(&mut self) -> Result<(), ModsSetErr> {
236        let info = ModListJson {
237            mods: self
238                .mods
239                .iter()
240                .map(|(_, mod_data)| {
241                    let (enabled, version) = match &mod_data.enabled {
242                        ModEnabledType::Disabled => (false, None),
243                        ModEnabledType::Latest => (true, None),
244                        ModEnabledType::Version(version) => (true, Some(version.clone())),
245                    };
246                    ModListJsonMod {
247                        enabled,
248                        name: mod_data.name.clone(),
249                        version,
250                    }
251                })
252                .collect(),
253        };
254
255        let 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
263        Ok(())
264    }
265
266    fn get_mod(&mut self, mod_name: &str) -> Result<&mut Mod, ModsSetErr> {
267        self.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}")]
276    CouldNotRemoveVersion(Version),
277    #[error("Could not write changes to mod-list.json")]
278    CouldNotWriteChanges,
279    #[error("Filesystem error")]
280    FilesystemError,
281    #[error("Invalid mod structure")]
282    InvalidModStructure,
283    #[error("Mod does not exist")]
284    ModDoesNotExist,
285    #[error("Version {0} does not exist")]
286    ModVersionDoesNotExist(Version),
287    #[error("No compatible dependency versions found TODO")]
288    NoCompatibleDependencyVersionsFound,
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>> {
293    let file = File::open(entry.path())?;
294    let 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
296    for i in 0..archive.len() {
297        let mut file = archive.by_index(i)?;
298        if file.name().contains("info.json") {
299            let mut contents = String::new();
300            file.read_to_string(&mut contents)?;
301            // FIXME: Doesn't work with special characters
302            let json: InfoJson = serde_json::from_str(&contents)?;
303            return Ok(json);
304        }
305    }
306    Err("Mod ZIP does not contain an info.json file".into())
307}
308
309#[derive(Debug)]
310struct Mod {
311    name: String,
312    versions: Vec<ModVersion>,
313    enabled: ModEnabledType,
314}
315
316impl Mod {
317    fn find_version(&self, version_ident: &ModEnabledType) -> Result<usize, ModsSetErr> {
318        match version_ident {
319            ModEnabledType::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 {
330    Disabled,
331    Latest,
332    Version(Version),
333}
334
335#[derive(Debug)]
336struct ModVersion {
337    entry: DirEntry,
338    // TODO: Use a HashSet for quick lookup?
339    dependencies: Vec<ModDependency>,
340    structure: ModVersionStructure,
341    version: Version,
342}
343
344impl ModVersion {
345    fn remove_from_disk(&self) -> Result<(), ModsSetErr> {
346        let entry = &self.entry;
347
348        if 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 {
362    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
363        self.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 {
374    fn cmp(&self, other: &Self) -> Ordering {
375        self.version.cmp(&other.version)
376    }
377}
378
379impl PartialEq for ModVersion {
380    fn eq(&self, other: &Self) -> bool {
381        self.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 {
395    Directory,
396    Symlink,
397    Zip,
398}
399
400impl ModVersionStructure {
401    fn parse(entry: &DirEntry) -> Result<Self, ModsSetErr> {
402        let path = entry.path();
403        let extension = path.extension();
404
405        if extension.is_some() && extension.unwrap() == OsStr::new("zip") {
406            return Ok(ModVersionStructure::Zip);
407        } else {
408            let file_type = entry.file_type().map_err(|_| ModsSetErr::FilesystemError)?;
409            if file_type.is_symlink() {
410                return Ok(ModVersionStructure::Symlink);
411            } else {
412                let mut path = entry.path();
413                path.push("info.json");
414                if path.exists() {
415                    return Ok(ModVersionStructure::Directory);
416                }
417            }
418        };
419
420        Err(ModsSetErr::InvalidModStructure)
421    }
422}