use super::errors::{wrap, WrappedError};
use super::io::ReportCopyProgress;
use std::fs::{self, File};
use std::io;
use std::path::Path;
use std::path::PathBuf;
use zip::read::ZipFile;
use zip::{self, ZipArchive};
fn should_skip_first_segment(archive: &mut ZipArchive<File>) -> bool {
let first_name = {
let file = archive
.by_index_raw(0)
.expect("expected not to have an empty archive");
let path = file
.enclosed_name()
.expect("expected to have path")
.iter()
.next()
.expect("expected to have non-empty name");
path.to_owned()
};
for i in 1..archive.len() {
if let Ok(file) = archive.by_index_raw(i) {
if let Some(name) = file.enclosed_name() {
if name.iter().next() != Some(&first_name) {
return false;
}
}
}
}
archive.len() > 1
}
pub fn unzip_file<T>(file: File, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError>
where
T: ReportCopyProgress,
{
let mut archive =
zip::ZipArchive::new(file).map_err(|e| wrap(e, "failed to open zip archive"))?;
let skip_segments_no = usize::from(should_skip_first_segment(&mut archive));
let report_progress_every = archive.len() / 20;
for i in 0..archive.len() {
if i % report_progress_every == 0 {
reporter.report_progress(i as u64, archive.len() as u64);
}
let mut file = archive
.by_index(i)
.map_err(|e| wrap(e, format!("could not open zip entry {i}")))?;
let outpath: PathBuf = match file.enclosed_name() {
Some(path) => {
let mut full_path = PathBuf::from(parent_path);
full_path.push(PathBuf::from_iter(path.iter().skip(skip_segments_no)));
full_path
}
None => continue,
};
if file.is_dir() || file.name().ends_with('/') {
fs::create_dir_all(&outpath)
.map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?;
apply_permissions(&file, &outpath)?;
continue;
}
if let Some(p) = outpath.parent() {
fs::create_dir_all(p)
.map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?;
}
#[cfg(unix)]
{
use libc::S_IFLNK;
use std::io::Read;
use std::os::unix::ffi::OsStringExt;
#[cfg(target_os = "macos")]
const S_IFLINK_32: u32 = S_IFLNK as u32;
#[cfg(target_os = "linux")]
const S_IFLINK_32: u32 = S_IFLNK;
if matches!(file.unix_mode(), Some(mode) if mode & S_IFLINK_32 == S_IFLINK_32) {
let mut link_to = Vec::new();
file.read_to_end(&mut link_to).map_err(|e| {
wrap(
e,
format!("could not read symlink linkpath {}", outpath.display()),
)
})?;
let link_path = PathBuf::from(std::ffi::OsString::from_vec(link_to));
std::os::unix::fs::symlink(link_path, &outpath).map_err(|e| {
wrap(e, format!("could not create symlink {}", outpath.display()))
})?;
continue;
}
}
let mut outfile = fs::File::create(&outpath).map_err(|e| {
wrap(
e,
format!(
"unable to open file to write {} (from {:?})",
outpath.display(),
file.enclosed_name().map(|p| p.to_string_lossy()),
),
)
})?;
io::copy(&mut file, &mut outfile)
.map_err(|e| wrap(e, format!("error copying file {}", outpath.display())))?;
apply_permissions(&file, &outpath)?;
}
reporter.report_progress(archive.len() as u64, archive.len() as u64);
Ok(())
}
#[cfg(unix)]
fn apply_permissions(file: &ZipFile, outpath: &Path) -> Result<(), WrappedError> {
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(outpath, fs::Permissions::from_mode(mode)).map_err(|e| {
wrap(
e,
format!("error setting permissions on {}", outpath.display()),
)
})?;
}
Ok(())
}
#[cfg(windows)]
fn apply_permissions(_file: &ZipFile, _outpath: &Path) -> Result<(), WrappedError> {
Ok(())
}