Repository: ouch-org/ouch (language: rust)

Issue:
Support brotli format
https://www.brotli.org/

extension: `.br`

crate: https://lib.rs/crates/brotli


Discussion / hints:
I'd like to have this as well.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e44b061e..ab5fda3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ Categories Used:
 - Add multithreading support for `zstd` compression [\#689](https://github.com/ouch-org/ouch/pull/689) ([nalabrie](https://github.com/nalabrie))
 - Add `bzip3` support [\#522](https://github.com/ouch-org/ouch/pull/522) ([freijon](https://github.com/freijon))
 - Add `--remove` flag for decompression subcommand to remove files after successful decompression [\#757](https://github.com/ouch-org/ouch/pull/757) ([ttys3](https://github.com/ttys3))
+- Add `br` (Brotli) support [\#765](https://github.com/ouch-org/ouch/pull/765) ([killercup](https://github.com/killercup))
 
 ### Bug Fixes
 
diff --git a/Cargo.lock b/Cargo.lock
index 83712b57..5fad15bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -28,6 +28,21 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
 [[package]]
 name = "anstream"
 version = "0.6.18"
@@ -198,6 +213,27 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "brotli"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
 [[package]]
 name = "bstr"
 version = "1.11.0"
@@ -1032,6 +1068,7 @@ version = "0.5.1"
 dependencies = [
  "assert_cmd",
  "atty",
+ "brotli",
  "bstr",
  "bytesize",
  "bzip2",
diff --git a/Cargo.toml b/Cargo.toml
index f9f602d5..4192a770 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ description = "A command-line utility for easily compressing and decompressing f
 
 [dependencies]
 atty = "0.2.14"
+brotli = "7.0.0"
 bstr = { version = "1.10.0", default-features = false, features = ["std"] }
 bytesize = "1.3.0"
 bzip2 = "0.4.4"
diff --git a/README.md b/README.md
index 70caede7..ae344146 100644
--- a/README.md
+++ b/README.md
@@ -111,9 +111,9 @@ Output:
 
 # Supported formats
 
-| Format    | `.tar` | `.zip` | `7z` | `.gz` | `.xz`, `.lzma` | `.bz`, `.bz2` | `.bz3` | `.lz4` | `.sz` (Snappy) | `.zst` | `.rar` |
-|:---------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
-| Supported | ✓ | ✓¹ | ✓¹ | ✓² | ✓ | ✓ | ✓ | ✓ | ✓² | ✓² | ✓³ |
+| Format    | `.tar` | `.zip` | `7z` | `.gz` | `.xz`, `.lzma` | `.bz`, `.bz2` | `.bz3` | `.lz4` | `.sz` (Snappy) | `.zst` | `.rar` | `.br` |
+|:---------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| Supported | ✓ | ✓¹ | ✓¹ | ✓² | ✓ | ✓ | ✓ | ✓ | ✓² | ✓² | ✓³ | ✓ |
 
 ✓: Supports compression and decompression.
 
diff --git a/src/cli/args.rs b/src/cli/args.rs
index 22b8372f..5701f4e4 100644
--- a/src/cli/args.rs
+++ b/src/cli/args.rs
@@ -5,7 +5,7 @@ use clap::{Parser, ValueHint};
 // Ouch command line options (docstrings below are part of --help)
 /// A command-line utility for easily compressing and decompressing files and directories.
 ///
-/// Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst and rar.
+/// Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst, rar and br.
 ///
 /// Repository: https://github.com/ouch-org/ouch
 #[derive(Parser, Debug, PartialEq)]
diff --git a/src/commands/compress.rs b/src/commands/compress.rs
index 180bb2eb..59987b85 100644
--- a/src/commands/compress.rs
+++ b/src/commands/compress.rs
@@ -83,6 +83,12 @@ pub fn compress_files(
                 zstd_encoder.multithread(num_cpus::get_physical() as u32)?;
                 Box::new(zstd_encoder.auto_finish())
             }
+            Brotli => {
+                let default_level = 11; // Same as brotli CLI, default to highest compression
+                let level = level.unwrap_or(default_level).clamp(0, 11) as u32;
+                let win_size = 22; // default to 2^22 = 4 MiB window size
+                Box::new(brotli::CompressorWriter::new(encoder, BUFFER_CAPACITY, level, win_size))
+            }
             Tar | Zip | Rar | SevenZip => unreachable!(),
         };
         Ok(encoder)
@@ -95,7 +101,7 @@ pub fn compress_files(
     }
 
     match first_format {
-        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
+        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd | Brotli => {
             writer = chain_writer_encoder(&first_format, writer)?;
             let mut reader = fs::File::open(&files[0])?;
 
diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs
index 1d0152a6..6d2efee8 100644
--- a/src/commands/decompress.rs
+++ b/src/commands/decompress.rs
@@ -119,6 +119,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
+            Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)),
             Tar | Zip | Rar | SevenZip => unreachable!(),
         };
         Ok(decoder)
@@ -131,7 +132,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
     }
 
     let files_unpacked = match first_extension {
-        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
+        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd | Brotli => {
             reader = chain_reader_decoder(&first_extension, reader)?;
 
             let mut writer = match utils::ask_to_create_file(&options.output_file_path, options.question_policy)? {
diff --git a/src/commands/list.rs b/src/commands/list.rs
index 1821d6da..a64c55cb 100644
--- a/src/commands/list.rs
+++ b/src/commands/list.rs
@@ -55,6 +55,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)?),
+                Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)),
                 Tar | Zip | Rar | SevenZip => unreachable!(),
             };
             Ok(decoder)
