use crate::result::{ZipResult, invalid};
use crate::{ZipArchive, result::ZipError};
use std::ffi::OsStr;
use std::io::{self, Read, Seek};
use std::path::Path;
use indexmap::IndexMap;
pub(crate) const MAX_SYMLINK_TARGET_LEN: u64 = 4096;
#[derive(Debug, Default, Copy, Clone)]
pub enum SymlinkExtractAction {
NoExtract,
ExtractAsFile,
#[cfg_attr(not(any(unix, windows)), default)]
ExtractAsFileUtf8,
#[cfg(any(unix, windows))]
#[default]
ExtractRecursiveInFolder,
#[cfg(any(unix, windows))]
ExtractInFolder,
#[cfg(any(unix, windows))]
ExtractNoRestrictions,
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct ExtractOptions {
pub(crate) symlink_action: SymlinkExtractAction,
}
impl ExtractOptions {
#[must_use]
pub fn symlink_action(self, symlink_action: SymlinkExtractAction) -> Self {
Self {
symlink_action,
..self
}
}
}
pub(crate) fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
use std::fs;
fs::create_dir_all(outpath.as_ref())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
outpath.as_ref(),
std::fs::Permissions::from_mode(
0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(),
),
)?;
}
Ok(())
}
#[cfg(unix)]
pub(crate) fn make_symlink_impl<T>(
outpath: &Path,
target_str: &str,
_existing_files: &IndexMap<Box<[u8]>, T>,
) -> ZipResult<()> {
std::os::unix::fs::symlink(Path::new(&target_str), outpath)?;
Ok(())
}
#[cfg(windows)]
pub(crate) fn make_symlink_impl<T>(
outpath: &Path,
target_str: &str,
existing_files: &IndexMap<Box<[u8]>, T>,
) -> ZipResult<()> {
use crate::format::functions::is_dir;
let target = Path::new(OsStr::new(&target_str));
let target_is_dir_from_archive =
is_dir(target_str.as_bytes()) && existing_files.contains_key(target_str.as_bytes());
let target_is_dir = if target_is_dir_from_archive {
true
} else if let Ok(meta) = std::fs::metadata(target) {
meta.is_dir()
} else {
false
};
if target_is_dir {
std::os::windows::fs::symlink_dir(target, outpath)?;
} else {
std::os::windows::fs::symlink_file(target, outpath)?;
}
Ok(())
}
#[cfg(any(windows, unix))]
pub(crate) fn make_symlink<T>(
base: &Path, outpath: &Path,
target: &[u8],
#[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap<Box<[u8]>, T>,
symlink_action: SymlinkExtractAction,
) -> ZipResult<()> {
let Ok(target_str) = std::str::from_utf8(target) else {
return Err(invalid!("Invalid UTF-8 as symlink target"));
};
let target_path = Path::new(target_str);
let (start, rest) = if target_path.is_absolute() {
let Some(rest) = crate::path::strip_base_prefix(base, target_path) else {
return Err(invalid!(
"make_symlink: Symlink target escapes the destination directory"
));
};
(base, rest)
} else {
if target_path.components().any(|component| {
matches!(
component,
std::path::Component::Prefix(_) | std::path::Component::RootDir
)
}) {
return Err(invalid!("Invalid symlink target path"));
}
let parent = outpath.parent().unwrap_or(base);
if !parent.starts_with(base) {
return Err(invalid!("Symlink is not inside the destination directory"));
}
(parent, target_path)
};
#[cfg(any(windows, unix))]
let allow_outside = matches!(
symlink_action,
SymlinkExtractAction::ExtractNoRestrictions
| SymlinkExtractAction::ExtractRecursiveInFolder
);
#[cfg(not(any(windows, unix)))]
let allow_outside = false;
if allow_outside {
#[cfg(any(unix, windows))]
if matches!(
symlink_action,
SymlinkExtractAction::ExtractRecursiveInFolder
) {
let abs_to_canonicalize = if target_path.is_absolute() {
target_path
} else {
&start.join(target_path)
};
match std::fs::canonicalize(abs_to_canonicalize) {
Ok(canonical) => {
if !canonical.starts_with(base) {
return Err(invalid!("Path escapes the destination directory"));
}
}
Err(_) => {
crate::path::resolve_enclosed(
base,
start,
rest.components().map(|c| c.as_os_str().to_os_string()),
false,
)?;
}
}
}
} else {
crate::path::resolve_enclosed(
base,
start,
rest.components().map(|c| c.as_os_str().to_os_string()),
false, )?;
}
make_symlink_impl(outpath, target_str, existing_files)
}
#[cfg(not(any(windows, unix)))]
pub(crate) fn make_symlink<T>(
_base: &Path,
outpath: &Path,
target: &[u8],
#[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap<Box<[u8]>, T>,
_symlink_action: SymlinkExtractAction,
) -> ZipResult<()> {
make_symlink_as_file_utf8(outpath, target)?;
Ok(())
}
pub(crate) fn make_symlink_as_file_utf8(outpath: &Path, target: &[u8]) -> ZipResult<()> {
let Ok(_) = std::str::from_utf8(target) else {
return Err(invalid!("Invalid UTF-8 as symlink target"));
};
make_symlink_as_file(outpath, target)?;
Ok(())
}
pub(crate) fn make_symlink_as_file(outpath: &Path, target: &[u8]) -> std::io::Result<()> {
use std::fs::File;
use std::io::Write;
let output = File::create(outpath);
output?.write_all(target)?;
Ok(())
}
#[cfg(unix)]
pub(crate) fn set_permissions_no_follow_symlink(
path: &Path,
mode: std::fs::Permissions,
) -> std::io::Result<()> {
use std::fs;
if fs::symlink_metadata(path)?.file_type().is_symlink() {
#[cfg(any(
target_os = "macos",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
{
use nix::fcntl::AT_FDCWD;
use nix::sys::stat::{FchmodatFlags, Mode, fchmodat};
use std::os::unix::fs::PermissionsExt;
let mode = Mode::from_bits_truncate(mode.mode() as u16);
fchmodat(AT_FDCWD, path, mode, FchmodatFlags::NoFollowSymlink)?;
}
} else {
fs::set_permissions(path, mode)?;
}
Ok(())
}
#[derive(Default, Debug)]
struct UnixFileModes {
#[cfg(unix)]
map: std::collections::BTreeMap<std::path::PathBuf, u32>,
#[cfg(not(unix))]
read_only_set: std::collections::BTreeSet<std::path::PathBuf>,
}
impl UnixFileModes {
pub fn add_mode(&mut self, path: std::path::PathBuf, mode: u32) {
#[cfg(unix)]
self.map.insert(path, mode);
#[cfg(not(unix))]
if mode & 0o222 == 0 {
self.read_only_set.insert(path);
} else {
self.read_only_set.remove(&path);
}
}
#[cfg(unix)]
pub fn all_perms_with_children_first(
self,
) -> impl IntoIterator<Item = (std::path::PathBuf, std::fs::Permissions)> {
use std::os::unix::fs::PermissionsExt;
self.map
.into_iter()
.rev()
.map(|(p, m)| (p, std::fs::Permissions::from_mode(m)))
}
#[cfg(not(unix))]
pub fn all_read_only_with_children_first(self) -> impl IntoIterator<Item = std::path::PathBuf> {
self.read_only_set.into_iter().rev()
}
}
impl<R: Read + Seek> ZipArchive<R> {
pub fn extract<P: AsRef<Path>>(&mut self, directory: P) -> ZipResult<()> {
self.extract_internal(
directory,
None::<fn(&Path) -> bool>,
ExtractOptions::default(),
)
}
pub fn extract_with_options<P: AsRef<Path>>(
&mut self,
directory: P,
extract_options: ExtractOptions,
) -> ZipResult<()> {
self.extract_internal(directory, None::<fn(&Path) -> bool>, extract_options)
}
pub fn extract_unwrapped_root_dir<P: AsRef<Path>>(
&mut self,
directory: P,
root_dir_filter: impl RootDirFilter,
) -> ZipResult<()> {
self.extract_internal(directory, Some(root_dir_filter), ExtractOptions::default())
}
fn extract_internal<P: AsRef<Path>>(
&mut self,
directory: P,
root_dir_filter: Option<impl RootDirFilter>,
extract_options: ExtractOptions,
) -> ZipResult<()> {
use std::fs;
fs::create_dir_all(&directory)?;
let directory = directory.as_ref().canonicalize()?;
let root_dir = root_dir_filter
.and_then(|filter| {
self.root_dir(&filter)
.transpose()
.map(|root_dir| root_dir.map(|root_dir| (root_dir, filter)))
})
.transpose()?;
let root_dir = root_dir
.as_ref()
.map(|(root_dir, filter)| {
crate::path::simplified_components(root_dir)
.ok_or_else(|| {
debug_assert!(false, "Invalid root dir path");
invalid!("Invalid root dir path")
})
.map(|root_dir| (root_dir, filter))
})
.transpose()?;
let mut files_by_unix_mode = UnixFileModes::default();
for i in 0..self.len() {
let mut file = self.by_index(i)?;
let mut outpath = directory.clone();
file.safe_prepare_path(directory.as_ref(), &mut outpath, root_dir.as_ref())?;
if file.is_symlink() {
let declared_len = file.size();
if declared_len > MAX_SYMLINK_TARGET_LEN {
return Err(invalid!(
"symlink target declares {} bytes, more than the {} byte limit",
declared_len,
MAX_SYMLINK_TARGET_LEN
));
}
let mut target = Vec::with_capacity(declared_len as usize);
file.read_to_end(&mut target)?;
drop(file);
match extract_options.symlink_action {
SymlinkExtractAction::NoExtract => {
}
SymlinkExtractAction::ExtractAsFile => {
make_symlink_as_file(&outpath, &target)?;
}
SymlinkExtractAction::ExtractAsFileUtf8 => {
make_symlink_as_file_utf8(&outpath, &target)?;
}
#[cfg(any(unix, windows))]
SymlinkExtractAction::ExtractRecursiveInFolder => {
make_symlink(
directory.as_ref(),
&outpath,
&target,
&self.shared.files,
extract_options.symlink_action,
)?;
}
#[cfg(any(unix, windows))]
SymlinkExtractAction::ExtractInFolder => {
make_symlink(
directory.as_ref(),
&outpath,
&target,
&self.shared.files,
extract_options.symlink_action,
)?;
}
#[cfg(any(unix, windows))]
SymlinkExtractAction::ExtractNoRestrictions => {
let Ok(target_str) = std::str::from_utf8(&target) else {
return Err(invalid!("Invalid UTF-8 as symlink target"));
};
make_symlink_impl(&outpath, target_str, &self.shared.files)?;
}
}
} else if file.is_dir() {
make_writable_dir_all(&outpath)?;
if let Some(mode) = file.unix_mode() {
files_by_unix_mode.add_mode(outpath, mode);
}
} else {
let mut outfile = fs::File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
#[cfg(feature = "chrono")]
if let Some(last_modified) = file.last_modified()
&& let Some(t) = last_modified.datetime_to_systemtime()
{
outfile.set_modified(t)?;
}
#[cfg(not(unix))]
if file.is_read_only() {
let mut permissions = outfile.metadata()?.permissions();
permissions.set_readonly(true);
outfile.set_permissions(permissions)?;
}
#[cfg(unix)]
if let Some(mode) = file.unix_mode() {
use std::os::unix::fs::PermissionsExt;
outfile.set_permissions(fs::Permissions::from_mode(mode))?;
}
}
}
#[cfg(unix)]
for (path, perms) in files_by_unix_mode.all_perms_with_children_first() {
set_permissions_no_follow_symlink(&path, perms)?;
}
#[cfg(not(unix))]
for path in files_by_unix_mode.all_read_only_with_children_first() {
let metadata = fs::symlink_metadata(&path)?;
if metadata.is_dir() {
let mut permissions = metadata.permissions();
permissions.set_readonly(true);
fs::set_permissions(path, permissions)?;
}
}
Ok(())
}
}
pub trait RootDirFilter: Fn(&Path) -> bool {}
impl<F: Fn(&Path) -> bool> RootDirFilter for F {}
#[must_use]
pub fn root_dir_common_filter(path: &Path) -> bool {
const COMMON_FILTER_ROOT_FILES: &[&str] = &[".DS_Store", "Thumbs.db"];
if path.starts_with("__MACOSX") {
return false;
}
if path.components().count() == 1
&& path.file_name().is_some_and(|file_name| {
COMMON_FILTER_ROOT_FILES
.iter()
.map(OsStr::new)
.any(|cmp| cmp == file_name)
})
{
return false;
}
true
}