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

Issue:
Add support for bzip3
See : https://github.com/kspalaiologos/bzip3
Also see: https://crates.io/crates/bzip3


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e76aef768..a22b3ecc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ Categories Used:
 ### New Features
 
 - 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))
 
 ### Bug Fixes
 
diff --git a/Cargo.lock b/Cargo.lock
index 06a96be0e..caf9997ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,6 +116,28 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
+[[package]]
+name = "bindgen"
+version = "0.63.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
+dependencies = [
+ "bitflags 1.3.2",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 1.0.109",
+ "which",
+]
+
 [[package]]
 name = "bit-set"
 version = "0.5.3"
@@ -205,6 +227,12 @@ version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 
+[[package]]
+name = "bytesize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
+
 [[package]]
 name = "bzip2"
 version = "0.4.4"
@@ -226,6 +254,18 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "bzip3"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e908c8163856f1138894e7bce70d2ae79c2d9a19a515dfe96e10888a839aa10c"
+dependencies = [
+ "byteorder",
+ "bytesize",
+ "libbzip3-sys",
+ "thiserror",
+]
+
 [[package]]
 name = "cbc"
 version = "0.1.2"
@@ -246,6 +286,15 @@ dependencies = [
  "shlex",
 ]
 
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
 [[package]]
 name = "cfb"
 version = "0.7.3"
@@ -282,6 +331,17 @@ dependencies = [
  "inout",
 ]
 
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.20"
@@ -322,7 +382,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -601,6 +661,12 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
 [[package]]
 name = "globset"
 version = "0.4.15"
@@ -661,6 +727,15 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "ignore"
 version = "0.4.23"
