Skip to main content

mecha10_cli/services/
build_source.rs

1//! Project source zip extraction for the CLI's remote/dashboard build-worker flow (LAB-5442,
2//! part of the "build via mecha10 dashboard" feature, LAB-2153).
3
4use anyhow::{Context, Result};
5use std::path::Path;
6
7/// Extract a project source zip into `dest_dir`, reconstructing the project root in place -
8/// entries are already project-root-relative with no enclosing top-level directory, so each
9/// entry's name is joined directly onto `dest_dir`.
10///
11/// Used by the build-worker's source-fetch path (LAB-5440/5443): the dispatched build
12/// container fetches a project source zip and needs this to reconstruct the project before
13/// building it.
14pub fn extract_project_zip(zip_path: &Path, dest_dir: &Path) -> Result<()> {
15    let file = std::fs::File::open(zip_path).with_context(|| format!("Failed to open {}", zip_path.display()))?;
16    let mut archive = zip::ZipArchive::new(file).context("Failed to read project source zip")?;
17
18    for index in 0..archive.len() {
19        let mut entry = archive
20            .by_index(index)
21            .with_context(|| format!("Failed to read zip entry #{index}"))?;
22
23        let Some(relative_path) = entry.enclosed_name() else {
24            anyhow::bail!("Zip entry #{index} has an unsafe or invalid path: {:?}", entry.name());
25        };
26        let out_path = dest_dir.join(relative_path);
27
28        if entry.is_dir() {
29            std::fs::create_dir_all(&out_path)
30                .with_context(|| format!("Failed to create directory {}", out_path.display()))?;
31            continue;
32        }
33
34        if let Some(parent) = out_path.parent() {
35            std::fs::create_dir_all(parent)
36                .with_context(|| format!("Failed to create directory {}", parent.display()))?;
37        }
38
39        let mut out_file =
40            std::fs::File::create(&out_path).with_context(|| format!("Failed to create {}", out_path.display()))?;
41        std::io::copy(&mut entry, &mut out_file).with_context(|| format!("Failed to write {}", out_path.display()))?;
42    }
43
44    Ok(())
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use std::io::Write;
51    use tempfile::tempdir;
52
53    fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Vec<u8> {
54        let mut bytes = Vec::new();
55        {
56            let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut bytes));
57            let options = zip::write::SimpleFileOptions::default();
58            for (name, contents) in entries {
59                zip.start_file(*name, options).unwrap();
60                zip.write_all(contents).unwrap();
61            }
62            zip.finish().unwrap();
63        }
64        bytes
65    }
66
67    #[test]
68    fn extract_project_zip_round_trips_a_nested_projects_files() {
69        let zip_bytes = build_zip_with_entries(&[
70            ("src/main.rs", b"fn main() {}"),
71            ("Cargo.toml", b"[package]\nname=\"x\"\n"),
72        ]);
73        let src_dir = tempdir().unwrap();
74        let zip_path = src_dir.path().join("out.zip");
75        std::fs::write(&zip_path, &zip_bytes).unwrap();
76
77        let dest_dir = tempdir().unwrap();
78        extract_project_zip(&zip_path, dest_dir.path()).unwrap();
79
80        assert_eq!(
81            std::fs::read_to_string(dest_dir.path().join("src/main.rs")).unwrap(),
82            "fn main() {}"
83        );
84        assert_eq!(
85            std::fs::read_to_string(dest_dir.path().join("Cargo.toml")).unwrap(),
86            "[package]\nname=\"x\"\n"
87        );
88    }
89
90    #[test]
91    fn extract_project_zip_errors_on_a_missing_zip_file() {
92        let dest_dir = tempdir().unwrap();
93        let err = extract_project_zip(&dest_dir.path().join("missing.zip"), dest_dir.path()).unwrap_err();
94        assert!(err.to_string().contains("Failed to open"));
95    }
96}