Skip to content

Commit 4faf2cf

Browse files
committed
feat: add unsquashfs util to Squashfs
This is mostly a copy from `backhand-cli`'s `unsquashfs`, but with all the args/progress handling removed and more thorough error handling. Due to the dependency on `rayon`, the utility is currently gated behind the `util` feature of `backhand`. I tried also gating the `nix` dependency on the feature, but it was hard due to it showing up in `BackhandError`. Fixes: #354
1 parent 02ded3a commit 4faf2cf

File tree

6 files changed

+281
-4
lines changed

6 files changed

+281
-4
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ repository = "https://github.com/wcampbell0x2a/backhand"
1717
keywords = ["filesystem", "deku", "squashfs", "linux"]
1818
categories = ["filesystem", "parsing"]
1919

20+
[workspace.dependencies]
21+
nix = { version = "0.27.1", default-features = false, features = ["fs"] }
22+
rayon = "1.8.0"
23+
2024
# Release(dist) binaries are setup for maximum runtime speed, at the cost of CI time
2125
[profile.dist]
2226
inherits = "release"

backhand-cli/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ rust-version = "1.73.0"
1111
description = "Binaries for the reading, creating, and modification of SquashFS file systems"
1212

1313
[dependencies]
14-
nix = { version = "0.27.1", default-features = false, features = ["fs"] }
14+
nix.workspace = true
1515
clap = { version = "4.4.11", features = ["derive", "wrap_help"] }
1616
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] }
1717
libc = "0.2.150"
1818
clap_complete = "4.4.4"
1919
indicatif = "0.17.7"
2020
console = "0.15.7"
21-
rayon = "1.8.0"
21+
rayon.workspace = true
2222
backhand = { path = "../backhand", default-features = false }
2323
tracing = "0.1.40"
2424

@@ -41,4 +41,4 @@ zstd = ["backhand/zstd"]
4141

4242
[package.metadata.docs.rs]
4343
all-features = true
44-
rustdoc-args = ["--cfg", "docsrs"]
44+
rustdoc-args = ["--cfg", "docsrs"]

backhand/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ rust-lzo = { version = "0.6.2", optional = true }
2121
zstd = { version = "0.13.0", optional = true }
2222
rustc-hash = "1.1.0"
2323
document-features = { version = "0.2.7", optional = true }
24+
nix.workspace = true
25+
rayon = { workspace = true, optional = true }
2426

2527
[features]
2628
default = ["xz", "gzip", "zstd"]
@@ -34,6 +36,8 @@ gzip = ["dep:flate2"]
3436
lzo = ["dep:rust-lzo"]
3537
## Enables zstd compression inside library and binaries
3638
zstd = ["dep:zstd"]
39+
## Enables higher level helpers and utilities
40+
util = ["dep:rayon"]
3741

3842
[dev-dependencies]
3943
test-log = { version = "0.2.14", features = ["trace"] }

backhand/src/error.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ pub enum BackhandError {
5151

5252
#[error("file duplicated in squashfs image")]
5353
DuplicatedFileName,
54+
55+
#[error("invalid path filter for unsquashing, path doesn't exist: {0:?}")]
56+
InvalidPathFilter(std::path::PathBuf),
57+
58+
#[error("failed to unsquash file '{path:?}'")]
59+
UnsquashFile { source: std::io::Error, path: std::path::PathBuf },
60+
61+
#[error("failed to unsquash symlink '{from:?}' -> '{to:?}'")]
62+
UnsquashSymlink { source: std::io::Error, from: std::path::PathBuf, to: std::path::PathBuf },
63+
64+
#[error("failed to unsquash character device '{path:?}'")]
65+
UnsquashCharDev { source: nix::Error, path: std::path::PathBuf },
66+
67+
#[error("failed to unsquash block device '{path:?}'")]
68+
UnsquashBlockDev { source: nix::Error, path: std::path::PathBuf },
69+
70+
#[error("failed to set attributes for '{path:?}'")]
71+
SetAttributes { source: std::io::Error, path: std::path::PathBuf },
72+
73+
#[error("failed to set utimes for '{path:?}'")]
74+
SetUtimes { source: nix::Error, path: std::path::PathBuf },
5475
}
5576

