use std::cell::RefCell;
use camino::{Utf8Path, Utf8PathBuf};
use sevenz_rust2::encoder_options::{AesEncoderOptions, Lzma2Options};
use sevenz_rust2::{ArchiveEntry, EncoderConfiguration, EncoderMethod, Password};
use crate::error::{Error, Result};
use crate::{ArchiveInfo, CompressOpts, DecompressOpts, Entry};
pub fn compress(inputs: &[Utf8PathBuf], output: &Utf8Path, opts: &CompressOpts<'_>) -> Result<()> {
let inputs = crate::filter::validate_inputs(inputs, opts)?;
let result = compress_validated(&inputs, output, opts);
if result.is_err() {
let _ = fs_err::remove_file(output);
}
result
}
fn compress_validated(
inputs: &[Utf8PathBuf],
output: &Utf8Path,
opts: &CompressOpts<'_>,
) -> Result<()> {
let mut writer = sevenz_rust2::ArchiveWriter::create(output)?;
let comp_cfg: Option<EncoderConfiguration> = match opts.level {
Some(0) => Some(EncoderConfiguration::new(EncoderMethod::COPY)),
Some(level) => Some(Lzma2Options::from_level(level).into()),
None => None,
};
if let Some(pwd) = &opts.password {
let aes_cfg: EncoderConfiguration =
AesEncoderOptions::new(Password::from(pwd.as_str())).into();
let comp_cfg = comp_cfg.unwrap_or_else(|| EncoderConfiguration::new(EncoderMethod::LZMA2));
writer.set_content_methods(vec![aes_cfg, comp_cfg]);
} else if let Some(comp_cfg) = comp_cfg {
writer.set_content_methods(vec![comp_cfg]);
}
for input in inputs {
let meta = crate::filter::input_metadata(input, opts.follow_symlinks)?;
let name = crate::filter::input_base_name(input)?;
if opts.excludes.is_match(&name) {
continue;
}
let link_meta = fs_err::symlink_metadata(input)?;
if !opts.follow_symlinks && link_meta.file_type().is_symlink() {
push_symlink_entry(&mut writer, input, &name, &link_meta, opts)?;
} else if meta.is_dir() {
if opts.no_recursion {
push_dir_entry(&mut writer, input, &name, &meta)?;
} else {
push_dir_walked(&mut writer, input, &name, opts)?;
}
} else if !crate::filter::skip_unarchivable_special(&meta, &name) {
push_file_entry(&mut writer, input, &name, &meta, opts)?;
}
}
let file = writer.finish()?;
file.sync_all()?;
Ok(())
}
const ATTR_UNIX_EXTENSION: u32 = 0x8000;
const ATTR_READONLY: u32 = 0x1;
const ATTR_DIRECTORY: u32 = 0x10;
const ATTR_ARCHIVE: u32 = 0x20;
#[cfg(unix)]
fn unix_attributes(mode: u32) -> u32 {
let mut low = if mode & 0xF000 == 0o040000 {
ATTR_DIRECTORY
} else {
ATTR_ARCHIVE
};
if mode & 0o222 == 0 {
low |= ATTR_READONLY;
}
low | ATTR_UNIX_EXTENSION | ((mode & 0xFFFF) << 16)
}
fn apply_unix_attributes(entry: &mut ArchiveEntry, meta: &std::fs::Metadata) {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
entry.windows_attributes = unix_attributes(meta.mode());
entry.has_windows_attributes = true;
}
#[cfg(not(unix))]
{
let _ = (entry, meta);
}
}
fn entry_unix_mode(entry: &ArchiveEntry) -> Option<u32> {
(entry.has_windows_attributes && entry.windows_attributes & ATTR_UNIX_EXTENSION != 0)
.then_some((entry.windows_attributes >> 16) & 0xFFFF)
}
fn push_dir_walked(
writer: &mut sevenz_rust2::ArchiveWriter<std::fs::File>,
dir: &Utf8Path,
prefix: &str,
opts: &CompressOpts<'_>,
) -> Result<()> {
crate::filter::walk_dir(dir, prefix, opts, &mut |entry| {
let link_meta = fs_err::symlink_metadata(&entry.fs_path)?;
if !opts.follow_symlinks && link_meta.file_type().is_symlink() {
push_symlink_entry(writer, &entry.fs_path, &entry.archive_name, &link_meta, opts)
} else if entry.is_dir {
let meta = crate::filter::input_metadata(&entry.fs_path, opts.follow_symlinks)?;
push_dir_entry(writer, &entry.fs_path, &entry.archive_name, &meta)
} else {
let meta = crate::filter::input_metadata(&entry.fs_path, opts.follow_symlinks)?;
if crate::filter::skip_unarchivable_special(&meta, &entry.archive_name) {
return Ok(());
}
push_file_entry(writer, &entry.fs_path, &entry.archive_name, &meta, opts)
}
})
}
fn push_file_entry(
writer: &mut sevenz_rust2::ArchiveWriter<std::fs::File>,
fs_path: &Utf8Path,
archive_name: &str,
meta: &std::fs::Metadata,
opts: &CompressOpts<'_>,
) -> Result<()> {
let mut entry = ArchiveEntry::from_path(fs_path.as_std_path(), archive_name.to_owned());
apply_unix_attributes(&mut entry, meta);
let file = fs_err::File::open(fs_path)?;
writer.push_archive_entry(entry, Some(file))?;
opts.progress.set_entry(archive_name);
opts.progress.inc(meta.len());
Ok(())
}
fn push_dir_entry(
writer: &mut sevenz_rust2::ArchiveWriter<std::fs::File>,
fs_path: &Utf8Path,
archive_name: &str,
meta: &std::fs::Metadata,
) -> Result<()> {
let mut entry = ArchiveEntry::from_path(fs_path.as_std_path(), archive_name.to_owned());
apply_unix_attributes(&mut entry, meta);
writer.push_archive_entry::<std::fs::File>(entry, None)?;
Ok(())
}
fn push_symlink_entry(
writer: &mut sevenz_rust2::ArchiveWriter<std::fs::File>,
fs_path: &Utf8Path,
archive_name: &str,
link_meta: &std::fs::Metadata,
opts: &CompressOpts<'_>,
) -> Result<()> {
let target = fs_err::read_link(fs_path)?;
let target_str = target
.to_str()
.ok_or_else(|| Error::InvalidUtf8Path(target.display().to_string()))?
.to_owned();
let mut entry = ArchiveEntry::new_file(archive_name);
if let Ok(modified) = link_meta.modified()
&& let Ok(date) = sevenz_rust2::NtTime::try_from(modified)
{
entry.last_modified_date = date;
entry.has_last_modified_date = u64::from(date) > 0;
}
apply_unix_attributes(&mut entry, link_meta);
let len = target_str.len() as u64;
writer.push_archive_entry(entry, Some(std::io::Cursor::new(target_str.into_bytes())))?;
opts.progress.set_entry(archive_name);
opts.progress.inc(len);
Ok(())
}
pub fn decompress(input: &Utf8Path, output: &Utf8Path, opts: &DecompressOpts<'_>) -> Result<()> {
if opts.strip_components > 0 {
return Err(Error::StripComponentsUnsupported("7z".to_owned()));
}
if opts.keep_newer {
return Err(Error::KeepNewerUnsupported("7z".to_owned()));
}
let file = fs_err::File::open(input)?;
let password = opts
.password
.as_deref()
.map_or_else(Password::empty, Password::from);
let parked: RefCell<Option<Error>> = RefCell::new(None);
let deferred_dirs: RefCell<Vec<(Utf8PathBuf, u32)>> = RefCell::new(Vec::new());
let walked = sevenz_rust2::decompress_with_extract_fn_and_password(
file,
output,
password,
|entry, reader, _entry_dest| {
match extract_entry(entry, reader, output, opts, &deferred_dirs) {
Ok(keep_walking) => Ok(keep_walking),
Err(e) => {
let msg = e.to_string();
*parked.borrow_mut() = Some(e);
Err(sevenz_rust2::Error::Io(
std::io::Error::other(msg),
entry.name.clone().into(),
))
}
}
},
);
if let Some(e) = parked.into_inner() {
return Err(e);
}
walked?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut dirs = deferred_dirs.into_inner();
dirs.sort_by(|a, b| b.0.as_str().cmp(a.0.as_str()));
for (path, mode) in dirs {
fs_err::set_permissions(&path, std::fs::Permissions::from_mode(mode & 0o7777))?;
}
}
#[cfg(not(unix))]
drop(deferred_dirs);
Ok(())
}
fn extract_entry(
entry: &sevenz_rust2::ArchiveEntry,
reader: &mut dyn std::io::Read,
output: &Utf8Path,
opts: &DecompressOpts<'_>,
deferred_dirs: &RefCell<Vec<(Utf8PathBuf, u32)>>,
) -> Result<bool> {
crate::filter::safe_entry_path(&entry.name)?;
if !crate::filter::should_extract(&entry.name, &opts.includes, &opts.excludes) {
return skip_entry(reader);
}
if opts.no_directory && entry.is_directory {
return skip_entry(reader);
}
let base_name = if opts.no_directory {
match Utf8Path::new(&entry.name).file_name() {
Some(name) => Utf8PathBuf::from(name),
None => return skip_entry(reader),
}
} else {
Utf8PathBuf::from(&entry.name)
};
let dest_path =
match crate::filter::apply_path_rewrites(base_name, &opts.renames, opts.prefix.as_deref())?
{
p if p.as_str().is_empty() => return skip_entry(reader),
p => p,
};
let out_path = output.join(&dest_path);
if entry.is_directory {
fs_err::create_dir_all(&out_path)?;
if opts.preserve_permissions
&& let Some(mode) = entry_unix_mode(entry)
&& mode & 0xF000 == 0o040000
{
deferred_dirs.borrow_mut().push((out_path, mode));
}
return Ok(true);
}
if let Some(parent) = out_path.parent() {
fs_err::create_dir_all(parent)?;
}
let existed = fs_err::symlink_metadata(&out_path).is_ok();
if existed {
if let Some(suffix) = &opts.backup_suffix {
let backup = Utf8PathBuf::from(format!("{out_path}{suffix}"));
fs_err::rename(&out_path, &backup)?;
} else if opts.no_overwrite {
return skip_entry(reader);
} else if !opts.force {
return Err(Error::FileExists(out_path));
}
}
if let Some(mode) = entry_unix_mode(entry)
&& mode & 0xF000 == 0o120000
{
return extract_symlink_entry(reader, &out_path, &dest_path, opts);
}
if fs_err::symlink_metadata(&out_path)
.is_ok_and(|m| m.file_type().is_symlink())
{
fs_err::remove_file(&out_path)?;
}
let mut out_file = fs_err::File::create(&out_path)?;
let written = std::io::copy(reader, &mut out_file)?;
restore_mtime(&out_file, entry);
#[cfg(unix)]
if opts.preserve_permissions
&& let Some(mode) = entry_unix_mode(entry)
&& mode & 0xF000 == 0o100000
{
use std::os::unix::fs::PermissionsExt;
fs_err::set_permissions(&out_path, std::fs::Permissions::from_mode(mode & 0o7777))?;
}
opts.progress.set_entry(dest_path.as_str());
opts.progress.inc(written);
Ok(true)
}
const MAX_SYMLINK_TARGET: u64 = 8 * 1024;
fn extract_symlink_entry(
reader: &mut dyn std::io::Read,
out_path: &Utf8Path,
dest_path: &Utf8Path,
opts: &DecompressOpts<'_>,
) -> Result<bool> {
let mut target_bytes = Vec::new();
let read = std::io::copy(
&mut std::io::Read::take(&mut *reader, MAX_SYMLINK_TARGET),
&mut target_bytes,
)?;
if read >= MAX_SYMLINK_TARGET {
return Err(Error::SymlinkTargetTooLong {
path: dest_path.to_owned(),
max: MAX_SYMLINK_TARGET,
});
}
let target = std::str::from_utf8(&target_bytes)
.map_err(|_| Error::InvalidUtf8Path(dest_path.to_string()))?;
crate::filter::safe_link_target(dest_path.as_str(), target)?;
if fs_err::symlink_metadata(out_path).is_ok() {
fs_err::remove_file(out_path)?;
}
#[cfg(unix)]
std::os::unix::fs::symlink(target, out_path)?;
#[cfg(not(unix))]
fs_err::write(out_path, &target_bytes)?;
opts.progress.set_entry(dest_path.as_str());
opts.progress.inc(target_bytes.len() as u64);
Ok(true)
}
fn skip_entry(reader: &mut dyn std::io::Read) -> Result<bool> {
std::io::copy(reader, &mut std::io::sink())?;
Ok(true)
}
fn restore_mtime(file: &fs_err::File, entry: &sevenz_rust2::ArchiveEntry) {
if !entry.has_last_modified_date {
return;
}
let times = std::fs::FileTimes::new().set_modified(entry.last_modified_date.into());
let _ = file.file().set_times(times);
}
pub fn decompress_to_writer<W: std::io::Write>(
input: &Utf8Path,
writer: &mut W,
opts: &DecompressOpts<'_>,
) -> Result<()> {
if opts.strip_components > 0 {
return Err(Error::StripComponentsUnsupported("7z".to_owned()));
}
let file = fs_err::File::open(input)?;
let password = opts
.password
.as_deref()
.map(Password::from)
.unwrap_or_else(Password::empty);
sevenz_rust2::decompress_with_extract_fn_and_password(
file,
".",
password,
|entry, reader, _dest| {
crate::filter::safe_entry_path(&entry.name).map_err(|e| {
sevenz_rust2::Error::Io(
std::io::Error::other(e.to_string()),
entry.name.clone().into(),
)
})?;
if entry.is_directory {
return Ok(true);
}
if !crate::filter::should_extract(&entry.name, &opts.includes, &opts.excludes) {
return Ok(true);
}
if opts.no_directory {
let display_name = Utf8Path::new(&entry.name)
.file_name()
.unwrap_or(&entry.name);
opts.progress.set_entry(display_name);
} else {
opts.progress.set_entry(&entry.name);
}
std::io::copy(reader, writer)
.map_err(|e| sevenz_rust2::Error::Io(e, "decompress to writer".into()))?;
Ok(true) },
)?;
Ok(())
}
pub fn test(
input: &Utf8Path,
password: Option<&str>,
progress: &dyn crate::progress::ProgressReport,
) -> Result<()> {
let file = fs_err::File::open(input)?;
let pwd = password.map_or_else(Password::empty, Password::from);
sevenz_rust2::decompress_with_extract_fn_and_password(
file,
".",
pwd,
|entry, reader, _dest| {
progress.set_entry(&entry.name);
let written = std::io::copy(reader, &mut std::io::sink())
.map_err(|e| sevenz_rust2::Error::Io(e, "test: reading entry".into()))?;
progress.inc(written);
Ok(true) },
)?;
Ok(())
}
pub fn list(input: &Utf8Path) -> Result<Vec<Entry>> {
let archive = sevenz_rust2::Archive::open(input)?;
let mut entries = Vec::new();
for file in &archive.files {
let path = Utf8PathBuf::from(&file.name);
let mtime = if file.has_last_modified_date {
let st: std::time::SystemTime = file.last_modified_date.into();
st.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
} else {
0
};
entries.push(Entry {
path,
size: file.size,
mtime,
mode: entry_unix_mode(file).unwrap_or(0),
is_dir: file.is_directory,
link_target: None,
});
}
Ok(entries)
}
pub fn info(input: &Utf8Path) -> Result<ArchiveInfo> {
let compressed_size = fs_err::metadata(input)?.len();
let archive = sevenz_rust2::Archive::open(input)?;
Ok(ArchiveInfo {
format: "7z",
entry_count: archive.files.len(),
total_uncompressed: archive
.files
.iter()
.fold(0u64, |acc, f| acc.saturating_add(f.size)),
compressed_size,
})
}