cli/util/
zipper.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 super::errors::{wrap, WrappedError};
6use super::io::ReportCopyProgress;
7use std::fs::{self, File};
8use std::io;
9use std::path::Path;
10use std::path::PathBuf;
11use zip::read::ZipFile;
12use zip::{self, ZipArchive};
13
14// Borrowed and modified from https://github.com/zip-rs/zip/blob/master/examples/extract.rs
15
16/// Returns whether all files in the archive start with the same path segment.
17/// If so, it's an indication we should skip that segment when extracting.
18fn should_skip_first_segment(archive: &mut ZipArchive<File>) -> bool {
19	let first_name = {
20		let file = archive
21			.by_index_raw(0)
22			.expect("expected not to have an empty archive");
23
24		let path = file
25			.enclosed_name()
26			.expect("expected to have path")
27			.iter()
28			.next()
29			.expect("expected to have non-empty name");
30
31		path.to_owned()
32	};
33
34	for i in 1..archive.len() {
35		if let Ok(file) = archive.by_index_raw(i) {
36			if let Some(name) = file.enclosed_name() {
37				if name.iter().next() != Some(&first_name) {
38					return false;
39				}
40			}
41		}
42	}
43
44	archive.len() > 1 // prefix removal is invalid if there's only a single file
45}
46
47pub fn unzip_file<T>(path: &Path, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError>
48where
49	T: ReportCopyProgress,
50{
51	let file = fs::File::open(path)
52		.map_err(|e| wrap(e, format!("unable to open file {}", path.display())))?;
53
54	let mut archive = zip::ZipArchive::new(file)
55		.map_err(|e| wrap(e, format!("failed to open zip archive {}", path.display())))?;
56
57	let skip_segments_no = usize::from(should_skip_first_segment(&mut archive));
58	for i in 0..archive.len() {
59		reporter.report_progress(i as u64, archive.len() as u64);
60		let mut file = archive
61			.by_index(i)
62			.map_err(|e| wrap(e, format!("could not open zip entry {}", i)))?;
63
64		let outpath: PathBuf = match file.enclosed_name() {
65			Some(path) => {
66				let mut full_path = PathBuf::from(parent_path);
67				full_path.push(PathBuf::from_iter(path.iter().skip(skip_segments_no)));
68				full_path
69			}
70			None => continue,
71		};
72
73		if file.is_dir() || file.name().ends_with('/') {
74			fs::create_dir_all(&outpath)
75				.map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?;
76			apply_permissions(&file, &outpath)?;
77			continue;
78		}
79
80		if let Some(p) = outpath.parent() {
81			fs::create_dir_all(p)
82				.map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?;
83		}
84
85		#[cfg(unix)]
86		{
87			use libc::S_IFLNK;
88			use std::io::Read;
89			use std::os::unix::ffi::OsStringExt;
90
91			#[cfg(target_os = "macos")]
92			const S_IFLINK_32: u32 = S_IFLNK as u32;
93
94			#[cfg(target_os = "linux")]
95			const S_IFLINK_32: u32 = S_IFLNK;
96
97			if matches!(file.unix_mode(), Some(mode) if mode & S_IFLINK_32 == S_IFLINK_32) {
98				let mut link_to = Vec::new();
99				file.read_to_end(&mut link_to).map_err(|e| {
100					wrap(
101						e,
102						format!("could not read symlink linkpath {}", outpath.display()),
103					)
104				})?;
105
106				let link_path = PathBuf::from(std::ffi::OsString::from_vec(link_to));
107				std::os::unix::fs::symlink(link_path, &outpath).map_err(|e| {
108					wrap(e, format!("could not create symlink {}", outpath.display()))
109				})?;
110				continue;
111			}
112		}
113
114		let mut outfile = fs::File::create(&outpath).map_err(|e| {
115			wrap(
116				e,
117				format!(
118					"unable to open file to write {} (from {:?})",
119					outpath.display(),
120					file.enclosed_name().map(|p| p.to_string_lossy()),
121				),
122			)
123		})?;
124
125		io::copy(&mut file, &mut outfile)
126			.map_err(|e| wrap(e, format!("error copying file {}", outpath.display())))?;
127
128		apply_permissions(&file, &outpath)?;
129	}
130
131	reporter.report_progress(archive.len() as u64, archive.len() as u64);
132
133	Ok(())
134}
135
136#[cfg(unix)]
137fn apply_permissions(file: &ZipFile, outpath: &Path) -> Result<(), WrappedError> {
138	use std::os::unix::fs::PermissionsExt;
139
140	if let Some(mode) = file.unix_mode() {
141		fs::set_permissions(outpath, fs::Permissions::from_mode(mode)).map_err(|e| {
142			wrap(
143				e,
144				format!("error setting permissions on {}", outpath.display()),
145			)
146		})?;
147	}
148
149	Ok(())
150}
151
152#[cfg(windows)]
153fn apply_permissions(_file: &ZipFile, _outpath: &Path) -> Result<(), WrappedError> {
154	Ok(())
155}