Skip to main content

conduit_cli/core/mods/
inspector.rs

1use serde::Deserialize;
2use std::fs::File;
3use std::io::Read;
4use std::path::Path;
5use zip::ZipArchive;
6
7use crate::core::io::project::lock::ModSide;
8
9#[derive(Debug, Deserialize)]
10pub struct NeoForgeMetadata {
11    pub dependencies: Option<std::collections::HashMap<String, Vec<Dependency>>>,
12}
13
14#[derive(Debug, Deserialize)]
15pub struct NeoForgeModsList {
16    pub mods: Option<Vec<NeoForgeModEntry>>,
17}
18
19#[derive(Debug, Deserialize)]
20pub struct NeoForgeModEntry {
21    #[serde(rename = "modId")]
22    pub mod_id: String,
23}
24
25#[derive(Debug, Deserialize, Clone)]
26pub struct Dependency {
27    #[serde(rename = "modId")]
28    pub mod_id: String,
29    pub r#type: String,
30}
31
32#[derive(Debug, Deserialize)]
33pub struct JarJarMetadata {
34    pub jars: Vec<JarJarEntry>,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct JarJarEntry {
39    pub identifier: JarJarIdentifier,
40}
41
42#[derive(Debug, Deserialize)]
43pub struct JarJarIdentifier {
44    pub artifact: String,
45}
46
47#[derive(Debug, Deserialize)]
48pub struct NeoForgeModMetadata {
49    pub mods: Vec<NeoForgeModInfo>,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct NeoForgeModInfo {
54    #[serde(rename = "modId")]
55    pub mod_id: String,
56    pub side: Option<String>,
57}
58
59pub struct JarInspector;
60
61impl JarInspector {
62    pub fn inspect_neoforge<P: AsRef<Path>>(
63        path: P,
64    ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
65        let file = File::open(path)?;
66        let mut archive = ZipArchive::new(file)?;
67
68        let toml_content = {
69            let mut toml_file = archive.by_name("META-INF/neoforge.mods.toml")?;
70            let mut content = String::new();
71            toml_file.read_to_string(&mut content)?;
72            content
73        };
74
75        let decoded: NeoForgeMetadata = toml::from_str(&toml_content)?;
76
77        let mut embedded_mods = Vec::new();
78        if let Ok(mut jarjar_file) = archive.by_name("META-INF/jarjar/metadata.json") {
79            let mut jarjar_content = String::new();
80            jarjar_file.read_to_string(&mut jarjar_content)?;
81            if let Ok(jarjar_data) = serde_json::from_str::<JarJarMetadata>(&jarjar_content) {
82                for entry in jarjar_data.jars {
83                    let clean_id = entry
84                        .identifier
85                        .artifact
86                        .split('-')
87                        .next()
88                        .unwrap_or("")
89                        .to_string();
90                    embedded_mods.push(clean_id.to_lowercase());
91                    embedded_mods.push(entry.identifier.artifact.to_lowercase());
92                }
93            }
94        }
95
96        let mut required_deps = Vec::new();
97
98        if let Some(all_deps) = decoded.dependencies {
99            for (_, deps_list) in all_deps {
100                for dep in deps_list {
101                    let dep_id_lower = dep.mod_id.to_lowercase();
102
103                    if dep.r#type == "required"
104                        && dep_id_lower != "neoforge"
105                        && dep_id_lower != "minecraft"
106                        && !embedded_mods
107                            .iter()
108                            .any(|embedded| dep_id_lower.contains(embedded))
109                    {
110                        required_deps.push(dep.mod_id);
111                    }
112                }
113            }
114        }
115
116        required_deps.sort();
117        required_deps.dedup();
118
119        Ok(required_deps)
120    }
121
122    pub fn extract_primary_mod_id<P: AsRef<Path>>(
123        path: P,
124    ) -> Result<Option<String>, Box<dyn std::error::Error>> {
125        let file = File::open(path)?;
126        let mut archive = ZipArchive::new(file)?;
127
128        let toml_content = if let Ok(mut toml_file) = archive.by_name("META-INF/neoforge.mods.toml")
129        {
130            let mut content = String::new();
131            toml_file.read_to_string(&mut content)?;
132            content
133        } else if let Ok(mut toml_file) = archive.by_name("META-INF/mods.toml") {
134            let mut content = String::new();
135            toml_file.read_to_string(&mut content)?;
136            content
137        } else {
138            return Ok(None);
139        };
140
141        let decoded: NeoForgeModsList = toml::from_str(&toml_content)?;
142        let mod_id = decoded
143            .mods
144            .unwrap_or_default()
145            .into_iter()
146            .next()
147            .map(|m| m.mod_id);
148        Ok(mod_id)
149    }
150
151    pub fn detect_side<P: AsRef<Path>>(path: P) -> ModSide {
152        let Ok(file) = File::open(path) else {
153            return ModSide::Both;
154        };
155        let Ok(mut archive) = ZipArchive::new(file) else {
156            return ModSide::Both;
157        };
158
159        let toml_file = if let Ok(f) = archive.by_name("META-INF/neoforge.mods.toml") {
160            Ok(f)
161        } else {
162            archive.by_name("META-INF/mods.toml")
163        };
164
165        let Ok(mut toml_file) = toml_file else {
166            return ModSide::Both;
167        };
168
169        let mut content = String::new();
170        if toml_file.read_to_string(&mut content).is_err() {
171            return ModSide::Both;
172        }
173
174        if let Ok(decoded) = toml::from_str::<NeoForgeModMetadata>(&content)
175            && let Some(first_mod) = decoded.mods.first()
176                && let Some(s) = &first_mod.side {
177                    return match s.to_lowercase().as_str() {
178                        "client" => ModSide::Client,
179                        "server" => ModSide::Server,
180                        _ => ModSide::Both,
181                    };
182                }
183
184        ModSide::Both
185    }
186}