mecha10-cli 4.2.15

Mecha10 CLI tool
Documentation
//! Project source zip extraction for the CLI's remote/dashboard build-worker flow (LAB-5442,
//! part of the "build via mecha10 dashboard" feature, LAB-2153).

use anyhow::{Context, Result};
use std::path::Path;

/// Extract a project source zip into `dest_dir`, reconstructing the project root in place -
/// entries are already project-root-relative with no enclosing top-level directory, so each
/// entry's name is joined directly onto `dest_dir`.
///
/// Used by the build-worker's source-fetch path (LAB-5440/5443): the dispatched build
/// container fetches a project source zip and needs this to reconstruct the project before
/// building it.
pub fn extract_project_zip(zip_path: &Path, dest_dir: &Path) -> Result<()> {
    let file = std::fs::File::open(zip_path).with_context(|| format!("Failed to open {}", zip_path.display()))?;
    let mut archive = zip::ZipArchive::new(file).context("Failed to read project source zip")?;

    for index in 0..archive.len() {
        let mut entry = archive
            .by_index(index)
            .with_context(|| format!("Failed to read zip entry #{index}"))?;

        let Some(relative_path) = entry.enclosed_name() else {
            anyhow::bail!("Zip entry #{index} has an unsafe or invalid path: {:?}", entry.name());
        };
        let out_path = dest_dir.join(relative_path);

        if entry.is_dir() {
            std::fs::create_dir_all(&out_path)
                .with_context(|| format!("Failed to create directory {}", out_path.display()))?;
            continue;
        }

        if let Some(parent) = out_path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create directory {}", parent.display()))?;
        }

        let mut out_file =
            std::fs::File::create(&out_path).with_context(|| format!("Failed to create {}", out_path.display()))?;
        std::io::copy(&mut entry, &mut out_file).with_context(|| format!("Failed to write {}", out_path.display()))?;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Vec<u8> {
        let mut bytes = Vec::new();
        {
            let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut bytes));
            let options = zip::write::SimpleFileOptions::default();
            for (name, contents) in entries {
                zip.start_file(*name, options).unwrap();
                zip.write_all(contents).unwrap();
            }
            zip.finish().unwrap();
        }
        bytes
    }

    #[test]
    fn extract_project_zip_round_trips_a_nested_projects_files() {
        let zip_bytes = build_zip_with_entries(&[
            ("src/main.rs", b"fn main() {}"),
            ("Cargo.toml", b"[package]\nname=\"x\"\n"),
        ]);
        let src_dir = tempdir().unwrap();
        let zip_path = src_dir.path().join("out.zip");
        std::fs::write(&zip_path, &zip_bytes).unwrap();

        let dest_dir = tempdir().unwrap();
        extract_project_zip(&zip_path, dest_dir.path()).unwrap();

        assert_eq!(
            std::fs::read_to_string(dest_dir.path().join("src/main.rs")).unwrap(),
            "fn main() {}"
        );
        assert_eq!(
            std::fs::read_to_string(dest_dir.path().join("Cargo.toml")).unwrap(),
            "[package]\nname=\"x\"\n"
        );
    }

    #[test]
    fn extract_project_zip_errors_on_a_missing_zip_file() {
        let dest_dir = tempdir().unwrap();
        let err = extract_project_zip(&dest_dir.path().join("missing.zip"), dest_dir.path()).unwrap_err();
        assert!(err.to_string().contains("Failed to open"));
    }
}