conduit_cli/core/mods/
inspector.rs1use 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}