@@ -112,7 +113,7 @@ pub fn list_archive_contents(
 
             Box::new(sevenz::list_archive(archive_path, password)?)
         }
-        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
+        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd | Brotli => {
             panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
         }
     };
diff --git a/src/extension.rs b/src/extension.rs
index f8d73899..7250a574 100644
--- a/src/extension.rs
+++ b/src/extension.rs
@@ -21,6 +21,7 @@ pub const SUPPORTED_EXTENSIONS: &[&str] = &[
     #[cfg(feature = "unrar")]
     "rar",
     "7z",
+    "br",
 ];
 
 pub const SUPPORTED_ALIASES: &[&str] = &["tgz", "tbz", "tlz4", "txz", "tzlma", "tsz", "tzst"];
@@ -96,6 +97,8 @@ pub enum CompressionFormat {
     Rar,
     /// .7z
     SevenZip,
+    /// .br
+    Brotli,
 }
 
 impl CompressionFormat {
@@ -111,6 +114,7 @@ impl CompressionFormat {
             Lzma => false,
             Snappy => false,
             Zstd => false,
+            Brotli => false,
         }
     }
 }
@@ -136,6 +140,7 @@ fn to_extension(ext: &[u8]) -> Option<Extension> {
             b"zst" => &[Zstd],
             b"rar" => &[Rar],
             b"7z" => &[SevenZip],
+            b"br" => &[Brotli],
             _ => return None,
         },
         ext.to_str_lossy(),


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index 779766a6..7241cd1a 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -44,6 +44,7 @@ enum FileExtension {
     Sz,
     Xz,
     Zst,
+    Br,
 }
 
 #[derive(Arbitrary, Debug, Display)]
diff --git a/tests/snapshots/ui__ui_test_usage_help_flag.snap b/tests/snapshots/ui__ui_test_usage_help_flag.snap
index e8075652..6913b83d 100644
--- a/tests/snapshots/ui__ui_test_usage_help_flag.snap
+++ b/tests/snapshots/ui__ui_test_usage_help_flag.snap
@@ -5,7 +5,7 @@ snapshot_kind: text
 ---
 A command-line utility for easily compressing and decompressing files and directories.
 
-Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst and rar.
+Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst, rar and br.
 
 Repository: https://github.com/ouch-org/ouch