Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 1 addition & 7 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
uses: actions/checkout@v4

- name: Install cross (non-x86_64 linux)
if: "!contains(matrix.target, 'x86_64') && runner.os == 'Linux'"
if: "matrix.target != 'x86_64-unknown-linux-gnu' && runner.os == 'Linux'"
run: |
pushd "$(mktemp -d)"
wget https://github.com/cross-rs/cross/releases/download/v0.2.4/cross-x86_64-unknown-linux-musl.tar.gz
Expand All @@ -70,12 +70,6 @@ jobs:
popd
echo CARGO=cross >> $GITHUB_ENV

- name: Install dependencies (x86_64-unknown-linux-musl)
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update
sudo apt-get install musl-tools

- name: Set up extra cargo flags
if: matrix.no-zstd-thin
run: |
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 32 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ snap = "1.1.0"
tar = "0.4.40"
tempfile = "3.8.1"
time = { version = "0.3.30", default-features = false }
unrar = "0.5.2"
xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = ["time"] }
zstd = { version = "0.13.0", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions src/archive/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Archive compression algorithms

pub mod rar;
pub mod tar;
pub mod zip;
56 changes: 56 additions & 0 deletions src/archive/rar.rs
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 5 additions & 1 deletion src/commands/compress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 19 additions & 1 deletion src/commands/decompress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand All @@ -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!");
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ pub fn run(
}
} else {
for path in files.iter() {
let (path, mut file_formats) = extension::separate_known_extensions_from_name(path);
let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);

if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
return Ok(());
}

output_paths.push(path);
output_paths.push(pathbase);
formats.push(file_formats);
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -72,14 +72,16 @@ pub enum CompressionFormat {
Zstd,
/// .zip
Zip,
/// .rar
Rar,
}

impl CompressionFormat {
/// Currently supported archive formats are .tar (and aliases to it) and .zip
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,
Expand Down Expand Up @@ -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(),
Expand Down
7 changes: 7 additions & 0 deletions src/utils/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
}
Expand Down
Binary file added tests/data/testfile.rar3.rar.gz
Binary file not shown.
Binary file added tests/data/testfile.rar5.rar
Binary file not shown.
28 changes: 27 additions & 1 deletion tests/integration.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading