Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add unsquashfs util to Squashfs #389

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ repository = "https://github.com/wcampbell0x2a/backhand"
keywords = ["filesystem", "deku", "squashfs", "linux"]
categories = ["filesystem", "parsing"]

[workspace.dependencies]
nix = { version = "0.27.1", default-features = false, features = ["fs"] }
rayon = "1.8.0"

# Release(dist) binaries are setup for maximum runtime speed, at the cost of CI time
[profile.dist]
inherits = "release"
Expand Down
4 changes: 2 additions & 2 deletions backhand-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ rust-version = "1.73.0"
description = "Binaries for the reading, creating, and modification of SquashFS file systems"

[dependencies]
nix = { version = "0.27.1", default-features = false, features = ["fs"] }
nix.workspace = true
clap = { version = "4.4.11", features = ["derive", "wrap_help"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] }
libc = "0.2.150"
clap_complete = "4.4.4"
indicatif = "0.17.7"
console = "0.15.7"
rayon = "1.8.0"
rayon.workspace = true
backhand = { path = "../backhand", default-features = false }
tracing = "0.1.40"
color-print = "0.3.5"
Expand Down
6 changes: 5 additions & 1 deletion backhand/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ rust-lzo = { version = "0.6.2", optional = true }
zstd = { version = "0.13.0", optional = true }
rustc-hash = "1.1.0"
document-features = { version = "0.2.7", optional = true }
nix = { workspace = true, optional = true }
rayon = { workspace = true, optional = true }

[features]
default = ["xz", "gzip", "zstd"]
Expand All @@ -34,6 +36,8 @@ gzip = ["dep:flate2"]
lzo = ["dep:rust-lzo"]
## Enables zstd compression inside library and binaries
zstd = ["dep:zstd"]
## Enables higher level helpers and utilities
util = ["dep:rayon", "dep:nix"]

[dev-dependencies]
test-log = { version = "0.2.14", features = ["trace"] }
Expand All @@ -53,4 +57,4 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[lib]
bench = false
bench = false
37 changes: 37 additions & 0 deletions backhand/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ pub enum BackhandError {

#[error("file duplicated in squashfs image")]
DuplicatedFileName,

#[cfg(feature = "util")]
#[error("invalid path filter for unsquashing, path doesn't exist: {0:?}")]

This comment was marked as resolved.

InvalidPathFilter(std::path::PathBuf),

#[cfg(feature = "util")]
#[error("failed to unsquash file '{path:?}'")]
UnsquashFile { source: std::io::Error, path: std::path::PathBuf },

#[cfg(feature = "util")]
#[error("failed to unsquash symlink '{from:?}' -> '{to:?}'")]
UnsquashSymlink { source: std::io::Error, from: std::path::PathBuf, to: std::path::PathBuf },

#[cfg(feature = "util")]
#[error("failed to unsquash character device '{path:?}'")]
UnsquashCharDev { source: nix::Error, path: std::path::PathBuf },

#[cfg(feature = "util")]
#[error("failed to unsquash block device '{path:?}'")]
UnsquashBlockDev { source: nix::Error, path: std::path::PathBuf },

#[cfg(feature = "util")]
#[error("failed to set attributes for '{path:?}'")]
SetAttributes { source: std::io::Error, path: std::path::PathBuf },

#[cfg(feature = "util")]
#[error("failed to set utimes for '{path:?}'")]
SetUtimes { source: nix::Error, path: std::path::PathBuf },
}

impl From<BackhandError> for io::Error {
Expand All @@ -61,6 +89,10 @@ impl From<BackhandError> for io::Error {
Deku(e) => e.into(),
StringUtf8(e) => Self::new(io::ErrorKind::InvalidData, e),
StrUtf8(e) => Self::new(io::ErrorKind::InvalidData, e),
#[cfg(feature = "util")]
UnsquashFile { source, .. }
| UnsquashSymlink { source, .. }
| SetAttributes { source, .. } => source,
e @ UnsupportedCompression(_) => Self::new(io::ErrorKind::Unsupported, e),
e @ FileNotFound => Self::new(io::ErrorKind::NotFound, e),
e @ (Unreachable
Expand All @@ -71,6 +103,11 @@ impl From<BackhandError> for io::Error {
| InvalidFilePath
| UndefineFileName
| DuplicatedFileName) => Self::new(io::ErrorKind::InvalidData, e),
#[cfg(feature = "util")]
e @ (InvalidPathFilter(_)
| UnsquashCharDev { .. }
| UnsquashBlockDev { .. }
| SetUtimes { .. }) => Self::new(io::ErrorKind::InvalidData, e),
}
}
}
241 changes: 241 additions & 0 deletions backhand/src/squashfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,4 +639,245 @@ impl<'b> Squashfs<'b> {
};
Ok(filesystem)
}

