use std::io::{self, Read, Seek, SeekFrom};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::FileReadHandle;
use crate::fs::squashfs::Compression;
use crate::fs::squashfs::fragment::{self, FragmentEntry};
use crate::fs::squashfs::inode::FileInode;
use crate::fs::squashfs::metablock::compression_to_algo;
pub struct FileReader<'a> {
dev: &'a mut dyn BlockDevice,
compression: Compression,
fragment_table_start: u64,
fragment_count: u32,
next_disk_offset: u64,
block_sizes: Vec<u32>,
next_block_idx: usize,
full_block_size: u32,
remaining: u64,
buf: Vec<u8>,
buf_pos: usize,
fragment_index: u32,
fragment_offset: u32,
fragment_consumed: bool,
}
impl<'a> FileReader<'a> {
pub fn new(
dev: &'a mut dyn BlockDevice,
inode: &FileInode,
compression: Compression,
fragment_table_start: u64,
fragment_count: u32,
full_block_size: u32,
) -> Self {
Self {
dev,
compression,
fragment_table_start,
fragment_count,
next_disk_offset: inode.blocks_start,
block_sizes: inode.block_sizes.clone(),
next_block_idx: 0,
full_block_size,
remaining: inode.file_size,
buf: Vec::new(),
buf_pos: 0,
fragment_index: inode.fragment_index,
fragment_offset: inode.fragment_offset,
fragment_consumed: false,
}
}
fn has_fragment(&self) -> bool {
self.fragment_index != 0xFFFF_FFFF
}
fn refill(&mut self) -> Result<bool> {
if self.remaining == 0 {
return Ok(false);
}
if self.next_block_idx < self.block_sizes.len() {
let size_word = self.block_sizes[self.next_block_idx];
let uncompressed = size_word & 0x0100_0000 != 0;
let on_disk = size_word & 0x00FF_FFFF;
self.buf_pos = 0;
if on_disk == 0 {
let take = (self.full_block_size as u64).min(self.remaining) as usize;
self.buf = vec![0u8; take];
} else {
let mut payload = vec![0u8; on_disk as usize];
self.dev.read_at(self.next_disk_offset, &mut payload)?;
let decoded = if uncompressed {
payload
} else {
let expected = self.remaining.min(self.full_block_size as u64) as usize;
let algo = compression_to_algo(self.compression).ok_or_else(|| {
crate::Error::InvalidImage(format!(
"squashfs: unknown compressor id {}",
compression_label(self.compression)
))
})?;
crate::compression::decompress(algo, &payload, expected)?
};
let take = (decoded.len() as u64).min(self.remaining) as usize;
let mut trimmed = decoded;
trimmed.truncate(take);
self.buf = trimmed;
}
self.next_disk_offset = self.next_disk_offset.saturating_add(on_disk as u64);
self.next_block_idx += 1;
return Ok(true);
}
if self.has_fragment() && !self.fragment_consumed {
self.fragment_consumed = true;
let entry: FragmentEntry = fragment::read_fragment(
self.dev,
self.fragment_table_start,
self.fragment_count,
self.compression,
self.fragment_index,
)?;
let frag_size = entry.on_disk_size() as usize;
let mut frag_buf = vec![0u8; frag_size];
self.dev.read_at(entry.start, &mut frag_buf)?;
let frag_buf = if entry.is_uncompressed() {
frag_buf
} else {
let algo = compression_to_algo(self.compression).ok_or_else(|| {
crate::Error::InvalidImage(format!(
"squashfs: unknown compressor id {}",
compression_label(self.compression)
))
})?;
crate::compression::decompress(algo, &frag_buf, self.full_block_size as usize)?
};
let off = self.fragment_offset as usize;
let take = self.remaining as usize;
if off.checked_add(take).is_none_or(|end| end > frag_buf.len()) {
return Err(crate::Error::InvalidImage(format!(
"squashfs: fragment tail [{off}..{}] exceeds fragment block size {}",
off + take,
frag_buf.len()
)));
}
self.buf = frag_buf[off..off + take].to_vec();
self.buf_pos = 0;
return Ok(true);
}
Ok(false)
}
}
impl<'a> Read for FileReader<'a> {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if out.is_empty() {
return Ok(0);
}
if self.buf_pos >= self.buf.len() {
match self.refill() {
Ok(true) => {}
Ok(false) => return Ok(0),
Err(e) => return Err(io::Error::other(format!("{e}"))),
}
}
let avail = self.buf.len() - self.buf_pos;
let n = avail.min(out.len());
out[..n].copy_from_slice(&self.buf[self.buf_pos..self.buf_pos + n]);
self.buf_pos += n;
self.remaining = self.remaining.saturating_sub(n as u64);
Ok(n)
}
}
fn compression_label(c: Compression) -> &'static str {
match c {
Compression::Gzip => "gzip",
Compression::Lzma => "lzma",
Compression::Lzo => "lzo",
Compression::Xz => "xz",
Compression::Lz4 => "lz4",
Compression::Zstd => "zstd",
Compression::Unknown(_) => "unknown",
}
}
pub struct SquashfsFileReadHandle<'a> {
dev: &'a mut dyn BlockDevice,
compression: Compression,
fragment_table_start: u64,
fragment_count: u32,
blocks_start: u64,
block_sizes: Vec<u32>,
block_disk_offsets: Vec<u64>,
full_block_size: u32,
len: u64,
pos: u64,
cached_block_idx: usize,
cached_buf: Vec<u8>,
fragment_index: u32,
fragment_offset: u32,
cached_fragment_tail: Vec<u8>,
cached_fragment_loaded: bool,
}
impl<'a> SquashfsFileReadHandle<'a> {
pub fn new(
dev: &'a mut dyn BlockDevice,
inode: &FileInode,
compression: Compression,
fragment_table_start: u64,
fragment_count: u32,
full_block_size: u32,
) -> Self {
let mut block_disk_offsets = Vec::with_capacity(inode.block_sizes.len() + 1);
block_disk_offsets.push(0u64);
let mut acc = 0u64;
for &sz in &inode.block_sizes {
let on_disk = sz & 0x00FF_FFFF;
acc = acc.saturating_add(u64::from(on_disk));
block_disk_offsets.push(acc);
}
Self {
dev,
compression,
fragment_table_start,
fragment_count,
blocks_start: inode.blocks_start,
block_sizes: inode.block_sizes.clone(),
block_disk_offsets,
full_block_size,
len: inode.file_size,
pos: 0,
cached_block_idx: usize::MAX,
cached_buf: Vec::new(),
fragment_index: inode.fragment_index,
fragment_offset: inode.fragment_offset,
cached_fragment_tail: Vec::new(),
cached_fragment_loaded: false,
}
}
fn has_fragment(&self) -> bool {
self.fragment_index != 0xFFFF_FFFF
}
fn fragment_start_pos(&self) -> u64 {
u64::from(self.full_block_size) * self.block_sizes.len() as u64
}
fn ensure_block_cached(&mut self, idx: usize) -> Result<()> {
if self.cached_block_idx == idx {
return Ok(());
}
let size_word = self.block_sizes[idx];
let uncompressed = size_word & 0x0100_0000 != 0;
let on_disk = size_word & 0x00FF_FFFF;
let disk_offset = self.blocks_start + self.block_disk_offsets[idx];
let logical_start = u64::from(self.full_block_size) * idx as u64;
let logical_end = (logical_start + u64::from(self.full_block_size)).min(self.len);
let block_logical_len = (logical_end - logical_start) as usize;
if on_disk == 0 {
self.cached_buf = vec![0u8; block_logical_len];
} else {
let mut payload = vec![0u8; on_disk as usize];
self.dev.read_at(disk_offset, &mut payload)?;
let decoded = if uncompressed {
payload
} else {
let algo = compression_to_algo(self.compression).ok_or_else(|| {
crate::Error::InvalidImage(format!(
"squashfs: unknown compressor id {}",
compression_label(self.compression)
))
})?;
crate::compression::decompress(algo, &payload, block_logical_len)?
};
let mut trimmed = decoded;
trimmed.truncate(block_logical_len);
self.cached_buf = trimmed;
}
self.cached_block_idx = idx;
Ok(())
}
fn ensure_fragment_cached(&mut self) -> Result<()> {
if self.cached_fragment_loaded {
return Ok(());
}
let frag_start_pos = self.fragment_start_pos();
let tail_len = (self.len - frag_start_pos) as usize;
let entry: FragmentEntry = fragment::read_fragment(
self.dev,
self.fragment_table_start,
self.fragment_count,
self.compression,
self.fragment_index,
)?;
let mut frag_buf = vec![0u8; entry.on_disk_size() as usize];
self.dev.read_at(entry.start, &mut frag_buf)?;
let frag_buf = if entry.is_uncompressed() {
frag_buf
} else {
let algo = compression_to_algo(self.compression).ok_or_else(|| {
crate::Error::InvalidImage(format!(
"squashfs: unknown compressor id {}",
compression_label(self.compression)
))
})?;
crate::compression::decompress(algo, &frag_buf, self.full_block_size as usize)?
};
let off = self.fragment_offset as usize;
if off
.checked_add(tail_len)
.is_none_or(|end| end > frag_buf.len())
{
return Err(crate::Error::InvalidImage(format!(
"squashfs: fragment tail [{off}..{}] exceeds fragment block size {}",
off + tail_len,
frag_buf.len()
)));
}
self.cached_fragment_tail = frag_buf[off..off + tail_len].to_vec();
self.cached_fragment_loaded = true;
Ok(())
}
}
impl<'a> Read for SquashfsFileReadHandle<'a> {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if out.is_empty() || self.pos >= self.len {
return Ok(0);
}
let frag_start = self.fragment_start_pos();
if self.pos < frag_start || !self.has_fragment() {
let block_idx = (self.pos / u64::from(self.full_block_size)) as usize;
if block_idx >= self.block_sizes.len() {
return Ok(0);
}
self.ensure_block_cached(block_idx)
.map_err(|e| io::Error::other(format!("{e}")))?;
let block_logical_start = u64::from(self.full_block_size) * block_idx as u64;
let off_in_block = (self.pos - block_logical_start) as usize;
let avail = self.cached_buf.len() - off_in_block;
let n = avail.min(out.len());
out[..n].copy_from_slice(&self.cached_buf[off_in_block..off_in_block + n]);
self.pos += n as u64;
Ok(n)
} else {
self.ensure_fragment_cached()
.map_err(|e| io::Error::other(format!("{e}")))?;
let off_in_tail = (self.pos - frag_start) as usize;
let avail = self.cached_fragment_tail.len() - off_in_tail;
let n = avail.min(out.len());
out[..n].copy_from_slice(&self.cached_fragment_tail[off_in_tail..off_in_tail + n]);
self.pos += n as u64;
Ok(n)
}
}
}
impl<'a> Seek for SquashfsFileReadHandle<'a> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let target_i128 = match pos {
SeekFrom::Start(n) => n as i128,
SeekFrom::Current(d) => self.pos as i128 + d as i128,
SeekFrom::End(d) => self.len as i128 + d as i128,
};
if target_i128 < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"squashfs: seek to negative offset",
));
}
let target = (target_i128 as u128).min(self.len as u128) as u64;
self.pos = target;
Ok(self.pos)
}
}
impl<'a> FileReadHandle for SquashfsFileReadHandle<'a> {
fn len(&self) -> u64 {
self.len
}
}