Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support decompress and list archive file with password #646

Merged
merged 24 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9043fbc
feat: add password support for decompress and list
ttys3 Mar 24, 2024
b2e2233
feat(list): support list and decompress 7z files with password
ttys3 Mar 24, 2024
3e05417
feat: support decompress and list zip file
ttys3 Mar 24, 2024
2ae3295
style: lint the code
ttys3 Mar 24, 2024
2fa2852
test: fix warning: use of deprecated macro `ui` (it's alias, actual i…
ttys3 Mar 24, 2024
687662c
chore: fix tests
ttys3 Mar 24, 2024
86d855b
style: cargo fmt
ttys3 Sep 1, 2024
9ff7309
ci: use newer gcc version for cross target aarch64-unknown-linux-gnu …
ttys3 Sep 1, 2024
c31cf0f
chore: remove comment from Cross configuration
ttys3 Sep 6, 2024
287940d
fix(password): update password handling for archives
ttys3 Sep 6, 2024
c19edd0
fix(archive): replace unwrap with error handling in zip.rs
ttys3 Sep 6, 2024
cae0bed
fix: remove unnecessary clones in zip decryption
ttys3 Sep 6, 2024
116d0b4
fix: fix windows build
ttys3 Sep 6, 2024
fb4d1d2
fix(archive): handle invalid UTF-8 passwords in 7z decompression
ttys3 Sep 6, 2024
5f11600
fix(archive): handle file open error in list_archive method
ttys3 Sep 6, 2024
534d338
fix(archive): handle errors for unsupported formats
ttys3 Sep 6, 2024
acc751c
fix: replace UnsupportedFormat error with UnrarError
ttys3 Sep 6, 2024
164f800
fix(archive): handle mangled zip file names properly
ttys3 Sep 6, 2024
1a52c3c
fix(error): return result in list_archive
ttys3 Sep 6, 2024
0c52765
fix(archive): return result in list_archive functions
ttys3 Sep 6, 2024
a469bc6
refactor(zip): remove redundant password byte conversion
ttys3 Sep 6, 2024
271ac72
style: cargo fmt
ttys3 Sep 6, 2024
259f854
refactor(rar): simplify list_archive logic and remove UnrarError
ttys3 Sep 6, 2024
d37d22d
fix: simplify error handling for invalid password
ttys3 Sep 6, 2024
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
722 changes: 375 additions & 347 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ num_cpus = "1.16.0"
once_cell = "1.19.0"
rayon = "1.10.0"
same-file = "1.0.6"
sevenz-rust = { version = "0.6.1", features = ["compress"] }
sevenz-rust = { version = "0.6.1", features = ["compress", "aes256"] }
snap = "1.1.1"
tar = "0.4.41"
tempfile = "3.10.1"
time = { version = "0.3.36", default-features = false }
unrar = { version = "0.5.3", optional = true }
unrar = { version = "0.5.6", optional = true }
xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = ["time"] }
zip = { version = "0.6.6", default-features = false, features = ["time", "aes-crypto"] }
zstd = { version = "0.13.2", default-features = false, features = ["zstdmt"]}

[target.'cfg(not(unix))'.dependencies]
Expand Down
6 changes: 6 additions & 0 deletions Cross.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
[build.env]
passthrough = ["RUSTFLAGS", "OUCH_ARTIFACTS_FOLDER"]

