cli/util/
tar.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5use crate::util::errors::{wrap, WrappedError};
6
7use flate2::read::GzDecoder;
8use std::fs;
9use std::io::Seek;
10use std::path::{Path, PathBuf};
11use tar::Archive;
12
13use super::errors::wrapdbg;
14use super::io::ReportCopyProgress;
15
16fn should_skip_first_segment(file: &fs::File) -> Result<bool, WrappedError> {
17	// unfortunately, we need to re-read the archive here since you cannot reuse
18	// `.entries()`. But this will generally only look at one or two files, so this
19	// should be acceptably speedy... If not, we could hardcode behavior for
20	// different types of archives.
21
22	let tar = GzDecoder::new(file);
23	let mut archive = Archive::new(tar);
24	let mut entries = archive
25		.entries()
26		.map_err(|e| wrap(e, "error opening archive"))?;
27
28	let first_name = {
29		let file = entries
30			.next()
31			.expect("expected not to have an empty archive")
32			.map_err(|e| wrap(e, "error reading entry file"))?;
33
34		let path = file.path().expect("expected to have path");
35
36		path.iter()
37			.next()
38			.expect("expected to have non-empty name")
39			.to_owned()
40	};
41
42	let mut had_multiple = false;
43	for file in entries.flatten() {
44		had_multiple = true;
45		if let Ok(name) = file.path() {
46			if name.iter().next() != Some(&first_name) {
47				return Ok(false);
48			}
49		}
50	}
51
52	Ok(had_multiple) // prefix removal is invalid if there's only a single file
53}
54
55pub fn decompress_tarball<T>(
56	path: &Path,
57	parent_path: &Path,
58	mut reporter: T,
59) -> Result<(), WrappedError>
60where
61	T: ReportCopyProgress,
62{
63	let mut tar_gz = fs::File::open(path)
64		.map_err(|e| wrap(e, format!("error opening file {}", path.display())))?;
65	let skip_first = should_skip_first_segment(&tar_gz)?;
66
67	// reset since skip logic read the tar already:
68	tar_gz
69		.rewind()
70		.map_err(|e| wrap(e, "error resetting seek position"))?;
71
72	let tar = GzDecoder::new(tar_gz);
73	let mut archive = Archive::new(tar);
74
75	let results = archive
76		.entries()
77		.map_err(|e| wrap(e, format!("error opening archive {}", path.display())))?
78		.filter_map(|e| e.ok())
79		.map(|mut entry| {
80			let entry_path = entry
81				.path()
82				.map_err(|e| wrap(e, "error reading entry path"))?;
83
84			let path = parent_path.join(if skip_first {
85				entry_path.iter().skip(1).collect::<PathBuf>()
86			} else {
87				entry_path.into_owned()
88			});
89
90			if let Some(p) = path.parent() {
91				fs::create_dir_all(p)
92					.map_err(|e| wrap(e, format!("could not create dir for {}", p.display())))?;
93			}
94
95			entry
96				.unpack(&path)
97				.map_err(|e| wrapdbg(e, format!("error unpacking {}", path.display())))?;
98			Ok(path)
99		})
100		.collect::<Result<Vec<PathBuf>, WrappedError>>()?;
101
102	// Tarballs don't have a way to get the number of entries ahead of time
103	reporter.report_progress(results.len() as u64, results.len() as u64);
104
105	Ok(())
106}