mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-02 18:06:58 +00:00
Files inside extended SquashFS directories are recovered (R0.5) (#434)
Extended SquashFS directory inodes read their listing offset from the right field, so files inside them are recovered. Fixes #338.
This commit is contained in:
@@ -142,7 +142,7 @@ fn walk(data: &[u8], sb: &Sb, inodes: &Meta, dirs: &Meta, inode_ref: u64, parent
|
||||
// Directory inode (basic=1, extended=8).
|
||||
let (start_block, size, offset) = match itype {
|
||||
1 => (r.u32_le(pos + 16), r.u16_le(pos + 24).map(u32::from), r.u16_le(pos + 26)),
|
||||
8 => (r.u32_le(pos + 24), r.u32_le(pos + 20), r.u16_le(pos + 28)),
|
||||
8 => (r.u32_le(pos + 24), r.u32_le(pos + 20), r.u16_le(pos + 34)),
|
||||
_ => return,
|
||||
};
|
||||
let (Some(start_block), Some(size), Some(offset)) = (start_block, size, offset) else { return };
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use craft_anything_formats::fsread::{self, Limits};
|
||||
use craft_anything_testkit::firmware::{squashfs_image, squashfs_image_with_extended_directories};
|
||||
|
||||
fn check_directories(root: bool, nested: bool) {
|
||||
let limits = Limits::default();
|
||||
let expected = fsread::read("squashfs", &squashfs_image(), &limits).unwrap();
|
||||
assert!(expected.complete);
|
||||
assert_eq!(expected.entries.len(), 3);
|
||||
let bytes = squashfs_image_with_extended_directories(root, nested);
|
||||
let actual = fsread::read("squashfs", &bytes, &limits).unwrap();
|
||||
// Compare the names, kinds, contents and completeness, not only the number of entries.
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_an_extended_root_directory() {
|
||||
check_directories(true, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_an_extended_nested_directory_at_a_nonzero_offset() {
|
||||
check_directories(false, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_extended_root_and_nested_directories_together() {
|
||||
check_directories(true, true);
|
||||
}
|
||||
@@ -436,6 +436,12 @@ pub const SQUASHFS_README: &[u8] = b"This is the squashfs readme.\n";
|
||||
/// A small, walkable SquashFS 4.0 image with uncompressed metadata and data blocks (the reader
|
||||
/// also handles gzip/xz/zstd): root holds `readme` and `etc/` (with `etc/motd`).
|
||||
pub fn squashfs_image() -> Vec<u8> {
|
||||
squashfs_image_with_extended_directories(false, false)
|
||||
}
|
||||
|
||||
/// The same tree with independently selectable basic or extended root and nested directory inodes.
|
||||
/// The nested directory's listing starts at a nonzero offset, distinct from its parent inode number.
|
||||
pub fn squashfs_image_with_extended_directories(extended_root: bool, extended_nested: bool) -> Vec<u8> {
|
||||
fn u16(v: &mut Vec<u8>, x: u16) {
|
||||
v.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
@@ -449,7 +455,7 @@ pub fn squashfs_image() -> Vec<u8> {
|
||||
let motd_start = 96 + r as u32;
|
||||
let inode_table = 96 + r as u64 + motd.len() as u64;
|
||||
|
||||
// Inode metadata block (decompressed). Order: readme(0), motd(36), etc(72), root(104).
|
||||
// Inode metadata block (decompressed). Order: readme(0), motd(36), etc(72), then root.
|
||||
let mut inodes = Vec::new();
|
||||
// readme: basic file (type 2)
|
||||
u16(&mut inodes, 2);
|
||||
@@ -475,30 +481,32 @@ pub fn squashfs_image() -> Vec<u8> {
|
||||
u32(&mut inodes, 0);
|
||||
u32(&mut inodes, motd.len() as u32);
|
||||
u32(&mut inodes, (motd.len() as u32) | 0x0100_0000);
|
||||
// etc: basic directory (type 1)
|
||||
u16(&mut inodes, 1);
|
||||
u16(&mut inodes, 0x41ed);
|
||||
u16(&mut inodes, 0);
|
||||
u16(&mut inodes, 0);
|
||||
u32(&mut inodes, 0);
|
||||
u32(&mut inodes, 2);
|
||||
u32(&mut inodes, 0); // start_block (dir table block location)
|
||||
u32(&mut inodes, 2); // nlink
|
||||
u16(&mut inodes, 27); // file_size (listing + 3)
|
||||
u16(&mut inodes, 37); // offset into dir block
|
||||
u32(&mut inodes, 1); // parent inode
|
||||
// root: basic directory
|
||||
u16(&mut inodes, 1);
|
||||
u16(&mut inodes, 0x41ed);
|
||||
u16(&mut inodes, 0);
|
||||
u16(&mut inodes, 0);
|
||||
u32(&mut inodes, 0);
|
||||
u32(&mut inodes, 1);
|
||||
u32(&mut inodes, 0);
|
||||
u32(&mut inodes, 3);
|
||||
u16(&mut inodes, 40);
|
||||
u16(&mut inodes, 0);
|
||||
u32(&mut inodes, 1);
|
||||
fn directory(out: &mut Vec<u8>, extended: bool, number: u32, links: u32, size: u16, offset: u16) {
|
||||
u16(out, if extended { 8 } else { 1 });
|
||||
u16(out, 0x41ed);
|
||||
u16(out, 0);
|
||||
u16(out, 0);
|
||||
u32(out, 0); // mtime
|
||||
u32(out, number);
|
||||
if extended {
|
||||
u32(out, links);
|
||||
u32(out, u32::from(size));
|
||||
u32(out, 0); // start_block
|
||||
u32(out, 1); // parent inode
|
||||
u16(out, 0); // no directory indexes
|
||||
u16(out, offset);
|
||||
u32(out, u32::MAX); // no xattrs
|
||||
} else {
|
||||
u32(out, 0); // start_block
|
||||
u32(out, links);
|
||||
u16(out, size);
|
||||
u16(out, offset);
|
||||
u32(out, 1); // parent inode
|
||||
}
|
||||
}
|
||||
directory(&mut inodes, extended_nested, 2, 2, 27, 37);
|
||||
let root_ref = inodes.len() as u64;
|
||||
directory(&mut inodes, extended_root, 1, 3, 40, 0);
|
||||
|
||||
// Directory metadata block (decompressed): root listing @0, etc listing @37.
|
||||
let mut dirs = Vec::new();
|
||||
@@ -543,7 +551,7 @@ pub fn squashfs_image() -> Vec<u8> {
|
||||
u16(&mut v, 1); // id count
|
||||
u16(&mut v, 4); // version major
|
||||
u16(&mut v, 0); // version minor
|
||||
v.extend_from_slice(&104u64.to_le_bytes()); // root inode ref (block 0, offset 104)
|
||||
v.extend_from_slice(&root_ref.to_le_bytes()); // root inode ref (block 0)
|
||||
v.extend_from_slice(&bytes_used.to_le_bytes());
|
||||
v.extend_from_slice(&id_table.to_le_bytes());
|
||||
v.extend_from_slice(&u64::MAX.to_le_bytes()); // xattr
|
||||
|
||||
@@ -171,6 +171,7 @@ Owner: open · Status: done (branch chore/r0.5-cleanups)
|
||||
- [x] rustfmt drift on main (11 files)
|
||||
- [x] Checked arithmetic on file offsets in `fsread/extfs.rs`: an inode offset near `usize::MAX`
|
||||
overflows `o + 128` (two sites), found by a lint scan of `crates/formats` (#42)
|
||||
- [x] Read extended SquashFS directory offsets correctly (#338). — claimed: Afloat16, branch bugfix/r0.5-squashfs-extended-directory-20261102
|
||||
- [x] `macho.rs` `method_list`: `list + 8 + i * entsize` adds to a pointer read from the file, so a
|
||||
segment that ends at `u64::MAX` overflowed it (found by a lint scan; now reproduced by a
|
||||
regression test, and the entry addresses are checked)
|
||||
|
||||
@@ -60,6 +60,13 @@ checked with Rizin 0.9.1 on Windows) and radio decoding with rtl_433 (`radio_rec
|
||||
shares with `=h& 9090` (`native_cutter_sessions_list`, `native_cutter_command_run`; tested
|
||||
against a stand-in server, not yet a real Cutter). Every tool is listed in `docs/tools.md`.
|
||||
|
||||
### SquashFS extended directories
|
||||
|
||||
The filesystem reader recovers the same files and bytes from basic and extended directory inodes.
|
||||
Synthetic images cover an extended root, a nested extended directory with a nonzero listing offset,
|
||||
and both together; the results are compared with the existing basic-directory fixture. Compressed
|
||||
metadata, directory indexes and a real firmware extraction were not newly checked for this fix.
|
||||
|
||||
## Known limits
|
||||
|
||||
- Locks: every mutex but the browser viewer's goes through `platform::sync`. A deadlock on one thread or across
|
||||
|
||||
Reference in New Issue
Block a user