added support for listing and decompressing .rar archives

This commit is contained in:
Łukasz Krawiec 2023-10-01 00:05:43 +02:00 committed by João Marcos
parent a3dca85cdd
commit dade163243
18 changed files with 175 additions and 11 deletions

View File

@ -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

View File

@ -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
View File

@ -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"

View File

@ -1,4 +1,5 @@
//! Archive compression algorithms
pub mod rar;
pub mod tar;
pub mod zip;

56
src/archive/rar.rs Normal file
View 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);
}

View File

@ -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)

View File

@ -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,

View File

@ -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!");
}

View File

@ -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 {

View File

@ -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(),

View File

@ -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
}

Binary file not shown.

Binary file not shown.

View File

@ -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(())
}

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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