feat: add decompressing support for squashfs

This commit is contained in:
oxalica 2025-07-01 02:52:18 -04:00
parent bcdff0f46b
commit 87cf8529f2
2 changed files with 144 additions and 18 deletions

View File

@ -1,8 +1,19 @@
use std::path::Path; use std::{
fs,
io::{self, BufWriter, Write},
path::Path,
};
use backhand::{FilesystemReader, InnerNode}; use backhand::{FilesystemReader, InnerNode, SquashfsFileReader};
use filetime_creation::{set_file_handle_times, set_file_mtime, FileTime};
use crate::list::FileInArchive; use crate::{
list::FileInArchive,
utils::{
logger::{info, warning},
Bytes,
},
};
pub fn list_archive<'a>(archive: FilesystemReader<'a>) -> impl Iterator<Item = crate::Result<FileInArchive>> + 'a { pub fn list_archive<'a>(archive: FilesystemReader<'a>) -> impl Iterator<Item = crate::Result<FileInArchive>> + 'a {
archive.root.nodes.into_iter().filter_map(move |f| { archive.root.nodes.into_iter().filter_map(move |f| {
@ -21,3 +32,102 @@ pub fn list_archive<'a>(archive: FilesystemReader<'a>) -> impl Iterator<Item = c
})) }))
}) })
} }
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)
}

View File

@ -4,6 +4,7 @@ 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"))]
@ -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>,
@ -59,21 +57,32 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
// //
// Any other Zip decompression done can take up the whole RAM and freeze ouch. // Any other Zip 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_in_memory(".zip"); 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,
@ -174,14 +183,15 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
return Ok(()); return Ok(());
} }
} }
Squashfs => todo!(), Zip | Squashfs => {
Zip => { 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_in_memory(".zip"); 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,
@ -193,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,