winbrew_core\fs\archive\extract/
sevenz.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use crate::env::{LOCALAPPDATA, WINBREW_PATHS_ROOT};
7use crate::fs::{FsError, Result};
8use crate::paths::{
9    sevenz_bin_path_from_runtime_root, sevenz_dll_path_from_runtime_root, system_sevenz_binary_path,
10};
11
12const SEVENZ_RELATIVE_EXE: &str = "bin/7zip/7z.exe";
13
14pub(crate) trait SevenZipLauncher {
15    fn extract(
16        &self,
17        binary_path: &Path,
18        archive_path: &Path,
19        destination_dir: &Path,
20    ) -> io::Result<()>;
21}
22
23pub(crate) struct SystemSevenZipLauncher;
24
25impl SevenZipLauncher for SystemSevenZipLauncher {
26    fn extract(
27        &self,
28        binary_path: &Path,
29        archive_path: &Path,
30        destination_dir: &Path,
31    ) -> io::Result<()> {
32        let status = Command::new(binary_path)
33            .arg("x")
34            .arg("-y")
35            .arg("-bd")
36            .arg(format!("-o{}", destination_dir.display()))
37            .arg(archive_path)
38            .status()?;
39
40        if status.success() {
41            Ok(())
42        } else {
43            Err(io::Error::other(format!("7z exited with status {status}")))
44        }
45    }
46}
47
48pub(crate) fn extract_sevenz(archive_path: &Path, destination_dir: &Path) -> Result<()> {
49    #[cfg(windows)]
50    {
51        if let Some(system_binary_path) = system_sevenz_binary_path() {
52            return extract_sevenz_with_binary_path(
53                archive_path,
54                destination_dir,
55                &system_binary_path,
56                &SystemSevenZipLauncher,
57            );
58        }
59    }
60
61    let runtime_root = resolve_local_runtime_root().map_err(|err| {
62        FsError::archive_backend_failed("7z", archive_path, Path::new(SEVENZ_RELATIVE_EXE), err)
63    })?;
64
65    extract_sevenz_with_runtime_root(
66        archive_path,
67        destination_dir,
68        &runtime_root,
69        &SystemSevenZipLauncher,
70    )
71}
72
73pub(crate) fn extract_sevenz_with_runtime_root<L: SevenZipLauncher>(
74    archive_path: &Path,
75    destination_dir: &Path,
76    runtime_root: &Path,
77    launcher: &L,
78) -> Result<()> {
79    let binary_path = sevenz_bin_path_from_runtime_root(runtime_root);
80    let _dll_path = sevenz_dll_path_from_runtime_root(runtime_root);
81    extract_sevenz_with_binary_path(archive_path, destination_dir, &binary_path, launcher)
82}
83
84pub(crate) fn extract_sevenz_with_binary_path<L: SevenZipLauncher>(
85    archive_path: &Path,
86    destination_dir: &Path,
87    binary_path: &Path,
88    launcher: &L,
89) -> Result<()> {
90    let dll_path = binary_path.with_file_name("7z.dll");
91
92    if !binary_path.exists() {
93        let missing_binary_error = io::Error::new(
94            io::ErrorKind::NotFound,
95            format!("missing 7z binary at {}", binary_path.display()),
96        );
97        return Err(FsError::archive_backend_failed(
98            "7z",
99            archive_path,
100            binary_path,
101            missing_binary_error,
102        ));
103    }
104
105    if !dll_path.exists() {
106        let missing_dll_error = io::Error::new(
107            io::ErrorKind::NotFound,
108            format!("missing 7z runtime library at {}", dll_path.display()),
109        );
110        return Err(FsError::archive_backend_failed(
111            "7z",
112            archive_path,
113            &dll_path,
114            missing_dll_error,
115        ));
116    }
117
118    fs::create_dir_all(destination_dir)
119        .map_err(|err| FsError::create_directory(destination_dir, err))?;
120
121    launcher
122        .extract(binary_path, archive_path, destination_dir)
123        .map_err(|err| FsError::archive_backend_failed("7z", archive_path, binary_path, err))?;
124
125    Ok(())
126}
127
128fn resolve_local_runtime_root() -> io::Result<PathBuf> {
129    if let Some(runtime_root) = std::env::var_os(WINBREW_PATHS_ROOT) {
130        return Ok(PathBuf::from(runtime_root));
131    }
132
133    let local_app_data = std::env::var_os(LOCALAPPDATA).ok_or_else(|| {
134        io::Error::new(
135            io::ErrorKind::NotFound,
136            "LOCALAPPDATA is not set on this Windows session",
137        )
138    })?;
139
140    Ok(PathBuf::from(local_app_data).join("winbrew"))
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use std::cell::RefCell;
147    use std::fs;
148    use tempfile::tempdir;
149
150    struct RecordingSevenZipLauncher {
151        calls: RefCell<Vec<(PathBuf, PathBuf, PathBuf)>>,
152    }
153
154    impl RecordingSevenZipLauncher {
155        fn new() -> Self {
156            Self {
157                calls: RefCell::new(Vec::new()),
158            }
159        }
160    }
161
162    impl SevenZipLauncher for RecordingSevenZipLauncher {
163        fn extract(
164            &self,
165            binary_path: &std::path::Path,
166            archive_path: &std::path::Path,
167            destination_dir: &std::path::Path,
168        ) -> io::Result<()> {
169            self.calls.borrow_mut().push((
170                binary_path.to_path_buf(),
171                archive_path.to_path_buf(),
172                destination_dir.to_path_buf(),
173            ));
174
175            Ok(())
176        }
177    }
178
179    #[test]
180    fn extract_sevenz_uses_runtime_root_and_launcher() {
181        let temp_dir = tempdir().expect("temp dir");
182        let runtime_root = temp_dir.path().join("runtime");
183        let archive_path = temp_dir.path().join("archive.7z");
184        let destination_dir = temp_dir.path().join("dest");
185        let launcher = RecordingSevenZipLauncher::new();
186        let binary_path = sevenz_bin_path_from_runtime_root(&runtime_root);
187        let dll_path = sevenz_dll_path_from_runtime_root(&runtime_root);
188
189        fs::create_dir_all(binary_path.parent().expect("binary parent")).expect("binary dir");
190        fs::write(&binary_path, b"placeholder").expect("fake binary");
191        fs::write(&dll_path, b"placeholder").expect("fake dll");
192        fs::write(&archive_path, b"archive contents").expect("archive file");
193
194        extract_sevenz_with_runtime_root(&archive_path, &destination_dir, &runtime_root, &launcher)
195            .expect("sevenzip extraction");
196
197        let calls = launcher.calls.borrow();
198        assert_eq!(calls.len(), 1);
199        assert_eq!(calls[0].0, binary_path);
200        assert_eq!(calls[0].1, archive_path);
201        assert_eq!(calls[0].2, destination_dir);
202    }
203
204    #[test]
205    fn extract_sevenz_rejects_missing_binary_before_launch() {
206        let temp_dir = tempdir().expect("temp dir");
207        let runtime_root = temp_dir.path().join("runtime");
208        let archive_path = temp_dir.path().join("archive.7z");
209        let destination_dir = temp_dir.path().join("dest");
210        let launcher = RecordingSevenZipLauncher::new();
211
212        fs::create_dir_all(&runtime_root).expect("runtime dir");
213        fs::write(&archive_path, b"archive contents").expect("archive file");
214
215        let error = extract_sevenz_with_runtime_root(
216            &archive_path,
217            &destination_dir,
218            &runtime_root,
219            &launcher,
220        )
221        .expect_err("expected missing binary rejection");
222
223        assert!(error.to_string().contains("failed to extract 7z archive"));
224        assert!(launcher.calls.borrow().is_empty());
225    }
226}