/// Extract the Squashfs into `dest`
#[cfg(unix)]
#[cfg(feature = "util")]
pub fn unsquashfs(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should gated behind #[cfg(unix)] because it requires std::os::unix::fs::lchown.

self,
dest: &std::path::Path,
path_filter: Option<PathBuf>,
force: bool,
) -> Result<(), BackhandError> {
use std::fs::{self, File};
use std::io::{self};
use std::os::unix::fs::lchown;
use std::path::{Component, Path};

use nix::{
libc::geteuid,
sys::stat::{dev_t, mknod, mode_t, umask, utimensat, Mode, SFlag, UtimensatFlags},
sys::time::TimeSpec,
};
use rayon::prelude::*;

// Quick hack to ensure we reset `umask` even when we return due to an error
struct UmaskGuard {
old: Mode,
}
impl UmaskGuard {
fn new(mode: Mode) -> Self {
let old = umask(mode);
Self { old }
}
}
impl Drop for UmaskGuard {
fn drop(&mut self) {
umask(self.old);
}
}

let root_process = unsafe { geteuid() == 0 };

// FIXME: Do we want to set `umask` here, or leave it up to the caller?
let _umask_guard = root_process.then(|| UmaskGuard::new(Mode::from_bits(0).unwrap()));

let filesystem = self.into_filesystem_reader()?;

let path_filter = path_filter.unwrap_or(PathBuf::from("/"));

// if we can find a parent, then a filter must be applied and the exact parent dirs must be
// found above it
let mut files: Vec<&Node<SquashfsFileReader>> = vec![];
if path_filter.parent().is_some() {
let mut current = PathBuf::new();
current.push("/");
for part in path_filter.iter() {
current.push(part);
if let Some(exact) = filesystem.files().find(|&a| a.fullpath == current) {
files.push(exact);
} else {
return Err(BackhandError::InvalidPathFilter(path_filter));
}
}
// remove the final node, this is a file and will be caught in the following statement
files.pop();
}

// gather all files and dirs
let nodes = files
.into_iter()
.chain(filesystem.files().filter(|a| a.fullpath.starts_with(&path_filter)))
.collect::<Vec<_>>();

nodes
.into_par_iter()
.map(|node| {
let path = &node.fullpath;
let fullpath = path.strip_prefix(Component::RootDir).unwrap_or(path);

let filepath = Path::new(&dest).join(fullpath);
// create required dirs, we will fix permissions later
let _ = fs::create_dir_all(filepath.parent().unwrap());

match &node.inner {
InnerNode::File(file) => {
// alloc required space for file data readers
let (mut buf_read, mut buf_decompress) = filesystem.alloc_read_buffers();

// check if file exists
if !force && filepath.exists() {
trace!(path=%filepath.display(), "file exists");
return Ok(());
}

// write to file
let mut fd = File::create(&filepath)?;
let file = filesystem.file(&file.basic);
let mut reader = file.reader(&mut buf_read, &mut buf_decompress);

io::copy(&mut reader, &mut fd).map_err(|e| {
BackhandError::UnsquashFile { source: e, path: filepath.clone() }
})?;
trace!(path=%filepath.display(), "unsquashed file");
}
InnerNode::Symlink(SquashfsSymlink { link }) => {
// check if file exists
if !force && filepath.exists() {
trace!(path=%filepath.display(), "symlink exists");
return Ok(());
}
// create symlink
std::os::unix::fs::symlink(link, &filepath).map_err(|e| {
BackhandError::UnsquashSymlink {
source: e,
from: link.to_path_buf(),
to: filepath.clone(),
}
})?;
// set attributes, but special to not follow the symlink
// TODO: unify with set_attributes?
if root_process {
// TODO: Use (unix_chown) when not nightly: https://github.com/rust-lang/rust/issues/88989
lchown(&filepath, Some(node.header.uid), Some(node.header.gid))
.map_err(|e| BackhandError::SetAttributes {
source: e,
path: filepath.to_path_buf(),
})?;
}

// TODO Use (file_set_times) when not nightly: https://github.com/rust-lang/rust/issues/98245
// Make sure this doesn't follow symlinks when changed to std library!
let timespec = TimeSpec::new(node.header.mtime as _, 0);
utimensat(
None,
&filepath,
&timespec,
&timespec,
UtimensatFlags::NoFollowSymlink,
)
.map_err(|e| BackhandError::SetUtimes {
source: e,
path: filepath.clone(),
})?;
trace!(from=%link.display(), to=%filepath.display(), "unsquashed symlink");
}
InnerNode::Dir(SquashfsDir { .. }) => {
// These permissions are corrected later (user default permissions for now)
//
// don't display error if this was already created, we might have already
// created it in another thread to put down a file
if std::fs::create_dir(&filepath).is_ok() {
trace!(path=%filepath.display(), "unsquashed dir");
}
}
InnerNode::CharacterDevice(SquashfsCharacterDevice { device_number }) => {
mknod(
&filepath,
SFlag::S_IFCHR,
Mode::from_bits(mode_t::from(node.header.permissions)).unwrap(),
dev_t::try_from(*device_number).unwrap(),
)
.map_err(|e| BackhandError::UnsquashCharDev {
source: e,
path: filepath.clone(),
})?;
set_attributes(&filepath, &node.header, root_process, true)?;
trace!(path=%filepath.display(), "unsquashed character device");
}
InnerNode::BlockDevice(SquashfsBlockDevice { device_number }) => {
mknod(
&filepath,
SFlag::S_IFBLK,
Mode::from_bits(mode_t::from(node.header.permissions)).unwrap(),
dev_t::try_from(*device_number).unwrap(),
)
.map_err(|e| BackhandError::UnsquashBlockDev {
source: e,
path: filepath.clone(),
})?;
set_attributes(&filepath, &node.header, root_process, true)?;
trace!(path=%filepath.display(), "unsquashed block device");
}
}
Ok(())
})
.collect::<Result<(), BackhandError>>()?;

// fixup dir permissions
for node in filesystem.files().filter(|a| a.fullpath.starts_with(&path_filter)) {
if let InnerNode::Dir(SquashfsDir { .. }) = &node.inner {
let path = &node.fullpath;
let path = path.strip_prefix(Component::RootDir).unwrap_or(path);
let path = Path::new(&dest).join(path);
set_attributes(&path, &node.header, root_process, false)?;
}
}

Ok(())
}
}

