feat: add listing support for squashfs

This commit is contained in:
oxalica 2025-07-01 01:38:42 -04:00
parent 945ffde551
commit bcdff0f46b
11 changed files with 385 additions and 40 deletions

281
Cargo.lock generated
View File

@ -126,6 +126,24 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 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]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.6.0" version = "1.6.0"
@ -181,6 +199,18 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -284,7 +314,7 @@ dependencies = [
"byteorder", "byteorder",
"bytesize", "bytesize",
"libbzip3-sys", "libbzip3-sys",
"thiserror", "thiserror 1.0.69",
] ]
[[package]] [[package]]
@ -520,6 +550,66 @@ dependencies = [
"typenum", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.4.0" version = "0.4.0"
@ -570,6 +660,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.10" version = "0.3.10"
@ -616,6 +712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"libz-sys", "libz-sys",
"miniz_oxide", "miniz_oxide",
] ]
@ -648,6 +745,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.31" version = "0.3.31"
@ -728,9 +831,15 @@ dependencies = [
"libz-sys", "libz-sys",
"num_cpus", "num_cpus",
"snap", "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]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -770,6 +879,12 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "ignore" name = "ignore"
version = "0.4.23" version = "0.4.23"
@ -786,6 +901,16 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "infer" name = "infer"
version = "0.16.0" version = "0.16.0"
@ -937,6 +1062,15 @@ dependencies = [
"redox_syscall", "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]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.21" version = "1.1.21"
@ -1036,6 +1170,15 @@ dependencies = [
"getrandom 0.2.15", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -1095,6 +1238,7 @@ version = "0.6.1"
dependencies = [ dependencies = [
"assert_cmd", "assert_cmd",
"atty", "atty",
"backhand",
"brotli", "brotli",
"bstr", "bstr",
"bytesize", "bytesize",
@ -1213,6 +1357,12 @@ dependencies = [
"syn 2.0.98", "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]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.31" version = "0.3.31"
@ -1286,6 +1436,15 @@ dependencies = [
"yansi", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.93" version = "1.0.93"
@ -1330,6 +1489,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1595,6 +1760,12 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
[[package]]
name = "solana-nohash-hasher"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@ -1667,6 +1838,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "tar" name = "tar"
version = "0.4.44" version = "0.4.44"
@ -1716,7 +1893,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ 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]] [[package]]
@ -1730,6 +1916,17 @@ dependencies = [
"syn 2.0.98", "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]] [[package]]
name = "time" name = "time"
version = "0.3.41" version = "0.3.41"
@ -1760,6 +1957,54 @@ dependencies = [
"time-core", "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]] [[package]]
name = "twox-hash" name = "twox-hash"
version = "1.6.3" version = "1.6.3"
@ -2058,6 +2303,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"
version = "0.33.0" version = "0.33.0"
@ -2067,6 +2321,15 @@ dependencies = [
"bitflags 2.8.0", "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]] [[package]]
name = "xattr" name = "xattr"
version = "1.4.0" version = "1.4.0"
@ -2078,6 +2341,12 @@ dependencies = [
"rustix", "rustix",
] ]
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]] [[package]]
name = "xz2" name = "xz2"
version = "0.1.7" version = "0.1.7"
@ -2152,6 +2421,12 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "zlib-rs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.3" version = "0.13.3"

View File

@ -15,6 +15,8 @@ description = "A command-line utility for easily compressing and decompressing f
[dependencies] [dependencies]
atty = "0.2.14" 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" brotli = "7.0.0"
bstr = { version = "1.10.0", default-features = false, features = ["std"] } bstr = { version = "1.10.0", default-features = false, features = ["std"] }
bytesize = "1.3.0" bytesize = "1.3.0"

View File

@ -7,5 +7,6 @@ pub mod rar;
#[cfg(not(feature = "unrar"))] #[cfg(not(feature = "unrar"))]
pub mod rar_stub; pub mod rar_stub;
pub mod sevenz; pub mod sevenz;
pub mod squashfs;
pub mod tar; pub mod tar;
pub mod zip; pub mod zip;

23
src/archive/squashfs.rs Normal file
View File

@ -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<Item = crate::Result<FileInArchive>> + '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(),
}))
})
}

View File

