feat: implement 7zip support for compression and decompression

This also fixes symlink canonicalization for Windows and fixes UI tests on Windows.
This commit is contained in:
Flat 2023-11-24 19:16:56 -05:00 committed by João Marcos
parent 9a6d73bf57
commit 97b4356aa8
9 changed files with 196 additions and 30 deletions

2
Cargo.lock generated
View File

@ -827,7 +827,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_mangen",
"filetime",
"filetime_creation",
"flate2",
"fs-err",
"gzp",

View File

@ -15,7 +15,7 @@ atty = "0.2.14"
bstr = { version = "1.8.0", default-features = false, features = ["std"] }
bzip2 = "0.4.4"
clap = { version = "4.4.8", features = ["derive", "env"] }
filetime = "0.2.22"
filetime_creation = "0.1"
flate2 = { version = "1.0.28", default-features = false }
fs-err = "2.11.0"
gzp = { version = "0.11.3", default-features = false, features = ["snappy_default"] }

View File

@ -1,36 +1,150 @@
//! SevenZip archive format compress function
use std::path::{Path, PathBuf};
use std::{
env,
fs::File,
path::{Path, PathBuf}, io::{Write, Seek, Read},
};
use crate::utils::strip_cur_dir;
use same_file::Handle;
pub fn compress_sevenz(files: Vec<PathBuf>, output_path: &Path) -> crate::Result<bool> {
let mut writer = sevenz_rust::SevenZWriter::create(output_path).map_err(crate::Error::SevenzipError)?;
use crate::{
info,
utils::{self, cd_into_same_dir_as, EscapedPathDisplay, FileVisibilityPolicy, Bytes},
warning,
};
for filep in files.iter() {
writer
.push_archive_entry::<std::fs::File>(
sevenz_rust::SevenZArchiveEntry::from_path(
filep,
strip_cur_dir(filep)
.as_os_str()
.to_str()
.unwrap()
.to_string(),
),
None,
)
.map_err(crate::Error::SevenzipError)?;
pub fn compress_sevenz<W>(
files: &[PathBuf],
output_path: &Path,
writer: W,
file_visibility_policy: FileVisibilityPolicy,
quiet: bool,
) -> crate::Result<W>
where
W: Write + Seek {
let mut writer = sevenz_rust::SevenZWriter::new(writer).map_err(crate::Error::SevenzipError)?;
let output_handle = Handle::from_path(output_path);
for filename in files {
let previous_location = cd_into_same_dir_as(filename)?;
// Safe unwrap, input shall be treated before
let filename = filename.file_name().unwrap();
for entry in file_visibility_policy.build_walker(filename) {
let entry = entry?;
let path = entry.path();
// If the output_path is the same as the input file, warn the user and skip the input (in order to avoid compression recursion)
if let Ok(ref handle) = output_handle {
if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
warning!(
"The output file and the input file are the same: `{}`, skipping...",
output_path.display()
);
continue;
}
}
// This is printed for every file in `input_filenames` and has
// little importance for most users, but would generate lots of
// spoken text for users using screen readers, braille displays
// and so on
if !quiet {
info!(inaccessible, "Compressing '{}'.", EscapedPathDisplay::new(path));
}
let metadata = match path.metadata() {
Ok(metadata) => metadata,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
// This path is for a broken symlink
// We just ignore it
continue;
}
return Err(e.into());
}
};
if metadata.is_dir() {
writer
.push_archive_entry::<std::fs::File>(
sevenz_rust::SevenZArchiveEntry::from_path(path, path.to_str().unwrap().to_owned()),
None,
)
.map_err(crate::Error::SevenzipError)?;
} else {
let reader = File::open(path)?;
writer
.push_archive_entry::<std::fs::File>(
sevenz_rust::SevenZArchiveEntry::from_path(path, path.to_str().unwrap().to_owned()),
Some(reader),
)
.map_err(crate::Error::SevenzipError)?;
}
}
env::set_current_dir(previous_location)?;
}
writer.finish()?;
Ok(true)
let bytes = writer.finish()?;
Ok(bytes)
}
pub fn decompress_sevenz(input_file_path: &Path, output_path: &Path) -> crate::Result<usize> {
pub fn decompress_sevenz<R>(reader: R, output_path: &Path, quiet: bool) -> crate::Result<usize>
where R: Read+ Seek {
let mut count: usize = 0;
sevenz_rust::decompress_file_with_extract_fn(input_file_path, output_path, |entry, reader, dest| {
sevenz_rust::decompress_with_extract_fn(reader, output_path, |entry, reader, dest| {
count += 1;
sevenz_rust::default_entry_extract_fn(entry, reader, dest)
// Manually handle writing all files from 7z archive, due to library exluding empty files
use std::io::BufWriter;
use filetime_creation as ft;
let file_path = output_path.join(entry.name());
if entry.is_directory() {
// This is printed for every file in the archive and has little
// importance for most users, but would generate lots of
// spoken text for users using screen readers, braille displays
// and so on
if !quiet {
info!(inaccessible, "File {} extracted to \"{}\"", entry.name(), file_path.display());
}
let dir = dest;
if !dir.exists() {
std::fs::create_dir_all(dir)?;
}
} else {
// same reason is in _is_dir: long, often not needed text
if !quiet {
info!(
inaccessible,
"{:?} extracted. ({})",
file_path.display(),
Bytes::new(entry.size()),
);
}
let path = dest;
path.parent().and_then(|p| {
if !p.exists() {
std::fs::create_dir_all(p).ok()
} else {
None
}
});
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
std::io::copy(reader, &mut writer)?;
ft::set_file_handle_times(
writer.get_ref(),
Some(ft::FileTime::from_system_time(entry.access_date().into())),
Some(ft::FileTime::from_system_time(entry.last_modified_date().into())),
Some(ft::FileTime::from_system_time(entry.creation_date().into())),
)
.unwrap_or_default();
}
Ok(true)
})
.map_err(crate::Error::SevenzipError)?;
Ok(count)

View File

@ -10,7 +10,7 @@ use std::{
thread,
};
use filetime::{set_file_mtime, FileTime};
use filetime_creation::{set_file_mtime, FileTime};
use fs_err as fs;
use same_file::Handle;
use time::OffsetDateTime;

View File

@ -13,6 +13,8 @@ use crate::{
QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
};
use super::warn_user_about_loading_sevenz_in_memory;
/// Compress files into `output_file`.
///
/// # Arguments:
@ -127,7 +129,19 @@ pub fn compress_files(
return Ok(false);
},
SevenZip => {
archive::sevenz::compress_sevenz(files, output_path)?;
if !formats.is_empty() {
warn_user_about_loading_sevenz_in_memory();
if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
return Ok(false);
}
}
let mut vec_buffer = Cursor::new(vec![]);
archive::sevenz::compress_sevenz(&files, output_path, &mut vec_buffer, file_visibility_policy, quiet)?;
vec_buffer.rewind()?;
io::copy(&mut vec_buffer, &mut writer)?;
}
}

View File

@ -7,7 +7,7 @@ use std::{
use fs_err as fs;
use crate::{
commands::warn_user_about_loading_zip_in_memory,
commands::{warn_user_about_loading_zip_in_memory, warn_user_about_loading_sevenz_in_memory},
extension::{
split_first_compression_format,
CompressionFormat::{self, *},
@ -165,8 +165,19 @@ pub fn decompress_file(
}
},
SevenZip => {
if formats.len() > 1 {
warn_user_about_loading_sevenz_in_memory();
if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? {
return Ok(());
}
}
let mut vec = vec![];
io::copy(&mut reader, &mut vec)?;
if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::sevenz::decompress_sevenz(input_file_path, output_dir),
|output_dir| crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, quiet),
output_dir,
&output_file_path,
question_policy,

View File

@ -31,6 +31,16 @@ fn warn_user_about_loading_zip_in_memory() {
warning!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
}
/// Warn the user that (de)compressing this .7z archive might freeze their system.
fn warn_user_about_loading_sevenz_in_memory() {
const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
\tThe format '.7z' is limited and cannot be (de)compressed using encoding streams.\n\
\tWhen using '.7z' with other formats, (de)compression must be done in-memory\n\
\tCareful, you might run out of RAM if the archive is too large!";
warning!("{}", SEVENZ_IN_MEMORY_LIMITATION_WARNING);
}
/// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
/// to assume everything is OK.
///

View File

@ -22,7 +22,7 @@ use utils::{QuestionAction, QuestionPolicy};
const BUFFER_CAPACITY: usize = 1024 * 32;
/// Current directory or empty directory
static CURRENT_DIRECTORY: Lazy<PathBuf> = Lazy::new(|| env::current_dir().unwrap_or_default());
static CURRENT_DIRECTORY: Lazy<PathBuf> = Lazy::new(|| std::fs::canonicalize(env::current_dir().unwrap_or_default()).unwrap_or_default());
/// The status code returned from `ouch` on error
pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE;

View File

@ -62,8 +62,12 @@ fn ui_test_err_compress_missing_extension() {
let (_dropper, dir) = testdir().unwrap();
// prepare
#[cfg(not(windows))]
run_in(dir, "touch", "input").unwrap();
#[cfg(windows)]
run_in(dir, "cmd", "/C copy nul input").unwrap();
ui!(run_ouch("ouch compress input output", dir));
}
@ -71,8 +75,16 @@ fn ui_test_err_compress_missing_extension() {
fn ui_test_err_decompress_missing_extension() {
let (_dropper, dir) = testdir().unwrap();
#[cfg(not(windows))]
run_in(dir, "touch", "a b.unknown").unwrap();
#[cfg(windows)]
run_in(dir, "cmd", "/C copy nul a").unwrap();
#[cfg(windows)]
run_in(dir, "cmd", "/C copy nul b.unknown").unwrap();
ui!(run_ouch("ouch decompress a", dir));
ui!(run_ouch("ouch decompress a b.unknown", dir));
ui!(run_ouch("ouch decompress b.unknown", dir));
@ -92,8 +104,12 @@ fn ui_test_ok_compress() {
let (_dropper, dir) = testdir().unwrap();
// prepare
#[cfg(not(windows))]
run_in(dir, "touch", "input").unwrap();
#[cfg(windows)]
run_in(dir, "cmd", "/C copy nul input").unwrap();
ui!(run_ouch("ouch compress input output.zip", dir));
ui!(run_ouch("ouch compress input output.gz", dir));
}
@ -103,6 +119,7 @@ fn ui_test_ok_decompress() {
let (_dropper, dir) = testdir().unwrap();
// prepare
#[cfg(not(windows))]
run_in(dir, "touch", "input").unwrap();
run_ouch("ouch compress input output.zst", dir);