#[cfg(unix)]
#[cfg(feature = "util")]
fn set_attributes(
path: &std::path::Path,
header: &NodeHeader,
root_process: bool,
is_file: bool,
) -> Result<(), BackhandError> {
// TODO Use (file_set_times) when not nightly: https://github.com/rust-lang/rust/issues/98245
use nix::{sys::stat::utimes, sys::time::TimeVal};
use std::os::unix::fs::lchown;

use std::{fs::Permissions, os::unix::fs::PermissionsExt};
let timeval = TimeVal::new(header.mtime as _, 0);
utimes(path, &timeval, &timeval)
.map_err(|e| BackhandError::SetUtimes { source: e, path: path.to_path_buf() })?;

let mut mode = u32::from(header.permissions);

// Only chown when root
if root_process {
// TODO: Use (unix_chown) when not nightly: https://github.com/rust-lang/rust/issues/88989
lchown(path, Some(header.uid), Some(header.gid))
.map_err(|e| BackhandError::SetAttributes { source: e, path: path.to_path_buf() })?;
} else if is_file {
// bitwise-not if not rooted (disable write permissions for user/group). Following
// squashfs-tools/unsquashfs behavior
mode &= !0o022;
}

// set permissions
//
// NOTE: In squashfs-tools/unsquashfs they remove the write bits for user and group?
// I don't know if there is a reason for that but I keep the permissions the same if possible
match std::fs::set_permissions(path, Permissions::from_mode(mode)) {
Ok(_) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {}
Err(e) => return Err(BackhandError::SetAttributes { source: e, path: path.to_path_buf() }),
};
// retry without sticky bit
std::fs::set_permissions(path, Permissions::from_mode(mode & !1000))
.map_err(|e| BackhandError::SetAttributes { source: e, path: path.to_path_buf() })
}
Loading