mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-03 10:00:19 +00:00
added support for listing and decompressing .rar archives
This commit is contained in:
parent
a3dca85cdd
commit
dade163243
1
.github/workflows/build-and-test.yml
vendored
1
.github/workflows/build-and-test.yml
vendored
@ -75,6 +75,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install musl-tools
|
||||
sudo ln -s "/usr/bin/g++" "/usr/bin/musl-g++"
|
||||
|
||||
- name: Set up extra cargo flags
|
||||
if: matrix.no-zstd-thin
|
||||
|
@ -19,6 +19,8 @@ Categories Used:
|
||||
**Bullet points in chronological order by PR**
|
||||
|
||||
## [Unreleased](https://github.com/ouch-org/ouch/compare/0.4.2...HEAD)
|
||||
- Add support for listing and decompressing `.rar` archives
|
||||
- Fix mime type detection
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
30
Cargo.lock
generated
30
Cargo.lock
generated
@ -763,6 +763,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"test-strategy",
|
||||
"time",
|
||||
"unrar",
|
||||
"xz2",
|
||||
"zip",
|
||||
"zstd",
|
||||
@ -1253,6 +1254,29 @@ version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
||||
|
||||
[[package]]
|
||||
name = "unrar"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "850dc99da3a3c12a9400a52a3c510057bf2b16c6c9a495492850d25ed9d757ab"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"regex",
|
||||
"unrar_sys",
|
||||
"widestring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unrar_sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95e536227cb6a91f8e88adb0219912e004a107c9724a094f90baad9229281efa"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
@ -1350,6 +1374,12 @@ version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Archive compression algorithms
|
||||
|
||||
pub mod rar;
|
||||
pub mod tar;
|
||||
pub mod zip;
|
||||
|
56
src/archive/rar.rs
Normal file
56
src/archive/rar.rs
Normal file
@ -0,0 +1,56 @@
|
||||
//! Contains RAR-specific building and unpacking functions
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use unrar::{self, Archive};
|
||||
|
||||
use crate::{info, list::FileInArchive, warning};
|
||||
|
||||
/// 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> {
|
||||
assert!(output_folder.read_dir().expect("dir exists").count() == 0);
|
||||
|
||||
let mut archive = Archive::new(archive_path).open_for_processing()?;
|
||||
let mut unpacked = 0;
|
||||
|
||||
while let Some(header) = archive.read_header()? {
|
||||
let entry = header.entry();
|
||||
archive = if entry.is_file() {
|
||||
if !quiet {
|
||||
info!(
|
||||
inaccessible,
|
||||
"{} extracted. ({})",
|
||||
entry.filename.display(),
|
||||
entry.unpacked_size
|
||||
);
|
||||
}
|
||||
unpacked += 1;
|
||||
header.extract_with_base(output_folder)?
|
||||
} else {
|
||||
header.skip()?
|
||||
};
|
||||
}
|
||||
|
||||
Ok(unpacked)
|
||||
}
|
||||
|
||||
/// 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 no_compression_notice() {
|
||||
const MESSAGE: &str = "Creating '.rar' archives is not supported due to licensing restrictions";
|
||||
|
||||
warning!("{}", MESSAGE);
|
||||
}
|
@ -79,7 +79,7 @@ pub fn compress_files(
|
||||
// is `clamp`ed and therefore guaranteed to be valid
|
||||
Box::new(zstd_encoder.unwrap().auto_finish())
|
||||
}
|
||||
Tar | Zip => unreachable!(),
|
||||
Tar | Zip | Rar => unreachable!(),
|
||||
};
|
||||
Ok(encoder)
|
||||
};
|
||||
@ -122,6 +122,10 @@ pub fn compress_files(
|
||||
vec_buffer.rewind()?;
|
||||
io::copy(&mut vec_buffer, &mut writer)?;
|
||||
}
|
||||
Rar => {
|
||||
archive::rar::no_compression_notice();
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
@ -86,7 +86,7 @@ pub fn decompress_file(
|
||||
Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
|
||||
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
|
||||
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
|
||||
Tar | Zip => unreachable!(),
|
||||
Tar | Zip | Rar => unreachable!(),
|
||||
};
|
||||
Ok(decoder)
|
||||
};
|
||||
@ -146,6 +146,24 @@ pub fn decompress_file(
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Rar => {
|
||||
type UnpackResult = crate::Result<usize>;
|
||||
let unpack_fn: Box<dyn FnOnce(&Path) -> UnpackResult> = if formats.len() > 1 {
|
||||
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))
|
||||
} else {
|
||||
Box::new(|output_dir| crate::archive::rar::unpack_archive(input_file_path, output_dir, quiet))
|
||||
};
|
||||
|
||||
if let ControlFlow::Continue(files) =
|
||||
smart_unpack(unpack_fn, output_dir, &output_file_path, question_policy)?
|
||||
{
|
||||
files
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this is only printed once, so it doesn't result in much text. On the other hand,
|
||||
|
@ -52,7 +52,7 @@ pub fn list_archive_contents(
|
||||
Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
|
||||
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
|
||||
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
|
||||
Tar | Zip => unreachable!(),
|
||||
Tar | Zip | Rar => unreachable!(),
|
||||
};
|
||||
Ok(decoder)
|
||||
};
|
||||
@ -78,6 +78,15 @@ pub fn list_archive_contents(
|
||||
|
||||
Box::new(crate::archive::zip::list_archive(zip_archive))
|
||||
}
|
||||
Rar => {
|
||||
if formats.len() > 1 {
|
||||
let mut temp_file = tempfile::NamedTempFile::new()?;
|
||||
io::copy(&mut reader, &mut temp_file)?;
|
||||
Box::new(crate::archive::rar::list_archive(temp_file.path()))
|
||||
} else {
|
||||
Box::new(crate::archive::rar::list_archive(archive_path))
|
||||
}
|
||||
}
|
||||
Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
|
||||
panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
|
||||
}
|
||||
|
@ -178,6 +178,14 @@ impl From<zip::result::ZipError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<unrar::error::UnrarError> for Error {
|
||||
fn from(err: unrar::error::UnrarError) -> Self {
|
||||
Self::Custom {
|
||||
reason: FinalError::with_title("Unexpected error in rar archive").detail(format!("{:?}", err.code)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ignore::Error> for Error {
|
||||
fn from(err: ignore::Error) -> Self {
|
||||
Self::WalkdirError {
|
||||
|
@ -7,9 +7,9 @@ use bstr::ByteSlice;
|
||||
use self::CompressionFormat::*;
|
||||
use crate::{error::Error, warning};
|
||||
|
||||
pub const SUPPORTED_EXTENSIONS: &[&str] = &["tar", "zip", "bz", "bz2", "gz", "lz4", "xz", "lzma", "sz", "zst"];
|
||||
pub const SUPPORTED_EXTENSIONS: &[&str] = &["tar", "zip", "bz", "bz2", "gz", "lz4", "xz", "lzma", "sz", "zst", "rar"];
|
||||
pub const SUPPORTED_ALIASES: &[&str] = &["tgz", "tbz", "tlz4", "txz", "tzlma", "tsz", "tzst"];
|
||||
pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst";
|
||||
pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar";
|
||||
pub const PRETTY_SUPPORTED_ALIASES: &str = "tgz, tbz, tlz4, txz, tzlma, tsz, tzst";
|
||||
|
||||
/// A wrapper around `CompressionFormat` that allows combinations like `tgz`
|
||||
@ -72,6 +72,8 @@ pub enum CompressionFormat {
|
||||
Zstd,
|
||||
/// .zip
|
||||
Zip,
|
||||
/// .rar
|
||||
Rar,
|
||||
}
|
||||
|
||||
impl CompressionFormat {
|
||||
@ -79,7 +81,7 @@ impl CompressionFormat {
|
||||
fn is_archive_format(&self) -> bool {
|
||||
// Keep this match like that without a wildcard `_` so we don't forget to update it
|
||||
match self {
|
||||
Tar | Zip => true,
|
||||
Tar | Zip | Rar => true,
|
||||
Gzip => false,
|
||||
Bzip => false,
|
||||
Lz4 => false,
|
||||
@ -107,6 +109,7 @@ fn to_extension(ext: &[u8]) -> Option<Extension> {
|
||||
b"xz" | b"lzma" => &[Lzma],
|
||||
b"sz" => &[Snappy],
|
||||
b"zst" => &[Zstd],
|
||||
b"rar" => &[Rar],
|
||||
_ => return None,
|
||||
},
|
||||
ext.to_str_lossy(),
|
||||
|
@ -86,6 +86,11 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
|
||||
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]))
|
||||
}
|
||||
|
||||
let buf = {
|
||||
let mut buf = [0; 270];
|
||||
@ -117,6 +122,8 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
|
||||
Some(Extension::new(&[Snappy], "sz"))
|
||||
} else if is_zst(&buf) {
|
||||
Some(Extension::new(&[Zstd], "zst"))
|
||||
} else if is_rar(&buf) {
|
||||
Some(Extension::new(&[Rar], "rar"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
BIN
tests/data/testfile.rar3.rar.gz
Normal file
BIN
tests/data/testfile.rar3.rar.gz
Normal file
Binary file not shown.
BIN
tests/data/testfile.rar5.rar
Normal file
BIN
tests/data/testfile.rar5.rar
Normal file
Binary file not shown.
@ -1,7 +1,10 @@
|
||||
#[macro_use]
|
||||
mod utils;
|
||||
|
||||
use std::{iter::once, path::PathBuf};
|
||||
use std::{
|
||||
iter::once,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use fs_err as fs;
|
||||
use parse_display::Display;
|
||||
@ -144,3 +147,26 @@ fn multiple_files(
|
||||
ouch!("-A", "d", archive, "-d", after);
|
||||
assert_same_directory(before, after, !matches!(ext, DirectoryExtension::Zip));
|
||||
}
|
||||
|
||||
// test .rar decompression
|
||||
fn test_unpack_rar_single(input: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dir = tempdir()?;
|
||||
let dirpath = dir.path();
|
||||
let unpacked_path = &dirpath.join("testfile.txt");
|
||||
ouch!("-A", "d", input, "-d", dirpath);
|
||||
let content = fs::read_to_string(unpacked_path)?;
|
||||
assert_eq!(content, "Testing 123\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpack_rar() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut datadir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
|
||||
datadir.push("tests/data");
|
||||
["testfile.rar3.rar.gz", "testfile.rar5.rar"]
|
||||
.iter()
|
||||
.try_for_each(|path| test_unpack_rar_single(&datadir.join(path)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,6 +7,6 @@ expression: "run_ouch(\"ouch decompress a b.unknown\", dir)"
|
||||
- Files with missing extensions: <FOLDER>/a
|
||||
- Decompression formats are detected automatically from file extension
|
||||
|
||||
hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst
|
||||
hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar
|
||||
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
|
||||
|
||||
|
@ -6,7 +6,7 @@ expression: "run_ouch(\"ouch decompress b.unknown\", dir)"
|
||||
- Files with unsupported extensions: <FOLDER>/b.unknown
|
||||
- Decompression formats are detected automatically from file extension
|
||||
|
||||
hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst
|
||||
hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar
|
||||
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
|
||||
hint:
|
||||
hint: Alternatively, you can pass an extension to the '--format' flag:
|
||||
|
@ -6,7 +6,7 @@ expression: "run_ouch(\"ouch decompress a\", dir)"
|
||||
- Files with missing extensions: <FOLDER>/a
|
||||
- Decompression formats are detected automatically from file extension
|
||||
|
||||
hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst
|
||||
hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar
|
||||
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
|
||||
hint:
|
||||
hint: Alternatively, you can pass an extension to the '--format' flag:
|
||||
|
@ -2,7 +2,6 @@
|
||||
source: tests/ui.rs
|
||||
expression: "run_ouch(\"ouch decompress output.zst\", dir)"
|
||||
---
|
||||
[INFO] Failed to confirm the format of `output` by sniffing the contents, file might be misnamed
|
||||
[INFO] Successfully decompressed archive in current directory.
|
||||
[INFO] Files unpacked: 1
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user