use std::{
collections::HashSet,
fs::{self, File, Permissions},
io::{BufReader, BufWriter, Read, Seek},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};
use backhand::{kind::Kind, FilesystemReader, InnerNode, NodeHeader, SquashfsFileReader};
use error::SquishyError;
#[cfg(feature = "rayon")]
use rayon::iter::{IntoParallelIterator, ParallelIterator};
#[cfg(feature = "appimage")]
pub mod appimage;
#[cfg(feature = "dwarfs")]
pub mod dwarfs;
pub mod error;
pub type Result<T> = std::result::Result<T, SquishyError>;
pub struct SquashFS<'a> {
reader: FilesystemReader<'a>,
}
#[derive(Debug)]
pub struct SquashFSEntry<'a> {
pub header: NodeHeader,
pub path: PathBuf,
pub size: u32,
pub kind: EntryKind<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntryKind<'a> {
File(&'a SquashfsFileReader),
Directory,
Symlink(PathBuf),
Unknown,
}
impl<'a> SquashFS<'a> {
pub fn new<R>(mut reader: BufReader<R>, offset: Option<u64>) -> Result<Self>
where
R: Read + Seek + Send + 'a,
{
let offset = offset.unwrap_or(
Self::find_squashfs_offset(&mut reader).map_err(|_| SquishyError::NoSquashFsFound)?,
);
let reader = FilesystemReader::from_reader_with_offset(reader, offset)
.map_err(|e| SquishyError::InvalidSquashFS(e.to_string()))?;
Ok(Self { reader })
}
pub fn from_path<P: AsRef<Path>>(path: &'a P) -> Result<Self> {
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
SquashFS::new(reader, None)
}
pub fn from_path_with_offset<P: AsRef<Path>>(path: &'a P, offset: u64) -> Result<Self> {
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
SquashFS::new(reader, Some(offset))
}
fn find_squashfs_offset<R>(file: &mut BufReader<R>) -> Result<u64>
where
R: Read + Seek,
{
let mut magic = [0_u8; 4];
let kind = Kind::from_target("le_v4_0").unwrap();
while file.read_exact(&mut magic).is_ok() {
if magic == kind.magic() {
let found = file.stream_position()? - magic.len() as u64;
file.rewind()?;
return Ok(found);
}
}
Err(SquishyError::NoSquashFsFound)
}
pub fn entries(&self) -> impl Iterator<Item = SquashFSEntry<'_>> + use<'_, 'a> {
self.reader.files().map(|node| {
let size = match &node.inner {
InnerNode::File(file) => file.file_len() as u32,
_ => 0,
};
let kind = match &node.inner {
InnerNode::File(file) => EntryKind::File(file),
InnerNode::Dir(_) => EntryKind::Directory,
InnerNode::Symlink(symlink) => EntryKind::Symlink(
PathBuf::from(format!("/{}", symlink.link.display())).clone(),
),
_ => EntryKind::Unknown,
};
SquashFSEntry {
header: node.header,
path: node.fullpath.clone(),
size,
kind,
}
})
}
#[cfg(feature = "rayon")]
pub fn par_entries(&self) -> impl ParallelIterator<Item = SquashFSEntry<'_>> + use<'_, 'a> {
self.reader
.files()
.map(|node| {
let size = match &node.inner {
InnerNode::File(file) => file.file_len() as u32,
_ => 0,
};
let kind = match &node.inner {
InnerNode::File(file) => EntryKind::File(file),
InnerNode::Dir(_) => EntryKind::Directory,
InnerNode::Symlink(symlink) => EntryKind::Symlink(
PathBuf::from(format!("/{}", symlink.link.display())).clone(),
),
_ => EntryKind::Unknown,
};
SquashFSEntry {
header: node.header,
path: node.fullpath.clone(),
size,
kind,
}
})
.collect::<Vec<SquashFSEntry>>()
.into_par_iter()
}
pub fn find_entries<F>(
&self,
predicate: F,
) -> impl Iterator<Item = SquashFSEntry<'_>> + use<'_, 'a, F>
where
F: Fn(&Path) -> bool + 'a,
{
self.entries().filter(move |entry| predicate(&entry.path))
}
pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
let path = path.as_ref();
for node in self.reader.files() {
if node.fullpath == path {
if let InnerNode::File(file) = &node.inner {
let mut reader = self.reader.file(file).reader().bytes();
let mut contents = Vec::new();
while let Some(Ok(byte)) = reader.next() {
contents.push(byte);
}
return Ok(contents);
}
}
}
Err(SquishyError::FileNotFound(path.to_path_buf()))
}
pub fn write_file<P: AsRef<Path>>(&self, file: &SquashfsFileReader, dest: P) -> Result<()> {
let output_file = File::create(dest)?;
let mut writer = BufWriter::with_capacity(file.file_len(), &output_file);
let file = self.reader.file(file);
let mut reader = file.reader();
std::io::copy(&mut reader, &mut writer)?;
Ok(())
}
pub fn write_file_with_permissions<P: AsRef<Path>>(
&self,
file: &SquashfsFileReader,
dest: P,
header: NodeHeader,
) -> Result<()> {
let output_file = File::create(&dest)?;
let mode = u32::from(header.permissions);
fs::set_permissions(dest, Permissions::from_mode(mode))?;
let mut writer = BufWriter::with_capacity(file.file_len(), &output_file);
let file = self.reader.file(file);
let mut reader = file.reader();
std::io::copy(&mut reader, &mut writer)?;
Ok(())
}
pub fn resolve_symlink(&self, entry: &SquashFSEntry) -> Result<Option<SquashFSEntry<'_>>> {
match &entry.kind {
EntryKind::Symlink(target) => {
let mut visited = HashSet::new();
visited.insert(entry.path.clone());
self.follow_symlink(target, &mut visited)
}
_ => Ok(None),
}
}
fn follow_symlink(
&self,
target: &Path,
visited: &mut HashSet<PathBuf>,
) -> Result<Option<SquashFSEntry<'_>>> {
if !visited.insert(target.to_path_buf()) {
return Err(SquishyError::SymlinkError("Cyclic symlink detected".into()));
}
let target_path = target.to_path_buf();
if let Some(target_entry) = self.find_entries(move |p| p == target_path).next() {
match &target_entry.kind {
EntryKind::Symlink(next_target) => self.follow_symlink(next_target, visited),
_ => Ok(Some(target_entry)),
}
} else {
Ok(None)
}
}
}