Merge bb45b2f4a29c5580bb48ca42792af1d532062e10 into d0494504a1df4e13ecd4eeae0495066c81060cad

This commit is contained in:
oxalica 2025-07-15 15:45:00 -03:00 committed by GitHub
commit e8e6ba4b9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 790 additions and 95 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

@ -111,9 +111,9 @@ Output:
# Supported formats # Supported formats
| Format | `.tar` | `.zip` | `7z` | `.gz` | `.xz`, `.lzma` | `.bz`, `.bz2` | `.bz3` | `.lz4` | `.sz` (Snappy) | `.zst` | `.rar` | `.br` | | Format | `.tar` | `.zip` | `7z` | `.gz` | `.xz`, `.lzma` | `.bz`, `.bz2` | `.bz3` | `.lz4` | `.sz` (Snappy) | `.zst` | `.rar` | `.br` | `.sqfs`, `.squashfs` |
|:---------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |:---------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Supported | ✓ | ✓¹ | ✓¹ | ✓² | ✓ | ✓ | ✓ | ✓ | ✓² | ✓² | ✓³ | ✓ | | Supported | ✓ | ✓¹ | ✓¹ | ✓² | ✓ | ✓ | ✓ | ✓ | ✓² | ✓² | ✓³ | ✓ | ✓¹ |
✓: Supports compression and decompression. ✓: Supports compression and decompression.

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;

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