5677
impl From<BackhandError> for io::Error {
@@ -61,6 +82,9 @@ impl From<BackhandError> for io::Error {
6182
Deku(e) => e.into(),
6283
StringUtf8(e) => Self::new(io::ErrorKind::InvalidData, e),
6384
StrUtf8(e) => Self::new(io::ErrorKind::InvalidData, e),
85+
UnsquashFile { source, .. }
86+
| UnsquashSymlink { source, .. }
87+
| SetAttributes { source, .. } => source,
6488
e @ UnsupportedCompression(_) => Self::new(io::ErrorKind::Unsupported, e),
6589
e @ FileNotFound => Self::new(io::ErrorKind::NotFound, e),
6690
e @ (Unreachable
@@ -70,7 +94,11 @@ impl From<BackhandError> for io::Error {
7094
| InvalidCompressionOption
7195
| InvalidFilePath
7296
| UndefineFileName
73-
| DuplicatedFileName) => Self::new(io::ErrorKind::InvalidData, e),
97+
| DuplicatedFileName
98+
| InvalidPathFilter(_)
99+
| UnsquashCharDev { .. }
100+
| UnsquashBlockDev { .. }
101+
| SetUtimes { .. }) => Self::new(io::ErrorKind::InvalidData, e),
74102
}
75103
}
76104
}