[target.aarch64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge"

[target.armv7-unknown-linux-gnueabihf]
image = "ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:edge"
43 changes: 30 additions & 13 deletions src/archive/rar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ use crate::{error::Error, list::FileInArchive, utils::logger::info};

/// Unpacks the archive given by `archive_path` into the folder given by `output_folder`.
/// Assumes that output_folder is empty
pub fn unpack_archive(archive_path: &Path, output_folder: &Path, quiet: bool) -> crate::Result<usize> {
pub fn unpack_archive(
archive_path: &Path,
output_folder: &Path,
password: Option<&[u8]>,
quiet: bool,
) -> crate::Result<usize> {
assert!(output_folder.read_dir().expect("dir exists").count() == 0);

let mut archive = Archive::new(archive_path).open_for_processing()?;
let archive = match password {
Some(password) => Archive::with_password(archive_path, password),
None => Archive::new(archive_path),
};

let mut archive = archive.open_for_processing()?;
let mut unpacked = 0;

while let Some(header) = archive.read_header()? {
Expand All @@ -35,17 +45,24 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, quiet: bool) ->
}

/// List contents of `archive_path`, returning a vector of archive entries
pub fn list_archive(archive_path: &Path) -> impl Iterator<Item = crate::Result<FileInArchive>> {
Archive::new(archive_path)
.open_for_listing()
.expect("cannot open archive")
.map(|item| {
let item = item?;
let is_dir = item.is_directory();
let path = item.filename;

Ok(FileInArchive { path, is_dir })
})
pub fn list_archive(
archive_path: &Path,
password: Option<&[u8]>,
) -> crate::Result<impl Iterator<Item = crate::Result<FileInArchive>>> {
let archive = match password {
Some(password) => Archive::with_password(archive_path, password),
None => Archive::new(archive_path),
};

let archive = archive.open_for_listing()?;

Ok(archive.map(|item| {
let item = item?;
let is_dir = item.is_directory();
let path = item.filename;

Ok(FileInArchive { path, is_dir })
}))
}

pub fn no_compression() -> Error {
Expand Down
68 changes: 64 additions & 4 deletions src/archive/sevenz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ use std::{
path::{Path, PathBuf},
};

use bstr::ByteSlice;
use fs_err as fs;
use same_file::Handle;
use sevenz_rust::SevenZArchiveEntry;

use crate::{
error::FinalError,
error::{Error, FinalError},
list::FileInArchive,
utils::{
self, cd_into_same_dir_as,
logger::{info, warning},
Expand Down Expand Up @@ -96,12 +99,13 @@ where
Ok(bytes)
}

pub fn decompress_sevenz<R>(reader: R, output_path: &Path, quiet: bool) -> crate::Result<usize>
pub fn decompress_sevenz<R>(reader: R, output_path: &Path, password: Option<&[u8]>, quiet: bool) -> crate::Result<usize>
where
R: Read + Seek,
{
let mut count: usize = 0;
sevenz_rust::decompress_with_extract_fn(reader, output_path, |entry, reader, path| {

let entry_extract_fn = |entry: &SevenZArchiveEntry, reader: &mut dyn Read, path: &PathBuf| {
count += 1;
// Manually handle writing all files from 7z archive, due to library exluding empty files
use std::io::BufWriter;
Expand Down Expand Up @@ -150,7 +154,63 @@ where
}

Ok(true)
})?;
};

match password {
Some(password) => sevenz_rust::decompress_with_extract_fn_and_password(
reader,
output_path,
sevenz_rust::Password::from(password.to_str().map_err(|err| Error::InvalidPassword {
reason: err.to_string(),
})?),
entry_extract_fn,
)?,
None => sevenz_rust::decompress_with_extract_fn(reader, output_path, entry_extract_fn)?,
}

Ok(count)
}

/// List contents of `archive_path`, returning a vector of archive entries
pub fn list_archive(
archive_path: &Path,
password: Option<&[u8]>,
) -> crate::Result<impl Iterator<Item = crate::Result<FileInArchive>>> {
let reader = fs::File::open(archive_path)?;

let mut files = Vec::new();

let entry_extract_fn = |entry: &SevenZArchiveEntry, _: &mut dyn Read, _: &PathBuf| {
files.push(Ok(FileInArchive {
path: entry.name().into(),
is_dir: entry.is_directory(),
}));
Ok(true)
};

let result = match password {
Some(password) => {
let password = match password.to_str() {
Ok(p) => p,
Err(err) => {
return Err(Error::InvalidPassword {
reason: err.to_string(),
})
}
};
sevenz_rust::decompress_with_extract_fn_and_password(
reader,
".",
sevenz_rust::Password::from(password),
entry_extract_fn,
)
}
None => sevenz_rust::decompress_with_extract_fn(reader, ".", entry_extract_fn),
};

if let Err(e) = result {
return Err(e.into());
}

Ok(files.into_iter())
}
4 changes: 2 additions & 2 deletions src/archive/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, quiet: bool)
/// List contents of `archive`, returning a vector of archive entries
pub fn list_archive(
mut archive: tar::Archive<impl Read + Send + 'static>,
) -> impl Iterator<Item = crate::Result<FileInArchive>> {
) -> crate::Result<impl Iterator<Item = crate::Result<FileInArchive>>> {
struct Files(Receiver<crate::Result<FileInArchive>>);
impl Iterator for Files {
type Item = crate::Result<FileInArchive>;
Expand All @@ -77,7 +77,7 @@ pub fn list_archive(
}
});

Files(rx)
Ok(Files(rx))
}

/// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
Expand Down
46 changes: 33 additions & 13 deletions src/archive/zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use filetime_creation::{set_file_mtime, FileTime};
use fs_err as fs;
use same_file::Handle;
use time::OffsetDateTime;
use zip::{read::ZipFile, DateTime, ZipArchive};
use zip::{self, read::ZipFile, DateTime, ZipArchive};

use crate::{
error::FinalError,
Expand All @@ -28,7 +28,12 @@ use crate::{

/// Unpacks the archive given by `archive` into the folder given by `output_folder`.
/// Assumes that output_folder is empty
pub fn unpack_archive<R>(mut archive: ZipArchive<R>, output_folder: &Path, quiet: bool) -> crate::Result<usize>
pub fn unpack_archive<R>(
mut archive: ZipArchive<R>,
output_folder: &Path,
password: Option<&[u8]>,
quiet: bool,
) -> crate::Result<usize>
where
R: Read + Seek,
{
Expand All @@ -37,7 +42,12 @@ where
let mut unpacked_files = 0;

for idx in 0..archive.len() {
let mut file = archive.by_index(idx)?;
let mut file = match password {
Some(password) => archive
.by_index_decrypt(idx, password)?
.map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file"))?,
None => archive.by_index(idx)?,
};
let file_path = match file.enclosed_name() {
Some(path) => path.to_owned(),
None => continue,
Expand Down Expand Up @@ -92,7 +102,10 @@ where
}

/// List contents of `archive`, returning a vector of archive entries
pub fn list_archive<R>(mut archive: ZipArchive<R>) -> impl Iterator<Item = crate::Result<FileInArchive>>
pub fn list_archive<R>(
mut archive: ZipArchive<R>,
password: Option<&[u8]>,
) -> crate::Result<impl Iterator<Item = crate::Result<FileInArchive>>>
where
R: Read + Seek + Send + 'static,
{
Expand All @@ -105,27 +118,34 @@ where
}
}

let password = password.map(|p| p.to_owned());

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for idx in 0..archive.len() {
let maybe_file_in_archive = (|| {
let file = match archive.by_index(idx) {
let file_in_archive = (|| {
let zip_result = match password.clone() {
Some(password) => archive
.by_index_decrypt(idx, &password)?
.map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file")),
None => archive.by_index(idx),
};

let file = match zip_result {
Ok(f) => f,
Err(e) => return Some(Err(e.into())),
Err(e) => return Err(e.into()),
};

let path = file.enclosed_name()?.to_owned();
let path = file.enclosed_name().unwrap_or(&*file.mangled_name()).to_owned();
let is_dir = file.is_dir();

Some(Ok(FileInArchive { path, is_dir }))
Ok(FileInArchive { path, is_dir })
})();
if let Some(file_in_archive) = maybe_file_in_archive {
tx.send(file_in_archive).unwrap();
}
tx.send(file_in_archive).unwrap();
}
});

Files(rx)
Ok(Files(rx))
}

/// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
Expand Down
5 changes: 5 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ pub struct CliArgs {
#[arg(short, long, global = true)]
pub format: Option<OsString>,

/// decompress or list with password
#[arg(short = 'p', long = "password", global = true)]
pub password: Option<OsString>,

// Ouch and claps subcommands
#[command(subcommand)]
pub cmd: Subcommand,
Expand Down Expand Up @@ -133,6 +137,7 @@ mod tests {
gitignore: false,
format: None,
// This is usually replaced in assertion tests
password: None,
cmd: Subcommand::Decompress {
// Put a crazy value here so no test can assert it unintentionally
files: vec!["\x00\x11\x22".into()],
Expand Down
15 changes: 10 additions & 5 deletions src/commands/decompress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub fn decompress_file(
output_file_path: PathBuf,
question_policy: QuestionPolicy,
quiet: bool,
password: Option<&[u8]>,
) -> crate::Result<()> {
assert!(output_dir.exists());
let input_is_stdin = is_path_stdin(input_file_path);
Expand All @@ -62,7 +63,7 @@ pub fn decompress_file(
};
let zip_archive = zip::ZipArchive::new(reader)?;
let files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, quiet),
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, password, quiet),
output_dir,
&output_file_path,
question_policy,
Expand Down Expand Up @@ -156,7 +157,7 @@ pub fn decompress_file(
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;

if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, quiet),
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, password, quiet),
output_dir,
&output_file_path,
question_policy,
Expand All @@ -172,9 +173,11 @@ pub fn decompress_file(
let unpack_fn: Box<dyn FnOnce(&Path) -> UnpackResult> = if formats.len() > 1 || input_is_stdin {
let mut temp_file = tempfile::NamedTempFile::new()?;
io::copy(&mut reader, &mut temp_file)?;
Box::new(move |output_dir| crate::archive::rar::unpack_archive(temp_file.path(), output_dir, quiet))
Box::new(move |output_dir| {
crate::archive::rar::unpack_archive(temp_file.path(), output_dir, password, quiet)
})
} else {
Box::new(|output_dir| crate::archive::rar::unpack_archive(input_file_path, output_dir, quiet))
Box::new(|output_dir| crate::archive::rar::unpack_archive(input_file_path, output_dir, password, quiet))
};

if let ControlFlow::Continue(files) =
Expand Down Expand Up @@ -205,7 +208,9 @@ pub fn decompress_file(
io::copy(&mut reader, &mut vec)?;

if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, quiet),
|output_dir| {
crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, password, quiet)
},
output_dir,
&output_file_path,
question_policy,
Expand Down
Loading