1#![forbid(unsafe_code)]
2
3use crate::error::{Ext4Error, Result};
4use bitflags::bitflags;
5use crc::{Algorithm, Crc};
6
7fn le16(buf: &[u8], off: usize) -> u16 {
12 u16::from_le_bytes([buf[off], buf[off + 1]])
13}
14
15fn le32(buf: &[u8], off: usize) -> u32 {
16 u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
17}
18
19bitflags! {
24 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
25 pub struct CompatFeatures: u32 {
26 const HAS_JOURNAL = 0x0004;
27 const EXT_ATTR = 0x0008;
28 const RESIZE_INODE = 0x0010;
29 const DIR_INDEX = 0x0020;
30 const SPARSE_SUPER2 = 0x0200;
31 }
32}
33
34bitflags! {
35 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
36 pub struct IncompatFeatures: u32 {
37 const FILETYPE = 0x0002;
38 const RECOVER = 0x0004;
39 const JOURNAL_DEV = 0x0008;
40 const META_BG = 0x0010;
41 const EXTENTS = 0x0040;
42 const IS_64BIT = 0x0080;
43 const MMP = 0x0100;
44 const FLEX_BG = 0x0200;
45 const EA_INODE = 0x0400;
46 const DIRDATA = 0x1000;
47 const CSUM_SEED = 0x2000;
48 const LARGEDIR = 0x4000;
49 const INLINE_DATA = 0x8000;
50 const ENCRYPT = 0x1_0000;
51 const CASEFOLD = 0x2_0000;
52 }
53}
54
55bitflags! {
56 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
57 pub struct RoCompatFeatures: u32 {
58 const SPARSE_SUPER = 0x0001;
59 const LARGE_FILE = 0x0002;
60 const BTREE_DIR = 0x0004;
61 const HUGE_FILE = 0x0008;
62 const GDT_CSUM = 0x0010;
63 const DIR_NLINK = 0x0020;
64 const EXTRA_ISIZE = 0x0040;
65 const HAS_SNAPSHOT = 0x0080;
66 const QUOTA = 0x0100;
67 const BIGALLOC = 0x0200;
68 const METADATA_CSUM = 0x0400;
69 const ORPHAN_PRESENT = 0x8000;
70 }
71}
72
73pub(crate) const EXT4_CRC32C: Algorithm<u32> = Algorithm {
84 width: 32,
85 poly: 0x1EDC_6F41,
86 init: 0xFFFF_FFFF,
87 refin: true,
88 refout: true,
89 xorout: 0x0000_0000, check: 0x0, residue: 0x0, };
93
94const MIN_SUPERBLOCK_LEN: usize = 0x5A;
100
101#[derive(Debug, Clone)]
104pub struct Superblock {
105 pub inodes_count: u32,
106 pub blocks_count: u64,
108 pub reserved_blocks: u64,
109 pub free_blocks: u64,
110 pub free_inodes: u32,
111 pub first_data_block: u32,
112 pub block_size: u32,
114 pub blocks_per_group: u32,
115 pub inodes_per_group: u32,
116 pub magic: u16,
117 pub state: u16,
118 pub rev_level: u32,
119 pub inode_size: u16,
120 pub desc_size: u16,
121 pub feature_compat: CompatFeatures,
122 pub feature_incompat: IncompatFeatures,
123 pub feature_ro_compat: RoCompatFeatures,
124 pub uuid: [u8; 16],
125 pub volume_name: [u8; 16],
126 pub last_mounted: [u8; 64],
127 pub mkfs_time: u32,
128 pub mount_time: u32,
129 pub write_time: u32,
130 pub lastcheck_time: u32,
131 pub journal_inum: u32,
132 pub hash_seed: [u32; 4],
133 pub def_hash_version: u8,
134 pub checksum_type: u8,
135 pub checksum_seed: u32,
136 pub checksum: u32,
137 pub is_64bit: bool,
138 pub log_groups_per_flex: u32,
139 pub last_orphan: u32,
140 pub first_error_time: u32,
141 pub last_error_time: u32,
142}
143
144impl Superblock {
145 pub fn parse(buf: &[u8]) -> Result<Self> {
152 if buf.len() < MIN_SUPERBLOCK_LEN {
153 return Err(Ext4Error::TooShort {
154 structure: "superblock",
155 expected: MIN_SUPERBLOCK_LEN,
156 found: buf.len(),
157 });
158 }
159
160 let magic = le16(buf, 0x38);
161 if magic != 0xEF53 {
162 return Err(Ext4Error::InvalidMagic { found: magic });
163 }
164
165 let rev_level = le32(buf, 0x4C);
166 let inode_size = if rev_level >= 1 { le16(buf, 0x58) } else { 128 };
167
168 let log_block_size = le32(buf, 0x18);
169 let shift = 10u32
170 .checked_add(log_block_size)
171 .filter(|&s| s < 32)
172 .ok_or_else(|| {
173 Ext4Error::InvalidSuperblock(format!(
174 "log_block_size {log_block_size} out of range (max 21)"
175 ))
176 })?;
177 let block_size = 1u32 << shift;
178
179 let feature_incompat_raw = if buf.len() > 0x64 { le32(buf, 0x60) } else { 0 };
180 let feature_incompat = IncompatFeatures::from_bits_truncate(feature_incompat_raw);
181 let is_64bit = feature_incompat.contains(IncompatFeatures::IS_64BIT);
182
183 let blocks_count_lo = u64::from(le32(buf, 0x04));
184 let blocks_count_hi = if is_64bit && buf.len() > 0x154 {
185 u64::from(le32(buf, 0x150))
186 } else {
187 0
188 };
189 let blocks_count = blocks_count_lo | (blocks_count_hi << 32);
190
191 let r_blocks_lo = u64::from(le32(buf, 0x08));
192 let r_blocks_hi = if is_64bit && buf.len() > 0x158 {
193 u64::from(le32(buf, 0x154))
194 } else {
195 0
196 };
197 let reserved_blocks = r_blocks_lo | (r_blocks_hi << 32);
198
199 let free_blocks_lo = u64::from(le32(buf, 0x0C));
200 let free_blocks_hi = if is_64bit && buf.len() > 0x15C {
201 u64::from(le32(buf, 0x158))
202 } else {
203 0
204 };
205 let free_blocks = free_blocks_lo | (free_blocks_hi << 32);
206
207 let feature_compat_raw = if buf.len() > 0x60 { le32(buf, 0x5C) } else { 0 };
208 let feature_ro_raw = if buf.len() > 0x68 { le32(buf, 0x64) } else { 0 };
209
210 let mut uuid = [0u8; 16];
211 if buf.len() >= 0x78 {
212 uuid.copy_from_slice(&buf[0x68..0x78]);
213 }
214
215 let mut volume_name = [0u8; 16];
216 if buf.len() >= 0x88 {
217 volume_name.copy_from_slice(&buf[0x78..0x88]);
218 }
219
220 let mut last_mounted = [0u8; 64];
221 if buf.len() >= 0xC8 {
222 last_mounted.copy_from_slice(&buf[0x88..0xC8]);
223 }
224
225 let desc_size = {
227 let raw = if buf.len() > 0x100 {
228 le16(buf, 0xFE)
229 } else {
230 0
231 };
232 if raw == 0 {
233 32u16
234 } else {
235 raw
236 }
237 };
238 if desc_size < 32 {
239 return Err(Ext4Error::InvalidSuperblock(format!(
240 "desc_size {desc_size} out of range (must be 0 or >= 32)"
241 )));
242 }
243
244 let journal_inum = if buf.len() > 0xE4 { le32(buf, 0xE0) } else { 0 };
245 let last_orphan = if buf.len() > 0xEC { le32(buf, 0xE8) } else { 0 };
246
247 let hash_seed = if buf.len() >= 0xFC {
248 [
249 le32(buf, 0xEC),
250 le32(buf, 0xF0),
251 le32(buf, 0xF4),
252 le32(buf, 0xF8),
253 ]
254 } else {
255 [0u32; 4]
256 };
257
258 let def_hash_version = if buf.len() > 0xFC { buf[0xFC] } else { 0 };
259
260 let mkfs_time = if buf.len() > 0x10C {
261 le32(buf, 0x108)
262 } else {
263 0
264 };
265
266 let log_groups_per_flex = if buf.len() > 0x16C {
267 le32(buf, 0x168)
268 } else {
269 0
270 };
271 let checksum_type = if buf.len() > 0x16D { buf[0x16C] } else { 0 };
272
273 let first_error_time = if buf.len() > 0x194 {
274 le32(buf, 0x190)
275 } else {
276 0
277 };
278 let last_error_time = if buf.len() > 0x1B4 {
279 le32(buf, 0x1B0)
280 } else {
281 0
282 };
283
284 let checksum_seed = if buf.len() > 0x260 {
285 le32(buf, 0x25C)
286 } else {
287 0
288 };
289 let checksum = if buf.len() >= 0x400 {
290 le32(buf, 0x3FC)
291 } else {
292 0
293 };
294
295 Ok(Superblock {
296 inodes_count: le32(buf, 0x00),
297 blocks_count,
298 reserved_blocks,
299 free_blocks,
300 free_inodes: le32(buf, 0x10),
301 first_data_block: le32(buf, 0x14),
302 block_size,
303 blocks_per_group: le32(buf, 0x20),
304 inodes_per_group: le32(buf, 0x28),
305 magic,
306 state: le16(buf, 0x3A),
307 rev_level,
308 inode_size,
309 desc_size,
310 feature_compat: CompatFeatures::from_bits_truncate(feature_compat_raw),
311 feature_incompat,
312 feature_ro_compat: RoCompatFeatures::from_bits_truncate(feature_ro_raw),
313 uuid,
314 volume_name,
315 last_mounted,
316 mkfs_time,
317 mount_time: le32(buf, 0x2C),
318 write_time: le32(buf, 0x30),
319 lastcheck_time: if buf.len() > 0x44 { le32(buf, 0x40) } else { 0 },
320 journal_inum,
321 hash_seed,
322 def_hash_version,
323 checksum_type,
324 checksum_seed,
325 checksum,
326 is_64bit,
327 log_groups_per_flex,
328 last_orphan,
329 first_error_time,
330 last_error_time,
331 })
332 }
333
334 pub fn label(&self) -> &str {
336 let end = self
337 .volume_name
338 .iter()
339 .position(|&b| b == 0)
340 .unwrap_or(self.volume_name.len());
341 std::str::from_utf8(&self.volume_name[..end]).unwrap_or("")
342 }
343
344 pub fn uuid_string(&self) -> String {
346 let u = &self.uuid;
347 format!(
348 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
349 u[0], u[1], u[2], u[3],
350 u[4], u[5],
351 u[6], u[7],
352 u[8], u[9],
353 u[10], u[11], u[12], u[13], u[14], u[15],
354 )
355 }
356
357 pub fn has_metadata_csum(&self) -> bool {
359 self.feature_ro_compat
360 .contains(RoCompatFeatures::METADATA_CSUM)
361 }
362
363 pub fn has_extents(&self) -> bool {
365 self.feature_incompat.contains(IncompatFeatures::EXTENTS)
366 }
367
368 pub fn verify_checksum(&self, raw_buf: &[u8]) -> bool {
382 if !self.has_metadata_csum() {
383 return true; }
385 if raw_buf.len() < 0x400 {
386 return false; }
388
389 let crc32c = Crc::<u32>::new(&EXT4_CRC32C);
390
391 let mut digest = crc32c.digest();
393 digest.update(&raw_buf[..0x3FC]);
394 let computed = digest.finalize();
395
396 computed == self.checksum
397 }
398
399 pub fn has_inline_data(&self) -> bool {
401 self.feature_incompat
402 .contains(IncompatFeatures::INLINE_DATA)
403 }
404
405 pub fn has_journal(&self) -> bool {
407 self.feature_compat.contains(CompatFeatures::HAS_JOURNAL)
408 }
409
410 pub fn group_count(&self) -> u32 {
412 if self.blocks_per_group == 0 {
413 return 0;
414 }
415 let total = self.blocks_count;
416 let per = u64::from(self.blocks_per_group);
417 total.div_ceil(per) as u32
418 }
419}
420
421#[cfg(test)]
426mod tests {
427 use super::*;
428
429 fn minimal_superblock_bytes() -> Vec<u8> {
430 let mut buf = vec![0u8; 1024];
431 buf[0x38] = 0x53;
432 buf[0x39] = 0xEF; buf[0x18] = 2; buf[0x00] = 64; buf[0x04] = 0x00;
436 buf[0x05] = 0x04; buf[0x28] = 64; buf[0x20] = 0x00;
439 buf[0x21] = 0x04; buf[0x58] = 0x00;
441 buf[0x59] = 0x01; buf[0x4C] = 1; buf[0xFE] = 32; buf
445 }
446
447 #[test]
448 fn parse_valid_superblock() {
449 let buf = minimal_superblock_bytes();
450 let sb = Superblock::parse(&buf).unwrap();
451 assert_eq!(sb.magic, 0xEF53);
452 assert_eq!(sb.block_size, 4096);
453 assert_eq!(sb.inodes_count, 64);
454 assert_eq!(sb.blocks_count, 1024);
455 assert_eq!(sb.inodes_per_group, 64);
456 assert_eq!(sb.inode_size, 256);
457 }
458
459 #[test]
460 fn reject_invalid_magic() {
461 let mut buf = minimal_superblock_bytes();
462 buf[0x38] = 0x00;
463 buf[0x39] = 0x00;
464 let err = Superblock::parse(&buf).unwrap_err();
465 assert!(matches!(
466 err,
467 crate::error::Ext4Error::InvalidMagic { found: 0 }
468 ));
469 }
470
471 #[test]
472 fn reject_too_short_buffer() {
473 let buf = vec![0u8; 50];
474 let err = Superblock::parse(&buf).unwrap_err();
475 assert!(matches!(err, crate::error::Ext4Error::TooShort { .. }));
476 }
477
478 #[test]
479 fn parse_from_minimal_image() {
480 let img_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
481 if !std::path::Path::new(img_path).exists() {
482 eprintln!("Skipping: minimal.img not found");
483 return;
484 }
485 let data = std::fs::read(img_path).unwrap();
486 let sb = Superblock::parse(&data[1024..2048]).unwrap();
487 assert_eq!(sb.magic, 0xEF53);
488 assert_eq!(sb.block_size, 4096);
489 assert_eq!(sb.label(), "test-ext4");
490 assert!(sb.has_extents());
491 assert!(sb.has_metadata_csum());
492 assert!(sb.is_64bit);
493 assert!(sb.inodes_count > 0);
494 assert!(sb.blocks_count > 0);
495 }
496
497 #[test]
498 fn group_count_calculation() {
499 let buf = minimal_superblock_bytes();
500 let sb = Superblock::parse(&buf).unwrap();
501 assert_eq!(sb.group_count(), 1);
503 }
504
505 #[test]
506 fn inode_size_default_for_rev0() {
507 let mut buf = minimal_superblock_bytes();
508 buf[0x4C] = 0;
510 buf[0x4D] = 0;
511 buf[0x4E] = 0;
512 buf[0x4F] = 0;
513 let sb = Superblock::parse(&buf).unwrap();
514 assert_eq!(sb.inode_size, 128);
515 }
516
517 #[test]
518 fn uuid_string_format() {
519 let mut buf = minimal_superblock_bytes();
520 let uuid_bytes: [u8; 16] = [
522 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
523 0x32, 0x10,
524 ];
525 buf[0x68..0x78].copy_from_slice(&uuid_bytes);
526 let sb = Superblock::parse(&buf).unwrap();
527 assert_eq!(sb.uuid_string(), "01234567-89ab-cdef-fedc-ba9876543210");
528 }
529
530 #[test]
531 fn label_with_nulls() {
532 let mut buf = minimal_superblock_bytes();
533 buf[0x78] = b'h';
535 buf[0x79] = b'e';
536 buf[0x7A] = b'l';
537 buf[0x7B] = b'l';
538 buf[0x7C] = b'o';
539 let sb = Superblock::parse(&buf).unwrap();
541 assert_eq!(sb.label(), "hello");
542 }
543
544 #[test]
545 fn feature_flags_roundtrip() {
546 let mut buf = minimal_superblock_bytes();
547 let compat = CompatFeatures::HAS_JOURNAL | CompatFeatures::DIR_INDEX;
549 let compat_bytes = compat.bits().to_le_bytes();
550 buf[0x5C..0x60].copy_from_slice(&compat_bytes);
551
552 let incompat =
554 IncompatFeatures::EXTENTS | IncompatFeatures::IS_64BIT | IncompatFeatures::FLEX_BG;
555 let incompat_bytes = incompat.bits().to_le_bytes();
556 buf[0x60..0x64].copy_from_slice(&incompat_bytes);
557
558 let ro = RoCompatFeatures::METADATA_CSUM | RoCompatFeatures::HUGE_FILE;
560 let ro_bytes = ro.bits().to_le_bytes();
561 buf[0x64..0x68].copy_from_slice(&ro_bytes);
562
563 let sb = Superblock::parse(&buf).unwrap();
564 assert!(sb.has_journal());
565 assert!(sb.has_extents());
566 assert!(sb.has_metadata_csum());
567 assert!(sb.is_64bit);
568 assert_eq!(sb.feature_compat, compat);
569 assert_eq!(sb.feature_incompat, incompat);
570 assert_eq!(sb.feature_ro_compat, ro);
571 }
572
573 #[test]
574 fn verify_superblock_checksum() {
575 let img_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
576 if !std::path::Path::new(img_path).exists() {
577 eprintln!("skip: minimal.img not found");
578 return;
579 }
580 let data = std::fs::read(img_path).unwrap();
581 let sb = Superblock::parse(&data[1024..2048]).unwrap();
582 if sb.has_metadata_csum() {
583 assert!(sb.verify_checksum(&data[1024..2048]));
584 }
585 }
586
587 #[test]
588 fn has_inline_data_feature() {
589 let mut buf = minimal_superblock_bytes();
590 buf[0x60..0x64].copy_from_slice(&0x8000u32.to_le_bytes());
592 let sb = Superblock::parse(&buf).unwrap();
593 assert!(sb.has_inline_data());
594 }
595
596 #[test]
597 fn blocks_count_64bit() {
598 let mut buf = minimal_superblock_bytes();
599 let incompat = IncompatFeatures::IS_64BIT;
601 let incompat_bytes = incompat.bits().to_le_bytes();
602 buf[0x60..0x64].copy_from_slice(&incompat_bytes);
603
604 buf[0x04] = 0x00;
606 buf[0x05] = 0x10;
607 buf[0x06] = 0x00;
608 buf[0x07] = 0x00;
609
610 buf[0x150] = 0x02;
612 buf[0x151] = 0x00;
613 buf[0x152] = 0x00;
614 buf[0x153] = 0x00;
615
616 let sb = Superblock::parse(&buf).unwrap();
617 assert_eq!(sb.blocks_count, 0x02_0000_1000);
619 }
620}