backhand/src/squashfs.rs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,4 +639,243 @@ impl<'b> Squashfs<'b> {
639639
};
640640
Ok(filesystem)
641641
}
642+
643+
/// Extract the Squashfs into `dest`
644+
#[cfg(feature = "util")]
645+
pub fn unsquashfs(
646+
self,
647+
dest: &std::path::Path,
648+
path_filter: Option<PathBuf>,
649+
force: bool,
650+
) -> Result<(), BackhandError> {
651+
use std::fs::{self, File};
652+
use std::io::{self};
653+
use std::os::unix::fs::lchown;
654+
use std::path::{Component, Path};
655+
656+
use nix::{
657+
libc::geteuid,
658+
sys::stat::{dev_t, mknod, mode_t, umask, utimensat, Mode, SFlag, UtimensatFlags},
659+
sys::time::TimeSpec,
660+
};
661+
use rayon::prelude::*;
662+
663+
// Quick hack to ensure we reset `umask` even when we return due to an error
664+
struct UmaskGuard {
665+
old: Mode,
666+
}
667+
impl UmaskGuard {
668+
fn new(mode: Mode) -> Self {
669+
let old = umask(mode);
670+
Self { old }
671+
}
672+
}
673+
impl Drop for UmaskGuard {
674+
fn drop(&mut self) {
675+
umask(self.old);
676+
}
677+
}
678+
679+
let root_process = unsafe { geteuid() == 0 };
680+
681+
// FIXME: Do we want to set `umask` here, or leave it up to the caller?
682+
let _umask_guard = root_process.then(|| UmaskGuard::new(Mode::from_bits(0).unwrap()));
683+
684+
let filesystem = self.into_filesystem_reader()?;
685+
686+
let path_filter = path_filter.unwrap_or(PathBuf::from("/"));
687+
688+
// if we can find a parent, then a filter must be applied and the exact parent dirs must be
689+
// found above it
690+
let mut files: Vec<&Node<SquashfsFileReader>> = vec![];
691+
if path_filter.parent().is_some() {
692+
let mut current = PathBuf::new();
693+
current.push("/");
694+
for part in path_filter.iter() {
695+
current.push(part);
696+
if let Some(exact) = filesystem.files().find(|&a| a.fullpath == current) {
697+
files.push(exact);
698+
} else {
699+
return Err(BackhandError::InvalidPathFilter(path_filter));
700+
}
701+
}
702+
// remove the final node, this is a file and will be caught in the following statement
703+
files.pop();
704+
}
705+
706+
// gather all files and dirs
707+
let nodes = files
708+
.into_iter()
709+
.chain(filesystem.files().filter(|a| a.fullpath.starts_with(&path_filter)))
710+
.collect::<Vec<_>>();
711+
712+
nodes
713+
.into_par_iter()
714+
.map(|node| {
715+
let path = &node.fullpath;
716+
let fullpath = path.strip_prefix(Component::RootDir).unwrap_or(path);
717+
718+
let filepath = Path::new(&dest).join(fullpath);
719+
// create required dirs, we will fix permissions later
720+
let _ = fs::create_dir_all(filepath.parent().unwrap());
721+
722+
match &node.inner {
723+
InnerNode::File(file) => {
724+
// alloc required space for file data readers
725+
let (mut buf_read, mut buf_decompress) = filesystem.alloc_read_buffers();
726+
727+
// check if file exists
728+
if !force && filepath.exists() {
729+
trace!(path=%filepath.display(), "file exists");
730+
return Ok(());
731+
}
732+
733+
// write to file
734+
let mut fd = File::create(&filepath)?;
735+
let file = filesystem.file(&file.basic);
736+
let mut reader = file.reader(&mut buf_read, &mut buf_decompress);
737+
738+
io::copy(&mut reader, &mut fd).map_err(|e| {
739+
BackhandError::UnsquashFile { source: e, path: filepath.clone() }
740+
})?;
741+
trace!(path=%filepath.display(), "unsquashed file");
742+
}
743+
InnerNode::Symlink(SquashfsSymlink { link }) => {
744+
// check if file exists
745+
if !force && filepath.exists() {
746+
trace!(path=%filepath.display(), "symlink exists");
747+
return Ok(());
748+
}
749+
// create symlink
750+
std::os::unix::fs::symlink(link, &filepath).map_err(|e| {
751+
BackhandError::UnsquashSymlink {
752+
source: e,
753+
from: link.to_path_buf(),
754+
to: filepath.clone(),
755+
}
756+
})?;
757+
// set attributes, but special to not follow the symlink
758+
// TODO: unify with set_attributes?
759+
if root_process {
760+
// TODO: Use (unix_chown) when not nightly: https://github.com/rust-lang/rust/issues/88989
761+
lchown(&filepath, Some(node.header.uid), Some(node.header.gid))
762+
.map_err(|e| BackhandError::SetAttributes {
763+
source: e,
764+
path: filepath.to_path_buf(),
765+
})?;
766+
}
767+
768+
// TODO Use (file_set_times) when not nightly: https://github.com/rust-lang/rust/issues/98245
769+
// Make sure this doesn't follow symlinks when changed to std library!
770+
let timespec = TimeSpec::new(node.header.mtime as _, 0);
771+
utimensat(
772+
None,
773+
&filepath,
774+
&timespec,
775+
&timespec,
776+
UtimensatFlags::NoFollowSymlink,
777+
)
778+
.map_err(|e| BackhandError::SetUtimes {
779+
source: e,
780+
path: filepath.clone(),
781+
})?;
782+
trace!(from=%link.display(), to=%filepath.display(), "unsquashed symlink");
783+
}
784+
InnerNode::Dir(SquashfsDir { .. }) => {
785+
// These permissions are corrected later (user default permissions for now)
786+
//
787+
// don't display error if this was already created, we might have already
788+
// created it in another thread to put down a file
789+
if std::fs::create_dir(&filepath).is_ok() {
790+
trace!(path=%filepath.display(), "unsquashed dir");
791+
}
792+
}
793+
InnerNode::CharacterDevice(SquashfsCharacterDevice { device_number }) => {
794+
mknod(
795+
&filepath,
796+
SFlag::S_IFCHR,
797+
Mode::from_bits(mode_t::from(node.header.permissions)).unwrap(),
798+
dev_t::try_from(*device_number).unwrap(),
799+
)
800+
.map_err(|e| BackhandError::UnsquashCharDev {
801+
source: e,
802+
path: filepath.clone(),
803+
})?;
804+
set_attributes(&filepath, &node.header, root_process, true)?;
805+
trace!(path=%filepath.display(), "unsquashed character device");
806+
}
807+
InnerNode::BlockDevice(SquashfsBlockDevice { device_number }) => {
808+
mknod(
809+
&filepath,
810+
SFlag::S_IFBLK,
811+
Mode::from_bits(mode_t::from(node.header.permissions)).unwrap(),
812+
dev_t::try_from(*device_number).unwrap(),
813+
)
814+
.map_err(|e| BackhandError::UnsquashBlockDev {
815+
source: e,
816+
path: filepath.clone(),
817+
})?;
818+
set_attributes(&filepath, &node.header, root_process, true)?;
819+
trace!(path=%filepath.display(), "unsquashed block device");
820+
}
821+
}
822+
Ok(())
823+
})
824+
.collect::<Result<(), BackhandError>>()?;
825+
826+
// fixup dir permissions
827+
for node in filesystem.files().filter(|a| a.fullpath.starts_with(&path_filter)) {
828+
if let InnerNode::Dir(SquashfsDir { .. }) = &node.inner {
829+
let path = &node.fullpath;
830+
let path = path.strip_prefix(Component::RootDir).unwrap_or(path);
831+
let path = Path::new(&dest).join(path);
832+
set_attributes(&path, &node.header, root_process, false)?;
833+
}
834+
}
835+
836+
Ok(())
837+
}
838+
}
839+
840+
#[cfg(feature = "util")]
841+
fn set_attributes(
842+
path: &std::path::Path,
843+
header: &NodeHeader,
844+
root_process: bool,
845+
is_file: bool,
846+
) -> Result<(), BackhandError> {
847+
// TODO Use (file_set_times) when not nightly: https://github.com/rust-lang/rust/issues/98245
848+
use nix::{sys::stat::utimes, sys::time::TimeVal};
849+
use std::os::unix::fs::lchown;
850+
851+
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
852+
let timeval = TimeVal::new(header.mtime as _, 0);
853+
utimes(path, &timeval, &timeval)
854+
.map_err(|e| BackhandError::SetUtimes { source: e, path: path.to_path_buf() })?;
855+
856+
let mut mode = u32::from(header.permissions);
857+
858+
// Only chown when root
859+
if root_process {
860+
// TODO: Use (unix_chown) when not nightly: https://github.com/rust-lang/rust/issues/88989
861+
lchown(path, Some(header.uid), Some(header.gid))
862+
.map_err(|e| BackhandError::SetAttributes { source: e, path: path.to_path_buf() })?;
863+
} else if is_file {
864+
// bitwise-not if not rooted (disable write permissions for user/group). Following
865+
// squashfs-tools/unsquashfs behavior
866+
mode &= !0o022;
867+
}
868+
869+
// set permissions
870+
//
871+
// NOTE: In squashfs-tools/unsquashfs they remove the write bits for user and group?
872+
// I don't know if there is a reason for that but I keep the permissions the same if possible
873+
match std::fs::set_permissions(path, Permissions::from_mode(mode)) {
874+
Ok(_) => return Ok(()),
875+
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {}
876+
Err(e) => return Err(BackhandError::SetAttributes { source: e, path: path.to_path_buf() }),
877+
};
878+
// retry without sticky bit
879+
std::fs::set_permissions(path, Permissions::from_mode(mode & !1000))
880+
.map_err(|e| BackhandError::SetAttributes { source: e, path: path.to_path_buf() })
642881
}

0 commit comments

Comments
 (0)