use std::{
borrow::Cow,
env,
io::{self, Read},
path::{Path, PathBuf},
};
use fs_err::{self as fs, PathExt};
use same_file::Handle;
use super::{question::FileConflitOperation, user_wants_to_overwrite};
use crate::{
FinalError, QuestionPolicy, Result,
error::Error,
extension::CompressionFormat,
info_accessible,
utils::{PathFmt, QuestionAction, strip_path_ascii_prefix},
};
pub fn is_path_stdin(path: &Path) -> bool {
path.as_os_str() == "-"
}
pub fn resolve_path_conflict(
path: &Path,
question_policy: QuestionPolicy,
question_action: QuestionAction,
) -> Result<Option<PathBuf>> {
if path.fs_err_try_exists()? {
match user_wants_to_overwrite(path, question_policy, question_action)? {
FileConflitOperation::Cancel => Ok(None),
FileConflitOperation::Overwrite => {
remove_file_or_dir(path)?;
Ok(Some(path.to_path_buf()))
}
FileConflitOperation::Rename => Ok(Some(find_available_filename_by_renaming(path)?)),
FileConflitOperation::Merge => Ok(Some(path.to_path_buf())),
}
} else {
Ok(Some(path.to_path_buf()))
}
}
pub fn remove_file_or_dir(path: &Path) -> Result<()> {
if path.is_dir() {
if let Ok(cwd) = env::current_dir() {
if matches!(
(Handle::from_path(path), Handle::from_path(&cwd)),
(Ok(a), Ok(b)) if a == b
) {
return Err(
FinalError::with_title("Refusing to delete the current working directory")
.detail(format!("Path {} is the current directory", PathFmt(path)))
.hint("Use a different output directory with `--dir` / `-d`")
.into(),
);
}
}
fs::remove_dir_all(path)?;
} else if path.is_file() {
fs::remove_file(path)?;
}
Ok(())
}
pub fn file_size(path: &Path) -> Result<u64> {
Ok(fs::metadata(path)?.len())
}
pub fn find_available_filename_by_renaming(path: &Path) -> Result<PathBuf> {
fn create_path_with_given_index(path: &Path, i: usize) -> PathBuf {
let parent = path.parent().unwrap_or_else(|| Path::new(""));
let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
let new_filename = match file_name.split_once('.') {
Some((stem, extension)) if !stem.is_empty() => format!("{stem}_{i}.{extension}"),
_ => format!("{file_name}_{i}"),
};
parent.join(new_filename)
}
for i in 1.. {
let renamed_path = create_path_with_given_index(path, i);
if !renamed_path.fs_err_try_exists()? {
return Ok(renamed_path);
}
}
unreachable!()
}
pub fn create_dir_if_non_existent(path: &Path) -> Result<()> {
if !path.fs_err_try_exists()? {
fs::create_dir_all(path)?;
info_accessible!("Directory {} created", PathFmt(path));
}
Ok(())
}
pub fn ensure_parent_dir_exists(file_path: &Path) -> io::Result<()> {
if let Some(parent) = file_path.parent() {
if !parent.fs_err_try_exists()? {
fs::create_dir_all(parent)?;
}
}
Ok(())
}
pub fn cd_into_same_dir_as(filename: &Path) -> Result<PathBuf> {
let previous_location = env::current_dir()?;
let parent = filename.parent().ok_or(Error::CompressingRootFolder)?;
env::set_current_dir(parent)?;
Ok(previous_location)
}
pub fn is_same_file_as_output(path: &Path, output_handle: &Handle) -> bool {
if matches!(Handle::from_path(path), Ok(x) if &x == output_handle) {
return true;
}
false
}
pub fn is_broken_symlink_error(error: &io::Error, path: &Path) -> bool {
error.kind() == io::ErrorKind::NotFound && path.is_symlink()
}
pub fn try_infer_format(path: &Path) -> Option<CompressionFormat> {
fn is_zip(buf: &[u8]) -> bool {
buf.len() >= 3
&& buf[..=1] == [0x50, 0x4B]
&& (buf[2..=3] == [0x3, 0x4] || buf[2..=3] == [0x5, 0x6] || buf[2..=3] == [0x7, 0x8])
}
fn is_tar(buf: &[u8]) -> bool {
buf.len() > 261 && buf[257..=261] == [0x75, 0x73, 0x74, 0x61, 0x72]
}
fn is_gz(buf: &[u8]) -> bool {
buf.starts_with(&[0x1F, 0x8B, 0x8])
}
fn is_bz2(buf: &[u8]) -> bool {
buf.starts_with(&[0x42, 0x5A, 0x68])
}
fn is_bz3(buf: &[u8]) -> bool {
buf.starts_with(b"BZ3v1")
}
fn is_lzma(buf: &[u8]) -> bool {
buf.len() >= 14 && buf[0] == 0x5d && (buf[12] == 0x00 || buf[12] == 0xff) && buf[13] == 0x00
}
fn is_xz(buf: &[u8]) -> bool {
buf.starts_with(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])
}
fn is_lzip(buf: &[u8]) -> bool {
buf.starts_with(&[0x4C, 0x5A, 0x49, 0x50])
}
fn is_lz4(buf: &[u8]) -> bool {
buf.starts_with(&[0x04, 0x22, 0x4D, 0x18])
}
fn is_sz(buf: &[u8]) -> bool {
buf.starts_with(&[0xFF, 0x06, 0x00, 0x00, 0x73, 0x4E, 0x61, 0x50, 0x70, 0x59])
}
fn is_zst(buf: &[u8]) -> bool {
buf.starts_with(&[0x28, 0xB5, 0x2F, 0xFD])
}
fn is_rar(buf: &[u8]) -> bool {
buf.len() >= 7
&& buf.starts_with(&[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07])
&& (buf[6] == 0x00 || (buf.len() >= 8 && buf[6..=7] == [0x01, 0x00]))
}
fn is_sevenz(buf: &[u8]) -> bool {
buf.starts_with(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])
}
let buf = {
let mut buf = [0; 270];
let result = std::fs::File::open(path).map(|mut file| file.read(&mut buf));
if result.is_err() {
return None;
}
buf
};
if is_zip(&buf) {
Some(CompressionFormat::Zip)
} else if is_tar(&buf) {
Some(CompressionFormat::Tar)
} else if is_gz(&buf) {
Some(CompressionFormat::Gzip)
} else if is_bz2(&buf) {
Some(CompressionFormat::Bzip)
} else if is_bz3(&buf) {
Some(CompressionFormat::Bzip3)
} else if is_lzma(&buf) {
Some(CompressionFormat::Lzma)
} else if is_xz(&buf) {
Some(CompressionFormat::Xz)
} else if is_lzip(&buf) {
Some(CompressionFormat::Lzip)
} else if is_lz4(&buf) {
Some(CompressionFormat::Lz4)
} else if is_sz(&buf) {
Some(CompressionFormat::Snappy)
} else if is_zst(&buf) {
Some(CompressionFormat::Zstd)
} else if is_rar(&buf) {
Some(CompressionFormat::Rar)
} else if is_sevenz(&buf) {
Some(CompressionFormat::SevenZip)
} else {
None
}
}
#[inline]
pub fn create_symlink(target: &Path, full_path: &Path) -> Result<()> {
#[cfg(unix)]
std::os::unix::fs::symlink(target, full_path)?;
#[cfg(windows)]
std::os::windows::fs::symlink_file(target, full_path)?;
Ok(())
}
#[cfg(unix)]
#[inline]
pub fn set_permission_mode(path: &Path, mode: u32) -> Result<()> {
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
fs::set_permissions(path, Permissions::from_mode(mode))?;
Ok(())
}
#[cfg(windows)]
#[inline]
pub fn set_permission_mode(_path: &Path, _mode: u32) -> Result<()> {
Ok(())
}
pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
let canonicalized = fs::canonicalize(path.as_ref())?;
Ok(if cfg!(windows) {
strip_path_ascii_prefix(Cow::Owned(canonicalized), r"\\?\").into_owned()
} else {
canonicalized
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIs)]
pub enum FileType {
Regular,
Directory,
Symlink,
}
pub fn read_file_type(path: impl AsRef<Path>) -> Result<FileType> {
use file_type_enum::FileType::*;
let path = path.as_ref();
match file_type_enum::FileType::symlink_read_at(path)? {
Regular => Ok(FileType::Regular),
Directory => Ok(FileType::Directory),
Symlink => Ok(FileType::Symlink),
variant => Err(FinalError::with_title(format!("unsupported file type {variant}"))
.detail(format!("found at {}", PathFmt(path)))
.into()),
}
}