1use colored::*;
2use mlua::{self, Lua, Table};
3use std::path::{Path, PathBuf};
4use zoi_core::utils;
5
6use ar::Archive as ArArchive;
7use flate2::read::GzDecoder;
8use sevenz_rust;
9use std::fs;
10use xz2::read::XzDecoder;
11use zip::ZipArchive;
12use zstd::stream::read::Decoder as ZstdDecoder;
13
14pub fn add_extract_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
24 let extract_fn =
25 lua.create_function(move |lua, (source, out_name): (String, Option<String>)| {
26 let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
27 let build_dir = Path::new(&build_dir_str);
28
29 let archive_file = if source.starts_with("http") {
30 if source.starts_with("http://") && !quiet {
31 println!("{}: downloading over insecure HTTP: {}", "Warning:".yellow(), source);
32 }
33 if !quiet {
34 println!("Downloading: {}", source);
35 }
36 let file_name = source.split('/').next_back().unwrap_or("download.tmp");
37 let temp_path = build_dir.join(file_name);
38 let client = utils::get_http_client()
39 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
40 let mut attempt = 0u32;
41 let mut response = loop {
42 attempt += 1;
43 match client.get(&source).send() {
44 Ok(resp) => break resp,
45 Err(e) => {
46 if attempt < 3 {
47 if !quiet {
48 eprintln!("Download failed ({}). Retrying...", e);
49 }
50 zoi_core::utils::retry_backoff_sleep(attempt);
51 continue;
52 } else {
53 return Err(mlua::Error::RuntimeError(e.to_string()));
54 }
55 }
56 }
57 };
58
59 if !response.status().is_success() {
60 return Err(mlua::Error::RuntimeError(format!("Failed to download {}: {}", source, response.status())));
61 }
62
63 let mut temp_file = fs::File::create(&temp_path)
64 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
65 std::io::copy(&mut response, &mut temp_file)
66 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
67
68 temp_path
69 } else {
70 PathBuf::from(source)
71 };
72
73 let out_dir_name = out_name.unwrap_or_else(|| "extracted".to_string());
74 let out_dir = build_dir.join(&out_dir_name);
75
76 if !out_dir.starts_with(build_dir) || out_dir == build_dir {
77 return Err(mlua::Error::RuntimeError(format!(
78 "Invalid output directory: {}. Extraction must be into a subdirectory of the build directory.",
79 out_dir_name
80 )));
81 }
82
83 fs::create_dir_all(&out_dir).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
84
85 if !quiet {
86 println!(
87 "Extracting {} to {}",
88 archive_file.display(),
89 out_dir.display()
90 );
91 }
92
93 let file = fs::File::open(&archive_file)
94 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
95
96 let archive_path_str = archive_file.to_string_lossy();
97
98 if archive_path_str.ends_with(".zip") {
99 let mut archive =
100 ZipArchive::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
101 archive
102 .extract(&out_dir)
103 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
104 } else if archive_path_str.ends_with(".tar.gz") || archive_path_str.ends_with(".tgz") {
105 let tar_gz = GzDecoder::new(file);
106 let mut archive = tar::Archive::new(tar_gz);
107 archive
108 .unpack(&out_dir)
109 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
110 } else if archive_path_str.ends_with(".tar.zst") {
111 let tar_zst =
112 ZstdDecoder::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
113 let mut archive = tar::Archive::new(tar_zst);
114 archive
115 .unpack(&out_dir)
116 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
117 } else if archive_path_str.ends_with(".tar.xz") {
118 let tar_xz = XzDecoder::new(file);
119 let mut archive = tar::Archive::new(tar_xz);
120 archive
121 .unpack(&out_dir)
122 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
123 } else if archive_path_str.ends_with(".7z") {
124 sevenz_rust::decompress_file(&archive_file, &out_dir)
125 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
126 } else if archive_path_str.ends_with(".dmg") {
127 if !cfg!(target_os = "macos") {
128 return Err(mlua::Error::RuntimeError(
129 "Extracting .dmg files is only supported on macOS.".to_string(),
130 ));
131 }
132 let output = std::process::Command::new("hdiutil")
133 .arg("attach")
134 .arg("-nobrowse")
135 .arg("-readonly")
136 .arg(&archive_file)
137 .output()
138 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to execute hdiutil: {}", e)))?;
139 if !output.status.success() {
140 let stderr = String::from_utf8_lossy(&output.stderr);
141 return Err(mlua::Error::RuntimeError(format!("hdiutil failed: {}", stderr)));
142 }
143 let output_str = String::from_utf8_lossy(&output.stdout);
144 let mut mount_point = None;
145 for line in output_str.lines() {
146 if line.contains("/Volumes/")
147 && let Some(idx) = line.find("/Volumes/") {
148 mount_point = Some(line[idx..].trim().to_string());
149 break;
150 }
151 }
152 let mount_point = mount_point.ok_or_else(|| {
153 mlua::Error::RuntimeError("Failed to parse mount point from hdiutil output.".to_string())
154 })?;
155 let mount_path = std::path::Path::new(&mount_point);
156 if let Err(e) = zoi_core::utils::copy_dir_all(mount_path, &out_dir) {
157 let _ = std::process::Command::new("hdiutil").arg("detach").arg(&mount_point).status();
158 return Err(mlua::Error::RuntimeError(format!("Failed to copy contents from dmg: {}", e)));
159 }
160 let detach_status = std::process::Command::new("hdiutil")
161 .arg("detach")
162 .arg(&mount_point)
163 .status()
164 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to execute hdiutil detach: {}", e)))?;
165 if !detach_status.success() {
166 eprintln!("Warning: failed to detach dmg volume at {}", mount_point);
167 }
168 } else if archive_path_str.ends_with(".pkg") {
169 if !cfg!(target_os = "macos") {
170 return Err(mlua::Error::RuntimeError(
171 "Extracting .pkg files natively is only supported on macOS.".to_string(),
172 ));
173 }
174 let temp_extract_dir = out_dir.join(".pkg_extract_tmp");
175 let status = std::process::Command::new("pkgutil")
176 .arg("--expand-full")
177 .arg(&archive_file)
178 .arg(&temp_extract_dir)
179 .status()
180 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to execute pkgutil: {}", e)))?;
181 if !status.success() {
182 return Err(mlua::Error::RuntimeError("pkgutil failed to expand the package.".to_string()));
183 }
184 zoi_core::utils::copy_dir_all(&temp_extract_dir, &out_dir)
185 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to copy pkg contents: {}", e)))?;
186 let _ = fs::remove_dir_all(&temp_extract_dir);
187
188 } else if archive_path_str.ends_with(".rar") {
189 if zoi_core::utils::command_exists("unrar") {
190 let status = std::process::Command::new("unrar")
191 .arg("x")
192 .arg("-y")
193 .arg(&archive_file)
194 .arg(&out_dir)
195 .status()
196 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
197 if !status.success() {
198 return Err(mlua::Error::RuntimeError("unrar failed".to_string()));
199 }
200 } else {
201 return Err(mlua::Error::RuntimeError(
202 "unrar command not found. Please install unrar to extract .rar files."
203 .to_string(),
204 ));
205 }
206 } else if archive_path_str.ends_with(".deb") {
207 let mut ar = ArArchive::new(file);
208 while let Some(entry_result) = ar.next_entry() {
209 let mut entry =
210 entry_result.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
211 let name = String::from_utf8_lossy(entry.header().identifier())
212 .trim()
213 .trim_end_matches('/')
214 .to_string();
215 if name.starts_with("data.tar") {
216 let temp_data_path = build_dir.join(&name);
217 let mut temp_file = fs::File::create(&temp_data_path)
218 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to create temp file for {}: {}", name, e)))?;
219 std::io::copy(&mut entry, &mut temp_file)
220 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to copy entry data for {}: {}", name, e)))?;
221
222 let data_file = fs::File::open(&temp_data_path)
223 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to reopen temp file for {}: {}", name, e)))?;
224 if name.ends_with(".gz") {
225 let mut archive = tar::Archive::new(GzDecoder::new(data_file));
226 archive
227 .unpack(&out_dir)
228 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to unpack {}: {}", name, e)))?;
229 } else if name.ends_with(".xz") {
230 let mut archive = tar::Archive::new(XzDecoder::new(data_file));
231 archive
232 .unpack(&out_dir)
233 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to unpack {}: {}", name, e)))?;
234 } else if name.ends_with(".zst") {
235 let mut archive = tar::Archive::new(
236 ZstdDecoder::new(data_file)
237 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to initialize zstd for {}: {}", name, e)))?,
238 );
239 archive
240 .unpack(&out_dir)
241 .map_err(|e| mlua::Error::RuntimeError(format!("Failed to unpack {}: {}", name, e)))?;
242 }
243 fs::remove_file(temp_data_path).ok();
244 }
245 }
246 } else {
247 return Err(mlua::Error::RuntimeError(format!(
248 "Unsupported archive format for file: {}",
249 archive_path_str
250 )));
251 }
252
253 Ok(())
254 })?;
255
256 let utils_table: Table = lua.globals().get("UTILS")?;
257 utils_table.set("EXTRACT", extract_fn)?;
258
259 Ok(())
260}
261
262pub fn add_archive_util(lua: &Lua) -> Result<(), mlua::Error> {
263 let archive_table = lua.create_table()?;
264
265 let list_fn = lua.create_function(|lua, path: String| {
266 let p = Path::new(&path);
267 let actual_path = if p.exists() {
268 p.to_path_buf()
269 } else if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR") {
270 Path::new(&build_dir).join(p)
271 } else {
272 p.to_path_buf()
273 };
274
275 let file = fs::File::open(&actual_path).map_err(|e| {
276 mlua::Error::RuntimeError(format!("Failed to open archive {:?}: {}", actual_path, e))
277 })?;
278 let mut files = Vec::new();
279
280 if path.ends_with(".zip") {
281 let mut archive =
282 ZipArchive::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
283 for i in 0..archive.len() {
284 let file = archive
285 .by_index(i)
286 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
287 files.push(file.name().to_string());
288 }
289 } else if path.ends_with(".tar.gz") || path.ends_with(".tgz") {
290 let tar_gz = GzDecoder::new(file);
291 let mut archive = tar::Archive::new(tar_gz);
292 for entry in archive
293 .entries()
294 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
295 {
296 let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
297 files.push(
298 entry
299 .path()
300 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
301 .to_string_lossy()
302 .to_string(),
303 );
304 }
305 } else if path.ends_with(".tar.zst") {
306 let tar_zst =
307 ZstdDecoder::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
308 let mut archive = tar::Archive::new(tar_zst);
309 for entry in archive
310 .entries()
311 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
312 {
313 let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
314 files.push(
315 entry
316 .path()
317 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
318 .to_string_lossy()
319 .to_string(),
320 );
321 }
322 } else if path.ends_with(".tar.xz") {
323 let tar_xz = XzDecoder::new(file);
324 let mut archive = tar::Archive::new(tar_xz);
325 for entry in archive
326 .entries()
327 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
328 {
329 let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
330 files.push(
331 entry
332 .path()
333 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
334 .to_string_lossy()
335 .to_string(),
336 );
337 }
338 } else if path.ends_with(".7z") {
339 let file =
340 fs::File::open(&path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
341 let len = file
342 .metadata()
343 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
344 .len();
345 let reader = sevenz_rust::SevenZReader::new(file, len, sevenz_rust::Password::empty())
346 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
347 for entry in &reader.archive().files {
348 files.push(entry.name.to_string());
349 }
350 } else if path.ends_with(".rar") {
351 if zoi_core::utils::command_exists("unrar") {
352 let output = std::process::Command::new("unrar")
353 .arg("lb")
354 .arg(&path)
355 .output()
356 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
357 if output.status.success() {
358 let list = String::from_utf8_lossy(&output.stdout);
359 for line in list.lines() {
360 files.push(line.to_string());
361 }
362 }
363 }
364 } else if path.ends_with(".deb") {
365 let mut ar = ArArchive::new(file);
366 while let Some(entry_result) = ar.next_entry() {
367 let entry = entry_result.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
368 let header = entry.header();
369 files.push(String::from_utf8_lossy(header.identifier()).to_string());
370 }
371 } else {
372 return Err(mlua::Error::RuntimeError(format!(
373 "Unsupported archive format: {}",
374 path
375 )));
376 }
377
378 Ok(files)
379 })?;
380 archive_table.set("list", list_fn)?;
381
382 let utils_table: Table = lua.globals().get("UTILS")?;
383 utils_table.set("ARCHIVE", archive_table)?;
384
385 Ok(())
386}