@@ -748,12 +823,40 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libbzip3-sys"
+version = "0.4.0+1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "001a0e4d146f247a741755ebc538d592b3603923df85d6e4472e4268822b5d6b"
+dependencies = [
+ "bindgen",
+ "cc",
+ "cfg-if",
+ "regex",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.158"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
 
+[[package]]
+name = "libloading"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
+dependencies = [
+ "cfg-if",
+ "windows-targets",
+]
+
 [[package]]
 name = "libm"
 version = "0.2.8"
@@ -846,6 +949,12 @@ version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.8.0"
@@ -864,6 +973,16 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
 [[package]]
 name = "nt-time"
 version = "0.8.1"
@@ -913,7 +1032,9 @@ dependencies = [
  "assert_cmd",
  "atty",
  "bstr",
+ "bytesize",
  "bzip2",
+ "bzip3",
  "clap",
  "clap_complete",
  "clap_mangen",
@@ -970,7 +1091,7 @@ dependencies = [
  "regex",
  "regex-syntax",
  "structmeta",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -996,6 +1117,12 @@ dependencies = [
  "sha2",
 ]
 
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
 [[package]]
 name = "pin-project"
 version = "1.1.5"
@@ -1013,7 +1140,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -1211,6 +1338,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
 
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
 [[package]]
 name = "rustix"
 version = "0.38.35"
@@ -1268,7 +1401,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -1361,7 +1494,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "structmeta-derive",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -1372,7 +1505,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -1381,6 +1514,17 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
 
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "syn"
 version = "2.0.77"
@@ -1431,7 +1575,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "structmeta",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -1451,7 +1595,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
@@ -1606,7 +1750,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
  "wasm-bindgen-shared",
 ]
 
@@ -1628,7 +1772,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -1639,6 +1783,18 @@ version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
 
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
 [[package]]
 name = "widestring"
 version = "1.1.0"
@@ -1796,7 +1952,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.77",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index b4516b2e4..3c34e34e0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,9 @@ description = "A command-line utility for easily compressing and decompressing f
 [dependencies]
 atty = "0.2.14"
 bstr = { version = "1.10.0", default-features = false, features = ["std"] }
+bytesize = "1.3.0"
 bzip2 = "0.4.4"
+bzip3 = { version = "0.9.0", features = ["bundled"] }
 clap = { version = "4.5.20", features = ["derive", "env"] }
 filetime_creation = "0.2"
 flate2 = { version = "1.0.30", default-features = false }
diff --git a/LICENSE b/LICENSE
index 701117709..df500f524 100644
--- a/LICENSE
+++ b/LICENSE
@@ -20,7 +20,18 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
+---
+
 Copyright notices from other projects:
 
-Copyright (c) 2019 Bojan
-https://github.com/bojand/infer
+Infer crate (MIT LICENSE):
+> Copyright (c) 2019 Bojan
+> Code at https://github.com/bojand/infer
+
+Bzip3-rs crate (LGPL 3.0):
+> Code for this crate is available at https://github.com/bczhc/bzip3-rs
+> See its license at https://github.com/bczhc/bzip3-rs/blob/master/LICENSE
+
+Bzip3 library (LGPL 3.0):
+> Code for this library is available at https://github.com/kspalaiologos/bzip3
+> See its license at https://github.com/kspalaiologos/bzip3/blob/master/LICENSE
diff --git a/README.md b/README.md
index ee5a6cf4b..70caede73 100644
--- a/README.md
+++ b/README.md
@@ -111,9 +111,9 @@ Output:
 
 # Supported formats
 
-| Format    | `.tar` | `.zip` | `7z` | `.gz` | `.xz`, `.lzma` | `.bz`, `.bz2` | `.lz4` | `.sz` (Snappy) | `.zst` | `.rar` |
-|:---------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
-| Supported | ✓ | ✓¹ | ✓¹ | ✓² | ✓ | ✓ | ✓ | ✓² | ✓² | ✓³ |
+| Format    | `.tar` | `.zip` | `7z` | `.gz` | `.xz`, `.lzma` | `.bz`, `.bz2` | `.bz3` | `.lz4` | `.sz` (Snappy) | `.zst` | `.rar` |
+|:---------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| Supported | ✓ | ✓¹ | ✓¹ | ✓² | ✓ | ✓ | ✓ | ✓ | ✓² | ✓² | ✓³ |
 
 ✓: Supports compression and decompression.
 
@@ -176,6 +176,7 @@ Otherwise, you'll need these libraries installed on your system:
 
 * [liblzma](https://www.7-zip.org/sdk.html)
 * [libbz2](https://www.sourceware.org/bzip2)
+* [libbz3](https://github.com/kspalaiologos/bzip3)
 * [libz](https://www.zlib.net)
 
 These should be available in your system's package manager.
diff --git a/src/cli/args.rs b/src/cli/args.rs
index dcdf768ca..05b522c60 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, lz4, sz (Snappy), zst and rar.
+/// Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst and rar.
 ///
 /// Repository: https://github.com/ouch-org/ouch
 #[derive(Parser, Debug, PartialEq)]
diff --git a/src/commands/compress.rs b/src/commands/compress.rs
index 00f448a23..180bb2eb8 100644
--- a/src/commands/compress.rs
+++ b/src/commands/compress.rs
@@ -56,6 +56,10 @@ pub fn compress_files(
                 encoder,
                 level.map_or_else(Default::default, |l| bzip2::Compression::new((l as u32).clamp(1, 9))),
             )),
+            Bzip3 => Box::new(
+                // Use block size of 16 MiB
+                bzip3::write::Bz3Encoder::new(encoder, 16 * 2_usize.pow(20))?,
+            ),
             Lz4 => Box::new(lz4_flex::frame::FrameEncoder::new(encoder).auto_finish()),
             Lzma => Box::new(xz2::write::XzEncoder::new(
                 encoder,
@@ -91,9 +95,9 @@ pub fn compress_files(
     }
 
     match first_format {
-        Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
+        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
             writer = chain_writer_encoder(&first_format, writer)?;
-            let mut reader = fs::File::open(&files[0]).unwrap();
+            let mut reader = fs::File::open(&files[0])?;
 
             io::copy(&mut reader, &mut writer)?;
         }
diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs
index 0e9853337..7c5c842bc 100644
--- a/src/commands/decompress.rs
+++ b/src/commands/decompress.rs
@@ -100,6 +100,7 @@ pub fn decompress_file(
         let decoder: Box<dyn Read> = match format {
             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
+            Bzip3 => Box::new(bzip3::read::Bz3Decoder::new(decoder)?),
             Lz4 => Box::new(lz4_flex::frame::FrameDecoder::new(decoder)),
             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
@@ -116,7 +117,7 @@ pub fn decompress_file(
     }
 
     let files_unpacked = match first_extension {
-        Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
+        Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
             reader = chain_reader_decoder(&first_extension, reader)?;
 
             let mut writer = match utils::ask_to_create_file(&output_file_path, question_policy)? {
diff --git a/src/commands/list.rs b/src/commands/list.rs
index fc8aa75a2..4d87a8c4c 100644
--- a/src/commands/list.rs
+++ b/src/commands/list.rs
@@ -50,6 +50,7 @@ pub fn list_archive_contents(
             let decoder: Box<dyn Read + Send> = match format {
                 Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
                 Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
+                Bzip3 => Box::new(bzip3::read::Bz3Decoder::new(decoder).unwrap()),
                 Lz4 => Box::new(lz4_flex::frame::FrameDecoder::new(decoder)),
                 Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
                 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
@@ -111,7 +112,7 @@ pub fn list_archive_contents(
 
             Box::new(sevenz::list_archive(archive_path, password)?)
         }
-        Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
+        Gzip | Bzip | Bzip3 | 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!");
         }
     };
diff --git a/src/error.rs b/src/error.rs
index 2a3a2b1bd..b8c274dc7 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -200,6 +200,18 @@ impl From<std::io::Error> for Error {
     }
 }
 
+impl From<bzip3::Error> for Error {
+    fn from(err: bzip3::Error) -> Self {
+        use bzip3::Error as Bz3Error;
+        match err {
+            Bz3Error::Io(inner) => inner.into(),
+            Bz3Error::BlockSize | Bz3Error::ProcessBlock(_) | Bz3Error::InvalidSignature => {
+                FinalError::with_title("bzip3 error").detail(err.to_string()).into()
+            }
+        }
+    }
+}
+
 impl From<zip::result::ZipError> for Error {
     fn from(err: zip::result::ZipError) -> Self {
         use zip::result::ZipError;
diff --git a/src/extension.rs b/src/extension.rs
index 4b0057e28..0b6af9a16 100644
--- a/src/extension.rs
+++ b/src/extension.rs
@@ -26,9 +26,9 @@ pub const SUPPORTED_EXTENSIONS: &[&str] = &[
 pub const SUPPORTED_ALIASES: &[&str] = &["tgz", "tbz", "tlz4", "txz", "tzlma", "tsz", "tzst"];
 
 #[cfg(not(feature = "unrar"))]
-pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z";
+pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z";
 #[cfg(feature = "unrar")]
-pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z";
+pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z";
 
 pub const PRETTY_SUPPORTED_ALIASES: &str = "tgz, tbz, tlz4, txz, tzlma, tsz, tzst";
 
@@ -77,13 +77,15 @@ pub enum CompressionFormat {
     Gzip,
     /// .bz .bz2
     Bzip,
+    /// .bz3
+    Bzip3,
     /// .lz4
     Lz4,
     /// .xz .lzma
     Lzma,
     /// .sz
     Snappy,
-    /// tar, tgz, tbz, tbz2, txz, tlz4, tlzma, tsz, tzst
+    /// tar, tgz, tbz, tbz2, tbz3, txz, tlz4, tlzma, tsz, tzst
     Tar,
     /// .zst
     Zstd,
@@ -104,6 +106,7 @@ impl CompressionFormat {
             Tar | Zip | Rar | SevenZip => true,
             Gzip => false,
             Bzip => false,
+            Bzip3 => false,
             Lz4 => false,
             Lzma => false,
             Snappy => false,
@@ -118,12 +121,14 @@ fn to_extension(ext: &[u8]) -> Option<Extension> {
             b"tar" => &[Tar],
             b"tgz" => &[Tar, Gzip],
             b"tbz" | b"tbz2" => &[Tar, Bzip],
+            b"tbz3" => &[Tar, Bzip3],
             b"tlz4" => &[Tar, Lz4],
             b"txz" | b"tlzma" => &[Tar, Lzma],
             b"tsz" => &[Tar, Snappy],
             b"tzst" => &[Tar, Zstd],
             b"zip" => &[Zip],
             b"bz" | b"bz2" => &[Bzip],
+            b"bz3" => &[Bzip3],
             b"gz" => &[Gzip],
             b"lz4" => &[Lz4],
             b"xz" | b"lzma" => &[Lzma],
diff --git a/src/utils/fs.rs b/src/utils/fs.rs
index 2e4f7666c..a0928f289 100644
--- a/src/utils/fs.rs
+++ b/src/utils/fs.rs
@@ -82,6 +82,9 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
     fn is_bz2(buf: &[u8]) -> bool {
         buf.starts_with(&[0x42, 0x5A, 0x68])
     }
+    fn is_bz3(buf: &[u8]) -> bool {
+        buf.starts_with(bzip3::MAGIC_NUMBER)
+    }
     fn is_xz(buf: &[u8]) -> bool {
         buf.starts_with(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])
     }
@@ -125,6 +128,8 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
         Some(Extension::new(&[Gzip], "gz"))
     } else if is_bz2(&buf) {
         Some(Extension::new(&[Bzip], "bz2"))
+    } else if is_bz3(&buf) {
+        Some(Extension::new(&[Bzip3], "bz3"))
     } else if is_xz(&buf) {
         Some(Extension::new(&[Lzma], "xz"))
     } else if is_lz4(&buf) {
@@ -143,6 +148,7 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
 }
 
 /// Returns true if a path is a symlink.
+///
 /// This is the same as the nightly <https://doc.rust-lang.org/std/path/struct.Path.html#method.is_symlink>
 /// Useful to detect broken symlinks when compressing. (So we can safely ignore them)
 pub fn is_symlink(path: &Path) -> bool {


Test patch:
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 8200d44bc..861707bfe 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -122,7 +122,6 @@ jobs:
             target/${{ matrix.target }}/release/ouch.exe
             artifacts/
 
-
   clippy-rustfmt:
     name: clippy-rustfmt
     runs-on: ubuntu-latest
diff --git a/tests/integration.rs b/tests/integration.rs
index b31258e88..82315c3e5 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -21,6 +21,7 @@ enum DirectoryExtension {
     Tar,
     Tbz,
     Tbz2,
+    Tbz3,
     Tgz,
     Tlz4,
     Tlzma,
@@ -36,6 +37,7 @@ enum DirectoryExtension {
 enum FileExtension {
     Bz,
     Bz2,
+    Bz3,
     Gz,
     Lz4,
     Lzma,
diff --git a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-1.snap b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-1.snap
index 8322cf23e..61ffed686 100644
--- a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-1.snap
+++ b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-1.snap
@@ -6,7 +6,7 @@ expression: "run_ouch(\"ouch decompress a\", dir)"
  - Files with missing extensions: <TMP_DIR>/a
  - Decompression formats are detected automatically from file extension
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Alternatively, you can pass an extension to the '--format' flag:
diff --git a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-2.snap b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-2.snap
index 643702b6a..fe1bbf9aa 100644
--- a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-2.snap
+++ b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-2.snap
@@ -7,5 +7,5 @@ expression: "run_ouch(\"ouch decompress a b.unknown\", dir)"
  - Files with missing extensions: <TMP_DIR>/a
  - Decompression formats are detected automatically from file extension
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
diff --git a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-3.snap b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-3.snap
index 96a49a250..0ef66a216 100644
--- a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-3.snap
+++ b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_with_rar-3.snap
@@ -6,7 +6,7 @@ expression: "run_ouch(\"ouch decompress b.unknown\", dir)"
  - Files with unsupported extensions: <TMP_DIR>/b.unknown
  - Decompression formats are detected automatically from file extension
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Alternatively, you can pass an extension to the '--format' flag:
diff --git a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-1.snap b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-1.snap
index fec78bb68..2885d83bd 100644
--- a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-1.snap
+++ b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-1.snap
@@ -6,7 +6,7 @@ expression: "run_ouch(\"ouch decompress a\", dir)"
  - Files with missing extensions: <TMP_DIR>/a
  - Decompression formats are detected automatically from file extension
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Alternatively, you can pass an extension to the '--format' flag:
diff --git a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-2.snap b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-2.snap
index 4ccff1374..9cdcbd6ea 100644
--- a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-2.snap
+++ b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-2.snap
@@ -7,5 +7,5 @@ expression: "run_ouch(\"ouch decompress a b.unknown\", dir)"
  - Files with missing extensions: <TMP_DIR>/a
  - Decompression formats are detected automatically from file extension
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
diff --git a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-3.snap b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-3.snap
index d8b9076b6..5e57d8b04 100644
--- a/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-3.snap
+++ b/tests/snapshots/ui__ui_test_err_decompress_missing_extension_without_rar-3.snap
@@ -6,7 +6,7 @@ expression: "run_ouch(\"ouch decompress b.unknown\", dir)"
  - Files with unsupported extensions: <TMP_DIR>/b.unknown
  - Decompression formats are detected automatically from file extension
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Alternatively, you can pass an extension to the '--format' flag:
diff --git a/tests/snapshots/ui__ui_test_err_format_flag_with_rar-1.snap b/tests/snapshots/ui__ui_test_err_format_flag_with_rar-1.snap
index ca5e05cdc..5cb36e405 100644
--- a/tests/snapshots/ui__ui_test_err_format_flag_with_rar-1.snap
+++ b/tests/snapshots/ui__ui_test_err_format_flag_with_rar-1.snap
@@ -5,7 +5,7 @@ expression: "run_ouch(\"ouch compress input output --format tar.gz.unknown\", di
 [ERROR] Failed to parse `--format tar.gz.unknown`
  - Unsupported extension 'unknown'
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Examples:
diff --git a/tests/snapshots/ui__ui_test_err_format_flag_with_rar-2.snap b/tests/snapshots/ui__ui_test_err_format_flag_with_rar-2.snap
index d8a14c962..a1196c786 100644
--- a/tests/snapshots/ui__ui_test_err_format_flag_with_rar-2.snap
+++ b/tests/snapshots/ui__ui_test_err_format_flag_with_rar-2.snap
@@ -5,7 +5,7 @@ expression: "run_ouch(\"ouch compress input output --format targz\", dir)"
 [ERROR] Failed to parse `--format targz`
  - Unsupported extension 'targz'
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Examples:
diff --git a/tests/snapshots/ui__ui_test_err_format_flag_with_rar-3.snap b/tests/snapshots/ui__ui_test_err_format_flag_with_rar-3.snap
index a1c8aabd0..4269a2399 100644
--- a/tests/snapshots/ui__ui_test_err_format_flag_with_rar-3.snap
+++ b/tests/snapshots/ui__ui_test_err_format_flag_with_rar-3.snap
@@ -5,7 +5,7 @@ expression: "run_ouch(\"ouch compress input output --format .tar.$#!@.rest\", di
 [ERROR] Failed to parse `--format .tar.$#!@.rest`
  - Unsupported extension '$#!@'
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, rar, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, rar, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Examples:
diff --git a/tests/snapshots/ui__ui_test_err_format_flag_without_rar-1.snap b/tests/snapshots/ui__ui_test_err_format_flag_without_rar-1.snap
index e386260bb..cd7ccbc3c 100644
--- a/tests/snapshots/ui__ui_test_err_format_flag_without_rar-1.snap
+++ b/tests/snapshots/ui__ui_test_err_format_flag_without_rar-1.snap
@@ -5,7 +5,7 @@ expression: "run_ouch(\"ouch compress input output --format tar.gz.unknown\", di
 [ERROR] Failed to parse `--format tar.gz.unknown`
  - Unsupported extension 'unknown'
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Examples:
diff --git a/tests/snapshots/ui__ui_test_err_format_flag_without_rar-2.snap b/tests/snapshots/ui__ui_test_err_format_flag_without_rar-2.snap
index 3ac4dfa1e..0913264f7 100644
--- a/tests/snapshots/ui__ui_test_err_format_flag_without_rar-2.snap
+++ b/tests/snapshots/ui__ui_test_err_format_flag_without_rar-2.snap
@@ -5,7 +5,7 @@ expression: "run_ouch(\"ouch compress input output --format targz\", dir)"
 [ERROR] Failed to parse `--format targz`
  - Unsupported extension 'targz'
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Examples:
diff --git a/tests/snapshots/ui__ui_test_err_format_flag_without_rar-3.snap b/tests/snapshots/ui__ui_test_err_format_flag_without_rar-3.snap
index f317bbf29..7d9b12c0f 100644
--- a/tests/snapshots/ui__ui_test_err_format_flag_without_rar-3.snap
+++ b/tests/snapshots/ui__ui_test_err_format_flag_without_rar-3.snap
@@ -5,7 +5,7 @@ expression: "run_ouch(\"ouch compress input output --format .tar.$#!@.rest\", di
 [ERROR] Failed to parse `--format .tar.$#!@.rest`
  - Unsupported extension '$#!@'
 
-hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst, 7z
+hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z
 hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst
 hint: 
 hint: Examples:
diff --git a/tests/snapshots/ui__ui_test_usage_help_flag.snap b/tests/snapshots/ui__ui_test_usage_help_flag.snap
index 3b5cd8f34..921aa8d07 100644
--- a/tests/snapshots/ui__ui_test_usage_help_flag.snap
+++ b/tests/snapshots/ui__ui_test_usage_help_flag.snap
@@ -4,7 +4,7 @@ expression: "output_to_string(ouch!(\"--help\"))"
 ---
 A command-line utility for easily compressing and decompressing files and directories.
 
-Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, lz4, sz (Snappy), zst and rar.
+Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst and rar.
 
 Repository: https://github.com/ouch-org/ouch