winbrew_core\fs\archive\extract/
sevenz.rs1use 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}