From bcdff0f46b8ff16b4f03d1d574c9f229b4b16f89 Mon Sep 17 00:00:00 2001 From: oxalica Date: Tue, 1 Jul 2025 01:38:42 -0400 Subject: [PATCH] feat: add listing support for squashfs --- Cargo.lock | 281 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/archive/mod.rs | 1 + src/archive/squashfs.rs | 23 +++ src/commands/compress.rs | 10 +- src/commands/decompress.rs | 11 +- src/commands/list.rs | 30 ++-- src/commands/mod.rs | 27 ++-- src/error.rs | 23 +++ src/extension.rs | 11 +- src/utils/fs.rs | 6 + 11 files changed, 385 insertions(+), 40 deletions(-) create mode 100644 src/archive/squashfs.rs diff --git a/Cargo.lock b/Cargo.lock index 71b8cf9..7b5f97d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,24 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backhand" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c45726a83c67f85d931ef28d331cbc4108b179e06359ed84944313dfc2bb9ce1" +dependencies = [ + "deku", + "flate2", + "lz4_flex", + "rayon", + "solana-nohash-hasher", + "thiserror 2.0.12", + "tracing", + "xxhash-rust", + "zstd", + "zstd-safe", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -181,6 +199,18 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -284,7 +314,7 @@ dependencies = [ "byteorder", "bytesize", "libbzip3-sys", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -520,6 +550,66 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.98", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "deku" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476a022dcfbb013d1365734a42e05b6aca967ebe0d3bb38170086abd9ea3324" +dependencies = [ + "bitvec", + "deku_derive", + "no_std_io2", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb216d425bdf810c165a8ae1649523033e88b5f795480ccec63926295541b084" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "deranged" version = "0.4.0" @@ -570,6 +660,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.10" @@ -616,6 +712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", + "libz-rs-sys", "libz-sys", "miniz_oxide", ] @@ -648,6 +745,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-core" version = "0.3.31" @@ -728,9 +831,15 @@ dependencies = [ "libz-sys", "num_cpus", "snap", - "thiserror", + "thiserror 1.0.69", ] +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + [[package]] name = "heck" version = "0.5.0" @@ -770,6 +879,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "ignore" version = "0.4.23" @@ -786,6 +901,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "infer" version = "0.16.0" @@ -937,6 +1062,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d" +dependencies = [ + "zlib-rs", +] + [[package]] name = "libz-sys" version = "1.1.21" @@ -1036,6 +1170,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "no_std_io2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2b9acd47481ab557a89a5665891be79e43cce8a29ad77aa9419d7be5a7c06a" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -1095,6 +1238,7 @@ version = "0.6.1" dependencies = [ "assert_cmd", "atty", + "backhand", "brotli", "bstr", "bytesize", @@ -1213,6 +1357,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkg-config" version = "0.3.31" @@ -1286,6 +1436,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1330,6 +1489,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1595,6 +1760,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +[[package]] +name = "solana-nohash-hasher" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" + [[package]] name = "spin" version = "0.9.8" @@ -1667,6 +1838,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -1716,7 +1893,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1730,6 +1916,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "time" version = "0.3.41" @@ -1760,6 +1957,54 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -2058,6 +2303,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" @@ -2067,6 +2321,15 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "1.4.0" @@ -2078,6 +2341,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "xz2" version = "0.1.7" @@ -2152,6 +2421,12 @@ dependencies = [ "time", ] +[[package]] +name = "zlib-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 5d31493..bf4c460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ description = "A command-line utility for easily compressing and decompressing f [dependencies] atty = "0.2.14" +# FIXME: `xz` features cannot be enabled because it uses `lzma-sys` which cannot co-exist with our dependency `xz2`. +backhand = { version = "0.23.0", default-features = false, features = ["parallel", "gzip", "lz4", "zstd"] } brotli = "7.0.0" bstr = { version = "1.10.0", default-features = false, features = ["std"] } bytesize = "1.3.0" diff --git a/src/archive/mod.rs b/src/archive/mod.rs index 6412a8a..2e037fb 100644 --- a/src/archive/mod.rs +++ b/src/archive/mod.rs @@ -7,5 +7,6 @@ pub mod rar; #[cfg(not(feature = "unrar"))] pub mod rar_stub; pub mod sevenz; +pub mod squashfs; pub mod tar; pub mod zip; diff --git a/src/archive/squashfs.rs b/src/archive/squashfs.rs new file mode 100644 index 0000000..032c2c5 --- /dev/null +++ b/src/archive/squashfs.rs @@ -0,0 +1,23 @@ +use std::path::Path; + +use backhand::{FilesystemReader, InnerNode}; + +use crate::list::FileInArchive; + +pub fn list_archive<'a>(archive: FilesystemReader<'a>) -> impl Iterator> + 'a { + archive.root.nodes.into_iter().filter_map(move |f| { + // The reported paths are absolute, and include the root directory `/`. + // To be consistent with outputs of other formats, we strip the prefix `/` and ignore the root directory. + if f.fullpath == Path::new("/") { + return None; + } + Some(Ok(FileInArchive { + is_dir: matches!(f.inner, InnerNode::Dir(_)), + path: f + .fullpath + .strip_prefix("/") + .expect("paths must be absolute") + .to_path_buf(), + })) + }) +} diff --git a/src/commands/compress.rs b/src/commands/compress.rs index 15880bd..1737359 100644 --- a/src/commands/compress.rs +++ b/src/commands/compress.rs @@ -5,10 +5,9 @@ use std::{ use fs_err as fs; -use super::warn_user_about_loading_sevenz_in_memory; use crate::{ archive, - commands::warn_user_about_loading_zip_in_memory, + commands::warn_user_about_loading_in_memory, extension::{split_first_compression_format, CompressionFormat::*, Extension}, utils::{io::lock_and_flush_output_stdio, user_wants_to_continue, FileVisibilityPolicy}, QuestionAction, QuestionPolicy, BUFFER_CAPACITY, @@ -96,7 +95,7 @@ pub fn compress_files( 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!(), + Tar | Zip | Rar | SevenZip | Squashfs => unreachable!(), }; Ok(encoder) }; @@ -125,13 +124,14 @@ pub fn compress_files( )?; writer.flush()?; } + Squashfs => todo!(), Zip => { if !formats.is_empty() { // Locking necessary to guarantee that warning and question // messages stay adjacent let _locks = lock_and_flush_output_stdio(); - warn_user_about_loading_zip_in_memory(); + warn_user_about_loading_in_memory(".zip"); if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? { return Ok(false); } @@ -163,7 +163,7 @@ pub fn compress_files( // messages stay adjacent let _locks = lock_and_flush_output_stdio(); - warn_user_about_loading_sevenz_in_memory(); + warn_user_about_loading_in_memory(".7z"); if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? { return Ok(false); } diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 6c254ad..e620229 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -9,7 +9,7 @@ use fs_err as fs; #[cfg(not(feature = "bzip3"))] use crate::archive; use crate::{ - commands::{warn_user_about_loading_sevenz_in_memory, warn_user_about_loading_zip_in_memory}, + commands::warn_user_about_loading_in_memory, extension::{ split_first_compression_format, CompressionFormat::{self, *}, @@ -65,7 +65,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { { let mut vec = vec![]; let reader: Box = if input_is_stdin { - warn_user_about_loading_zip_in_memory(); + warn_user_about_loading_in_memory(".zip"); io::copy(&mut io::stdin(), &mut vec)?; Box::new(io::Cursor::new(vec)) } else { @@ -132,7 +132,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { 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 => decoder, + Tar | Zip | Rar | SevenZip | Squashfs => decoder, }; Ok(decoder) }; @@ -174,13 +174,14 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { return Ok(()); } } + Squashfs => todo!(), Zip => { if options.formats.len() > 1 { // Locking necessary to guarantee that warning and question // messages stay adjacent let _locks = lock_and_flush_output_stdio(); - warn_user_about_loading_zip_in_memory(); + warn_user_about_loading_in_memory(".zip"); if !user_wants_to_continue( options.input_file_path, options.question_policy, @@ -252,7 +253,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { // messages stay adjacent let _locks = lock_and_flush_output_stdio(); - warn_user_about_loading_sevenz_in_memory(); + warn_user_about_loading_in_memory(".7z"); if !user_wants_to_continue( options.input_file_path, options.question_policy, diff --git a/src/commands/list.rs b/src/commands/list.rs index 4a344a7..9b3ee2a 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -7,7 +7,7 @@ use fs_err as fs; use crate::{ archive, - commands::warn_user_about_loading_zip_in_memory, + commands::warn_user_about_loading_in_memory, extension::CompressionFormat::{self, *}, list::{self, FileInArchive, ListOptions}, utils::{io::lock_and_flush_output_stdio, user_wants_to_continue}, @@ -38,6 +38,13 @@ pub fn list_archive_contents( list::list_files(archive_path, files, list_options)?; return Ok(()); } + if let &[Squashfs] = formats.as_slice() { + let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader); + let archive = backhand::FilesystemReader::from_reader(reader)?; + let files = crate::archive::squashfs::list_archive(archive); + list::list_files(archive_path, files, list_options)?; + return Ok(()); + } // Will be used in decoder chaining let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader); @@ -61,7 +68,7 @@ pub fn list_archive_contents( 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!("should be treated by caller"), + Tar | Zip | Rar | SevenZip | Squashfs => unreachable!("should be treated by caller"), }; Ok(decoder) }; @@ -78,13 +85,15 @@ pub fn list_archive_contents( let archive_format = misplaced_archive_format.unwrap_or(formats[0]); let files: Box>> = match archive_format { Tar => Box::new(crate::archive::tar::list_archive(tar::Archive::new(reader))), - Zip => { + Zip | Squashfs => { + let is_zip = matches!(archive_format, Zip); + if formats.len() > 1 { // Locking necessary to guarantee that warning and question // messages stay adjacent let _locks = lock_and_flush_output_stdio(); - warn_user_about_loading_zip_in_memory(); + warn_user_about_loading_in_memory(if is_zip { ".zip" } else { ".sqfs" }); if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? { return Ok(()); } @@ -92,9 +101,14 @@ pub fn list_archive_contents( let mut vec = vec![]; io::copy(&mut reader, &mut vec)?; - let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; - - Box::new(crate::archive::zip::list_archive(zip_archive, password)) + let reader = io::Cursor::new(vec); + if is_zip { + let zip_archive = zip::ZipArchive::new(reader)?; + Box::new(crate::archive::zip::list_archive(zip_archive, password)) + } else { + let archive = backhand::FilesystemReader::from_reader(reader)?; + Box::new(crate::archive::squashfs::list_archive(archive)) + } } #[cfg(feature = "unrar")] Rar => { @@ -116,7 +130,7 @@ pub fn list_archive_contents( // messages stay adjacent let _locks = lock_and_flush_output_stdio(); - warn_user_about_loading_zip_in_memory(); + warn_user_about_loading_in_memory(".7z"); if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? { return Ok(()); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4a81d85..ed97c54 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -25,24 +25,15 @@ use crate::{ CliArgs, QuestionPolicy, }; -/// Warn the user that (de)compressing this .zip archive might freeze their system. -fn warn_user_about_loading_zip_in_memory() { - const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n \ - The format '.zip' is limited by design and cannot be (de)compressed with encoding streams.\n \ - When chaining '.zip' with other formats, all (de)compression needs to be done in-memory\n \ - Careful, you might run out of RAM if the archive is too large!"; - - eprintln!("{}[WARNING]{}: {ZIP_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET); -} - -/// Warn the user that (de)compressing this .7z archive might freeze their system. -fn warn_user_about_loading_sevenz_in_memory() { - const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n \ - The format '.7z' is limited by design and cannot be (de)compressed with encoding streams.\n \ - When chaining '.7z' with other formats, all (de)compression needs to be done in-memory\n \ - Careful, you might run out of RAM if the archive is too large!"; - - eprintln!("{}[WARNING]{}: {SEVENZ_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET); +/// Warn the user that (de)compressing this format might freeze their system. +fn warn_user_about_loading_in_memory(ext: &str) { + eprintln!( + "{}[WARNING]{}:\n \ + The format '{ext}' is limited by design and cannot be (de)compressed with encoding streams.\n \ + When chaining '{ext}' with other formats, all (de)compression needs to be done in-memory\n \ + Careful, you might run out of RAM if the archive is too large!", + *ORANGE, *RESET + ); } /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks diff --git a/src/error.rs b/src/error.rs index b3c33df..37b5cfd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,10 @@ pub enum Error { UnsupportedFormat { reason: String }, /// Invalid password provided InvalidPassword { reason: String }, + /// From backhand::BackhandError + InvalidSquashfs { reason: String }, + /// From backhand::BackhandError + UnsupportedSquashfs { reason: String }, } /// Alias to std's Result with ouch's Error @@ -158,8 +162,10 @@ impl From for FinalError { Error::Lz4Error { reason } => FinalError::with_title(reason), Error::AlreadyExists { error_title } => FinalError::with_title(error_title).detail("File already exists"), Error::InvalidZipArchive(reason) => FinalError::with_title("Invalid zip archive").detail(reason), + Error::InvalidSquashfs { reason } => FinalError::with_title("Invalid squashfs").detail(reason), Error::PermissionDenied { error_title } => FinalError::with_title(error_title).detail("Permission denied"), Error::UnsupportedZipArchive(reason) => FinalError::with_title("Unsupported zip archive").detail(reason), + Error::UnsupportedSquashfs { reason } => FinalError::with_title("Unsupported squashfs").detail(reason), Error::InvalidFormatFlag { reason, text } => { FinalError::with_title(format!("Failed to parse `--format {}`", os_str_to_str(&text))) .detail(reason) @@ -227,6 +233,23 @@ impl From for Error { } } +impl From for Error { + fn from(err: backhand::BackhandError) -> Self { + use backhand::BackhandError; + match err { + BackhandError::StdIo(io_err) => Self::from(io_err), + err @ (BackhandError::UnsupportedCompression(_) | BackhandError::UnsupportedInode(_)) => { + Self::UnsupportedSquashfs { + reason: err.to_string(), + } + } + err => Self::InvalidSquashfs { + reason: err.to_string(), + }, + } + } +} + #[cfg(feature = "unrar")] impl From for Error { fn from(err: unrar::error::UnrarError) -> Self { diff --git a/src/extension.rs b/src/extension.rs index 48be0cb..f9e9c42 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -102,6 +102,14 @@ pub enum CompressionFormat { SevenZip, /// .br Brotli, + /// .squashfs, .sqfs + // + // Note: There is not canonical extension for squashfs, we pick the two semi-official ones: + // - `.squashfs`: The most popular one from a quick search on GitHub. Also used by some distros. + // https://github.com/NixOS/nixpkgs/blob/6576d979e9a64d870b1f298a5c598116891a6c25/nixos/modules/installer/cd-dvd/iso-image.nix#L797 + // - `.sqfs`: Mentioned in `man mksquashfs` by squashfs-tools, the reference implementation. + // https://github.com/plougher/squashfs-tools/blob/9f9bbd79016ff04967800f4301c7a9d0024c0c91/Documentation/manpages/mksquashfs.1#L494 + Squashfs, } impl CompressionFormat { @@ -109,7 +117,7 @@ impl CompressionFormat { pub fn archive_format(&self) -> bool { // Keep this match like that without a wildcard `_` so we don't forget to update it match self { - Tar | Zip | Rar | SevenZip => true, + Tar | Zip | Rar | SevenZip | Squashfs => true, Gzip => false, Bzip => false, Bzip3 => false, @@ -144,6 +152,7 @@ fn to_extension(ext: &[u8]) -> Option { b"rar" => &[Rar], b"7z" => &[SevenZip], b"br" => &[Brotli], + b"sqfs" | b"squashfs" => &[Squashfs], _ => return None, }, ext.to_str_lossy(), diff --git a/src/utils/fs.rs b/src/utils/fs.rs index f65f152..53e09c4 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -158,6 +158,10 @@ pub fn try_infer_extension(path: &Path) -> Option { fn is_sevenz(buf: &[u8]) -> bool { buf.starts_with(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) } + fn is_squashfs(buf: &[u8]) -> bool { + // Ref: https://dr-emann.github.io/squashfs/squashfs.html#_the_superblock + buf.starts_with(b"hsqs") + } let buf = { let mut buf = [0; 270]; @@ -195,6 +199,8 @@ pub fn try_infer_extension(path: &Path) -> Option { Some(Extension::new(&[Rar], "rar")) } else if is_sevenz(&buf) { Some(Extension::new(&[SevenZip], "7z")) + } else if is_squashfs(&buf) { + Some(Extension::new(&[Squashfs], "sqfs")) } else { None }