conduit_cli/core/io/modpack/
conduit.rs1use std::collections::HashSet;
2use std::fs::{self, File};
3use std::io::{Write, copy};
4use std::path::Path;
5use walkdir::WalkDir;
6use zip::write::SimpleFileOptions;
7
8use crate::core::error::{CoreError, CoreResult};
9use crate::core::events::{CoreCallbacks, CoreEvent};
10use crate::core::io::modpack::metadata::{
11 ConduitInfo, ConduitPackMetadata, ContentFlags, PackInfo,
12};
13use crate::core::io::modpack::{ModpackProvider, PackAnalysis};
14use crate::core::io::project::ProjectFiles;
15use crate::core::paths::CorePaths;
16
17pub struct ConduitProvider;
18
19impl ConduitProvider {
20 fn is_dangerous(extension: &str) -> bool {
21 let blacklist = [
22 "exe", "bat", "sh", "py", "js", "vbs", "msi", "com", "cmd", "scr",
23 ];
24 blacklist.contains(&extension.to_lowercase().as_str())
25 }
26}
27
28impl ModpackProvider for ConduitProvider {
29 fn export(
30 &self,
31 paths: &CorePaths,
32 output_path: &Path,
33 include_config: bool,
34 ) -> CoreResult<()> {
35 let manifest = ProjectFiles::load_manifest(paths)?;
36 let mut local_mod_filenames = HashSet::new();
37
38 if paths.lock_path().exists() {
39 let lock = ProjectFiles::load_lock(paths)?;
40 for (slug, locked_mod) in lock.locked_mods {
41 if slug.starts_with("local:") || slug.starts_with("f:") || slug.starts_with("file:")
42 {
43 local_mod_filenames.insert(locked_mod.filename);
44 }
45 }
46 }
47
48 let file = File::create(output_path)?;
49 let mut zip = zip::ZipWriter::new(file);
50 let options =
51 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
52
53 zip.start_file("conduit.json", options)?;
54 zip.write_all(manifest.to_json()?.as_bytes())?;
55
56 if paths.lock_path().exists() {
57 let lock = ProjectFiles::load_lock(paths)?;
58 zip.start_file("conduit.lock", options)?;
59 zip.write_all(lock.to_toml_with_header()?.as_bytes())?;
60 }
61
62 let meta = ConduitPackMetadata {
63 conduit: ConduitInfo {
64 version: env!("CARGO_PKG_VERSION").to_string(),
65 format_version: 1,
66 },
67 pack: PackInfo {
68 title: manifest.name.clone(),
69 creator: Some("Conduit User".to_string()),
70 description: None,
71 homepage: None,
72 repository: None,
73 pack_type: manifest.instance_type,
74 },
75 content: ContentFlags {
76 has_configs: include_config && paths.project_dir().join("config").exists(),
77 has_mods_overrides: !local_mod_filenames.is_empty(),
78 },
79 };
80
81 zip.start_file("metadata.toml", options)?;
82 zip.write_all(meta.to_toml()?.as_bytes())?;
83
84 let mut folders = Vec::new();
85 if include_config {
86 folders.push("config");
87 }
88 if paths.mods_dir().exists() {
89 folders.push("mods");
90 }
91
92 for folder_name in folders {
93 let folder_path = paths.project_dir().join(folder_name);
94 let is_mods_folder = folder_name == "mods";
95
96 for entry in WalkDir::new(&folder_path)
97 .into_iter()
98 .filter_map(Result::ok)
99 {
100 if entry.path().is_file() {
101 let filename = entry.file_name().to_string_lossy().to_string();
102
103 if is_mods_folder && !local_mod_filenames.contains(&filename) {
104 continue;
105 }
106
107 let relative_path = entry
108 .path()
109 .strip_prefix(paths.project_dir())
110 .map_err(|_| CoreError::RuntimeError("Path error".into()))?;
111
112 let zip_path = format!(
113 "overrides/{}",
114 relative_path.to_string_lossy().replace('\\', "/")
115 );
116
117 zip.start_file(zip_path, options)?;
118 zip.write_all(&fs::read(entry.path())?)?;
119 }
120 }
121 }
122
123 zip.finish()
124 .map_err(|e| CoreError::RuntimeError(e.to_string()))?;
125 Ok(())
126 }
127
128 fn analyze(&self, input_path: &Path) -> CoreResult<PackAnalysis> {
129 let file = File::open(input_path)?;
130 let mut archive =
131 zip::ZipArchive::new(file).map_err(|e| CoreError::RuntimeError(e.to_string()))?;
132
133 let mut files = Vec::new();
134 let mut extensions = HashSet::new();
135 let mut suspicious = Vec::new();
136 let mut dangerous_count = 0;
137 let mut local_jars_count = 0;
138
139 for i in 0..archive.len() {
140 let file = archive
141 .by_index(i)
142 .map_err(|e| CoreError::RuntimeError(e.to_string()))?;
143 let name = file.name().to_string();
144
145 if file.is_file() {
146 files.push(name.clone());
147
148 if let Some(ext) = Path::new(&name).extension().and_then(|e| e.to_str()) {
149 let ext_lower = ext.to_lowercase();
150 extensions.insert(ext_lower.clone());
151
152 if Self::is_dangerous(&ext_lower) {
153 dangerous_count += 1;
154 suspicious.push(format!("[DANGER] {name}"));
155 }
156 }
157
158 if name.contains("overrides/")
159 && Path::new(&name)
160 .extension()
161 .is_some_and(|ext| ext.eq_ignore_ascii_case("jar"))
162 {
163 local_jars_count += 1;
164 suspicious.push(format!("[LOCAL JAR] {name}"));
165 }
166 }
167 }
168
169 Ok(PackAnalysis {
170 files,
171 extensions: extensions.into_iter().collect(),
172 dangerous_count,
173 local_jars_count,
174 suspicious_files: suspicious,
175 })
176 }
177
178 fn import(
179 &self,
180 paths: &CorePaths,
181 input_path: &Path,
182 callbacks: &mut dyn CoreCallbacks,
183 ) -> CoreResult<()> {
184 let file = File::open(input_path)?;
185 let mut archive = zip::ZipArchive::new(file)?;
186
187 let target_dir = paths.project_dir();
188 if !target_dir.exists() {
189 fs::create_dir_all(target_dir)?;
190 }
191
192 for i in 0..archive.len() {
193 let mut file = archive.by_index(i)?;
194 let raw_name = file.name().to_string();
195
196 let outpath = if let Some(stripped) = raw_name.strip_prefix("overrides/") {
197 target_dir.join(stripped)
198 } else {
199 target_dir.join(&raw_name)
200 };
201
202 if file.is_dir() {
203 fs::create_dir_all(&outpath)?;
204 } else {
205 if let Some(p) = outpath.parent() {
206 fs::create_dir_all(p)?;
207 }
208 let mut outfile = File::create(&outpath)?;
209 copy(&mut file, &mut outfile)?;
210
211 if Path::new(&raw_name)
212 .extension()
213 .is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
214 || Path::new(&raw_name)
215 .extension()
216 .is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
217 {
218 callbacks.on_event(CoreEvent::LinkedFile { filename: raw_name });
219 }
220 }
221 }
222
223 callbacks.on_event(CoreEvent::Success(
224 "Modpack imported successfully".to_string(),
225 ));
226 Ok(())
227 }
228}