Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/archive/rar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use crate::{
error::{Error, FinalError, Result},
info,
list::{FileInArchive, ListFileType},
utils::{BytesFmt, PathFmt, resolve_extraction_conflict, validate_entry_path},
utils::{
BytesFmt, PathFmt, canonical_dest_inside_root, create_dir_inside_root, resolve_extraction_conflict,
validate_dest_inside_root, validate_entry_path,
},
warning,
};

Expand All @@ -40,12 +43,13 @@ fn move_into_place(root: &Path, dir: &Path, output_folder: &Path, question_polic
let source = entry?.path();
let dest = output_folder.join(source.strip_prefix(root).expect("child of staging root"));

validate_dest_inside_root(output_folder, &dest)?;

if fs::symlink_metadata(&source)?.is_dir() {
std::fs::create_dir_all(&dest).map_err(|err| Error::Custom {
reason: FinalError::with_title(format!("failed to create {}", PathFmt(&dest))).detail(err.to_string()),
})?;
files_unpacked += move_into_place(root, &source, output_folder, question_policy)?;
create_dir_inside_root(output_folder, &dest)?;
files_unpacked += 1 + move_into_place(root, &source, output_folder, question_policy)?;
} else if let Some(target) = resolve_extraction_conflict(&dest, question_policy)? {
let target = canonical_dest_inside_root(output_folder, &target)?;
let size = fs::symlink_metadata(&source)?.len();
std::fs::rename(&source, &target).map_err(|err| Error::Custom {
reason: FinalError::with_title(format!("failed to extract {}", PathFmt(&target)))
Expand Down
31 changes: 21 additions & 10 deletions src/archive/sevenz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use std::{

use bstr::ByteSlice;
use fs_err as fs;
use fs_err::PathExt;
use same_file::Handle;
use sevenz_rust2::ArchiveEntry;

Expand All @@ -17,9 +16,9 @@ use crate::{
info,
list::{FileInArchive, ListFileType},
utils::{
BytesFmt, FileVisibilityPolicy, PathFmt, cd_into_same_dir_as, copy_limited_decompression,
ensure_parent_dir_exists, is_same_file_as_output, resolve_extraction_conflict, validate_dest_inside_root,
validate_entry_path,
BytesFmt, FileVisibilityPolicy, PathFmt, canonical_dest_inside_root, cd_into_same_dir_as,
copy_limited_decompression, create_dir_inside_root, is_same_file_as_output, resolve_extraction_conflict,
validate_dest_inside_root, validate_entry_path,
},
warning,
};
Expand All @@ -38,7 +37,7 @@ where
let mut conflict_error = None;

let entry_extract_fn =
|entry: &ArchiveEntry, reader: &mut dyn Read, path: &PathBuf| -> Result<bool, sevenz_rust2::Error> {
|entry: &ArchiveEntry, reader: &mut dyn Read, _path: &PathBuf| -> Result<bool, sevenz_rust2::Error> {
// Manually handle writing all files from 7z archive (the library defaults ignore empty files)

let name_as_path = Path::new(entry.name());
Expand All @@ -58,11 +57,12 @@ where

if entry.is_directory() {
info!("File {} extracted to {}", entry.name(), PathFmt(&file_path));
if !path.fs_err_try_exists()? {
fs::create_dir_all(path)?;
if let Err(err) = create_dir_inside_root(output_path, &file_path) {
conflict_error = Some(err);
return Ok(false);
}
} else {
let dest = match resolve_extraction_conflict(path, question_policy) {
let dest = match resolve_extraction_conflict(&file_path, question_policy) {
Ok(Some(dest)) => dest,
Ok(None) => return Ok(true),
Err(err) => {
Expand All @@ -73,9 +73,20 @@ where

info!("extracted ({}) {}", BytesFmt(entry.size()), PathFmt(&dest));

ensure_parent_dir_exists(&dest)?;
if let Err(err) = create_dir_inside_root(output_path, dest.parent().unwrap_or(output_path)) {
conflict_error = Some(err);
return Ok(false);
}

let dest = match canonical_dest_inside_root(output_path, &dest) {
Ok(dest) => dest,
Err(err) => {
conflict_error = Some(err);
return Ok(false);
}
};

let file = fs::File::create(&dest)?;
let file = fs::OpenOptions::new().write(true).create_new(true).open(&dest)?;
let mut writer = BufWriter::new(file);
copy_limited_decompression(reader, &mut writer)?;

Expand Down
31 changes: 24 additions & 7 deletions src/archive/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use crate::{
info,
list::{FileInArchive, ListFileType},
utils::{
self, BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, create_symlink, is_same_file_as_output,
read_file_type, resolve_extraction_conflict, sanitize_archive_mode, set_permission_mode,
validate_dest_inside_root, validate_entry_path, validate_symlink_target,
self, BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonical_dest_inside_root, canonicalize,
create_symlink, is_same_file_as_output, read_file_type, resolve_extraction_conflict, sanitize_archive_mode,
set_permission_mode, validate_dest_inside_root, validate_entry_path, validate_symlink_target,
},
warning,
};
Expand Down Expand Up @@ -50,7 +50,13 @@ pub fn unpack_archive(reader: impl Read, output_folder: &Path, question_policy:

validate_symlink_target(&safe_relpath, &target)?;
validate_dest_inside_root(output_folder, &full_path)?;
create_symlink(&target, &full_path)?;

let Some(dest) = resolve_extraction_conflict(&full_path, question_policy)? else {
continue;
};
let dest = canonical_dest_inside_root(output_folder, &dest)?;
create_symlink(&target, &dest)?;
written = Some(dest);
}
tar::EntryType::Link => {
let raw_link = entry.path()?.into_owned();
Expand All @@ -66,22 +72,33 @@ pub fn unpack_archive(reader: impl Read, output_folder: &Path, question_policy:

validate_dest_inside_root(output_folder, &full_link_path)?;
validate_dest_inside_root(output_folder, &full_target_path)?;
fs::hard_link(&full_target_path, &full_link_path)?;

let Some(dest) = resolve_extraction_conflict(&full_link_path, question_policy)? else {
continue;
};
let dest = canonical_dest_inside_root(output_folder, &dest)?;
fs::hard_link(&full_target_path, &dest)?;
written = Some(dest);
}
tar::EntryType::Regular | tar::EntryType::GNUSparse => {
let raw_path = entry.path()?.into_owned();
let safe_relpath = validate_entry_path(&raw_path)?;
let full_path = output_folder.join(&safe_relpath);

validate_dest_inside_root(output_folder, &full_path)?;

let Some(dest) = resolve_extraction_conflict(&full_path, question_policy)? else {
continue;
};

if dest == full_path {
let dest = if dest == full_path {
entry.unpack_in(output_folder)?;
dest
} else {
let dest = canonical_dest_inside_root(output_folder, &dest)?;
entry.unpack(&dest)?;
}
dest
};
written = Some(dest);
}
tar::EntryType::Directory => {
Expand Down
35 changes: 15 additions & 20 deletions src/archive/zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ use crate::{
info, info_accessible,
list::{FileInArchive, ListFileType},
utils::{
BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, cd_into_same_dir_as,
copy_limited_decompression, create_symlink, ensure_parent_dir_exists, get_invalid_utf8_paths,
is_same_file_as_output, pretty_format_list_of_paths, read_file_type, resolve_extraction_conflict,
strip_cur_dir, validate_dest_inside_root, validate_symlink_target,
BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonical_dest_inside_root, canonicalize,
cd_into_same_dir_as, copy_limited_decompression, create_dir_inside_root, create_symlink,
get_invalid_utf8_paths, is_same_file_as_output, pretty_format_list_of_paths, read_file_type,
resolve_extraction_conflict, validate_dest_inside_root, validate_symlink_target,
},
warning,
};
Expand Down Expand Up @@ -77,30 +77,26 @@ where
let target = symlink_target_from_bytes(&target_bytes);

validate_symlink_target(&relpath, &target)?;
let file_path = canonical_dest_inside_root(output_folder, &file_path)?;
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &file_path)?;
#[cfg(windows)]
std::os::windows::fs::symlink_dir(&target, file_path)?;
std::os::windows::fs::symlink_dir(&target, &file_path)?;
} else {
fs::create_dir_all(&file_path)?;
create_dir_inside_root(output_folder, &file_path)?;
}
}
_is_file @ false => {
ensure_parent_dir_exists(&file_path)?;
let file_path = strip_cur_dir(file_path.as_path());
create_dir_inside_root(output_folder, file_path.parent().unwrap_or(output_folder))?;

let mode = file.unix_mode();
let is_symlink = mode.is_some_and(|mode| mode & 0o170000 == 0o120000);

// Symlink creation fails on its own when the path is taken.
let mut resolved = None;
if !is_symlink {
let Some(path) = resolve_extraction_conflict(file_path, question_policy)? else {
continue;
};
resolved = Some(path);
}
let file_path = resolved.as_deref().unwrap_or(file_path);
let Some(resolved) = resolve_extraction_conflict(&file_path, question_policy)? else {
continue;
};
let resolved = canonical_dest_inside_root(output_folder, &resolved)?;
let file_path = resolved.as_path();

if is_symlink {
// Symlink targets are arbitrary bytes on Unix, not guaranteed UTF-8; read as bytes.
Expand All @@ -119,13 +115,12 @@ where
let mode = file.unix_mode().and_then(valid_unix_permissions).unwrap_or(0o644);
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.create_new(true)
.mode(mode)
.open(file_path)?
};
#[cfg(not(unix))]
let mut output_file = fs::File::create(file_path)?;
let mut output_file = fs::OpenOptions::new().write(true).create_new(true).open(file_path)?;
{
copy_limited_decompression(&mut file, &mut output_file)?;
}
Expand Down
20 changes: 17 additions & 3 deletions src/commands/decompress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ pub fn prepare_decompress_target(
let name = output_file_path.file_name().unwrap_or_default().to_os_string();
let file_target = output_dir.join(&name);
let claimed = claimed_targets.contains(&file_target);
let resolved = if !claimed && !file_target.fs_err_try_exists()? {
// symlink_metadata makes a symlink in the way a conflict, not a path to write through
let resolved = if !claimed && fs::symlink_metadata(&file_target).is_err() {
file_target
} else if let Some(p) = resolve_path_conflict(&file_target, question_policy, QuestionAction::Decompression)? {
p
Expand Down Expand Up @@ -100,8 +101,10 @@ pub fn prepare_decompress_target(
let is_cwd = target == *INITIAL_CURRENT_DIR;
// merging several inputs into the CWD is intentional so the claimed check skips it
let claimed_by_earlier_input = !is_cwd && claimed_targets.contains(&target);
let is_valid = !claimed_by_earlier_input
&& (is_cwd || !target.fs_err_try_exists()? || (target.is_dir() && target.read_dir()?.next().is_none()));
let existing = fs::symlink_metadata(&target).ok();
// a symlink is never a valid target, writing into it would leave the output directory
let is_empty_dir = existing.as_ref().is_some_and(|md| md.is_dir()) && target.read_dir()?.next().is_none();
let is_valid = !claimed_by_earlier_input && (is_cwd || existing.is_none() || is_empty_dir);

let resolved = if is_valid {
target
Expand Down Expand Up @@ -197,6 +200,17 @@ pub fn decompress_file(options: DecompressOptions) -> Result<()> {
};

let final_output_path = dir.join(file_name);
#[cfg(unix)]
let mut writer = {
use fs_err::os::unix::fs::OpenOptionsExt;
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW)
.open(&final_output_path)?
};
#[cfg(not(unix))]
let mut writer = fs::File::create(&final_output_path)?;
io::copy(&mut reader, &mut writer)?;
ControlFlow::Continue(DecompressionSummary::NonArchive {
Expand Down
Loading
Loading