#![allow(dead_code)]
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::EntryKind;
use crate::fs::squashfs::metablock::MetadataReader;
pub const INODE_BASIC_DIR: u16 = 1;
pub const INODE_BASIC_FILE: u16 = 2;
pub const INODE_BASIC_SYMLINK: u16 = 3;
pub const INODE_BASIC_BLOCK: u16 = 4;
pub const INODE_BASIC_CHAR: u16 = 5;
pub const INODE_BASIC_FIFO: u16 = 6;
pub const INODE_BASIC_SOCKET: u16 = 7;
pub const INODE_EXT_DIR: u16 = 8;
pub const INODE_EXT_FILE: u16 = 9;
pub const INODE_EXT_SYMLINK: u16 = 10;
pub const INODE_EXT_BLOCK: u16 = 11;
pub const INODE_EXT_CHAR: u16 = 12;
pub const INODE_EXT_FIFO: u16 = 13;
pub const INODE_EXT_SOCKET: u16 = 14;
#[derive(Debug, Clone, Copy)]
pub struct InodeHeader {
pub kind: u16,
pub permissions: u16,
pub uid_idx: u16,
pub gid_idx: u16,
pub mtime: u32,
pub inode_number: u32,
}
impl InodeHeader {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < 16 {
return Err(crate::Error::InvalidImage(
"squashfs: short inode header".into(),
));
}
Ok(Self {
kind: u16::from_le_bytes(buf[0..2].try_into().unwrap()),
permissions: u16::from_le_bytes(buf[2..4].try_into().unwrap()),
uid_idx: u16::from_le_bytes(buf[4..6].try_into().unwrap()),
gid_idx: u16::from_le_bytes(buf[6..8].try_into().unwrap()),
mtime: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
inode_number: u32::from_le_bytes(buf[12..16].try_into().unwrap()),
})
}
}
#[derive(Debug, Clone)]
pub struct DirInode {
pub header: InodeHeader,
pub block_index: u32,
pub block_offset: u16,
pub file_size: u32,
pub parent_inode: u32,
}
#[derive(Debug, Clone)]
pub struct FileInode {
pub header: InodeHeader,
pub blocks_start: u64,
pub file_size: u64,
pub fragment_index: u32,
pub fragment_offset: u32,
pub block_sizes: Vec<u32>,
}
impl FileInode {
pub fn has_fragment(&self) -> bool {
self.fragment_index != 0xFFFF_FFFF
}
}
#[derive(Debug, Clone)]
pub struct SymlinkInode {
pub header: InodeHeader,
pub target: String,
}
#[derive(Debug, Clone)]
pub enum Inode {
Dir(DirInode),
File(FileInode),
Symlink(SymlinkInode),
Other {
header: InodeHeader,
kind: EntryKind,
},
}
impl Inode {
pub fn header(&self) -> &InodeHeader {
match self {
Inode::Dir(d) => &d.header,
Inode::File(f) => &f.header,
Inode::Symlink(s) => &s.header,
Inode::Other { header, .. } => header,
}
}
pub fn entry_kind(&self) -> EntryKind {
match self {
Inode::Dir(_) => EntryKind::Dir,
Inode::File(_) => EntryKind::Regular,
Inode::Symlink(_) => EntryKind::Symlink,
Inode::Other { kind, .. } => *kind,
}
}
}
pub fn inode_ref(reference: u64) -> (u64, u16) {
let offset = (reference & 0xFFFF) as u16;
let block = (reference >> 16) & 0xFFFF_FFFF;
(block, offset)
}
pub fn read_inode(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: u16,
block_size: u32,
) -> Result<Inode> {
let header_bytes = read_bytes(dev, mr, block_rel, offset as usize, 16)?;
let header = InodeHeader::decode(&header_bytes.0)?;
match header.kind {
INODE_BASIC_DIR => decode_basic_dir(dev, mr, header_bytes.1, header_bytes.2, header),
INODE_EXT_DIR => decode_ext_dir(dev, mr, header_bytes.1, header_bytes.2, header),
INODE_BASIC_FILE => {
decode_basic_file(dev, mr, header_bytes.1, header_bytes.2, header, block_size)
}
INODE_EXT_FILE => {
decode_ext_file(dev, mr, header_bytes.1, header_bytes.2, header, block_size)
}
INODE_BASIC_SYMLINK | INODE_EXT_SYMLINK => {
decode_symlink(dev, mr, header_bytes.1, header_bytes.2, header)
}
INODE_BASIC_BLOCK | INODE_EXT_BLOCK => Ok(Inode::Other {
header,
kind: EntryKind::Block,
}),
INODE_BASIC_CHAR | INODE_EXT_CHAR => Ok(Inode::Other {
header,
kind: EntryKind::Char,
}),
INODE_BASIC_FIFO | INODE_EXT_FIFO => Ok(Inode::Other {
header,
kind: EntryKind::Fifo,
}),
INODE_BASIC_SOCKET | INODE_EXT_SOCKET => Ok(Inode::Other {
header,
kind: EntryKind::Socket,
}),
other => Err(crate::Error::InvalidImage(format!(
"squashfs: unknown inode type {other}"
))),
}
}
fn decode_basic_dir(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
header: InodeHeader,
) -> Result<Inode> {
let (buf, _, _) = read_bytes(dev, mr, block_rel, offset, 16)?;
let block_index = u32::from_le_bytes(buf[0..4].try_into().unwrap());
let _link_count = u32::from_le_bytes(buf[4..8].try_into().unwrap());
let raw_file_size = u16::from_le_bytes(buf[8..10].try_into().unwrap());
let block_offset = u16::from_le_bytes(buf[10..12].try_into().unwrap());
let parent_inode = u32::from_le_bytes(buf[12..16].try_into().unwrap());
let file_size = if raw_file_size <= 3 {
0
} else {
(raw_file_size as u32) - 3
};
Ok(Inode::Dir(DirInode {
header,
block_index,
block_offset,
file_size,
parent_inode,
}))
}
fn decode_ext_dir(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
header: InodeHeader,
) -> Result<Inode> {
let (buf, _, _) = read_bytes(dev, mr, block_rel, offset, 24)?;
let _link_count = u32::from_le_bytes(buf[0..4].try_into().unwrap());
let raw_file_size = u32::from_le_bytes(buf[4..8].try_into().unwrap());
let block_index = u32::from_le_bytes(buf[8..12].try_into().unwrap());
let parent_inode = u32::from_le_bytes(buf[12..16].try_into().unwrap());
let _index_count = u16::from_le_bytes(buf[16..18].try_into().unwrap());
let block_offset = u16::from_le_bytes(buf[18..20].try_into().unwrap());
let _xattr_index = u32::from_le_bytes(buf[20..24].try_into().unwrap());
let file_size = raw_file_size.saturating_sub(3);
Ok(Inode::Dir(DirInode {
header,
block_index,
block_offset,
file_size,
parent_inode,
}))
}
fn decode_basic_file(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
header: InodeHeader,
block_size: u32,
) -> Result<Inode> {
let (buf, b2, o2) = read_bytes(dev, mr, block_rel, offset, 16)?;
let blocks_start = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as u64;
let fragment_index = u32::from_le_bytes(buf[4..8].try_into().unwrap());
let fragment_offset = u32::from_le_bytes(buf[8..12].try_into().unwrap());
let file_size = u32::from_le_bytes(buf[12..16].try_into().unwrap()) as u64;
let num_full_blocks =
full_block_count(file_size, block_size as u64, fragment_index != 0xFFFF_FFFF);
let (block_sizes, _, _) = read_block_sizes(dev, mr, b2, o2, num_full_blocks)?;
Ok(Inode::File(FileInode {
header,
blocks_start,
file_size,
fragment_index,
fragment_offset,
block_sizes,
}))
}
fn decode_ext_file(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
header: InodeHeader,
block_size: u32,
) -> Result<Inode> {
let (buf, b2, o2) = read_bytes(dev, mr, block_rel, offset, 40)?;
let blocks_start = u64::from_le_bytes(buf[0..8].try_into().unwrap());
let file_size = u64::from_le_bytes(buf[8..16].try_into().unwrap());
let _sparse = u64::from_le_bytes(buf[16..24].try_into().unwrap());
let _link_count = u32::from_le_bytes(buf[24..28].try_into().unwrap());
let fragment_index = u32::from_le_bytes(buf[28..32].try_into().unwrap());
let fragment_offset = u32::from_le_bytes(buf[32..36].try_into().unwrap());
let _xattr_index = u32::from_le_bytes(buf[36..40].try_into().unwrap());
let num_full_blocks =
full_block_count(file_size, block_size as u64, fragment_index != 0xFFFF_FFFF);
let (block_sizes, _, _) = read_block_sizes(dev, mr, b2, o2, num_full_blocks)?;
Ok(Inode::File(FileInode {
header,
blocks_start,
file_size,
fragment_index,
fragment_offset,
block_sizes,
}))
}
fn decode_symlink(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
header: InodeHeader,
) -> Result<Inode> {
let (head, b2, o2) = read_bytes(dev, mr, block_rel, offset, 8)?;
let _link_count = u32::from_le_bytes(head[0..4].try_into().unwrap());
let target_size = u32::from_le_bytes(head[4..8].try_into().unwrap());
if target_size > 65535 {
return Err(crate::Error::InvalidImage(format!(
"squashfs: symlink target size {target_size} exceeds 65535"
)));
}
let (raw, _, _) = read_bytes(dev, mr, b2, o2, target_size as usize)?;
let target = String::from_utf8(raw).map_err(|e| {
crate::Error::InvalidImage(format!("squashfs: symlink target is not utf-8: {e}"))
})?;
Ok(Inode::Symlink(SymlinkInode { header, target }))
}
fn full_block_count(file_size: u64, block_size: u64, has_fragment: bool) -> usize {
if block_size == 0 {
return 0;
}
if has_fragment {
(file_size / block_size) as usize
} else {
file_size.div_ceil(block_size) as usize
}
}
fn read_block_sizes(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
count: usize,
) -> Result<(Vec<u32>, u64, usize)> {
let need = count * 4;
let (raw, nb, no) = read_bytes(dev, mr, block_rel, offset, need)?;
let mut sizes = Vec::with_capacity(count);
for i in 0..count {
let off = i * 4;
sizes.push(u32::from_le_bytes(raw[off..off + 4].try_into().unwrap()));
}
Ok((sizes, nb, no))
}
fn read_bytes(
dev: &mut dyn BlockDevice,
mr: &mut MetadataReader,
block_rel: u64,
offset: usize,
n: usize,
) -> Result<(Vec<u8>, u64, usize)> {
mr.read(dev, block_rel, offset, n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_block_count_with_fragment() {
assert_eq!(full_block_count(0, 4096, true), 0);
assert_eq!(full_block_count(4095, 4096, true), 0);
assert_eq!(full_block_count(4096, 4096, true), 1);
assert_eq!(full_block_count(4097, 4096, true), 1);
}
#[test]
fn full_block_count_without_fragment() {
assert_eq!(full_block_count(0, 4096, false), 0);
assert_eq!(full_block_count(1, 4096, false), 1);
assert_eq!(full_block_count(4096, 4096, false), 1);
assert_eq!(full_block_count(4097, 4096, false), 2);
}
#[test]
fn inode_ref_splits_block_and_offset() {
let r = (0x1234u64 << 16) | 0xABCDu64;
let (b, o) = inode_ref(r);
assert_eq!(b, 0x1234);
assert_eq!(o, 0xABCD);
}
}