use std::io::{self, Read, Seek, SeekFrom};
use crate::tree::TreeNode;
const MAGIC_LE: u32 = 0x7371_7368;
const MAGIC_BE: u32 = 0x6873_7173;
const SUPERBLOCK_SIZE: u64 = 96;
const FLAG_UNCOMPRESSED_INODES: u16 = 0x0001;
const FLAG_UNCOMPRESSED_DATA: u16 = 0x0002;
#[cfg(test)]
const FLAG_UNCOMPRESSED_FRAGS: u16 = 0x0008;
#[cfg(test)]
const FLAG_NO_FRAGMENTS: u16 = 0x0010;
const MAX_DEPTH: usize = 64;
const INODE_DIR: u16 = 1;
const INODE_REG: u16 = 2;
const INODE_SYMLINK: u16 = 3;
const INODE_LDIR: u16 = 8;
const INODE_LREG: u16 = 9;
const INODE_LSYMLINK: u16 = 10;
#[derive(Debug)]
pub enum Error {
TooShort,
BadMagic,
BadVersion,
Compressed,
Io(io::Error),
TooDeep,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::TooShort => write!(f, "image shorter than SquashFS superblock (96 bytes)"),
Error::BadMagic => write!(f, "SquashFS magic bytes not found"),
Error::BadVersion => write!(f, "SquashFS version is not 4.0"),
Error::Compressed => {
write!(
f,
"SquashFS uses compression; enable a codec feature to read it"
)
}
Error::Io(e) => write!(f, "SquashFS I/O error: {e}"),
Error::TooDeep => write!(
f,
"SquashFS directory tree exceeds {MAX_DEPTH}-level depth limit"
),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
struct Superblock {
block_size: u32,
flags: u16,
root_inode: u64,
inode_table_start: u64,
directory_table_start: u64,
}
impl Superblock {
fn read<R: Read + Seek>(r: &mut R) -> Result<Self, Error> {
r.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; SUPERBLOCK_SIZE as usize];
r.read_exact(&mut buf).map_err(|e| {
if e.kind() == io::ErrorKind::UnexpectedEof {
Error::TooShort
} else {
Error::Io(e)
}
})?;
let magic_le = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let magic_be = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
let little_endian = if magic_le == MAGIC_LE {
true
} else if magic_be == MAGIC_BE {
false
} else {
return Err(Error::BadMagic);
};
let u16_at = |off: usize| -> u16 {
if little_endian {
u16::from_le_bytes([buf[off], buf[off + 1]])
} else {
u16::from_be_bytes([buf[off], buf[off + 1]])
}
};
let u32_at = |off: usize| -> u32 {
if little_endian {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
} else {
u32::from_be_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}
};
let u64_at = |off: usize| -> u64 {
if little_endian {
u64::from_le_bytes([
buf[off],
buf[off + 1],
buf[off + 2],
buf[off + 3],
buf[off + 4],
buf[off + 5],
buf[off + 6],
buf[off + 7],
])
} else {
u64::from_be_bytes([
buf[off],
buf[off + 1],
buf[off + 2],
buf[off + 3],
buf[off + 4],
buf[off + 5],
buf[off + 6],
buf[off + 7],
])
}
};
let block_size = u32_at(12);
if !little_endian {
return Err(Error::BadMagic);
}
if block_size == 0 {
return Err(Error::BadMagic);
}
let flags = u16_at(24);
let version_major = u16_at(28);
let version_minor = u16_at(30);
let root_inode = u64_at(32);
let inode_table_start = u64_at(64);
let directory_table_start = u64_at(72);
if version_major != 4 || version_minor != 0 {
return Err(Error::BadVersion);
}
Ok(Superblock {
block_size,
flags,
root_inode,
inode_table_start,
directory_table_start,
})
}
fn is_inodes_uncompressed(&self) -> bool {
self.flags & FLAG_UNCOMPRESSED_INODES != 0
}
fn is_data_uncompressed(&self) -> bool {
self.flags & FLAG_UNCOMPRESSED_DATA != 0
}
}
fn read_metadata_block<R: Read>(r: &mut R) -> Result<Vec<u8>, Error> {
let mut hdr = [0u8; 2];
r.read_exact(&mut hdr)?;
let header = u16::from_le_bytes(hdr);
if header & 0x8000 == 0 {
return Err(Error::Compressed);
}
let size = (header & 0x7FFF) as usize;
let mut data = vec![0u8; size];
r.read_exact(&mut data)?;
Ok(data)
}
fn seek_to_metadata_block<R: Read + Seek>(
r: &mut R,
table_start: u64,
block_count: u64,
) -> Result<Vec<u8>, Error> {
r.seek(SeekFrom::Start(table_start))?;
for _ in 0..block_count {
let mut hdr = [0u8; 2];
r.read_exact(&mut hdr)?;
let header = u16::from_le_bytes(hdr);
if header & 0x8000 == 0 {
return Err(Error::Compressed);
}
let size = (header & 0x7FFF) as usize;
r.seek(SeekFrom::Current(size as i64))?;
}
read_metadata_block(r)
}
fn read_inode_block<R: Read + Seek>(
r: &mut R,
inode_table_start: u64,
block_idx: u64,
) -> Result<Vec<u8>, Error> {
seek_to_metadata_block(r, inode_table_start, block_idx)
}
struct Inode {
inode_type: u16,
dir_info: Option<(u32, u16, u32)>,
reg_info: Option<(u64, u64, Vec<u32>, u32)>,
}
fn parse_inode_body(body: &[u8], inode_type: u16, block_size: u32) -> Result<Inode, Error> {
let too_short = |needed: usize| -> Result<(), Error> {
if body.len() < needed {
Err(Error::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"inode body truncated",
)))
} else {
Ok(())
}
};
let u16le = |off: usize| u16::from_le_bytes([body[off], body[off + 1]]);
let u32le =
|off: usize| u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let u64le = |off: usize| {
u64::from_le_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
])
};
match inode_type {
INODE_DIR => {
too_short(16)?;
let start_block = u32le(0);
let file_size = u16le(8) as u32;
let offset = u16le(10);
Ok(Inode {
inode_type,
dir_info: Some((start_block, offset, file_size)),
reg_info: None,
})
}
INODE_LDIR => {
too_short(24)?;
let file_size = u32le(4);
let start_block = u32le(8);
let offset = u16le(20);
Ok(Inode {
inode_type,
dir_info: Some((start_block, offset, file_size)),
reg_info: None,
})
}
INODE_REG => {
too_short(16)?;
let start_block = u32le(0) as u64;
let fragment = u32le(4);
let file_size = u32le(12) as u64;
let nblocks = block_count_for(file_size, block_size, fragment);
too_short(16 + nblocks * 4)?;
let block_sizes: Vec<u32> = (0..nblocks).map(|i| u32le(16 + i * 4)).collect();
Ok(Inode {
inode_type,
dir_info: None,
reg_info: Some((start_block, file_size, block_sizes, fragment)),
})
}
INODE_LREG => {
too_short(40)?;
let start_block = u64le(0);
let file_size = u64le(8);
let fragment = u32le(28);
let nblocks = block_count_for(file_size, block_size, fragment);
too_short(40 + nblocks * 4)?;
let block_sizes: Vec<u32> = (0..nblocks).map(|i| u32le(40 + i * 4)).collect();
Ok(Inode {
inode_type,
dir_info: None,
reg_info: Some((start_block, file_size, block_sizes, fragment)),
})
}
_ => Ok(Inode {
inode_type,
dir_info: None,
reg_info: None,
}),
}
}
fn block_count_for(file_size: u64, block_size: u32, fragment: u32) -> usize {
if fragment == 0xFFFF_FFFF {
file_size.div_ceil(block_size as u64) as usize
} else {
(file_size / block_size as u64) as usize
}
}
fn file_location_for_reg(start_block: u64, block_sizes: &[u32], fragment: u32) -> Option<u64> {
if fragment != 0xFFFF_FFFF {
return None;
}
if block_sizes.len() != 1 {
return None;
}
if block_sizes[0] & 0x0100_0000 == 0 {
return None;
}
Some(start_block)
}
fn read_and_parse_inode<R: Read + Seek>(
r: &mut R,
inode_table_start: u64,
block_idx: u64,
offset: u16,
block_size: u32,
) -> Result<Inode, Error> {
let block_data = read_inode_block(r, inode_table_start, block_idx)?;
let off = offset as usize;
if block_data.len() < off + 16 {
return Err(Error::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"inode common header extends past block boundary",
)));
}
let inode_type = u16::from_le_bytes([block_data[off], block_data[off + 1]]);
let body = &block_data[off + 16..];
parse_inode_body(body, inode_type, block_size)
}
fn parse_directory<R: Read + Seek>(
r: &mut R,
directory_table_start: u64,
dir_start_block: u32,
dir_offset: u16,
dir_file_size: u32,
) -> Result<Vec<(String, u64, u16)>, Error> {
let block_data = seek_to_metadata_block(r, directory_table_start, dir_start_block as u64)?;
let off = dir_offset as usize;
if block_data.len() < off {
return Err(Error::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"directory offset past metadata block end",
)));
}
let available = block_data.len() - off;
let total = (dir_file_size as usize).min(available);
let dir_bytes = &block_data[off..off + total];
let mut entries: Vec<(String, u64, u16)> = Vec::new();
let mut pos = 0usize;
while pos + 12 <= dir_bytes.len() {
let count = u32::from_le_bytes([
dir_bytes[pos],
dir_bytes[pos + 1],
dir_bytes[pos + 2],
dir_bytes[pos + 3],
]) as usize;
let header_start_block = u32::from_le_bytes([
dir_bytes[pos + 4],
dir_bytes[pos + 5],
dir_bytes[pos + 6],
dir_bytes[pos + 7],
]);
pos += 12;
for _ in 0..=count {
if pos + 8 > dir_bytes.len() {
break;
}
let entry_inode_offset = u16::from_le_bytes([dir_bytes[pos], dir_bytes[pos + 1]]);
let name_size =
u16::from_le_bytes([dir_bytes[pos + 6], dir_bytes[pos + 7]]) as usize + 1;
pos += 8;
if pos + name_size > dir_bytes.len() {
break;
}
let name_bytes = &dir_bytes[pos..pos + name_size];
pos += name_size;
let name = String::from_utf8_lossy(name_bytes).into_owned();
if name == "." || name == ".." {
continue;
}
entries.push((name, header_start_block as u64, entry_inode_offset));
}
}
Ok(entries)
}
fn build_tree<R: Read + Seek>(
r: &mut R,
sb: &Superblock,
name: String,
block_idx: u64,
offset: u16,
depth: usize,
) -> Result<TreeNode, Error> {
if depth > MAX_DEPTH {
return Err(Error::TooDeep);
}
let inode = read_and_parse_inode(r, sb.inode_table_start, block_idx, offset, sb.block_size)?;
match inode.inode_type {
INODE_DIR | INODE_LDIR => {
let (dir_start_block, dir_offset, dir_file_size) =
inode.dir_info.expect("dir_info always set for dir inodes");
let mut node = TreeNode::new_directory(name);
let child_refs = parse_directory(
r,
sb.directory_table_start,
dir_start_block,
dir_offset,
dir_file_size,
)?;
for (child_name, child_block_idx, child_inode_offset) in child_refs {
let child = build_tree(
r,
sb,
child_name,
child_block_idx,
child_inode_offset,
depth + 1,
)?;
node.add_child(child);
}
Ok(node)
}
INODE_REG | INODE_LREG => {
let (start_block, file_size, block_sizes, fragment) =
inode.reg_info.expect("reg_info always set for reg inodes");
let location = file_location_for_reg(start_block, &block_sizes, fragment);
if let Some(loc) = location {
Ok(TreeNode::new_file_with_location(
name, file_size, loc, file_size,
))
} else {
let mut node = TreeNode::new_file(name, file_size);
node.file_length = Some(file_size);
Ok(node)
}
}
INODE_SYMLINK | INODE_LSYMLINK | 4..=7 | 11..=14 => {
Ok(TreeNode::new_file(name, 0))
}
_ => Ok(TreeNode::new_file(name, 0)),
}
}
pub fn detect<R: Read + Seek>(r: &mut R) -> Result<(), Error> {
let pos = r.stream_position()?;
let result = detect_inner(r);
let _ = r.seek(SeekFrom::Start(pos));
result
}
fn detect_inner<R: Read + Seek>(r: &mut R) -> Result<(), Error> {
r.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; 4];
match r.read_exact(&mut buf) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(Error::TooShort),
Err(e) => return Err(Error::Io(e)),
}
let le = u32::from_le_bytes(buf);
let be = u32::from_be_bytes(buf);
if le == MAGIC_LE || be == MAGIC_BE {
Ok(())
} else {
Err(Error::BadMagic)
}
}
pub fn detect_and_parse<R: Read + Seek>(r: &mut R) -> Result<TreeNode, Error> {
let sb = Superblock::read(r)?;
if !sb.is_inodes_uncompressed() || !sb.is_data_uncompressed() {
return Err(Error::Compressed);
}
let root_block_idx = sb.root_inode >> 16;
let root_offset = (sb.root_inode & 0xFFFF) as u16;
let mut root = build_tree(r, &sb, "/".to_string(), root_block_idx, root_offset, 0)?;
root.calculate_directory_size();
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const FILE_INODE_OFFSET: u16 = 32;
fn build_image(file_name: &str, file_data: &[u8]) -> Vec<u8> {
let block_size: u32 = 4096;
let file_size = file_data.len() as u32;
let name_bytes = file_name.as_bytes();
let dir_listing_size: u16 = (12 + 8 + name_bytes.len()) as u16;
let mut inode_block: Vec<u8> = Vec::new();
inode_block.extend_from_slice(&INODE_DIR.to_le_bytes()); inode_block.extend_from_slice(&0o755u16.to_le_bytes()); inode_block.extend_from_slice(&0u16.to_le_bytes()); inode_block.extend_from_slice(&0u16.to_le_bytes()); inode_block.extend_from_slice(&0u32.to_le_bytes()); inode_block.extend_from_slice(&1u32.to_le_bytes()); inode_block.extend_from_slice(&0u32.to_le_bytes()); inode_block.extend_from_slice(&2u32.to_le_bytes()); inode_block.extend_from_slice(&dir_listing_size.to_le_bytes()); inode_block.extend_from_slice(&0u16.to_le_bytes()); inode_block.extend_from_slice(&1u32.to_le_bytes()); assert_eq!(inode_block.len(), FILE_INODE_OFFSET as usize);
let file_inode_body_len = 20usize;
let inode_block_content_len = FILE_INODE_OFFSET as usize + 16 + file_inode_body_len;
let inode_table_start: u64 = 96;
let inode_meta_total: u64 = 2 + inode_block_content_len as u64;
let dir_table_start: u64 = inode_table_start + inode_meta_total;
let dir_block_content_len: usize = 12 + 8 + name_bytes.len();
let dir_meta_total: u64 = 2 + dir_block_content_len as u64;
let file_data_start: u64 = dir_table_start + dir_meta_total;
inode_block.extend_from_slice(&INODE_REG.to_le_bytes()); inode_block.extend_from_slice(&0o644u16.to_le_bytes()); inode_block.extend_from_slice(&0u16.to_le_bytes()); inode_block.extend_from_slice(&0u16.to_le_bytes()); inode_block.extend_from_slice(&0u32.to_le_bytes()); inode_block.extend_from_slice(&2u32.to_le_bytes()); inode_block.extend_from_slice(&(file_data_start as u32).to_le_bytes()); inode_block.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); inode_block.extend_from_slice(&0u32.to_le_bytes()); inode_block.extend_from_slice(&file_size.to_le_bytes()); let block_entry = file_size | 0x0100_0000;
inode_block.extend_from_slice(&block_entry.to_le_bytes());
assert_eq!(inode_block.len(), inode_block_content_len);
let mut dir_block: Vec<u8> = Vec::new();
dir_block.extend_from_slice(&0u32.to_le_bytes()); dir_block.extend_from_slice(&0u32.to_le_bytes()); dir_block.extend_from_slice(&1u32.to_le_bytes()); dir_block.extend_from_slice(&FILE_INODE_OFFSET.to_le_bytes()); dir_block.extend_from_slice(&1i16.to_le_bytes()); dir_block.extend_from_slice(&INODE_REG.to_le_bytes()); dir_block.extend_from_slice(&((name_bytes.len() - 1) as u16).to_le_bytes()); dir_block.extend_from_slice(name_bytes); assert_eq!(dir_block.len(), dir_block_content_len);
let flags = FLAG_UNCOMPRESSED_INODES
| FLAG_UNCOMPRESSED_DATA
| FLAG_UNCOMPRESSED_FRAGS
| FLAG_NO_FRAGMENTS;
let root_inode_ref: u64 = 0;
let mut sb = vec![0u8; 96];
sb[0..4].copy_from_slice(&MAGIC_LE.to_le_bytes()); sb[4..8].copy_from_slice(&2u32.to_le_bytes()); sb[8..12].copy_from_slice(&0u32.to_le_bytes()); sb[12..16].copy_from_slice(&block_size.to_le_bytes()); sb[16..20].copy_from_slice(&0u32.to_le_bytes()); sb[20..22].copy_from_slice(&1u16.to_le_bytes()); sb[22..24].copy_from_slice(&12u16.to_le_bytes()); sb[24..26].copy_from_slice(&flags.to_le_bytes()); sb[26..28].copy_from_slice(&1u16.to_le_bytes()); sb[28..30].copy_from_slice(&4u16.to_le_bytes()); sb[30..32].copy_from_slice(&0u16.to_le_bytes()); sb[32..40].copy_from_slice(&root_inode_ref.to_le_bytes()); sb[40..48].copy_from_slice(&0u64.to_le_bytes()); sb[48..56].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); sb[56..64].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); sb[64..72].copy_from_slice(&inode_table_start.to_le_bytes()); sb[72..80].copy_from_slice(&dir_table_start.to_le_bytes()); sb[80..88].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); sb[88..96].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes());
let mut image: Vec<u8> = Vec::new();
image.extend_from_slice(&sb);
let inode_hdr = 0x8000u16 | (inode_block.len() as u16);
image.extend_from_slice(&inode_hdr.to_le_bytes());
image.extend_from_slice(&inode_block);
let dir_hdr = 0x8000u16 | (dir_block.len() as u16);
image.extend_from_slice(&dir_hdr.to_le_bytes());
image.extend_from_slice(&dir_block);
image.extend_from_slice(file_data);
image
}
fn parse_image(image: &[u8]) -> TreeNode {
let mut c = Cursor::new(image);
detect_and_parse(&mut c).expect("detect_and_parse failed")
}
#[test]
fn detect_le_magic_ok() {
let img = build_image("hello.txt", b"hello");
let mut c = Cursor::new(&img);
assert!(detect(&mut c).is_ok());
}
#[test]
fn detect_restores_position() {
let img = build_image("f.txt", b"data");
let mut c = Cursor::new(&img);
c.seek(SeekFrom::Start(10)).unwrap();
detect(&mut c).unwrap();
assert_eq!(c.stream_position().unwrap(), 10);
}
#[test]
fn detect_rejects_bad_magic() {
let img = vec![0u8; 128];
let mut c = Cursor::new(&img);
assert!(matches!(detect(&mut c), Err(Error::BadMagic)));
}
#[test]
fn detect_rejects_too_short() {
let img = vec![0u8; 3];
let mut c = Cursor::new(&img);
assert!(matches!(detect(&mut c), Err(Error::TooShort)));
}
#[test]
fn root_is_slash_directory() {
let img = build_image("file.txt", b"content");
let tree = parse_image(&img);
assert_eq!(tree.name, "/");
assert!(tree.is_directory);
}
#[test]
fn single_file_child_name_and_type() {
let img = build_image("readme.txt", b"hello world");
let tree = parse_image(&img);
assert_eq!(tree.children.len(), 1);
let child = &tree.children[0];
assert_eq!(child.name, "readme.txt");
assert!(!child.is_directory);
}
#[test]
fn file_size_matches() {
let data = b"the quick brown fox";
let img = build_image("fox.txt", data);
let tree = parse_image(&img);
let child = &tree.children[0];
assert_eq!(child.size, data.len() as u64);
}
#[test]
fn file_location_set_for_uncompressed_single_block() {
let img = build_image("data.bin", b"some bytes");
let tree = parse_image(&img);
let child = &tree.children[0];
assert!(
child.file_location.is_some(),
"uncompressed single-block file should have file_location set"
);
}
#[test]
fn directory_size_is_sum_of_children() {
let data = b"twelve bytes";
let img = build_image("f.txt", data);
let tree = parse_image(&img);
let total: u64 = tree.children.iter().map(|c| c.size).sum();
assert_eq!(tree.size, total);
}
#[test]
fn reject_compressed_inodes_flag() {
let img = build_image("f.txt", b"x");
let mut patched = img.clone();
let flags = u16::from_le_bytes([patched[24], patched[25]]);
let new_flags = flags & !FLAG_UNCOMPRESSED_INODES;
patched[24..26].copy_from_slice(&new_flags.to_le_bytes());
let mut c = Cursor::new(&patched);
assert!(matches!(detect_and_parse(&mut c), Err(Error::Compressed)));
}
#[test]
fn reject_wrong_version() {
let img = build_image("f.txt", b"x");
let mut patched = img.clone();
patched[28..30].copy_from_slice(&3u16.to_le_bytes());
let mut c = Cursor::new(&patched);
assert!(matches!(detect_and_parse(&mut c), Err(Error::BadVersion)));
}
#[test]
fn error_display_too_short() {
let msg = format!("{}", Error::TooShort);
assert!(
msg.contains("SquashFS") || msg.contains("short"),
"unexpected: {msg}"
);
}
#[test]
fn error_display_bad_magic() {
let msg = format!("{}", Error::BadMagic);
assert!(
msg.contains("magic") || msg.contains("SquashFS"),
"unexpected: {msg}"
);
}
#[test]
fn error_display_bad_version() {
let msg = format!("{}", Error::BadVersion);
assert!(
msg.contains("version") || msg.contains("4.0"),
"unexpected: {msg}"
);
}
#[test]
fn error_display_compressed() {
let msg = format!("{}", Error::Compressed);
assert!(
msg.contains("compress") || msg.contains("codec"),
"unexpected: {msg}"
);
}
#[test]
fn error_display_io() {
let io = io::Error::other("disk");
let msg = format!("{}", Error::Io(io));
assert!(msg.contains("disk"), "unexpected: {msg}");
}
#[test]
fn error_display_too_deep() {
let msg = format!("{}", Error::TooDeep);
assert!(
msg.contains("depth") || msg.contains("deep") || msg.contains("64"),
"unexpected: {msg}"
);
}
#[test]
fn error_source_io() {
use std::error::Error as StdError;
let io = io::Error::other("src");
assert!(Error::Io(io).source().is_some());
}
#[test]
fn error_source_non_io() {
use std::error::Error as StdError;
assert!(Error::TooShort.source().is_none());
assert!(Error::BadMagic.source().is_none());
assert!(Error::TooDeep.source().is_none());
}
#[test]
fn parse_inode_body_ldir() {
let mut body = vec![0u8; 24];
body[4..8].copy_from_slice(&99u32.to_le_bytes()); body[8..12].copy_from_slice(&7u32.to_le_bytes()); body[20..22].copy_from_slice(&5u16.to_le_bytes()); let inode = parse_inode_body(&body, INODE_LDIR, 4096).expect("LDIR parse failed");
assert_eq!(inode.inode_type, INODE_LDIR);
assert!(inode.dir_info.is_some());
let (start, off, size) = inode.dir_info.unwrap();
assert_eq!(start, 7);
assert_eq!(off, 5);
assert_eq!(size, 99);
}
#[test]
fn parse_inode_body_lreg() {
let mut body = vec![0u8; 40];
body[..8].copy_from_slice(&42u64.to_le_bytes()); body[8..16].copy_from_slice(&100u64.to_le_bytes()); body[28..32].copy_from_slice(&0u32.to_le_bytes()); let inode = parse_inode_body(&body, INODE_LREG, 4096).expect("LREG parse failed");
assert_eq!(inode.inode_type, INODE_LREG);
assert!(inode.reg_info.is_some());
let (start, size, _, frag) = inode.reg_info.unwrap();
assert_eq!(start, 42);
assert_eq!(size, 100);
assert_eq!(frag, 0);
}
#[test]
fn parse_inode_body_symlink() {
let body = vec![0u8; 8]; let inode = parse_inode_body(&body, INODE_SYMLINK, 4096).expect("symlink parse failed");
assert_eq!(inode.inode_type, INODE_SYMLINK);
assert!(inode.dir_info.is_none());
assert!(inode.reg_info.is_none());
}
#[test]
fn parse_inode_body_lsymlink() {
let body = vec![0u8; 8];
let inode = parse_inode_body(&body, INODE_LSYMLINK, 4096).expect("lsymlink parse failed");
assert!(inode.dir_info.is_none());
assert!(inode.reg_info.is_none());
}
#[test]
fn parse_inode_body_unknown_type() {
let body = vec![0u8; 4];
let inode = parse_inode_body(&body, 99, 4096).expect("unknown type should not error");
assert!(inode.dir_info.is_none());
assert!(inode.reg_info.is_none());
}
#[test]
fn block_count_for_with_fragment() {
let bs = 4096u32;
assert_eq!(block_count_for(8000, bs, 0), 1);
assert_eq!(block_count_for(4096, bs, 0), 1);
assert_eq!(block_count_for(0, bs, 0), 0);
}
#[test]
fn block_count_for_no_fragment() {
let bs = 4096u32;
assert_eq!(block_count_for(8000, bs, 0xFFFF_FFFF), 2);
assert_eq!(block_count_for(4096, bs, 0xFFFF_FFFF), 1);
assert_eq!(block_count_for(1, bs, 0xFFFF_FFFF), 1);
}
#[test]
fn file_location_for_reg_with_fragment_returns_none() {
assert!(file_location_for_reg(100, &[0x0100_0000], 0).is_none());
}
#[test]
fn file_location_for_reg_multiple_blocks_returns_none() {
assert!(file_location_for_reg(100, &[0x0100_0010, 0x0100_0010], 0xFFFF_FFFF).is_none());
}
#[test]
fn file_location_for_reg_compressed_block_returns_none() {
assert!(file_location_for_reg(100, &[0x0000_0010], 0xFFFF_FFFF).is_none());
}
#[test]
fn file_location_for_reg_success() {
let loc = file_location_for_reg(200, &[0x0100_0020], 0xFFFF_FFFF);
assert_eq!(loc, Some(200));
}
#[test]
fn be_magic_constant_is_byte_swap_of_le() {
assert_eq!(MAGIC_LE.swap_bytes(), MAGIC_BE);
}
#[test]
fn error_from_io_error() {
let io_err = io::Error::other("disk error");
let err: Error = Error::from(io_err);
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn detect_and_parse_too_short_returns_too_short() {
let mut img = vec![0u8; 10];
img[0..4].copy_from_slice(&MAGIC_LE.to_le_bytes());
let mut c = Cursor::new(&img);
assert!(matches!(detect_and_parse(&mut c), Err(Error::TooShort)));
}
#[test]
fn detect_and_parse_bad_magic_returns_bad_magic() {
let img = vec![0u8; 96]; let mut c = Cursor::new(&img);
assert!(matches!(detect_and_parse(&mut c), Err(Error::BadMagic)));
}
#[test]
fn detect_and_parse_block_size_zero_returns_bad_magic() {
let mut img = vec![0u8; 96];
img[0..4].copy_from_slice(&MAGIC_LE.to_le_bytes());
img[28..30].copy_from_slice(&4u16.to_le_bytes()); let mut c = Cursor::new(&img);
assert!(matches!(detect_and_parse(&mut c), Err(Error::BadMagic)));
}
#[test]
fn read_metadata_block_compressed_returns_error() {
let data = vec![0x05u8, 0x00];
let mut c = Cursor::new(&data);
assert!(matches!(
read_metadata_block(&mut c),
Err(Error::Compressed)
));
}
#[test]
fn seek_to_metadata_block_compressed_in_skip_returns_error() {
let data = vec![0x05u8, 0x00, 0xFF, 0xFF];
let mut c = Cursor::new(&data);
assert!(matches!(
seek_to_metadata_block(&mut c, 0, 1),
Err(Error::Compressed)
));
}
#[test]
fn read_and_parse_inode_offset_past_block_returns_error() {
let mut data = vec![0x08u8, 0x80]; data.extend_from_slice(&[0u8; 8]); let mut c = Cursor::new(&data);
let result = read_and_parse_inode(&mut c, 0, 0, 0, 4096);
assert!(matches!(result, Err(Error::Io(_))));
}
#[test]
fn parse_directory_offset_past_block_returns_error() {
let mut data = vec![0x04u8, 0x80]; data.extend_from_slice(&[0u8; 4]);
let mut c = Cursor::new(&data);
let result = parse_directory(&mut c, 0, 0, 10, 100);
assert!(matches!(result, Err(Error::Io(_))));
}
#[test]
fn parse_directory_entry_header_truncated_breaks_loop() {
let mut dir_block = vec![0u8; 12]; dir_block.extend_from_slice(&[0u8; 4]); let sz = dir_block.len(); let hdr = (sz | 0x8000) as u16;
let mut data = hdr.to_le_bytes().to_vec();
data.extend_from_slice(&dir_block);
let mut c = Cursor::new(&data);
let result = parse_directory(&mut c, 0, 0, 0, 100).unwrap();
assert!(
result.is_empty(),
"truncated entry header should produce no entries"
);
}
#[test]
fn parse_directory_name_overflow_breaks_loop() {
let mut dir_block = vec![0u8; 12]; dir_block.extend_from_slice(&0u16.to_le_bytes()); dir_block.extend_from_slice(&0i16.to_le_bytes()); dir_block.extend_from_slice(&2u16.to_le_bytes()); dir_block.extend_from_slice(&200u16.to_le_bytes()); let sz = dir_block.len(); let hdr = (sz | 0x8000) as u16;
let mut data = hdr.to_le_bytes().to_vec();
data.extend_from_slice(&dir_block);
let mut c = Cursor::new(&data);
let result = parse_directory(&mut c, 0, 0, 0, 100).unwrap();
assert!(
result.is_empty(),
"overflowing name should produce no entries"
);
}
#[test]
fn parse_directory_dot_dotdot_entries_skipped() {
let make_entry = |name: &[u8]| -> Vec<u8> {
let mut e = Vec::new();
e.extend_from_slice(&0u16.to_le_bytes()); e.extend_from_slice(&0i16.to_le_bytes()); e.extend_from_slice(&1u16.to_le_bytes()); e.extend_from_slice(&((name.len() - 1) as u16).to_le_bytes()); e.extend_from_slice(name);
e
};
let dot = make_entry(b".");
let dotdot = make_entry(b"..");
let count = 1u32; let mut dir_block = Vec::new();
dir_block.extend_from_slice(&count.to_le_bytes());
dir_block.extend_from_slice(&0u32.to_le_bytes()); dir_block.extend_from_slice(&0u32.to_le_bytes()); dir_block.extend_from_slice(&dot);
dir_block.extend_from_slice(&dotdot);
let sz = dir_block.len();
let hdr = (sz | 0x8000) as u16;
let mut data = hdr.to_le_bytes().to_vec();
data.extend_from_slice(&dir_block);
let mut c = Cursor::new(&data);
let result = parse_directory(&mut c, 0, 0, 0, 1000).unwrap();
assert!(result.is_empty(), "dot and dotdot entries must be skipped");
}
#[test]
fn build_tree_too_deep_returns_error() {
let img = build_image("f.txt", b"x");
let mut c = Cursor::new(&img);
let sb = Superblock::read(&mut c).unwrap();
let result = build_tree(&mut c, &sb, "x".to_string(), 0, 0, MAX_DEPTH + 1);
assert!(matches!(result, Err(Error::TooDeep)));
}
#[test]
fn file_with_fragment_has_no_file_location() {
let mut img = build_image("file.txt", b"hello");
img[150..154].copy_from_slice(&0u32.to_le_bytes()); let mut c = Cursor::new(&img);
let tree = detect_and_parse(&mut c).unwrap();
let child = &tree.children[0];
assert!(
child.file_location.is_none(),
"file with fragment must have no location"
);
assert_eq!(child.file_length, Some(5));
}
#[test]
fn symlink_inode_returns_zero_size_file() {
let mut img = build_image("link", b"target");
img[130..132].copy_from_slice(&INODE_SYMLINK.to_le_bytes());
let mut c = Cursor::new(&img);
let tree = detect_and_parse(&mut c).unwrap();
let child = &tree.children[0];
assert_eq!(child.name, "link");
assert_eq!(child.size, 0);
}
#[test]
fn unknown_inode_type_returns_zero_size_file() {
let mut img = build_image("node", b"x");
img[130..132].copy_from_slice(&20u16.to_le_bytes());
let mut c = Cursor::new(&img);
let tree = detect_and_parse(&mut c).unwrap();
let child = &tree.children[0];
assert_eq!(child.size, 0);
}
#[test]
fn seek_to_metadata_block_uncompressed_skip() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(&0x8004u16.to_le_bytes()); data.extend_from_slice(&[0xAAu8; 4]); data.extend_from_slice(&0x8008u16.to_le_bytes()); data.extend_from_slice(&[0xBBu8; 8]);
let mut c = Cursor::new(&data);
let result = seek_to_metadata_block(&mut c, 0, 1).unwrap();
assert_eq!(result, vec![0xBBu8; 8]);
}
#[test]
fn parse_inode_body_dir_too_short_returns_error() {
let body = vec![0u8; 10];
let result = parse_inode_body(&body, INODE_DIR, 4096);
assert!(matches!(result, Err(Error::Io(_))));
}
#[test]
fn superblock_read_big_endian_magic_returns_bad_magic() {
let mut data = vec![0u8; 200];
data[0..4].copy_from_slice(&MAGIC_BE.to_be_bytes());
let mut c = Cursor::new(data);
let result = Superblock::read(&mut c);
assert!(matches!(result, Err(Error::BadMagic)));
}
#[test]
fn detect_inner_big_endian_magic_returns_ok() {
let mut data = vec![0u8; 4];
data[0..4].copy_from_slice(&MAGIC_BE.to_be_bytes());
let mut c = Cursor::new(data);
assert!(matches!(detect_inner(&mut c), Ok(())));
}
}