xtask_lib/tasks/util/
fs.rs1use std::path::Path;
4
5use anyhow::{Context, Result};
6
7pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
14 std::fs::create_dir_all(dst)?;
15 for entry in std::fs::read_dir(src)? {
16 let entry = entry?;
17 let src_path = entry.path();
18 let dst_path = dst.join(entry.file_name());
19 if src_path.is_dir() {
20 copy_dir_all(&src_path, &dst_path)?;
21 } else {
22 std::fs::copy(&src_path, &dst_path).with_context(|| {
23 format!(
24 "failed to copy {} → {}",
25 src_path.display(),
26 dst_path.display()
27 )
28 })?;
29 }
30 }
31 Ok(())
32}
33
34pub fn create_tarball(dest: &Path, base_dir: &Path, entries: &[&str]) -> Result<()> {
41 if let Some(parent) = dest.parent() {
42 std::fs::create_dir_all(parent)
43 .with_context(|| format!("failed to create parent directory for {}", dest.display()))?;
44 }
45
46 let file = std::fs::File::create(dest)
47 .with_context(|| format!("failed to create archive {}", dest.display()))?;
48
49 let gz = flate2::write::GzEncoder::new(file, flate2::Compression::default());
50 let mut builder = tar::Builder::new(gz);
51
52 for entry in entries {
53 let entry_path = base_dir.join(entry);
54
55 if entry_path.is_dir() {
56 builder
57 .append_dir_all(entry, &entry_path)
58 .with_context(|| format!("failed to add directory {entry} to archive"))?;
59 continue;
60 }
61
62 builder
63 .append_path_with_name(&entry_path, entry)
64 .with_context(|| format!("failed to add file {entry} to archive"))?;
65 }
66
67 builder
68 .into_inner()
69 .context("failed to finalise tar builder")?
70 .finish()
71 .context("failed to finalise gzip stream")?;
72
73 Ok(())
74}
75
76pub fn extract_tarball(src: &Path, dest_dir: &Path) -> Result<()> {
83 std::fs::create_dir_all(dest_dir).with_context(|| {
84 format!(
85 "failed to create destination directory {}",
86 dest_dir.display()
87 )
88 })?;
89
90 let file = std::fs::File::open(src)
91 .with_context(|| format!("failed to open archive {}", src.display()))?;
92
93 let gz = flate2::read::GzDecoder::new(file);
94 let mut archive = tar::Archive::new(gz);
95
96 archive.unpack(dest_dir).with_context(|| {
97 format!(
98 "failed to extract {} into {}",
99 src.display(),
100 dest_dir.display()
101 )
102 })
103}
104
105pub fn extract_tarball_strip_one(src: &Path, dest_dir: &Path) -> Result<()> {
112 std::fs::create_dir_all(dest_dir).with_context(|| {
113 format!(
114 "failed to create destination directory {}",
115 dest_dir.display()
116 )
117 })?;
118
119 let file = std::fs::File::open(src)
120 .with_context(|| format!("failed to open archive {}", src.display()))?;
121
122 let gz = flate2::read::GzDecoder::new(file);
123 let mut archive = tar::Archive::new(gz);
124
125 for entry in archive
126 .entries()
127 .with_context(|| format!("failed to read entries from {}", src.display()))?
128 {
129 let mut entry =
130 entry.with_context(|| format!("failed to read entry from {}", src.display()))?;
131
132 let entry_path = entry
133 .path()
134 .with_context(|| format!("entry in {} has no path", src.display()))?
135 .into_owned();
136
137 let stripped = entry_path
138 .components()
139 .skip(1)
140 .collect::<std::path::PathBuf>();
141
142 if stripped.as_os_str().is_empty() {
143 continue;
144 }
145
146 let dest = dest_dir.join(&stripped);
147
148 if let Some(parent) = dest.parent() {
149 std::fs::create_dir_all(parent)
150 .with_context(|| format!("failed to create directory {}", parent.display()))?;
151 }
152
153 entry
154 .unpack(&dest)
155 .with_context(|| format!("failed to unpack entry to {}", dest.display()))?;
156 }
157
158 Ok(())
159}
160
161pub fn extract_tarball_paths(src: &Path, dest_dir: &Path, prefixes: &[&str]) -> Result<()> {
171 std::fs::create_dir_all(dest_dir).with_context(|| {
172 format!(
173 "failed to create destination directory {}",
174 dest_dir.display()
175 )
176 })?;
177
178 let file = std::fs::File::open(src)
179 .with_context(|| format!("failed to open archive {}", src.display()))?;
180
181 let gz = flate2::read::GzDecoder::new(file);
182 let mut archive = tar::Archive::new(gz);
183
184 for entry in archive
185 .entries()
186 .with_context(|| format!("failed to read entries from {}", src.display()))?
187 {
188 let mut entry =
189 entry.with_context(|| format!("failed to read entry from {}", src.display()))?;
190
191 let entry_path = entry
192 .path()
193 .with_context(|| format!("entry in {} has no path", src.display()))?
194 .into_owned();
195
196 let normalised = entry_path
197 .strip_prefix("./")
198 .unwrap_or(&entry_path)
199 .to_string_lossy();
200
201 let matches = prefixes.iter().any(|prefix| {
202 normalised == *prefix
203 || normalised.starts_with(&format!("{prefix}/"))
204 || normalised.starts_with(&format!("{prefix}\\"))
205 });
206
207 if !matches {
208 continue;
209 }
210
211 let dest = dest_dir.join(normalised.as_ref());
212
213 if let Some(parent) = dest.parent() {
214 std::fs::create_dir_all(parent)
215 .with_context(|| format!("failed to create directory {}", parent.display()))?;
216 }
217
218 entry
219 .unpack(&dest)
220 .with_context(|| format!("failed to unpack entry to {}", dest.display()))?;
221 }
222
223 Ok(())
224}
225
226pub fn extract_zip_paths(src: &Path, dest_dir: &Path, prefixes: &[&str]) -> Result<()> {
233 std::fs::create_dir_all(dest_dir).with_context(|| {
234 format!(
235 "failed to create destination directory {}",
236 dest_dir.display()
237 )
238 })?;
239
240 let file = std::fs::File::open(src)
241 .with_context(|| format!("failed to open archive {}", src.display()))?;
242
243 let mut archive = zip::ZipArchive::new(file)
244 .with_context(|| format!("failed to read zip {}", src.display()))?;
245
246 for i in 0..archive.len() {
247 let mut entry = archive
248 .by_index(i)
249 .with_context(|| format!("failed to read entry {i} from {}", src.display()))?;
250
251 let name = entry.name().to_owned();
252 let matches = prefixes
253 .iter()
254 .any(|prefix| name == *prefix || name.starts_with(&format!("{prefix}/")));
255
256 if !matches {
257 continue;
258 }
259
260 let dest = dest_dir.join(&name);
261
262 if entry.is_dir() {
263 std::fs::create_dir_all(&dest)
264 .with_context(|| format!("failed to create directory {}", dest.display()))?;
265 continue;
266 }
267
268 if let Some(parent) = dest.parent() {
269 std::fs::create_dir_all(parent)
270 .with_context(|| format!("failed to create directory {}", parent.display()))?;
271 }
272
273 #[cfg(unix)]
274 let unix_mode = entry.unix_mode();
275 let mut out = std::fs::File::create(&dest)
276 .with_context(|| format!("failed to create file {}", dest.display()))?;
277 std::io::copy(&mut entry, &mut out)
278 .with_context(|| format!("failed to write {}", dest.display()))?;
279
280 #[cfg(unix)]
281 {
282 use std::os::unix::fs::PermissionsExt;
283 if let Some(mode) = unix_mode {
284 std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(mode)).ok();
285 }
286 }
287 }
288
289 Ok(())
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use std::fs;
296
297 #[test]
298 fn copy_dir_all_copies_nested_structure() {
299 let tmp = tempfile::tempdir().unwrap();
300 let src = tmp.path().join("src");
301 let sub = src.join("sub");
302 fs::create_dir_all(&sub).unwrap();
303 fs::write(src.join("a.txt"), b"a").unwrap();
304 fs::write(sub.join("b.txt"), b"b").unwrap();
305
306 let dst = tmp.path().join("dst");
307 copy_dir_all(&src, &dst).unwrap();
308
309 assert_eq!(fs::read(dst.join("a.txt")).unwrap(), b"a");
310 assert_eq!(fs::read(dst.join("sub/b.txt")).unwrap(), b"b");
311 }
312
313 #[test]
314 fn create_tarball_writes_archive() {
315 let tmp = tempfile::tempdir().unwrap();
316
317 let src_dir = tmp.path().join("src");
318 fs::create_dir_all(&src_dir).unwrap();
319 fs::write(src_dir.join("hello.txt"), b"hello").unwrap();
320
321 let archive = tmp.path().join("out.tar.gz");
322 create_tarball(&archive, tmp.path(), &["src"]).unwrap();
323
324 assert!(archive.exists());
325 }
326
327 #[test]
328 fn create_and_extract_tarball_roundtrip() {
329 let tmp = tempfile::tempdir().unwrap();
330
331 let src_dir = tmp.path().join("src");
332 fs::create_dir_all(&src_dir).unwrap();
333 fs::write(src_dir.join("hello.txt"), b"hello").unwrap();
334
335 let archive = tmp.path().join("out.tar.gz");
336 create_tarball(&archive, tmp.path(), &["src"]).unwrap();
337
338 let extract_dir = tmp.path().join("extracted");
339 fs::create_dir_all(&extract_dir).unwrap();
340 extract_tarball(&archive, &extract_dir).unwrap();
341
342 assert!(extract_dir.join("src/hello.txt").exists());
343 }
344
345 #[test]
346 fn extract_zip_paths_extracts_only_matching_prefixes() {
347 let tmp = tempfile::tempdir().unwrap();
348
349 let zip_path = tmp.path().join("assets.zip");
350 let file = std::fs::File::create(&zip_path).unwrap();
351 let mut writer = zip::ZipWriter::new(file);
352 let options = zip::write::SimpleFileOptions::default()
353 .compression_method(zip::CompressionMethod::Stored);
354
355 writer.add_directory("bin/", options).unwrap();
356 writer.start_file("bin/tool", options).unwrap();
357 std::io::Write::write_all(&mut writer, b"tool binary").unwrap();
358
359 writer.add_directory("other/", options).unwrap();
360 writer.start_file("other/skip.txt", options).unwrap();
361 std::io::Write::write_all(&mut writer, b"skip me").unwrap();
362
363 writer.finish().unwrap();
364
365 let extract_dir = tmp.path().join("extracted");
366 extract_zip_paths(&zip_path, &extract_dir, &["bin"]).unwrap();
367
368 assert!(extract_dir.join("bin/tool").exists());
369 assert!(!extract_dir.join("other/skip.txt").exists());
370 }
371
372 #[test]
373 fn extract_tarball_strip_one_removes_top_level_dir() {
374 let tmp = tempfile::tempdir().unwrap();
375
376 let src_dir = tmp.path().join("toplevel");
377 fs::create_dir_all(&src_dir).unwrap();
378 fs::write(src_dir.join("file.txt"), b"content").unwrap();
379
380 let archive = tmp.path().join("strip.tar.gz");
381 create_tarball(&archive, tmp.path(), &["toplevel"]).unwrap();
382
383 let extract_dir = tmp.path().join("stripped");
384 fs::create_dir_all(&extract_dir).unwrap();
385 extract_tarball_strip_one(&archive, &extract_dir).unwrap();
386
387 assert!(extract_dir.join("file.txt").exists());
388 }
389
390 #[test]
391 fn extract_tarball_paths_extracts_only_matching_prefixes() {
392 let tmp = tempfile::tempdir().unwrap();
393
394 let libs_dir = tmp.path().join("libs");
395 let other_dir = tmp.path().join("other");
396 fs::create_dir_all(&libs_dir).unwrap();
397 fs::create_dir_all(&other_dir).unwrap();
398 fs::write(libs_dir.join("libfoo.so"), b"lib").unwrap();
399 fs::write(other_dir.join("skip.txt"), b"skip").unwrap();
400
401 let archive = tmp.path().join("out.tar.gz");
402 create_tarball(&archive, tmp.path(), &["libs", "other"]).unwrap();
403
404 let extract_dir = tmp.path().join("extracted");
405 extract_tarball_paths(&archive, &extract_dir, &["libs"]).unwrap();
406
407 assert!(extract_dir.join("libs/libfoo.so").exists());
408 assert!(!extract_dir.join("other/skip.txt").exists());
409 }
410}