@ -5,10 +5,9 @@ use std::{
use fs_err as fs; use fs_err as fs;
use super::warn_user_about_loading_sevenz_in_memory;
use crate::{ use crate::{
archive, archive,
commands::warn_user_about_loading_zip_in_memory, commands::warn_user_about_loading_in_memory,
extension::{split_first_compression_format, CompressionFormat::*, Extension}, extension::{split_first_compression_format, CompressionFormat::*, Extension},
utils::{io::lock_and_flush_output_stdio, user_wants_to_continue, FileVisibilityPolicy}, utils::{io::lock_and_flush_output_stdio, user_wants_to_continue, FileVisibilityPolicy},
QuestionAction, QuestionPolicy, BUFFER_CAPACITY, QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
@ -96,7 +95,7 @@ pub fn compress_files(
let win_size = 22; // default to 2^22 = 4 MiB window size let win_size = 22; // default to 2^22 = 4 MiB window size
Box::new(brotli::CompressorWriter::new(encoder, BUFFER_CAPACITY, level, win_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) Ok(encoder)
}; };
@ -125,13 +124,14 @@ pub fn compress_files(
)?; )?;
writer.flush()?; writer.flush()?;
} }
Squashfs => todo!(),
Zip => { Zip => {
if !formats.is_empty() { if !formats.is_empty() {
// Locking necessary to guarantee that warning and question // Locking necessary to guarantee that warning and question
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); 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)? { if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
return Ok(false); return Ok(false);
} }
@ -163,7 +163,7 @@ pub fn compress_files(
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); 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)? { if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
return Ok(false); return Ok(false);
} }

View File

