Skip to main content

conduit_cli/core/io/modpack/
conduit.rs

1use 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}