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