winbrew_app\operations\install/
sevenz.rs

1use anyhow::{Context, Result};
2use std::env;
3use std::ffi::OsString;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use crate::core::env::WINBREW_PATHS_ROOT;
9use crate::core::fs::{cleanup_path, replace_directory};
10use crate::core::hash::{hash_file, verify_hash};
11use crate::core::network::{build_client, download_url_to_temp_file, is_7z_path};
12use crate::core::paths::system_sevenz_binary_path;
13pub(crate) use crate::core::paths::{
14    sevenz_bin_path_from_runtime_root, sevenz_dll_path_from_runtime_root,
15    sevenz_runtime_dir_from_runtime_root,
16};
17use crate::models::shared::hash::HashAlgorithm;
18
19use super::InstallError;
20
21const SEVENZ_BOOTSTRAP_USER_AGENT: &str = "WinBrew 7-Zip bootstrap";
22const SEVENZ_BOOTSTRAP_VERSION: &str = "26.00";
23const SEVENZ_VERSION_FILENAME: &str = "VERSION";
24const SEVENZR_FILENAME: &str = "7zr.exe";
25const SEVENZR_DOWNLOAD_SHA256: &str =
26    "sha256:4bec0bc59836a890a11568b58bd12a3e7b23a683557340562da211b6088058ba";
27const SEVENZ_X86_DOWNLOAD_SHA256: &str =
28    "sha256:d605eb609aa67796dca7cfe26d7e28792090bb8048302d6e05ede16e8e33145c";
29
30fn sevenzr_download_url() -> String {
31    format!(
32        "https://github.com/ip7z/7zip/releases/download/{SEVENZ_BOOTSTRAP_VERSION}/{SEVENZR_FILENAME}"
33    )
34}
35
36fn sevenz_x86_download_url() -> String {
37    let x86_filename = sevenz_x86_filename();
38    format!(
39        "https://github.com/ip7z/7zip/releases/download/{SEVENZ_BOOTSTRAP_VERSION}/{x86_filename}"
40    )
41}
42
43fn sevenz_x86_filename() -> String {
44    format!("7z{}.exe", SEVENZ_BOOTSTRAP_VERSION.replace('.', ""))
45}
46
47pub(crate) fn sevenz_version_manifest_path(runtime_root: &Path) -> PathBuf {
48    sevenz_runtime_dir_from_runtime_root(runtime_root).join(SEVENZ_VERSION_FILENAME)
49}
50
51pub(crate) fn runtime_root_env_guard(root: &Path) -> RuntimeRootEnvGuard {
52    RuntimeRootEnvGuard::set(WINBREW_PATHS_ROOT, root)
53}
54
55pub(crate) fn ensure_runtime(
56    runtime_root: &Path,
57    installer_url: &str,
58    mut confirm_runtime_bootstrap: impl FnMut(&str, &Path) -> Result<bool>,
59) -> Result<(), InstallError> {
60    if !runtime_bootstrap_required(runtime_root, installer_url) {
61        return Ok(());
62    }
63
64    let runtime_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
65    if !confirm_runtime_bootstrap("7-Zip runtime", &runtime_dir)? {
66        return Err(InstallError::RuntimeBootstrapDeclined {
67            runtime: "7-Zip runtime".to_string(),
68        });
69    }
70
71    bootstrap_local_runtime(runtime_root).map_err(InstallError::from)
72}
73
74pub(crate) fn runtime_bootstrap_required(runtime_root: &Path, installer_url: &str) -> bool {
75    is_7z_path(installer_url)
76        && system_sevenz_binary_path().is_none()
77        && !local_runtime_available(runtime_root)
78}
79
80fn local_runtime_available(runtime_root: &Path) -> bool {
81    sevenz_bin_path_from_runtime_root(runtime_root).exists()
82        && sevenz_dll_path_from_runtime_root(runtime_root).exists()
83        && local_runtime_version_matches(runtime_root)
84}
85
86fn local_runtime_version_matches(runtime_root: &Path) -> bool {
87    let version_path = sevenz_version_manifest_path(runtime_root);
88    std::fs::read_to_string(&version_path)
89        .map(|content| content.trim() == SEVENZ_BOOTSTRAP_VERSION)
90        .unwrap_or(false)
91}
92
93fn bootstrap_local_runtime(runtime_root: &Path) -> Result<()> {
94    let target_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
95    let staging_dir = create_bootstrap_root();
96    let sevenzr_path = staging_dir.join(SEVENZR_FILENAME);
97    let sevenz_x86_filename = sevenz_x86_filename();
98    let installer_path = staging_dir.join(&sevenz_x86_filename);
99    let artifacts = BootstrapArtifacts::new(
100        staging_dir.clone(),
101        sevenzr_path.clone(),
102        installer_path.clone(),
103    );
104
105    fs::create_dir_all(&staging_dir).with_context(|| {
106        format!("failed to create 7z bootstrap staging directory {staging_dir:?}")
107    })?;
108
109    let client = build_client(SEVENZ_BOOTSTRAP_USER_AGENT)
110        .context("failed to build 7z bootstrap HTTP client")?;
111
112    let sevenzr_url = sevenzr_download_url();
113    let sevenz_x86_url = sevenz_x86_download_url();
114
115    download_verified_asset(
116        &client,
117        &sevenzr_url,
118        &sevenzr_path,
119        SEVENZR_FILENAME,
120        SEVENZR_DOWNLOAD_SHA256,
121    )?;
122    download_verified_asset(
123        &client,
124        &sevenz_x86_url,
125        &installer_path,
126        &sevenz_x86_filename,
127        SEVENZ_X86_DOWNLOAD_SHA256,
128    )?;
129
130    run_bootstrap_extractor(&sevenzr_path, &installer_path, &staging_dir)?;
131
132    let version_path = staging_dir.join(SEVENZ_VERSION_FILENAME);
133    fs::write(&version_path, SEVENZ_BOOTSTRAP_VERSION).with_context(|| {
134        format!(
135            "failed to write 7z bootstrap version file at {}",
136            version_path.display()
137        )
138    })?;
139
140    if let Some(parent) = target_dir.parent() {
141        fs::create_dir_all(parent).with_context(|| {
142            format!(
143                "failed to create parent directory for {}",
144                target_dir.display()
145            )
146        })?;
147    }
148
149    replace_directory(&staging_dir, &target_dir)
150        .with_context(|| format!("failed to publish 7z runtime into {}", target_dir.display()))?;
151
152    artifacts.commit();
153    Ok(())
154}
155
156fn download_verified_asset(
157    client: &crate::core::network::Client,
158    url: &str,
159    temp_path: &Path,
160    label: &str,
161    expected_hash: &str,
162) -> Result<()> {
163    download_url_to_temp_file(
164        client,
165        url,
166        temp_path,
167        label,
168        |_| {},
169        |_| {},
170        |_| Ok::<(), crate::core::network::BoxError>(()),
171    )
172    .with_context(|| format!("failed to download {label}"))?;
173
174    let actual_hash = hash_file(temp_path, HashAlgorithm::Sha256)
175        .with_context(|| format!("failed to hash downloaded {label}"))?;
176
177    verify_hash(expected_hash, actual_hash)
178        .with_context(|| format!("downloaded {label} hash mismatch"))?;
179
180    Ok(())
181}
182
183fn run_bootstrap_extractor(
184    sevenzr_path: &Path,
185    archive_path: &Path,
186    destination_dir: &Path,
187) -> Result<()> {
188    let status = Command::new(sevenzr_path)
189        .arg("x")
190        .arg("-y")
191        .arg("-bd")
192        .arg(format!("-o{}", destination_dir.display()))
193        .arg(archive_path)
194        .arg("7z.exe")
195        .arg("7z.dll")
196        .status()
197        .with_context(|| {
198            format!(
199                "failed to launch 7z bootstrap extractor at {}",
200                sevenzr_path.display()
201            )
202        })?;
203
204    if status.success() {
205        Ok(())
206    } else {
207        anyhow::bail!("7zr exited with status {status}");
208    }
209}
210
211fn create_bootstrap_root() -> PathBuf {
212    let mut bootstrap_root = env::temp_dir();
213    bootstrap_root.push(format!(
214        "winbrew-7zip-bootstrap-{}-{}",
215        std::process::id(),
216        std::time::SystemTime::now()
217            .duration_since(std::time::UNIX_EPOCH)
218            .unwrap_or_default()
219            .as_nanos()
220    ));
221
222    bootstrap_root
223}
224
225struct BootstrapArtifacts {
226    staging_dir: PathBuf,
227    sevenzr_path: PathBuf,
228    installer_path: PathBuf,
229    committed: bool,
230}
231
232impl BootstrapArtifacts {
233    fn new(staging_dir: PathBuf, sevenzr_path: PathBuf, installer_path: PathBuf) -> Self {
234        Self {
235            staging_dir,
236            sevenzr_path,
237            installer_path,
238            committed: false,
239        }
240    }
241
242    fn commit(mut self) {
243        let _ = cleanup_path(&self.staging_dir);
244        let _ = fs::remove_file(&self.sevenzr_path);
245        let _ = fs::remove_file(&self.installer_path);
246        self.committed = true;
247    }
248}
249
250impl Drop for BootstrapArtifacts {
251    fn drop(&mut self) {
252        if !self.committed {
253            let _ = cleanup_path(&self.staging_dir);
254            let _ = fs::remove_file(&self.sevenzr_path);
255            let _ = fs::remove_file(&self.installer_path);
256        }
257    }
258}
259
260pub(crate) struct RuntimeRootEnvGuard {
261    key: &'static str,
262    previous: Option<OsString>,
263}
264
265impl RuntimeRootEnvGuard {
266    fn set(key: &'static str, value: &Path) -> Self {
267        let previous = env::var_os(key);
268        unsafe {
269            env::set_var(key, value);
270        }
271
272        Self { key, previous }
273    }
274}
275
276impl Drop for RuntimeRootEnvGuard {
277    fn drop(&mut self) {
278        if let Some(previous) = self.previous.take() {
279            unsafe {
280                env::set_var(self.key, previous);
281            }
282        } else {
283            unsafe {
284                env::remove_var(self.key);
285            }
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use std::fs;
294    use tempfile::tempdir;
295
296    #[test]
297    fn bootstrap_urls_derive_from_version() {
298        let expected_version = SEVENZ_BOOTSTRAP_VERSION;
299        let expected_x86_filename = sevenz_x86_filename();
300        assert_eq!(
301            sevenzr_download_url(),
302            format!(
303                "https://github.com/ip7z/7zip/releases/download/{expected_version}/{SEVENZR_FILENAME}"
304            )
305        );
306        assert_eq!(
307            sevenz_x86_download_url(),
308            format!(
309                "https://github.com/ip7z/7zip/releases/download/{expected_version}/{expected_x86_filename}"
310            )
311        );
312    }
313
314    #[test]
315    fn local_runtime_available_requires_matching_version_manifest() -> Result<()> {
316        let temp_dir = tempdir().expect("temp dir");
317        let runtime_root = temp_dir.path();
318        let runtime_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
319        fs::create_dir_all(&runtime_dir)?;
320
321        fs::write(runtime_dir.join("7z.exe"), b"")?;
322        fs::write(runtime_dir.join("7z.dll"), b"")?;
323        fs::write(sevenz_version_manifest_path(runtime_root), b"25.50")?;
324
325        assert!(!local_runtime_available(runtime_root));
326
327        fs::write(
328            sevenz_version_manifest_path(runtime_root),
329            SEVENZ_BOOTSTRAP_VERSION,
330        )?;
331        assert!(local_runtime_available(runtime_root));
332
333        Ok(())
334    }
335
336    #[test]
337    fn runtime_bootstrap_required_rejects_mismatched_local_version() -> Result<()> {
338        let temp_dir = tempdir().expect("temp dir");
339        let runtime_root = temp_dir.path();
340        let runtime_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
341
342        fs::create_dir_all(&runtime_dir)?;
343        fs::write(runtime_dir.join("7z.exe"), b"")?;
344        fs::write(runtime_dir.join("7z.dll"), b"")?;
345        fs::write(sevenz_version_manifest_path(runtime_root), b"25.50")?;
346
347        assert!(runtime_bootstrap_required(
348            runtime_root,
349            "https://example.invalid/archive.7z"
350        ));
351
352        fs::write(
353            sevenz_version_manifest_path(runtime_root),
354            SEVENZ_BOOTSTRAP_VERSION,
355        )?;
356        assert!(!runtime_bootstrap_required(
357            runtime_root,
358            "https://example.invalid/archive.7z"
359        ));
360
361        Ok(())
362    }
363}