1#![forbid(unsafe_code)]
2use crate::block::BlockReader;
3use crate::error::{Ext4Error, Result};
4use crate::ondisk::xattr::XattrEntry;
5use crate::ondisk::{ExtentHeader, ExtentIndex, ExtentLeaf, Inode};
6use std::io::{Read, Seek};
7
8#[derive(Debug, Clone)]
15pub struct BlockMapping {
16 pub logical_block: u64,
17 pub physical_block: u64,
18 pub length: u64,
19 pub unwritten: bool,
20}
21
22fn find_system_data_xattr(ibody: &[u8]) -> Option<Vec<u8>> {
31 let mut offset = 0;
32 while offset + 16 <= ibody.len() {
33 if ibody[offset] == 0 {
35 break;
36 }
37 match XattrEntry::parse(&ibody[offset..]) {
38 Ok(entry) => {
39 if entry.name == b"data"
41 && matches!(
42 entry.name_index,
43 crate::ondisk::xattr::XattrNamespace::System
44 )
45 {
46 let vs = entry.value_offset as usize;
47 let ve = vs + entry.value_size as usize;
48 if ve <= ibody.len() {
49 return Some(ibody[vs..ve].to_vec());
50 }
51 }
52 offset += entry.entry_size;
53 }
54 Err(_) => break,
55 }
56 }
57 None
58}
59
60pub struct InodeReader<R: Read + Seek> {
65 pub(crate) block_reader: BlockReader<R>,
66}
67
68impl<R: Read + Seek> InodeReader<R> {
69 pub fn new(br: BlockReader<R>) -> Self {
71 Self { block_reader: br }
72 }
73
74 pub fn block_reader(&self) -> &BlockReader<R> {
76 &self.block_reader
77 }
78
79 pub fn block_reader_mut(&mut self) -> &mut BlockReader<R> {
81 &mut self.block_reader
82 }
83
84 pub fn read_inode(&self, ino: u64) -> Result<Inode> {
93 let sb = self.block_reader.superblock();
94 let max = u64::from(sb.inodes_count);
95 if ino == 0 || ino > max {
96 return Err(Ext4Error::InodeOutOfRange { ino, max });
97 }
98
99 let inodes_per_group = u64::from(sb.inodes_per_group);
100 let inode_size = u64::from(sb.inode_size);
101 let block_size = u64::from(sb.block_size);
102
103 let group = ((ino - 1) / inodes_per_group) as u32;
104 let index = (ino - 1) % inodes_per_group;
105
106 let inode_table = self.block_reader.inode_table_block(group)?;
107 let offset = inode_table * block_size + index * inode_size;
108
109 let buf = self.block_reader.read_bytes(offset, inode_size as usize)?;
110 Inode::parse(&buf, self.block_reader.superblock().inode_size)
111 }
112
113 pub fn read_inode_raw(&self, ino: u64) -> Result<Vec<u8>> {
120 let sb = self.block_reader.superblock();
121 let inode_size = u64::from(sb.inode_size);
122 let inodes_per_group = u64::from(sb.inodes_per_group);
123 let max = u64::from(sb.inodes_count);
124 if ino < 1 || ino > max {
125 return Err(Ext4Error::InodeOutOfRange { ino, max });
126 }
127 let group = ((ino - 1) / inodes_per_group) as u32;
128 let index = (ino - 1) % inodes_per_group;
129 let inode_table_block = self.block_reader.inode_table_block(group)?;
130 let block_size = u64::from(self.block_reader.superblock().block_size);
131 let byte_offset = inode_table_block * block_size + index * inode_size;
132 self.block_reader
133 .read_bytes(byte_offset, inode_size as usize)
134 }
135
136 pub fn inode_block_map(&self, ino: u64) -> Result<Vec<BlockMapping>> {
142 let inode = self.read_inode(ino)?;
143 if inode.uses_extents() {
144 self.walk_extent_tree(&inode.i_block)
145 } else {
146 self.walk_indirect_blocks(&inode.i_block)
147 }
148 }
149
150 pub fn walk_extent_tree(&self, i_block: &[u8; 60]) -> Result<Vec<BlockMapping>> {
156 let mut mappings = Vec::new();
157 self.walk_extent_node(i_block.as_slice(), &mut mappings)?;
158 Ok(mappings)
159 }
160
161 fn walk_extent_node(&self, buf: &[u8], out: &mut Vec<BlockMapping>) -> Result<()> {
162 let header = ExtentHeader::parse(buf)?;
163 let entries = header.entries as usize;
164
165 if header.depth == 0 {
166 for i in 0..entries {
168 let off = 12 + i * 12;
169 if off + 12 > buf.len() {
170 break;
171 }
172 let leaf = ExtentLeaf::parse(&buf[off..]);
173 out.push(BlockMapping {
174 logical_block: u64::from(leaf.logical_block),
175 physical_block: leaf.physical_block,
176 length: u64::from(leaf.length),
177 unwritten: leaf.unwritten,
178 });
179 }
180 } else {
181 for i in 0..entries {
183 let off = 12 + i * 12;
184 if off + 12 > buf.len() {
185 break;
186 }
187 let idx = ExtentIndex::parse(&buf[off..]);
188 let child_data = self.block_reader.read_block(idx.child_block)?;
189 self.walk_extent_node(&child_data, out)?;
190 }
191 }
192 Ok(())
193 }
194
195 pub fn walk_indirect_blocks(&self, i_block: &[u8; 60]) -> Result<Vec<BlockMapping>> {
203 let block_size = self.block_reader.block_size() as usize;
204 let ptrs_per_block = block_size / 4;
205
206 let read_u32 = |buf: &[u8], off: usize| -> u32 {
207 u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
208 };
209
210 let mut out = Vec::new();
211 let mut logical = 0u64;
212
213 for i in 0..12usize {
215 let ptr = u64::from(read_u32(i_block, i * 4));
216 if ptr != 0 {
217 out.push(BlockMapping {
218 logical_block: logical,
219 physical_block: ptr,
220 length: 1,
221 unwritten: false,
222 });
223 }
224 logical += 1;
225 }
226
227 let sind = u64::from(read_u32(i_block, 48));
229 if sind != 0 {
230 let blk = self.block_reader.read_block(sind)?;
231 for i in 0..ptrs_per_block {
232 let ptr = u64::from(read_u32(&blk, i * 4));
233 if ptr != 0 {
234 out.push(BlockMapping {
235 logical_block: logical,
236 physical_block: ptr,
237 length: 1,
238 unwritten: false,
239 });
240 }
241 logical += 1;
242 }
243 } else {
244 logical += ptrs_per_block as u64;
245 }
246
247 let dind = u64::from(read_u32(i_block, 52));
249 if dind != 0 {
250 let l1 = self.block_reader.read_block(dind)?;
251 for i in 0..ptrs_per_block {
252 let ptr1 = u64::from(read_u32(&l1, i * 4));
253 if ptr1 != 0 {
254 let l2 = self.block_reader.read_block(ptr1)?;
255 for j in 0..ptrs_per_block {
256 let ptr2 = u64::from(read_u32(&l2, j * 4));
257 if ptr2 != 0 {
258 out.push(BlockMapping {
259 logical_block: logical,
260 physical_block: ptr2,
261 length: 1,
262 unwritten: false,
263 });
264 }
265 logical += 1;
266 }
267 } else {
268 logical += ptrs_per_block as u64;
269 }
270 }
271 } else {
272 logical += (ptrs_per_block * ptrs_per_block) as u64;
273 }
274
275 let tind = u64::from(read_u32(i_block, 56));
277 if tind != 0 {
278 let l1 = self.block_reader.read_block(tind)?;
279 for i in 0..ptrs_per_block {
280 let ptr1 = u64::from(read_u32(&l1, i * 4));
281 if ptr1 != 0 {
282 let l2 = self.block_reader.read_block(ptr1)?;
283 for j in 0..ptrs_per_block {
284 let ptr2 = u64::from(read_u32(&l2, j * 4));
285 if ptr2 != 0 {
286 let l3 = self.block_reader.read_block(ptr2)?;
287 for k in 0..ptrs_per_block {
288 let ptr3 = u64::from(read_u32(&l3, k * 4));
289 if ptr3 != 0 {
290 out.push(BlockMapping {
291 logical_block: logical,
292 physical_block: ptr3,
293 length: 1,
294 unwritten: false,
295 });
296 }
297 logical += 1;
298 }
299 } else {
300 logical += ptrs_per_block as u64;
301 }
302 }
303 } else {
304 logical += (ptrs_per_block * ptrs_per_block) as u64;
305 }
306 }
307 }
308
309 Ok(out)
310 }
311
312 pub fn read_inode_data(&self, ino: u64) -> Result<Vec<u8>> {
321 let inode = self.read_inode(ino)?;
322 if inode.has_inline_data() {
323 let len = (inode.size as usize).min(60);
324 let mut data = inode.i_block[..len].to_vec();
325 if inode.size > 60 {
327 if let Ok(raw) = self.read_inode_raw(ino) {
328 let inode_size = self.block_reader.superblock().inode_size as usize;
329 let ibody_offset = 0x80 + inode.extra_isize as usize;
330 if inode_size > ibody_offset {
331 let ibody = &raw[ibody_offset..inode_size.min(raw.len())];
332 if let Some(value) = find_system_data_xattr(ibody) {
333 data.extend_from_slice(&value);
334 }
335 }
336 }
337 }
338 data.truncate(inode.size as usize);
339 return Ok(data);
340 }
341 let size = inode.size as usize;
342 let _block_size = self.block_reader.block_size() as usize;
343 let map = if inode.uses_extents() {
344 self.walk_extent_tree(&inode.i_block)?
345 } else {
346 self.walk_indirect_blocks(&inode.i_block)?
347 };
348
349 let mut data: Vec<u8> = Vec::with_capacity(size);
350 for mapping in &map {
351 for blk_offset in 0..mapping.length {
352 let phys = mapping.physical_block + blk_offset;
353 let blk_data = self.block_reader.read_block(phys)?;
354 data.extend_from_slice(&blk_data);
355 if data.len() >= size {
356 data.truncate(size);
357 return Ok(data);
358 }
359 }
360 }
361 data.truncate(size);
362 Ok(data)
363 }
364
365 pub fn read_inode_data_range(&self, ino: u64, offset: u64, len: usize) -> Result<Vec<u8>> {
371 let inode = self.read_inode(ino)?;
372 if inode.has_inline_data() {
373 let start = offset as usize;
374 let end = (start + len).min(60).min(inode.size as usize);
375 if start >= end {
376 return Ok(Vec::new());
377 }
378 return Ok(inode.i_block[start..end].to_vec());
379 }
380
381 let file_size = inode.size;
382 if offset >= file_size {
383 return Ok(Vec::new());
384 }
385 let want_end = (offset + len as u64).min(file_size);
386 let want_len = (want_end - offset) as usize;
387
388 let block_size = u64::from(self.block_reader.block_size());
389 let map = if inode.uses_extents() {
390 self.walk_extent_tree(&inode.i_block)?
391 } else {
392 self.walk_indirect_blocks(&inode.i_block)?
393 };
394
395 let mut out = vec![0u8; want_len];
396 let mut written = 0usize;
397
398 for mapping in &map {
399 for blk_offset in 0..mapping.length {
400 let logical = (mapping.logical_block + blk_offset) * block_size;
401 let logical_end = logical + block_size;
402 if logical_end <= offset || logical >= want_end {
403 continue;
404 }
405 let src_start = if logical < offset {
406 (offset - logical) as usize
407 } else {
408 0
409 };
410 let dst_start = if logical > offset {
411 (logical - offset) as usize
412 } else {
413 0
414 };
415 let phys = mapping.physical_block + blk_offset;
416 let blk_data = self.block_reader.read_block(phys)?;
417 let src_end = blk_data.len().min(src_start + (want_len - dst_start));
418 let copy_len = src_end - src_start;
419 if dst_start + copy_len <= out.len() {
420 out[dst_start..dst_start + copy_len]
421 .copy_from_slice(&blk_data[src_start..src_end]);
422 written += copy_len;
423 }
424 }
425 }
426 out.truncate(written.min(want_len));
427 Ok(out)
428 }
429
430 pub fn is_inode_allocated(&self, ino: u64) -> Result<bool> {
437 let sb = self.block_reader.superblock();
438 let max = u64::from(sb.inodes_count);
439 if ino == 0 || ino > max {
440 return Err(Ext4Error::InodeOutOfRange { ino, max });
441 }
442 let inodes_per_group = u64::from(sb.inodes_per_group);
443 let group = ((ino - 1) / inodes_per_group) as u32;
444 let index = ((ino - 1) % inodes_per_group) as usize;
445
446 let bitmap_block = self.block_reader.inode_bitmap_block(group)?;
447 let bitmap = self.block_reader.read_block(bitmap_block)?;
448 Ok((bitmap[index / 8] >> (index % 8)) & 1 == 1)
449 }
450
451 pub fn is_block_allocated(&self, block: u64) -> Result<bool> {
454 let sb = self.block_reader.superblock();
455 let blocks_per_group = u64::from(sb.blocks_per_group);
456 let group = (block / blocks_per_group) as u32;
457 let index = (block % blocks_per_group) as usize;
458
459 let bitmap_block = self.block_reader.block_bitmap_block(group)?;
460 let bitmap = self.block_reader.read_block(bitmap_block)?;
461 Ok((bitmap[index / 8] >> (index % 8)) & 1 == 1)
462 }
463
464 pub fn iter_inodes_in_group(&self, group: u32) -> Result<Vec<(u64, Inode)>> {
472 let sb = self.block_reader.superblock();
473 let inodes_per_group = u64::from(sb.inodes_per_group);
474 let inode_size = sb.inode_size as usize;
475 let block_size = u64::from(sb.block_size);
476 let first_ino = u64::from(group) * inodes_per_group + 1;
477
478 let inode_table = self.block_reader.inode_table_block(group)?;
479 let table_bytes = inode_size as u64 * inodes_per_group;
480 let table_offset = inode_table * block_size;
481 let buf = self
482 .block_reader
483 .read_bytes(table_offset, table_bytes as usize)?;
484
485 let stored_inode_size = self.block_reader.superblock().inode_size;
486 let mut result = Vec::new();
487 for i in 0..inodes_per_group as usize {
488 let off = i * inode_size;
489 let slice = &buf[off..off + inode_size];
490 let mode = u16::from_le_bytes([slice[0], slice[1]]);
492 let dtime = u32::from_le_bytes([slice[0x14], slice[0x15], slice[0x16], slice[0x17]]);
493 if mode == 0 && dtime == 0 {
494 continue;
495 }
496 if let Ok(inode) = Inode::parse(slice, stored_inode_size) {
497 result.push((first_ino + i as u64, inode));
498 }
499 }
500 Ok(result)
501 }
502
503 pub fn iter_all_inodes(&self) -> Result<Vec<(u64, Inode)>> {
505 let group_count = self.block_reader.group_count();
506 let mut all = Vec::new();
507 for g in 0..group_count {
508 let inodes = self.iter_inodes_in_group(g)?;
509 all.extend(inodes);
510 }
511 Ok(all)
512 }
513}
514
515#[cfg(test)]
520mod tests {
521 use super::*;
522 use crate::dir::DirReader;
523 use crate::ondisk::FileType;
524 use std::io::Cursor;
525
526 fn open_minimal() -> InodeReader<Cursor<Vec<u8>>> {
527 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
528 let data = std::fs::read(path).expect("minimal.img required");
529 let br = BlockReader::open(Cursor::new(data)).unwrap();
530 InodeReader::new(br)
531 }
532
533 fn resolve_minimal(path: &str) -> u64 {
535 let img_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
536 let data = std::fs::read(img_path).expect("minimal.img required");
537 let br = BlockReader::open(Cursor::new(data)).unwrap();
538 let ir = InodeReader::new(br);
539 let dr = DirReader::new(ir);
540 dr.resolve_path(path).unwrap()
541 }
542
543 fn resolve_forensic(path: &str) -> u64 {
545 let img_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
546 let data = std::fs::read(img_path).expect("forensic.img required");
547 let br = BlockReader::open(Cursor::new(data)).unwrap();
548 let ir = InodeReader::new(br);
549 let dr = DirReader::new(ir);
550 dr.resolve_path(path).unwrap()
551 }
552
553 #[test]
554 fn read_root_inode() {
555 let r = open_minimal();
556 let inode = r.read_inode(2).unwrap();
557 assert_eq!(inode.file_type(), FileType::Directory);
558 assert!(inode.links_count >= 2);
559 }
560
561 #[test]
562 fn read_inode_out_of_range() {
563 let r = open_minimal();
564 let err = r.read_inode(0).unwrap_err();
565 assert!(matches!(err, Ext4Error::InodeOutOfRange { .. }));
566 }
567
568 #[test]
569 fn read_file_data() {
570 let r = open_minimal();
571 let data = r.read_inode_data(2).unwrap();
572 assert!(!data.is_empty());
573 }
574
575 #[test]
576 fn inode_block_map_for_root() {
577 let r = open_minimal();
578 let inode = r.read_inode(2).unwrap();
579 if inode.uses_extents() {
580 let map = r.inode_block_map(2).unwrap();
581 assert!(!map.is_empty());
582 assert!(map[0].physical_block > 0);
583 }
584 }
585
586 #[test]
587 fn is_inode_allocated() {
588 let r = open_minimal();
589 assert!(r.is_inode_allocated(2).unwrap());
590 }
591
592 #[test]
593 fn read_inode_data_extent_path_returns_data() {
594 let reader = open_minimal();
595 let inode = reader.read_inode(2).unwrap();
597 assert!(!inode.has_inline_data());
598 assert!(inode.uses_extents());
599 let data = reader.read_inode_data(2).unwrap();
600 assert_eq!(data.len(), inode.size as usize);
601 assert!(!data.is_empty());
602 }
603
604 fn open_forensic() -> InodeReader<Cursor<Vec<u8>>> {
609 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
610 let data = std::fs::read(path).expect("forensic.img required");
611 let br = BlockReader::open(Cursor::new(data)).unwrap();
612 InodeReader::new(br)
613 }
614
615 #[test]
620 fn block_reader_accessor() {
621 let r = open_minimal();
622 let sb = r.block_reader().superblock();
623 assert!(sb.block_size >= 1024, "block size should be at least 1024");
624 assert!(sb.inodes_count > 0);
625 }
626
627 #[test]
628 fn block_reader_mut_accessor() {
629 let mut r = open_minimal();
630 let sb = r.block_reader_mut().superblock();
631 assert!(sb.block_size >= 1024);
632 }
633
634 #[test]
639 fn read_inode_raw_minimal() {
640 let r = open_minimal();
641 let inode_size = r.block_reader().superblock().inode_size as usize;
642 let raw = r.read_inode_raw(2).unwrap();
643 assert_eq!(
644 raw.len(),
645 inode_size,
646 "raw inode length should equal inode_size"
647 );
648 let mode = u16::from_le_bytes([raw[0], raw[1]]);
650 assert_ne!(mode, 0, "root inode mode should be non-zero");
651 }
652
653 #[test]
654 fn read_inode_raw_out_of_range() {
655 let r = open_minimal();
656 assert!(r.read_inode_raw(0).is_err());
657 assert!(r.read_inode_raw(u64::MAX).is_err());
658 }
659
660 #[test]
665 fn inode_block_map_hello_txt() {
666 let hello_ino = resolve_minimal("/hello.txt");
667 let r = open_minimal();
668 let map = r.inode_block_map(hello_ino).unwrap();
669 assert!(
670 !map.is_empty(),
671 "hello.txt should have at least one block mapping"
672 );
673 for m in &map {
674 assert!(m.physical_block > 0, "physical block should be non-zero");
675 assert!(m.length > 0, "mapping length should be positive");
676 }
677 }
678
679 #[test]
684 fn read_inode_data_range_hello_prefix() {
685 let hello_ino = resolve_minimal("/hello.txt");
686 let r = open_minimal();
687 let data = r.read_inode_data_range(hello_ino, 0, 5).unwrap();
688 assert_eq!(
689 &data, b"Hello",
690 "first 5 bytes of hello.txt should be 'Hello'"
691 );
692 }
693
694 #[test]
695 fn read_inode_data_range_past_eof() {
696 let hello_ino = resolve_minimal("/hello.txt");
697 let r = open_minimal();
698 let inode = r.read_inode(hello_ino).unwrap();
699 let data = r
700 .read_inode_data_range(hello_ino, inode.size + 100, 10)
701 .unwrap();
702 assert!(data.is_empty(), "reading past EOF should return empty");
703 }
704
705 #[test]
710 fn is_block_allocated_block_zero() {
711 let r = open_minimal();
712 let alloc = r.is_block_allocated(0).unwrap();
713 assert!(alloc, "block 0 (superblock) should be allocated");
714 }
715
716 #[test]
721 fn iter_inodes_in_group_zero() {
722 let r = open_minimal();
723 let inodes = r.iter_inodes_in_group(0).unwrap();
724 assert!(!inodes.is_empty(), "group 0 should have inodes");
725 let inos: Vec<u64> = inodes.iter().map(|(ino, _)| *ino).collect();
726 assert!(inos.contains(&2), "group 0 should contain root inode 2");
727 }
728
729 #[test]
734 fn iter_all_inodes_includes_root() {
735 let r = open_minimal();
736 let all = r.iter_all_inodes().unwrap();
737 assert!(all.len() >= 2, "should return multiple inodes");
738 let inos: Vec<u64> = all.iter().map(|(ino, _)| *ino).collect();
739 assert!(inos.contains(&2), "should include root inode 2");
740 }
741
742 #[test]
747 fn read_inode_data_directory() {
748 let r = open_minimal();
749 let inode = r.read_inode(2).unwrap();
750 assert_eq!(inode.file_type(), FileType::Directory);
751 let data = r.read_inode_data(2).unwrap();
752 assert!(!data.is_empty(), "root directory data should not be empty");
753 assert_eq!(data.len(), inode.size as usize);
754 }
755
756 #[test]
761 fn read_inode_hello_txt() {
762 let hello_ino = resolve_minimal("/hello.txt");
763 let r = open_minimal();
764 let inode = r.read_inode(hello_ino).unwrap();
765 assert_eq!(inode.file_type(), FileType::RegularFile);
766 assert!(
768 inode.size == 11 || inode.size == 12,
769 "hello.txt should be 11 or 12 bytes, got {}",
770 inode.size
771 );
772 }
773
774 #[test]
775 fn read_inode_lost_found() {
776 let lf_ino = resolve_minimal("/lost+found");
777 let r = open_minimal();
778 let inode = r.read_inode(lf_ino).unwrap();
779 assert_eq!(inode.file_type(), FileType::Directory);
780 assert!(
781 inode.links_count >= 2,
782 "lost+found should have at least 2 links"
783 );
784 }
785
786 #[test]
791 fn read_inode_raw_forensic() {
792 let r = open_forensic();
793 let inode_size = r.block_reader().superblock().inode_size as usize;
794 let raw = r.read_inode_raw(2).unwrap();
795 assert_eq!(raw.len(), inode_size);
796 let mode = u16::from_le_bytes([raw[0], raw[1]]);
798 assert_ne!(mode, 0);
799 }
800
801 #[test]
806 fn read_inode_data_range_forensic_middle() {
807 let hello_ino = resolve_forensic("/hello.txt");
808 let r = open_forensic();
809 let data = r.read_inode_data_range(hello_ino, 7, 8).unwrap();
812 assert_eq!(
813 std::str::from_utf8(&data).unwrap(),
814 "forensic",
815 "middle bytes of forensic hello.txt"
816 );
817 }
818
819 #[test]
820 fn read_inode_data_range_forensic_start() {
821 let hello_ino = resolve_forensic("/hello.txt");
822 let r = open_forensic();
823 let data = r.read_inode_data_range(hello_ino, 0, 5).unwrap();
824 assert_eq!(&data, b"Hello");
825 }
826
827 #[test]
832 fn is_block_allocated_forensic_block_zero() {
833 let r = open_forensic();
834 assert!(
835 r.is_block_allocated(0).unwrap(),
836 "block 0 should be allocated on forensic.img"
837 );
838 }
839
840 #[test]
841 fn is_block_allocated_forensic_high_block() {
842 let r = open_forensic();
843 let sb = r.block_reader().superblock();
846 let total_blocks = sb.blocks_count;
847 if total_blocks > 100 {
849 let result = r.is_block_allocated(total_blocks - 1);
850 assert!(result.is_ok());
852 }
853 }
854
855 #[test]
856 fn is_block_allocated_forensic_superblock_area() {
857 let r = open_forensic();
858 let alloc = r.is_block_allocated(1).unwrap();
860 assert!(alloc, "block 1 should be allocated (GDT/superblock area)");
861 }
862}