winbrew_engines\archive/
install.rs

1use anyhow::Result;
2use std::fs;
3use std::path::Path;
4
5use crate::core::fs::{cleanup_path, extract_archive, replace_directory};
6
7use crate::models::install::engine::EngineInstallReceipt;
8use crate::models::install::engine::EngineKind;
9
10use crate::payload::archive_kind_for_url;
11
12/// Extract an archive installer into the target install directory.
13///
14/// This preserves the packaged directory tree as-is and does not try to
15/// discover or promote a primary binary.
16pub(crate) fn install(
17    download_path: &Path,
18    install_dir: &Path,
19    installer_url: &str,
20) -> Result<EngineInstallReceipt> {
21    let stage_dir = install_dir.parent().unwrap_or(install_dir).join("staging");
22    let archive_kind = archive_kind_for_url(installer_url).unwrap_or(crate::core::ArchiveKind::Zip);
23
24    cleanup_path(&stage_dir)?;
25    fs::create_dir_all(&stage_dir)?;
26
27    extract_archive(archive_kind, download_path, &stage_dir)?;
28    replace_directory(&stage_dir, install_dir)?;
29
30    Ok(EngineInstallReceipt::new(
31        EngineKind::Zip,
32        install_dir.to_string_lossy().into_owned(),
33        None,
34    ))
35}
36
37#[cfg(test)]
38mod tests {
39    use super::install;
40    use flate2::Compression;
41    use flate2::write::GzEncoder;
42    use std::fs;
43    use std::io::{Read, Write};
44    use tar::Builder;
45    use tar::Header;
46    use tempfile::tempdir;
47    use zip::ZipWriter;
48    use zip::write::SimpleFileOptions;
49
50    fn create_zip_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
51        let file = fs::File::create(path).expect("create zip file");
52        let mut writer = ZipWriter::new(file);
53        writer
54            .start_file(file_name, SimpleFileOptions::default())
55            .expect("start zip entry");
56        writer.write_all(contents).expect("write zip contents");
57        writer.finish().expect("finish zip file");
58    }
59
60    fn create_tar_gz_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
61        let file = fs::File::create(path).expect("create tar.gz file");
62        let encoder = GzEncoder::new(file, Compression::default());
63        let mut builder = Builder::new(encoder);
64        let mut header = Header::new_gnu();
65        header.set_size(contents.len() as u64);
66        header.set_mode(0o644);
67        header.set_cksum();
68
69        builder
70            .append_data(&mut header, file_name, contents)
71            .expect("append tar.gz entry");
72        let encoder = builder.into_inner().expect("finish tar builder");
73        encoder.finish().expect("finish tar.gz file");
74    }
75
76    fn create_zip_archive_with_tree(path: &std::path::Path, entries: &[(&str, &[u8])]) {
77        let file = fs::File::create(path).expect("create zip file");
78        let mut writer = ZipWriter::new(file);
79
80        for (file_name, contents) in entries {
81            writer
82                .start_file(file_name, SimpleFileOptions::default())
83                .expect("start zip entry");
84            writer.write_all(contents).expect("write zip contents");
85        }
86
87        writer.finish().expect("finish zip file");
88    }
89
90    #[test]
91    fn install_extracts_archive_into_install_directory() {
92        let temp_root = tempdir().expect("temp root");
93        let download_path = temp_root.path().join("download.zip");
94        let install_dir = temp_root.path().join("packages").join("Contoso.Zip");
95
96        create_zip_archive(&download_path, "bin/tool.exe", b"zip-binary");
97
98        install(
99            &download_path,
100            &install_dir,
101            "https://example.invalid/download.zip",
102        )
103        .expect("zip install");
104
105        let installed_file = install_dir.join("bin").join("tool.exe");
106        let mut contents = String::default();
107        fs::File::open(&installed_file)
108            .expect("installed file")
109            .read_to_string(&mut contents)
110            .expect("read installed file");
111
112        assert_eq!(contents, "zip-binary");
113    }
114
115    #[test]
116    fn install_preserves_nested_archive_trees_without_promoting_a_binary() {
117        let temp_root = tempdir().expect("temp root");
118        let download_path = temp_root.path().join("download.zip");
119        let install_dir = temp_root.path().join("packages").join("Contoso.Tree");
120
121        create_zip_archive_with_tree(
122            &download_path,
123            &[
124                ("README.md", b"tree readme"),
125                ("docs/notes.txt", b"notes"),
126                ("bin/tool.exe", b"tree-binary"),
127            ],
128        );
129
130        install(
131            &download_path,
132            &install_dir,
133            "https://example.invalid/download.zip",
134        )
135        .expect("tree archive install");
136
137        let readme = install_dir.join("README.md");
138        let notes = install_dir.join("docs").join("notes.txt");
139        let binary = install_dir.join("bin").join("tool.exe");
140
141        assert!(readme.exists(), "README should stay at the archive root");
142        assert!(notes.exists(), "nested docs should be preserved");
143        assert!(binary.exists(), "binary should remain in its original path");
144
145        let mut readme_contents = String::default();
146        fs::File::open(&readme)
147            .expect("readme file")
148            .read_to_string(&mut readme_contents)
149            .expect("read readme");
150
151        let mut binary_contents = String::default();
152        fs::File::open(&binary)
153            .expect("binary file")
154            .read_to_string(&mut binary_contents)
155            .expect("read binary");
156
157        assert_eq!(readme_contents, "tree readme");
158        assert_eq!(binary_contents, "tree-binary");
159    }
160
161    #[test]
162    fn install_extracts_tar_gz_archive_into_install_directory() {
163        let temp_root = tempdir().expect("temp root");
164        let download_path = temp_root.path().join("download.tar.gz");
165        let install_dir = temp_root.path().join("packages").join("Contoso.Tar");
166
167        create_tar_gz_archive(&download_path, "bin/tool.exe", b"tar-binary");
168
169        install(
170            &download_path,
171            &install_dir,
172            "https://example.invalid/download.tar.gz",
173        )
174        .expect("tar.gz install");
175
176        let installed_file = install_dir.join("bin").join("tool.exe");
177        let mut contents = String::default();
178        fs::File::open(&installed_file)
179            .expect("installed file")
180            .read_to_string(&mut contents)
181            .expect("read installed file");
182
183        assert_eq!(contents, "tar-binary");
184    }
185}