@ -0,0 +1,343 @@
use std::{
env, fs,
io::{self, BufWriter, Seek, Write},
path::{Path, PathBuf},
time::SystemTime,
};
use backhand::{
compression::Compressor, FilesystemCompressor, FilesystemReader, FilesystemWriter, InnerNode, NodeHeader,
SquashfsFileReader,
};
use filetime_creation::{set_file_handle_times, FileTime};
use same_file::Handle;
use crate::{
error::FinalError,
list::FileInArchive,
utils::{
logger::{info, warning},
Bytes, FileVisibilityPolicy,
},
};
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(),
}))
})
}
pub fn unpack_archive(archive: FilesystemReader<'_>, output_folder: &Path, quiet: bool) -> crate::Result<usize> {
let mut unpacked_files = 0usize;
for f in archive.files() {
// `output_folder` should already be created.
if f.fullpath == Path::new("/") {
continue;
}
let relative_path = f.fullpath.strip_prefix("/").expect("paths must be absolute");
let file_path = output_folder.join(relative_path);
let mtime = FileTime::from_unix_time(f.header.mtime.into(), 0);
let warn_ignored = |inode_type: &str| {
warning(format!("ignored {inode_type} in archive {relative_path:?}"));
};
match &f.inner {
InnerNode::Dir(_) => {
if !quiet {
info(format!("extracting directory {file_path:?}"));
}
fs::create_dir(&file_path)?;
// Directory mtime is not recovered. It will be overwritten by
// the creation of inner files. We would need a second pass to do so.
}
InnerNode::File(file) => {
if !quiet {
let file_size = Bytes::new(match file {
SquashfsFileReader::Basic(f) => f.file_size.into(),
SquashfsFileReader::Extended(f) => f.file_size,
});
info(format!("extracting file ({file_size}) {file_path:?}"));
}
let mut reader = archive.file(file).reader();
let output_file = fs::File::create(&file_path)?;
let mut output_file = BufWriter::new(output_file);
io::copy(&mut reader, &mut output_file)?;
output_file.flush()?;
set_file_handle_times(output_file.get_ref(), None, Some(mtime), None)?;
}
InnerNode::Symlink(symlink) => {
if !quiet {
info(format!("extracting symlink {file_path:?}"));
}
let target = &symlink.link;
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, &file_path)?;
filetime_creation::set_symlink_file_times(&file_path, mtime, mtime, mtime)?;
// Note: Symlink permissions are ignored on *NIX anyway. No need to set them.
}
#[cfg(windows)]
std::os::windows::fs::symlink_file(&target, &file_path)?;
// Symlink mtime is specially handled above. Skip the normal handler.
unpacked_files += 1;
continue;
}
// TODO: Named pipes and sockets *CAN* be created by unprivileged users.
// Should we extract them by default?
InnerNode::NamedPipe => {
warn_ignored("named pipe");
continue;
}
InnerNode::Socket => {
warn_ignored("socket");
continue;
}
// Not possible without root permission.
InnerNode::CharacterDevice(_) => {
warn_ignored("character device");
continue;
}
InnerNode::BlockDevice(_) => {
warn_ignored("block device");
continue;
}
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&file_path, fs::Permissions::from_mode(f.header.permissions.into()))?;
}
unpacked_files += 1;
}
Ok(unpacked_files)
}
// Re-assignments work bettwe with `cfg` blocks.
#[allow(clippy::field_reassign_with_default)]
pub fn build_archive_from_paths<W>(
input_filenames: &[PathBuf],
output_path: &Path,
mut writer: W,
file_visibility_policy: FileVisibilityPolicy,
quiet: bool,
follow_symlinks: bool,
) -> crate::Result<W>
where
W: Write + Seek,
{
let root_dir = match input_filenames {
[path] if path.is_dir() => path,
_ => {
let error = FinalError::with_title("Cannot build squashfs")
.detail("Squashfs requires a single directory input for root directory")
.detail(if input_filenames.len() != 1 {
"Multiple paths are provided".into()
} else {
format!("Not a directory: {:?}", input_filenames[0])
});
return Err(error.into());
}
};
let output_handle = Handle::from_path(output_path);
let mut fs_writer = FilesystemWriter::default();
// Set the default compression to Gzip with default level, matching mksquashfs's default.
// The default choice of `backhand` is Xz which is not enabled by us.
// TODO: We do not support customization argument for archive formats.
fs_writer.set_compressor(FilesystemCompressor::new(Compressor::Gzip, None).expect("gzip is supported"));
// cd *into* the source directory, using it as the archive root.
let previous_cwd = env::current_dir()?;
env::set_current_dir(root_dir)?;
for entry in file_visibility_policy.build_walker(".") {
let entry = entry?;
let path = entry.path();
if let Ok(handle) = &output_handle {
if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
warning(format!(
"Cannot compress `{}` into itself, skipping",
output_path.display()
));
}
}
if !quiet {
// `fs_writer.push_*` only maintains metadata. We do not want to give
// users a false information that we are compressing during the
// traversal. So this is not "compressing".
// File reading, compression and writing are done in
// `fs_writer.write` below, after the hierarchy tree gets finalized.
info(format!("Found {path:?}"));
}
let metadata = entry.metadata()?;
let file_type = metadata.file_type();
let mut header = NodeHeader::default();
header.mtime = match metadata.modified() {
// Not available.
Err(_) => 0,
Ok(mtime) => {
let mtime = mtime
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.and_then(|dur| u32::try_from(dur.as_secs()).ok());
if mtime.is_none() {
warning(format!(
"Modification time of {path:?} exceeds the representable range (1970-01-01 ~ 2106-02-07) \
of squashfs. Recorded as 1970-01-01."
));
}
mtime.unwrap_or(0)
}
};
#[cfg(not(unix))]
{
header.permissions = 0o777;
}
#[cfg(unix)]
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Only permission bits, not file type bits.
header.permissions = metadata.permissions().mode() as u16 & !(libc::S_IFMT as u16);
header.uid = metadata.uid();
header.gid = metadata.gid();
}
// Root directory is special cased.
if path == Path::new(".") {
fs_writer.set_root_mode(header.permissions);
fs_writer.set_root_uid(header.uid);
fs_writer.set_root_gid(header.gid);
continue;
}
if file_type.is_dir() {
fs_writer.push_dir(path, header)?;
} else if !follow_symlinks && file_type.is_symlink() {
let target = fs::read_link(path)?;
fs_writer.push_symlink(target, path, header)?;
} else if maybe_push_unix_special_inode(&mut fs_writer, path, &metadata, header)? {
// Already handled.
} else {
// Fallback case: read as a regular file.
// See comments of `LazyFile` for why not `File::open` here.
let reader = LazyFile::Path(path.to_path_buf());
fs_writer.push_file(reader, path, header)?;
}
}
if !quiet {
info("Compressing data".to_string());
}
// Finalize the superblock and write data. This should be done before
// resetting current directory, because `LazyFile`s store relative paths.
fs_writer.write(&mut writer)?;
env::set_current_dir(previous_cwd)?;
Ok(writer)
}
#[cfg(not(unix))]
fn maybe_push_unix_special_inode(
_writer: &mut FilesystemWriter,
_path: &Path,
_metadata: &fs::Metadata,
_header: NodeHeader,
) -> io::Result<bool> {
Ok(false)
}
#[cfg(unix)]
fn maybe_push_unix_special_inode(
writer: &mut FilesystemWriter,
path: &Path,
metadata: &fs::Metadata,
header: NodeHeader,
) -> io::Result<bool> {
use std::os::unix::fs::{FileTypeExt, MetadataExt};
let file_type = metadata.file_type();
if file_type.is_fifo() {
writer.push_fifo(path, header)?;
} else if file_type.is_socket() {
writer.push_socket(path, header)?;
} else if file_type.is_block_device() {
let dev = metadata.rdev() as u32;
writer.push_block_device(dev, path, header)?;
} else if file_type.is_char_device() {
let dev = metadata.rdev() as u32;
writer.push_char_device(dev, path, header)?;
} else {
return Ok(false);
}
Ok(true)
}
/// Delay file opening until the first read and close it as soon as the EOF is encountered.
///
/// Due to design of `backhand`, we need to store all `impl Read` into the
/// builder during traversal and write out the squashfs later. But we cannot
/// open and store all file handles during traversal or it will exhaust file
/// descriptors on *NIX if there are thousands of files (a pretty low limit!).
///
/// Upstream discussion: https://github.com/wcampbell0x2a/backhand/discussions/614
enum LazyFile {
Path(PathBuf),
Opened(fs::File),
Closed,
}
impl io::Read for LazyFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
LazyFile::Path(path) => {
let file = fs::File::open(path)?;
*self = Self::Opened(file);
self.read(buf)
}
LazyFile::Opened(file) => {
let cnt = file.read(buf)?;
if !buf.is_empty() && cnt == 0 {
*self = Self::Closed;
}
Ok(cnt)
}
LazyFile::Closed => Ok(0),
}
}
}

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,28 +124,41 @@ pub fn compress_files(
)?; )?;
writer.flush()?; writer.flush()?;
} }
Zip => { Zip | Squashfs => {
let is_zip = matches!(first_format, 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(if is_zip { ".zip" } else { ".sqfs" });
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);
} }
} }
let mut vec_buffer = Cursor::new(vec![]); let mut vec_buffer = Cursor::new(vec![]);
if is_zip {
archive::zip::build_archive_from_paths(
&files,
output_path,
&mut vec_buffer,
file_visibility_policy,
quiet,
follow_symlinks,
)?;
} else {
archive::squashfs::build_archive_from_paths(
&files,
output_path,
&mut vec_buffer,
file_visibility_policy,
quiet,
follow_symlinks,
)?;
}
archive::zip::build_archive_from_paths(
&files,
output_path,
&mut vec_buffer,
file_visibility_policy,
quiet,
follow_symlinks,
)?;
vec_buffer.rewind()?; vec_buffer.rewind()?;
io::copy(&mut vec_buffer, &mut writer)?; io::copy(&mut vec_buffer, &mut writer)?;
} }
@ -163,7 +175,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