@ -9,7 +9,7 @@ use fs_err as fs;
#[cfg(not(feature = "bzip3"))] #[cfg(not(feature = "bzip3"))]
use crate::archive; use crate::archive;
use crate::{ 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::{ extension::{
split_first_compression_format, split_first_compression_format,
CompressionFormat::{self, *}, CompressionFormat::{self, *},
@ -65,7 +65,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
{ {
let mut vec = vec![]; let mut vec = vec![];
let reader: Box<dyn ReadSeek> = if input_is_stdin { let reader: Box<dyn ReadSeek> = 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)?; io::copy(&mut io::stdin(), &mut vec)?;
Box::new(io::Cursor::new(vec)) Box::new(io::Cursor::new(vec))
} else { } else {
@ -132,7 +132,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)), Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?), Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)), Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)),
Tar | Zip | Rar | SevenZip => decoder, Tar | Zip | Rar | SevenZip | Squashfs => decoder,
}; };
Ok(decoder) Ok(decoder)
}; };
@ -174,13 +174,14 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
return Ok(()); return Ok(());
} }
} }
Squashfs => todo!(),
Zip => { Zip => {
if options.formats.len() > 1 { if options.formats.len() > 1 {
// Locking necessary to guarantee that warning and question // Locking necessary to guarantee that warning and question
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); 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( if !user_wants_to_continue(
options.input_file_path, options.input_file_path,
options.question_policy, options.question_policy,
@ -252,7 +253,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); 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( if !user_wants_to_continue(
options.input_file_path, options.input_file_path,
options.question_policy, options.question_policy,

View File

@ -7,7 +7,7 @@ use fs_err as fs;
use crate::{ use crate::{
archive, archive,
commands::warn_user_about_loading_zip_in_memory, commands::warn_user_about_loading_in_memory,
extension::CompressionFormat::{self, *}, extension::CompressionFormat::{self, *},
list::{self, FileInArchive, ListOptions}, list::{self, FileInArchive, ListOptions},
utils::{io::lock_and_flush_output_stdio, user_wants_to_continue}, 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)?; list::list_files(archive_path, files, list_options)?;
return Ok(()); 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 // Will be used in decoder chaining
let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader); 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)), Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?), Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)), 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) Ok(decoder)
}; };
@ -78,13 +85,15 @@ pub fn list_archive_contents(
let archive_format = misplaced_archive_format.unwrap_or(formats[0]); let archive_format = misplaced_archive_format.unwrap_or(formats[0]);
let files: Box<dyn Iterator<Item = crate::Result<FileInArchive>>> = match archive_format { let files: Box<dyn Iterator<Item = crate::Result<FileInArchive>>> = match archive_format {
Tar => Box::new(crate::archive::tar::list_archive(tar::Archive::new(reader))), 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 { if formats.len() > 1 {
// Locking necessary to guarantee that warning and question // Locking necessary to guarantee that warning and question
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); 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)? { if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? {
return Ok(()); return Ok(());
} }
@ -92,9 +101,14 @@ pub fn list_archive_contents(
let mut vec = vec![]; let mut vec = vec![];
io::copy(&mut reader, &mut vec)?; io::copy(&mut reader, &mut vec)?;
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; let reader = io::Cursor::new(vec);
if is_zip {
Box::new(crate::archive::zip::list_archive(zip_archive, password)) 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")] #[cfg(feature = "unrar")]
Rar => { Rar => {
@ -116,7 +130,7 @@ pub fn list_archive_contents(
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); 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)? { if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? {
return Ok(()); return Ok(());
} }

View File

@ -25,24 +25,15 @@ use crate::{
CliArgs, QuestionPolicy, CliArgs, QuestionPolicy,
}; };
/// Warn the user that (de)compressing this .zip archive might freeze their system. /// Warn the user that (de)compressing this format might freeze their system.
fn warn_user_about_loading_zip_in_memory() { fn warn_user_about_loading_in_memory(ext: &str) {
const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n \ eprintln!(
The format '.zip' is limited by design and cannot be (de)compressed with encoding streams.\n \ "{}[WARNING]{}:\n \
When chaining '.zip' with other formats, all (de)compression needs to be done in-memory\n \ The format '{ext}' is limited by design and cannot be (de)compressed with encoding streams.\n \
Careful, you might run out of RAM if the archive is too large!"; 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!",
eprintln!("{}[WARNING]{}: {ZIP_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET); *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);
} }
/// This function checks what command needs to be run and performs A LOT of ahead-of-time checks /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks

View File

@ -47,6 +47,10 @@ pub enum Error {
UnsupportedFormat { reason: String }, UnsupportedFormat { reason: String },
/// Invalid password provided /// Invalid password provided
InvalidPassword { reason: String }, InvalidPassword { reason: String },
/// From backhand::BackhandError
InvalidSquashfs { reason: String },
/// From backhand::BackhandError
UnsupportedSquashfs { reason: String },
} }
/// Alias to std's Result with ouch's Error /// Alias to std's Result with ouch's Error
@ -158,8 +162,10 @@ impl From<Error> for FinalError {
Error::Lz4Error { reason } => FinalError::with_title(reason), Error::Lz4Error { reason } => FinalError::with_title(reason),
Error::AlreadyExists { error_title } => FinalError::with_title(error_title).detail("File already exists"), 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::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::PermissionDenied { error_title } => FinalError::with_title(error_title).detail("Permission denied"),
Error::UnsupportedZipArchive(reason) => FinalError::with_title("Unsupported zip archive").detail(reason), 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 } => { Error::InvalidFormatFlag { reason, text } => {
FinalError::with_title(format!("Failed to parse `--format {}`", os_str_to_str(&text))) FinalError::with_title(format!("Failed to parse `--format {}`", os_str_to_str(&text)))
.detail(reason) .detail(reason)
@ -227,6 +233,23 @@ impl From<zip::result::ZipError> for Error {
} }
} }
impl From<backhand::BackhandError> 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")] #[cfg(feature = "unrar")]
impl From<unrar::error::UnrarError> for Error { impl From<unrar::error::UnrarError> for Error {
fn from(err: unrar::error::UnrarError) -> Self { fn from(err: unrar::error::UnrarError) -> Self {

View File

@ -102,6 +102,14 @@ pub enum CompressionFormat {
SevenZip, SevenZip,
/// .br /// .br
Brotli, 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 { impl CompressionFormat {
@ -109,7 +117,7 @@ impl CompressionFormat {
pub fn archive_format(&self) -> bool { pub fn archive_format(&self) -> bool {
// Keep this match like that without a wildcard `_` so we don't forget to update it // Keep this match like that without a wildcard `_` so we don't forget to update it
match self { match self {
Tar | Zip | Rar | SevenZip => true, Tar | Zip | Rar | SevenZip | Squashfs => true,
Gzip => false, Gzip => false,
Bzip => false, Bzip => false,
Bzip3 => false, Bzip3 => false,
@ -144,6 +152,7 @@ fn to_extension(ext: &[u8]) -> Option<Extension> {
b"rar" => &[Rar], b"rar" => &[Rar],
b"7z" => &[SevenZip], b"7z" => &[SevenZip],
b"br" => &[Brotli], b"br" => &[Brotli],
b"sqfs" | b"squashfs" => &[Squashfs],
_ => return None, _ => return None,
}, },
ext.to_str_lossy(), ext.to_str_lossy(),

View File

@ -158,6 +158,10 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
fn is_sevenz(buf: &[u8]) -> bool { fn is_sevenz(buf: &[u8]) -> bool {
buf.starts_with(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) 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 buf = {
let mut buf = [0; 270]; let mut buf = [0; 270];
@ -195,6 +199,8 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
Some(Extension::new(&[Rar], "rar")) Some(Extension::new(&[Rar], "rar"))
} else if is_sevenz(&buf) { } else if is_sevenz(&buf) {
Some(Extension::new(&[SevenZip], "7z")) Some(Extension::new(&[SevenZip], "7z"))
} else if is_squashfs(&buf) {
Some(Extension::new(&[Squashfs], "sqfs"))
} else { } else {
None None
} }