@ -4,12 +4,13 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use backhand::BufReadSeek;
use fs_err as fs; 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, *},
@ -25,9 +26,6 @@ use crate::{
QuestionAction, QuestionPolicy, BUFFER_CAPACITY, QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
}; };
trait ReadSeek: Read + io::Seek {}
impl<T: Read + io::Seek> ReadSeek for T {}
pub struct DecompressOptions<'a> { pub struct DecompressOptions<'a> {
pub input_file_path: &'a Path, pub input_file_path: &'a Path,
pub formats: Vec<Extension>, pub formats: Vec<Extension>,
@ -51,29 +49,40 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
assert!(options.output_dir.exists()); assert!(options.output_dir.exists());
let input_is_stdin = is_path_stdin(options.input_file_path); let input_is_stdin = is_path_stdin(options.input_file_path);
// Zip archives are special, because they require io::Seek, so it requires it's logic separated // Zip and squashfs archives are special, because they require io::Seek, so it requires it's logic separated
// from decoder chaining. // from decoder chaining.
// //
// This is the only case where we can read and unpack it directly, without having to do // This is the only case where we can read and unpack it directly, without having to do
// in-memory decompression/copying first. // in-memory decompression/copying first.
// //
// Any other Zip decompression done can take up the whole RAM and freeze ouch. // Any other decompression done can take up the whole RAM and freeze ouch.
if let [Extension { if let [Extension {
compression_formats: [Zip], compression_formats: [archive_format @ (Zip | Squashfs)],
.. ..
}] = options.formats.as_slice() }] = options.formats.as_slice()
{ {
let is_zip = matches!(archive_format, Zip);
let mut vec = vec![]; let mut vec = vec![];
let reader: Box<dyn ReadSeek> = if input_is_stdin { let reader: Box<dyn BufReadSeek> = if input_is_stdin {
warn_user_about_loading_zip_in_memory(); warn_user_about_loading_in_memory(if is_zip { ".zip" } else { ".sqfs" });
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 {
Box::new(fs::File::open(options.input_file_path)?) let file = fs::File::open(options.input_file_path)?;
let file = BufReader::new(file);
Box::new(file)
}; };
let zip_archive = zip::ZipArchive::new(reader)?;
let files_unpacked = if let ControlFlow::Continue(files) = execute_decompression( let files_unpacked = if let ControlFlow::Continue(files) = execute_decompression(
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet), |output_dir| {
if is_zip {
let zip_archive = zip::ZipArchive::new(reader)?;
crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet)
} else {
let archive = backhand::FilesystemReader::from_reader(reader)?;
crate::archive::squashfs::unpack_archive(archive, output_dir, options.quiet)
}
},
options.output_dir, options.output_dir,
&options.output_file_path, &options.output_file_path,
options.question_policy, options.question_policy,
@ -132,7 +141,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 +183,15 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
return Ok(()); return Ok(());
} }
} }
Zip => { Zip | Squashfs => {
let is_zip = matches!(first_extension, 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(if is_zip { ".zip" } else { ".sqfs" });
if !user_wants_to_continue( if !user_wants_to_continue(
options.input_file_path, options.input_file_path,
options.question_policy, options.question_policy,
@ -192,11 +203,17 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
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 let ControlFlow::Continue(files) = execute_decompression( if let ControlFlow::Continue(files) = execute_decompression(
|output_dir| { move |output_dir| {
crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet) if is_zip {
let archive = zip::ZipArchive::new(reader)?;
crate::archive::zip::unpack_archive(archive, output_dir, options.password, options.quiet)
} else {
let archive = backhand::FilesystemReader::from_reader(reader)?;
crate::archive::squashfs::unpack_archive(archive, output_dir, options.quiet)
}
}, },
options.output_dir, options.output_dir,
&options.output_file_path, &options.output_file_path,
@ -252,7 +269,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},
@ -25,7 +25,7 @@ pub fn list_archive_contents(
) -> crate::Result<()> { ) -> crate::Result<()> {
let reader = fs::File::open(archive_path)?; let reader = fs::File::open(archive_path)?;
// Zip archives are special, because they require io::Seek, so it requires it's logic separated // Zip and squashfs archives are special, because they require io::Seek, so it requires it's logic separated
// from decoder chaining. // from decoder chaining.
// //
// This is the only case where we can read and unpack it directly, without having to do // This is the only case where we can read and unpack it directly, without having to do
@ -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

@ -25,16 +25,18 @@ pub const SUPPORTED_EXTENSIONS: &[&str] = &[
"rar", "rar",
"7z", "7z",
"br", "br",
// TODO(review): Which to use as "official" extension? squashfs or sqfs?
"sqfs",
]; ];
pub const SUPPORTED_ALIASES: &[&str] = &["tgz", "tbz", "tlz4", "txz", "tzlma", "tsz", "tzst"]; pub const SUPPORTED_ALIASES: &[&str] = &["tgz", "tbz", "tlz4", "txz", "tzlma", "tsz", "tzst", "squashfs"];
#[cfg(not(feature = "unrar"))] #[cfg(not(feature = "unrar"))]
pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, bz3, 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, sqfs";
#[cfg(feature = "unrar")] #[cfg(feature = "unrar")]
pub const PRETTY_SUPPORTED_EXTENSIONS: &str = "tar, zip, bz, bz2, bz3, 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, sqfs";
pub const PRETTY_SUPPORTED_ALIASES: &str = "tgz, tbz, tlz4, txz, tzlma, tsz, tzst"; pub const PRETTY_SUPPORTED_ALIASES: &str = "tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs";
/// A wrapper around `CompressionFormat` that allows combinations like `tgz` /// A wrapper around `CompressionFormat` that allows combinations like `tgz`
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -102,6 +104,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 +119,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 +154,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
} }

View File

@ -6,8 +6,8 @@ expression: "run_ouch(\"ouch decompress a\", dir)"
- Files with missing extensions: <TMP_DIR>/a - Files with missing extensions: <TMP_DIR>/a
- Decompression formats are detected automatically from file extension - Decompression formats are detected automatically from file extension
hint: Supported extensions are: tar, zip, bz, bz2, bz3, 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, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Alternatively, you can pass an extension to the '--format' flag: hint: Alternatively, you can pass an extension to the '--format' flag:
hint: ouch decompress <TMP_DIR>/a --format tar.gz hint: ouch decompress <TMP_DIR>/a --format tar.gz

View File

@ -7,5 +7,5 @@ expression: "run_ouch(\"ouch decompress a b.unknown\", dir)"
- Files with missing extensions: <TMP_DIR>/a - Files with missing extensions: <TMP_DIR>/a
- Decompression formats are detected automatically from file extension - Decompression formats are detected automatically from file extension
hint: Supported extensions are: tar, zip, bz, bz2, bz3, 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, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs

View File

@ -6,8 +6,8 @@ expression: "run_ouch(\"ouch decompress b.unknown\", dir)"
- Files with unsupported extensions: <TMP_DIR>/b.unknown - Files with unsupported extensions: <TMP_DIR>/b.unknown
- Decompression formats are detected automatically from file extension - Decompression formats are detected automatically from file extension
hint: Supported extensions are: tar, zip, bz, bz2, bz3, 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, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Alternatively, you can pass an extension to the '--format' flag: hint: Alternatively, you can pass an extension to the '--format' flag:
hint: ouch decompress <TMP_DIR>/b.unknown --format tar.gz hint: ouch decompress <TMP_DIR>/b.unknown --format tar.gz

View File

@ -6,8 +6,8 @@ expression: "run_ouch(\"ouch decompress a\", dir)"
- Files with missing extensions: <TMP_DIR>/a - Files with missing extensions: <TMP_DIR>/a
- Decompression formats are detected automatically from file extension - Decompression formats are detected automatically from file extension
hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Alternatively, you can pass an extension to the '--format' flag: hint: Alternatively, you can pass an extension to the '--format' flag:
hint: ouch decompress <TMP_DIR>/a --format tar.gz hint: ouch decompress <TMP_DIR>/a --format tar.gz

View File

@ -7,5 +7,5 @@ expression: "run_ouch(\"ouch decompress a b.unknown\", dir)"
- Files with missing extensions: <TMP_DIR>/a - Files with missing extensions: <TMP_DIR>/a
- Decompression formats are detected automatically from file extension - Decompression formats are detected automatically from file extension
hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs

View File

@ -6,8 +6,8 @@ expression: "run_ouch(\"ouch decompress b.unknown\", dir)"
- Files with unsupported extensions: <TMP_DIR>/b.unknown - Files with unsupported extensions: <TMP_DIR>/b.unknown
- Decompression formats are detected automatically from file extension - Decompression formats are detected automatically from file extension
hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Alternatively, you can pass an extension to the '--format' flag: hint: Alternatively, you can pass an extension to the '--format' flag:
hint: ouch decompress <TMP_DIR>/b.unknown --format tar.gz hint: ouch decompress <TMP_DIR>/b.unknown --format tar.gz

View File

@ -5,8 +5,8 @@ expression: "run_ouch(\"ouch compress input output --format tar.gz.unknown\", di
[ERROR] Failed to parse `--format tar.gz.unknown` [ERROR] Failed to parse `--format tar.gz.unknown`
- Unsupported extension 'unknown' - Unsupported extension 'unknown'
hint: Supported extensions are: tar, zip, bz, bz2, bz3, 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, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Examples: hint: Examples:
hint: --format tar hint: --format tar

View File

@ -5,8 +5,8 @@ expression: "run_ouch(\"ouch compress input output --format targz\", dir)"
[ERROR] Failed to parse `--format targz` [ERROR] Failed to parse `--format targz`
- Unsupported extension 'targz' - Unsupported extension 'targz'
hint: Supported extensions are: tar, zip, bz, bz2, bz3, 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, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Examples: hint: Examples:
hint: --format tar hint: --format tar

View File

@ -5,8 +5,8 @@ expression: "run_ouch(\"ouch compress input output --format .tar.$#!@.rest\", di
[ERROR] Failed to parse `--format .tar.$#!@.rest` [ERROR] Failed to parse `--format .tar.$#!@.rest`
- Unsupported extension '$#!@' - Unsupported extension '$#!@'
hint: Supported extensions are: tar, zip, bz, bz2, bz3, 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, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Examples: hint: Examples:
hint: --format tar hint: --format tar

View File

@ -5,8 +5,8 @@ expression: "run_ouch(\"ouch compress input output --format tar.gz.unknown\", di
[ERROR] Failed to parse `--format tar.gz.unknown` [ERROR] Failed to parse `--format tar.gz.unknown`
- Unsupported extension 'unknown' - Unsupported extension 'unknown'
hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Examples: hint: Examples:
hint: --format tar hint: --format tar

View File

@ -5,8 +5,8 @@ expression: "run_ouch(\"ouch compress input output --format targz\", dir)"
[ERROR] Failed to parse `--format targz` [ERROR] Failed to parse `--format targz`
- Unsupported extension 'targz' - Unsupported extension 'targz'
hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Examples: hint: Examples:
hint: --format tar hint: --format tar

View File

@ -5,8 +5,8 @@ expression: "run_ouch(\"ouch compress input output --format .tar.$#!@.rest\", di
[ERROR] Failed to parse `--format .tar.$#!@.rest` [ERROR] Failed to parse `--format .tar.$#!@.rest`
- Unsupported extension '$#!@' - Unsupported extension '$#!@'
hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z hint: Supported extensions are: tar, zip, bz, bz2, bz3, gz, lz4, xz, lzma, sz, zst, 7z, sqfs
hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst, squashfs
hint: hint:
hint: Examples: hint: Examples:
hint: --format tar hint: --format tar