Skip to main content

arcbox_ext4/
types.rs

1// On-disk ext4 structures with manual little-endian serialization.
2//
3// Every struct matches the canonical ext4 disk layout from
4// <https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout>.
5// No `unsafe`, no zerocopy -- just plain `read_from` / `write_to`.
6
7use crate::constants::*;
8use std::time::SystemTime;
9
10// ---------------------------------------------------------------------------
11// Helpers for reading / writing little-endian primitives
12// ---------------------------------------------------------------------------
13
14#[inline]
15fn get_u8(buf: &[u8], off: usize) -> u8 {
16    buf[off]
17}
18
19#[inline]
20fn put_u8(buf: &mut [u8], off: usize, v: u8) {
21    buf[off] = v;
22}
23
24#[inline]
25fn get_u16(buf: &[u8], off: usize) -> u16 {
26    u16::from_le_bytes([buf[off], buf[off + 1]])
27}
28
29#[inline]
30fn put_u16(buf: &mut [u8], off: usize, v: u16) {
31    let b = v.to_le_bytes();
32    buf[off] = b[0];
33    buf[off + 1] = b[1];
34}
35
36#[inline]
37fn get_u32(buf: &[u8], off: usize) -> u32 {
38    u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
39}
40
41#[inline]
42fn put_u32(buf: &mut [u8], off: usize, v: u32) {
43    let b = v.to_le_bytes();
44    buf[off..off + 4].copy_from_slice(&b);
45}
46
47#[inline]
48fn get_u64(buf: &[u8], off: usize) -> u64 {
49    u64::from_le_bytes([
50        buf[off],
51        buf[off + 1],
52        buf[off + 2],
53        buf[off + 3],
54        buf[off + 4],
55        buf[off + 5],
56        buf[off + 6],
57        buf[off + 7],
58    ])
59}
60
61#[inline]
62fn put_u64(buf: &mut [u8], off: usize, v: u64) {
63    let b = v.to_le_bytes();
64    buf[off..off + 8].copy_from_slice(&b);
65}
66
67#[inline]
68fn get_bytes<const N: usize>(buf: &[u8], off: usize) -> [u8; N] {
69    let mut out = [0u8; N];
70    out.copy_from_slice(&buf[off..off + N]);
71    out
72}
73
74#[inline]
75fn put_bytes(buf: &mut [u8], off: usize, src: &[u8]) {
76    buf[off..off + src.len()].copy_from_slice(src);
77}
78
79// ---------------------------------------------------------------------------
80// Timestamp helper
81// ---------------------------------------------------------------------------
82
83/// Return `(seconds_lo, extra)` for the current wall-clock time in ext4 format.
84///
85/// * `seconds_lo` -- lower 32 bits of seconds since the Unix epoch.
86/// * `extra`      -- upper 2 bits of seconds (epoch bits) in bits 0..1, plus
87///                   nanoseconds in bits 2..31.
88pub fn timestamp_now() -> (u32, u32) {
89    let dur = SystemTime::now()
90        .duration_since(SystemTime::UNIX_EPOCH)
91        .unwrap_or_default();
92
93    let secs = dur.as_secs();
94    let nanos = dur.subsec_nanos();
95
96    let lo = secs as u32; // lower 32 bits (wrapping)
97    let epoch_bits = ((secs >> 32) & 0x3) as u32; // 2-bit epoch extension
98    let extra = epoch_bits | (nanos << 2);
99
100    (lo, extra)
101}
102
103// ===========================================================================
104// 1. SuperBlock (1024 bytes)
105// ===========================================================================
106
107pub const SUPERBLOCK_SIZE: usize = 1024;
108
109#[derive(Debug, Clone)]
110pub struct SuperBlock {
111    // -- 0x000 --
112    pub inodes_count: u32,
113    pub blocks_count_lo: u32,
114    pub r_blocks_count_lo: u32,
115    pub free_blocks_count_lo: u32,
116    pub free_inodes_count: u32,
117    pub first_data_block: u32,
118    pub log_block_size: u32,
119    pub log_cluster_size: u32,
120    pub blocks_per_group: u32,
121    pub clusters_per_group: u32,
122    pub inodes_per_group: u32,
123    pub mtime: u32,
124    pub wtime: u32,
125    pub mount_count: u16,
126    pub max_mount_count: u16,
127    pub magic: u16,
128    pub state: u16,
129    pub errors: u16,
130    pub minor_rev_level: u16,
131    pub lastcheck: u32,
132    pub check_interval: u32,
133    pub creator_os: u32,
134    pub rev_level: u32,
135    pub def_resuid: u16,
136    pub def_resgid: u16,
137
138    // -- 0x054 -- (EXT4_DYNAMIC_REV superblocks only)
139    pub first_ino: u32,
140    pub inode_size: u16,
141    pub block_group_nr: u16,
142    pub feature_compat: u32,
143    pub feature_incompat: u32,
144    pub feature_ro_compat: u32,
145    pub uuid: [u8; 16],
146    pub volume_name: [u8; 16],
147    pub last_mounted: [u8; 64],
148    pub algorithm_usage_bitmap: u32,
149
150    // -- 0x0CC --
151    pub prealloc_blocks: u8,
152    pub prealloc_dir_blocks: u8,
153    pub reserved_gdt_blocks: u16,
154
155    // -- 0x0D0 -- journal fields
156    pub journal_uuid: [u8; 16],
157    pub journal_inum: u32,
158    pub journal_dev: u32,
159    pub last_orphan: u32,
160    pub hash_seed: [u32; 4],
161    pub def_hash_version: u8,
162    pub journal_backup_type: u8,
163    pub desc_size: u16,
164    pub default_mount_opts: u32,
165    pub first_meta_bg: u32,
166    pub mkfs_time: u32,
167    pub journal_blocks: [u32; 17],
168
169    // -- 0x150 -- 64-bit support
170    pub blocks_count_hi: u32,
171    pub r_blocks_count_hi: u32,
172    pub free_blocks_count_hi: u32,
173    pub min_extra_isize: u16,
174    pub want_extra_isize: u16,
175    pub flags: u32,
176    pub raid_stride: u16,
177    pub mmp_interval: u16,
178    pub mmp_block: u64,
179    pub raid_stripe_width: u32,
180    pub log_groups_per_flex: u8,
181    pub checksum_type: u8,
182    pub reserved_pad: u16,
183    pub kbytes_written: u64,
184
185    // -- 0x180 -- snapshot
186    pub snapshot_inum: u32,
187    pub snapshot_id: u32,
188    pub snapshot_r_blocks_count: u64,
189    pub snapshot_list: u32,
190
191    // -- 0x194 -- error tracking
192    pub error_count: u32,
193    pub first_error_time: u32,
194    pub first_error_ino: u32,
195    pub first_error_block: u64,
196    pub first_error_func: [u8; 32],
197    pub first_error_line: u32,
198    pub last_error_time: u32,
199    pub last_error_ino: u32,
200    pub last_error_line: u32,
201    pub last_error_block: u64,
202    pub last_error_func: [u8; 32],
203
204    // -- 0x200 --
205    pub mount_opts: [u8; 64],
206
207    // -- 0x240 --
208    pub usr_quota_inum: u32,
209    pub grp_quota_inum: u32,
210    pub overhead_blocks: u32,
211    pub backup_bgs: [u32; 2],
212    pub encrypt_algos: [u8; 4],
213    pub encrypt_pw_salt: [u8; 16],
214    pub lpf_ino: u32,
215    pub prj_quota_inum: u32,
216    pub checksum_seed: u32,
217
218    // -- 0x274 -- high-resolution timestamps
219    pub wtime_hi: u8,
220    pub mtime_hi: u8,
221    pub mkfs_time_hi: u8,
222    pub lastcheck_hi: u8,
223    pub first_error_time_hi: u8,
224    pub last_error_time_hi: u8,
225    pub pad: [u8; 2],
226
227    // -- 0x27C --
228    pub reserved: [u32; 96],
229
230    // -- 0x3FC --
231    pub checksum: u32,
232}
233
234impl Default for SuperBlock {
235    fn default() -> Self {
236        Self {
237            inodes_count: 0,
238            blocks_count_lo: 0,
239            r_blocks_count_lo: 0,
240            free_blocks_count_lo: 0,
241            free_inodes_count: 0,
242            first_data_block: 0,
243            log_block_size: 0,
244            log_cluster_size: 0,
245            blocks_per_group: 0,
246            clusters_per_group: 0,
247            inodes_per_group: 0,
248            mtime: 0,
249            wtime: 0,
250            mount_count: 0,
251            max_mount_count: 0,
252            magic: 0,
253            state: 0,
254            errors: 0,
255            minor_rev_level: 0,
256            lastcheck: 0,
257            check_interval: 0,
258            creator_os: 0,
259            rev_level: 0,
260            def_resuid: 0,
261            def_resgid: 0,
262            first_ino: 0,
263            inode_size: 0,
264            block_group_nr: 0,
265            feature_compat: 0,
266            feature_incompat: 0,
267            feature_ro_compat: 0,
268            uuid: [0; 16],
269            volume_name: [0; 16],
270            last_mounted: [0; 64],
271            algorithm_usage_bitmap: 0,
272            prealloc_blocks: 0,
273            prealloc_dir_blocks: 0,
274            reserved_gdt_blocks: 0,
275            journal_uuid: [0; 16],
276            journal_inum: 0,
277            journal_dev: 0,
278            last_orphan: 0,
279            hash_seed: [0; 4],
280            def_hash_version: 0,
281            journal_backup_type: 0,
282            desc_size: 0,
283            default_mount_opts: 0,
284            first_meta_bg: 0,
285            mkfs_time: 0,
286            journal_blocks: [0; 17],
287            blocks_count_hi: 0,
288            r_blocks_count_hi: 0,
289            free_blocks_count_hi: 0,
290            min_extra_isize: 0,
291            want_extra_isize: 0,
292            flags: 0,
293            raid_stride: 0,
294            mmp_interval: 0,
295            mmp_block: 0,
296            raid_stripe_width: 0,
297            log_groups_per_flex: 0,
298            checksum_type: 0,
299            reserved_pad: 0,
300            kbytes_written: 0,
301            snapshot_inum: 0,
302            snapshot_id: 0,
303            snapshot_r_blocks_count: 0,
304            snapshot_list: 0,
305            error_count: 0,
306            first_error_time: 0,
307            first_error_ino: 0,
308            first_error_block: 0,
309            first_error_func: [0; 32],
310            first_error_line: 0,
311            last_error_time: 0,
312            last_error_ino: 0,
313            last_error_line: 0,
314            last_error_block: 0,
315            last_error_func: [0; 32],
316            mount_opts: [0; 64],
317            usr_quota_inum: 0,
318            grp_quota_inum: 0,
319            overhead_blocks: 0,
320            backup_bgs: [0; 2],
321            encrypt_algos: [0; 4],
322            encrypt_pw_salt: [0; 16],
323            lpf_ino: 0,
324            prj_quota_inum: 0,
325            checksum_seed: 0,
326            wtime_hi: 0,
327            mtime_hi: 0,
328            mkfs_time_hi: 0,
329            lastcheck_hi: 0,
330            first_error_time_hi: 0,
331            last_error_time_hi: 0,
332            pad: [0; 2],
333            reserved: [0; 96],
334            checksum: 0,
335        }
336    }
337}
338
339impl SuperBlock {
340    pub const SIZE: usize = SUPERBLOCK_SIZE;
341
342    /// Deserialize a superblock from a 1024-byte buffer.
343    pub fn read_from(buf: &[u8]) -> Self {
344        debug_assert!(buf.len() >= Self::SIZE);
345
346        let mut hash_seed = [0u32; 4];
347        for i in 0..4 {
348            hash_seed[i] = get_u32(buf, 0xEC + i * 4);
349        }
350
351        let mut journal_blocks = [0u32; 17];
352        for i in 0..17 {
353            journal_blocks[i] = get_u32(buf, 0x10C + i * 4);
354        }
355
356        let mut backup_bgs = [0u32; 2];
357        backup_bgs[0] = get_u32(buf, 0x24C);
358        backup_bgs[1] = get_u32(buf, 0x250);
359
360        let mut reserved = [0u32; 96];
361        for i in 0..96 {
362            reserved[i] = get_u32(buf, 0x27C + i * 4);
363        }
364
365        Self {
366            inodes_count: get_u32(buf, 0x00),
367            blocks_count_lo: get_u32(buf, 0x04),
368            r_blocks_count_lo: get_u32(buf, 0x08),
369            free_blocks_count_lo: get_u32(buf, 0x0C),
370            free_inodes_count: get_u32(buf, 0x10),
371            first_data_block: get_u32(buf, 0x14),
372            log_block_size: get_u32(buf, 0x18),
373            log_cluster_size: get_u32(buf, 0x1C),
374            blocks_per_group: get_u32(buf, 0x20),
375            clusters_per_group: get_u32(buf, 0x24),
376            inodes_per_group: get_u32(buf, 0x28),
377            mtime: get_u32(buf, 0x2C),
378            wtime: get_u32(buf, 0x30),
379            mount_count: get_u16(buf, 0x34),
380            max_mount_count: get_u16(buf, 0x36),
381            magic: get_u16(buf, 0x38),
382            state: get_u16(buf, 0x3A),
383            errors: get_u16(buf, 0x3C),
384            minor_rev_level: get_u16(buf, 0x3E),
385            lastcheck: get_u32(buf, 0x40),
386            check_interval: get_u32(buf, 0x44),
387            creator_os: get_u32(buf, 0x48),
388            rev_level: get_u32(buf, 0x4C),
389            def_resuid: get_u16(buf, 0x50),
390            def_resgid: get_u16(buf, 0x52),
391            first_ino: get_u32(buf, 0x54),
392            inode_size: get_u16(buf, 0x58),
393            block_group_nr: get_u16(buf, 0x5A),
394            feature_compat: get_u32(buf, 0x5C),
395            feature_incompat: get_u32(buf, 0x60),
396            feature_ro_compat: get_u32(buf, 0x64),
397            uuid: get_bytes(buf, 0x68),
398            volume_name: get_bytes(buf, 0x78),
399            last_mounted: get_bytes(buf, 0x88),
400            algorithm_usage_bitmap: get_u32(buf, 0xC8),
401            prealloc_blocks: get_u8(buf, 0xCC),
402            prealloc_dir_blocks: get_u8(buf, 0xCD),
403            reserved_gdt_blocks: get_u16(buf, 0xCE),
404            journal_uuid: get_bytes(buf, 0xD0),
405            journal_inum: get_u32(buf, 0xE0),
406            journal_dev: get_u32(buf, 0xE4),
407            last_orphan: get_u32(buf, 0xE8),
408            hash_seed,
409            def_hash_version: get_u8(buf, 0xFC),
410            journal_backup_type: get_u8(buf, 0xFD),
411            desc_size: get_u16(buf, 0xFE),
412            default_mount_opts: get_u32(buf, 0x100),
413            first_meta_bg: get_u32(buf, 0x104),
414            mkfs_time: get_u32(buf, 0x108),
415            journal_blocks,
416            blocks_count_hi: get_u32(buf, 0x150),
417            r_blocks_count_hi: get_u32(buf, 0x154),
418            free_blocks_count_hi: get_u32(buf, 0x158),
419            min_extra_isize: get_u16(buf, 0x15C),
420            want_extra_isize: get_u16(buf, 0x15E),
421            flags: get_u32(buf, 0x160),
422            raid_stride: get_u16(buf, 0x164),
423            mmp_interval: get_u16(buf, 0x166),
424            mmp_block: get_u64(buf, 0x168),
425            raid_stripe_width: get_u32(buf, 0x170),
426            log_groups_per_flex: get_u8(buf, 0x174),
427            checksum_type: get_u8(buf, 0x175),
428            reserved_pad: get_u16(buf, 0x176),
429            kbytes_written: get_u64(buf, 0x178),
430            snapshot_inum: get_u32(buf, 0x180),
431            snapshot_id: get_u32(buf, 0x184),
432            snapshot_r_blocks_count: get_u64(buf, 0x188),
433            snapshot_list: get_u32(buf, 0x190),
434            error_count: get_u32(buf, 0x194),
435            first_error_time: get_u32(buf, 0x198),
436            first_error_ino: get_u32(buf, 0x19C),
437            first_error_block: get_u64(buf, 0x1A0),
438            first_error_func: get_bytes(buf, 0x1A8),
439            first_error_line: get_u32(buf, 0x1C8),
440            last_error_time: get_u32(buf, 0x1CC),
441            last_error_ino: get_u32(buf, 0x1D0),
442            last_error_line: get_u32(buf, 0x1D4),
443            last_error_block: get_u64(buf, 0x1D8),
444            last_error_func: get_bytes(buf, 0x1E0),
445            mount_opts: get_bytes(buf, 0x200),
446            usr_quota_inum: get_u32(buf, 0x240),
447            grp_quota_inum: get_u32(buf, 0x244),
448            overhead_blocks: get_u32(buf, 0x248),
449            backup_bgs,
450            encrypt_algos: get_bytes(buf, 0x254),
451            encrypt_pw_salt: get_bytes(buf, 0x258),
452            lpf_ino: get_u32(buf, 0x268),
453            prj_quota_inum: get_u32(buf, 0x26C),
454            checksum_seed: get_u32(buf, 0x270),
455            wtime_hi: get_u8(buf, 0x274),
456            mtime_hi: get_u8(buf, 0x275),
457            mkfs_time_hi: get_u8(buf, 0x276),
458            lastcheck_hi: get_u8(buf, 0x277),
459            first_error_time_hi: get_u8(buf, 0x278),
460            last_error_time_hi: get_u8(buf, 0x279),
461            pad: get_bytes(buf, 0x27A),
462            reserved,
463            checksum: get_u32(buf, 0x3FC),
464        }
465    }
466
467    /// Serialize this superblock into a 1024-byte buffer.
468    pub fn write_to(&self, buf: &mut [u8]) {
469        debug_assert!(buf.len() >= Self::SIZE);
470
471        // Zero the entire buffer first so padding / reserved areas are clean.
472        buf[..Self::SIZE].fill(0);
473
474        put_u32(buf, 0x00, self.inodes_count);
475        put_u32(buf, 0x04, self.blocks_count_lo);
476        put_u32(buf, 0x08, self.r_blocks_count_lo);
477        put_u32(buf, 0x0C, self.free_blocks_count_lo);
478        put_u32(buf, 0x10, self.free_inodes_count);
479        put_u32(buf, 0x14, self.first_data_block);
480        put_u32(buf, 0x18, self.log_block_size);
481        put_u32(buf, 0x1C, self.log_cluster_size);
482        put_u32(buf, 0x20, self.blocks_per_group);
483        put_u32(buf, 0x24, self.clusters_per_group);
484        put_u32(buf, 0x28, self.inodes_per_group);
485        put_u32(buf, 0x2C, self.mtime);
486        put_u32(buf, 0x30, self.wtime);
487        put_u16(buf, 0x34, self.mount_count);
488        put_u16(buf, 0x36, self.max_mount_count);
489        put_u16(buf, 0x38, self.magic);
490        put_u16(buf, 0x3A, self.state);
491        put_u16(buf, 0x3C, self.errors);
492        put_u16(buf, 0x3E, self.minor_rev_level);
493        put_u32(buf, 0x40, self.lastcheck);
494        put_u32(buf, 0x44, self.check_interval);
495        put_u32(buf, 0x48, self.creator_os);
496        put_u32(buf, 0x4C, self.rev_level);
497        put_u16(buf, 0x50, self.def_resuid);
498        put_u16(buf, 0x52, self.def_resgid);
499        put_u32(buf, 0x54, self.first_ino);
500        put_u16(buf, 0x58, self.inode_size);
501        put_u16(buf, 0x5A, self.block_group_nr);
502        put_u32(buf, 0x5C, self.feature_compat);
503        put_u32(buf, 0x60, self.feature_incompat);
504        put_u32(buf, 0x64, self.feature_ro_compat);
505        put_bytes(buf, 0x68, &self.uuid);
506        put_bytes(buf, 0x78, &self.volume_name);
507        put_bytes(buf, 0x88, &self.last_mounted);
508        put_u32(buf, 0xC8, self.algorithm_usage_bitmap);
509        put_u8(buf, 0xCC, self.prealloc_blocks);
510        put_u8(buf, 0xCD, self.prealloc_dir_blocks);
511        put_u16(buf, 0xCE, self.reserved_gdt_blocks);
512        put_bytes(buf, 0xD0, &self.journal_uuid);
513        put_u32(buf, 0xE0, self.journal_inum);
514        put_u32(buf, 0xE4, self.journal_dev);
515        put_u32(buf, 0xE8, self.last_orphan);
516        for i in 0..4 {
517            put_u32(buf, 0xEC + i * 4, self.hash_seed[i]);
518        }
519        put_u8(buf, 0xFC, self.def_hash_version);
520        put_u8(buf, 0xFD, self.journal_backup_type);
521        put_u16(buf, 0xFE, self.desc_size);
522        put_u32(buf, 0x100, self.default_mount_opts);
523        put_u32(buf, 0x104, self.first_meta_bg);
524        put_u32(buf, 0x108, self.mkfs_time);
525        for i in 0..17 {
526            put_u32(buf, 0x10C + i * 4, self.journal_blocks[i]);
527        }
528        put_u32(buf, 0x150, self.blocks_count_hi);
529        put_u32(buf, 0x154, self.r_blocks_count_hi);
530        put_u32(buf, 0x158, self.free_blocks_count_hi);
531        put_u16(buf, 0x15C, self.min_extra_isize);
532        put_u16(buf, 0x15E, self.want_extra_isize);
533        put_u32(buf, 0x160, self.flags);
534        put_u16(buf, 0x164, self.raid_stride);
535        put_u16(buf, 0x166, self.mmp_interval);
536        put_u64(buf, 0x168, self.mmp_block);
537        put_u32(buf, 0x170, self.raid_stripe_width);
538        put_u8(buf, 0x174, self.log_groups_per_flex);
539        put_u8(buf, 0x175, self.checksum_type);
540        put_u16(buf, 0x176, self.reserved_pad);
541        put_u64(buf, 0x178, self.kbytes_written);
542        put_u32(buf, 0x180, self.snapshot_inum);
543        put_u32(buf, 0x184, self.snapshot_id);
544        put_u64(buf, 0x188, self.snapshot_r_blocks_count);
545        put_u32(buf, 0x190, self.snapshot_list);
546        put_u32(buf, 0x194, self.error_count);
547        put_u32(buf, 0x198, self.first_error_time);
548        put_u32(buf, 0x19C, self.first_error_ino);
549        put_u64(buf, 0x1A0, self.first_error_block);
550        put_bytes(buf, 0x1A8, &self.first_error_func);
551        put_u32(buf, 0x1C8, self.first_error_line);
552        put_u32(buf, 0x1CC, self.last_error_time);
553        put_u32(buf, 0x1D0, self.last_error_ino);
554        put_u32(buf, 0x1D4, self.last_error_line);
555        put_u64(buf, 0x1D8, self.last_error_block);
556        put_bytes(buf, 0x1E0, &self.last_error_func);
557        put_bytes(buf, 0x200, &self.mount_opts);
558        put_u32(buf, 0x240, self.usr_quota_inum);
559        put_u32(buf, 0x244, self.grp_quota_inum);
560        put_u32(buf, 0x248, self.overhead_blocks);
561        put_u32(buf, 0x24C, self.backup_bgs[0]);
562        put_u32(buf, 0x250, self.backup_bgs[1]);
563        put_bytes(buf, 0x254, &self.encrypt_algos);
564        put_bytes(buf, 0x258, &self.encrypt_pw_salt);
565        put_u32(buf, 0x268, self.lpf_ino);
566        put_u32(buf, 0x26C, self.prj_quota_inum);
567        put_u32(buf, 0x270, self.checksum_seed);
568        put_u8(buf, 0x274, self.wtime_hi);
569        put_u8(buf, 0x275, self.mtime_hi);
570        put_u8(buf, 0x276, self.mkfs_time_hi);
571        put_u8(buf, 0x277, self.lastcheck_hi);
572        put_u8(buf, 0x278, self.first_error_time_hi);
573        put_u8(buf, 0x279, self.last_error_time_hi);
574        put_bytes(buf, 0x27A, &self.pad);
575        for i in 0..96 {
576            put_u32(buf, 0x27C + i * 4, self.reserved[i]);
577        }
578        put_u32(buf, 0x3FC, self.checksum);
579    }
580}
581
582// ===========================================================================
583// 2. GroupDescriptor (32 bytes -- the basic descriptor without 64-bit fields)
584// ===========================================================================
585
586/// Block group descriptor (32-byte variant).
587///
588/// When the `INCOMPAT_64BIT` feature is set *and* `desc_size >= 64`, the
589/// kernel uses an extended 64-byte descriptor.  We keep the 32-byte base
590/// here; callers can layer the hi-word fields on top when needed.
591#[derive(Debug, Clone, Default)]
592pub struct GroupDescriptor {
593    pub block_bitmap_lo: u32,
594    pub inode_bitmap_lo: u32,
595    pub inode_table_lo: u32,
596    pub free_blocks_count_lo: u16,
597    pub free_inodes_count_lo: u16,
598    pub used_dirs_count_lo: u16,
599    pub flags: u16,
600    pub exclude_bitmap_lo: u32,
601    pub block_bitmap_csum_lo: u16,
602    pub inode_bitmap_csum_lo: u16,
603    pub itable_unused_lo: u16,
604    pub checksum: u16,
605}
606
607impl GroupDescriptor {
608    pub const SIZE: usize = 32;
609
610    pub fn read_from(buf: &[u8]) -> Self {
611        debug_assert!(buf.len() >= Self::SIZE);
612        Self {
613            block_bitmap_lo: get_u32(buf, 0x00),
614            inode_bitmap_lo: get_u32(buf, 0x04),
615            inode_table_lo: get_u32(buf, 0x08),
616            free_blocks_count_lo: get_u16(buf, 0x0C),
617            free_inodes_count_lo: get_u16(buf, 0x0E),
618            used_dirs_count_lo: get_u16(buf, 0x10),
619            flags: get_u16(buf, 0x12),
620            exclude_bitmap_lo: get_u32(buf, 0x14),
621            block_bitmap_csum_lo: get_u16(buf, 0x18),
622            inode_bitmap_csum_lo: get_u16(buf, 0x1A),
623            itable_unused_lo: get_u16(buf, 0x1C),
624            checksum: get_u16(buf, 0x1E),
625        }
626    }
627
628    pub fn write_to(&self, buf: &mut [u8]) {
629        debug_assert!(buf.len() >= Self::SIZE);
630        buf[..Self::SIZE].fill(0);
631
632        put_u32(buf, 0x00, self.block_bitmap_lo);
633        put_u32(buf, 0x04, self.inode_bitmap_lo);
634        put_u32(buf, 0x08, self.inode_table_lo);
635        put_u16(buf, 0x0C, self.free_blocks_count_lo);
636        put_u16(buf, 0x0E, self.free_inodes_count_lo);
637        put_u16(buf, 0x10, self.used_dirs_count_lo);
638        put_u16(buf, 0x12, self.flags);
639        put_u32(buf, 0x14, self.exclude_bitmap_lo);
640        put_u16(buf, 0x18, self.block_bitmap_csum_lo);
641        put_u16(buf, 0x1A, self.inode_bitmap_csum_lo);
642        put_u16(buf, 0x1C, self.itable_unused_lo);
643        put_u16(buf, 0x1E, self.checksum);
644    }
645}
646
647// ===========================================================================
648// 3. Inode (256 bytes = 160 base + 96 inline xattrs)
649// ===========================================================================
650
651#[derive(Debug, Clone)]
652pub struct Inode {
653    // -- byte 0 --
654    pub mode: u16,
655    pub uid: u16,
656    pub size_lo: u32,
657    pub atime: u32,
658    pub ctime: u32,
659    pub mtime: u32,
660    pub dtime: u32,
661    pub gid: u16,
662    pub links_count: u16,
663    pub blocks_lo: u32,
664    pub flags: u32,
665    pub version: u32,
666
667    /// 60-byte block field: extent tree root or inline symlink data.
668    pub block: [u8; INODE_BLOCK_SIZE],
669
670    pub generation: u32,
671    pub xattr_block_lo: u32,
672    pub size_hi: u32,
673    pub obsolete_fragment_addr: u32,
674
675    // -- OS-dependent 2 (osd2) --
676    pub blocks_hi: u16,
677    pub xattr_block_hi: u16,
678    pub uid_hi: u16,
679    pub gid_hi: u16,
680    pub checksum_lo: u16,
681    pub reserved: u16,
682
683    // -- extra fields (requires extra_isize >= 32) --
684    pub extra_isize: u16,
685    pub checksum_hi: u16,
686    pub ctime_extra: u32,
687    pub mtime_extra: u32,
688    pub atime_extra: u32,
689    pub crtime: u32,
690    pub crtime_extra: u32,
691    pub version_hi: u32,
692    pub projid: u32,
693
694    /// Inline extended attribute space (fills out to 256 bytes).
695    pub inline_xattrs: [u8; 96],
696}
697
698impl Default for Inode {
699    fn default() -> Self {
700        Self {
701            mode: 0,
702            uid: 0,
703            size_lo: 0,
704            atime: 0,
705            ctime: 0,
706            mtime: 0,
707            dtime: 0,
708            gid: 0,
709            links_count: 0,
710            blocks_lo: 0,
711            flags: 0,
712            version: 0,
713            block: [0; INODE_BLOCK_SIZE],
714            generation: 0,
715            xattr_block_lo: 0,
716            size_hi: 0,
717            obsolete_fragment_addr: 0,
718            blocks_hi: 0,
719            xattr_block_hi: 0,
720            uid_hi: 0,
721            gid_hi: 0,
722            checksum_lo: 0,
723            reserved: 0,
724            extra_isize: 0,
725            checksum_hi: 0,
726            ctime_extra: 0,
727            mtime_extra: 0,
728            atime_extra: 0,
729            crtime: 0,
730            crtime_extra: 0,
731            version_hi: 0,
732            projid: 0,
733            inline_xattrs: [0; 96],
734        }
735    }
736}
737
738impl Inode {
739    /// Total on-disk size (base 160 + 96 bytes inline xattrs = 256).
740    pub const SIZE: usize = INODE_SIZE as usize;
741
742    // -- Offset constants within the 160-byte base --------------------------
743
744    const OFF_MODE: usize = 0x00;
745    const OFF_UID: usize = 0x02;
746    const OFF_SIZE_LO: usize = 0x04;
747    const OFF_ATIME: usize = 0x08;
748    const OFF_CTIME: usize = 0x0C;
749    const OFF_MTIME: usize = 0x10;
750    const OFF_DTIME: usize = 0x14;
751    const OFF_GID: usize = 0x18;
752    const OFF_LINKS: usize = 0x1A;
753    const OFF_BLOCKS_LO: usize = 0x1C;
754    const OFF_FLAGS: usize = 0x20;
755    const OFF_VERSION: usize = 0x24;
756    const OFF_BLOCK: usize = 0x28; // 60 bytes
757    const OFF_GENERATION: usize = 0x64;
758    const OFF_XATTR_LO: usize = 0x68;
759    const OFF_SIZE_HI: usize = 0x6C;
760    const OFF_FRAG_ADDR: usize = 0x70;
761    // osd2
762    const OFF_BLOCKS_HI: usize = 0x74;
763    const OFF_XATTR_HI: usize = 0x76;
764    const OFF_UID_HI: usize = 0x78;
765    const OFF_GID_HI: usize = 0x7A;
766    const OFF_CSUM_LO: usize = 0x7C;
767    const OFF_RESERVED: usize = 0x7E;
768    // extra isize region starts at 128
769    const OFF_EXTRA_ISIZE: usize = 0x80;
770    const OFF_CSUM_HI: usize = 0x82;
771    const OFF_CTIME_EXTRA: usize = 0x84;
772    const OFF_MTIME_EXTRA: usize = 0x88;
773    const OFF_ATIME_EXTRA: usize = 0x8C;
774    const OFF_CRTIME: usize = 0x90;
775    const OFF_CRTIME_EXTRA: usize = 0x94;
776    const OFF_VERSION_HI: usize = 0x98;
777    const OFF_PROJID: usize = 0x9C;
778    // inline xattrs start at 160
779    const OFF_INLINE_XATTRS: usize = INODE_ACTUAL_SIZE as usize;
780
781    // -- Serialization ------------------------------------------------------
782
783    pub fn read_from(buf: &[u8]) -> Self {
784        debug_assert!(buf.len() >= Self::SIZE);
785        Self {
786            mode: get_u16(buf, Self::OFF_MODE),
787            uid: get_u16(buf, Self::OFF_UID),
788            size_lo: get_u32(buf, Self::OFF_SIZE_LO),
789            atime: get_u32(buf, Self::OFF_ATIME),
790            ctime: get_u32(buf, Self::OFF_CTIME),
791            mtime: get_u32(buf, Self::OFF_MTIME),
792            dtime: get_u32(buf, Self::OFF_DTIME),
793            gid: get_u16(buf, Self::OFF_GID),
794            links_count: get_u16(buf, Self::OFF_LINKS),
795            blocks_lo: get_u32(buf, Self::OFF_BLOCKS_LO),
796            flags: get_u32(buf, Self::OFF_FLAGS),
797            version: get_u32(buf, Self::OFF_VERSION),
798            block: get_bytes(buf, Self::OFF_BLOCK),
799            generation: get_u32(buf, Self::OFF_GENERATION),
800            xattr_block_lo: get_u32(buf, Self::OFF_XATTR_LO),
801            size_hi: get_u32(buf, Self::OFF_SIZE_HI),
802            obsolete_fragment_addr: get_u32(buf, Self::OFF_FRAG_ADDR),
803            blocks_hi: get_u16(buf, Self::OFF_BLOCKS_HI),
804            xattr_block_hi: get_u16(buf, Self::OFF_XATTR_HI),
805            uid_hi: get_u16(buf, Self::OFF_UID_HI),
806            gid_hi: get_u16(buf, Self::OFF_GID_HI),
807            checksum_lo: get_u16(buf, Self::OFF_CSUM_LO),
808            reserved: get_u16(buf, Self::OFF_RESERVED),
809            extra_isize: get_u16(buf, Self::OFF_EXTRA_ISIZE),
810            checksum_hi: get_u16(buf, Self::OFF_CSUM_HI),
811            ctime_extra: get_u32(buf, Self::OFF_CTIME_EXTRA),
812            mtime_extra: get_u32(buf, Self::OFF_MTIME_EXTRA),
813            atime_extra: get_u32(buf, Self::OFF_ATIME_EXTRA),
814            crtime: get_u32(buf, Self::OFF_CRTIME),
815            crtime_extra: get_u32(buf, Self::OFF_CRTIME_EXTRA),
816            version_hi: get_u32(buf, Self::OFF_VERSION_HI),
817            projid: get_u32(buf, Self::OFF_PROJID),
818            inline_xattrs: get_bytes(buf, Self::OFF_INLINE_XATTRS),
819        }
820    }
821
822    pub fn write_to(&self, buf: &mut [u8]) {
823        debug_assert!(buf.len() >= Self::SIZE);
824        buf[..Self::SIZE].fill(0);
825
826        put_u16(buf, Self::OFF_MODE, self.mode);
827        put_u16(buf, Self::OFF_UID, self.uid);
828        put_u32(buf, Self::OFF_SIZE_LO, self.size_lo);
829        put_u32(buf, Self::OFF_ATIME, self.atime);
830        put_u32(buf, Self::OFF_CTIME, self.ctime);
831        put_u32(buf, Self::OFF_MTIME, self.mtime);
832        put_u32(buf, Self::OFF_DTIME, self.dtime);
833        put_u16(buf, Self::OFF_GID, self.gid);
834        put_u16(buf, Self::OFF_LINKS, self.links_count);
835        put_u32(buf, Self::OFF_BLOCKS_LO, self.blocks_lo);
836        put_u32(buf, Self::OFF_FLAGS, self.flags);
837        put_u32(buf, Self::OFF_VERSION, self.version);
838        put_bytes(buf, Self::OFF_BLOCK, &self.block);
839        put_u32(buf, Self::OFF_GENERATION, self.generation);
840        put_u32(buf, Self::OFF_XATTR_LO, self.xattr_block_lo);
841        put_u32(buf, Self::OFF_SIZE_HI, self.size_hi);
842        put_u32(buf, Self::OFF_FRAG_ADDR, self.obsolete_fragment_addr);
843        put_u16(buf, Self::OFF_BLOCKS_HI, self.blocks_hi);
844        put_u16(buf, Self::OFF_XATTR_HI, self.xattr_block_hi);
845        put_u16(buf, Self::OFF_UID_HI, self.uid_hi);
846        put_u16(buf, Self::OFF_GID_HI, self.gid_hi);
847        put_u16(buf, Self::OFF_CSUM_LO, self.checksum_lo);
848        put_u16(buf, Self::OFF_RESERVED, self.reserved);
849        put_u16(buf, Self::OFF_EXTRA_ISIZE, self.extra_isize);
850        put_u16(buf, Self::OFF_CSUM_HI, self.checksum_hi);
851        put_u32(buf, Self::OFF_CTIME_EXTRA, self.ctime_extra);
852        put_u32(buf, Self::OFF_MTIME_EXTRA, self.mtime_extra);
853        put_u32(buf, Self::OFF_ATIME_EXTRA, self.atime_extra);
854        put_u32(buf, Self::OFF_CRTIME, self.crtime);
855        put_u32(buf, Self::OFF_CRTIME_EXTRA, self.crtime_extra);
856        put_u32(buf, Self::OFF_VERSION_HI, self.version_hi);
857        put_u32(buf, Self::OFF_PROJID, self.projid);
858        put_bytes(buf, Self::OFF_INLINE_XATTRS, &self.inline_xattrs);
859    }
860
861    // -- Constructors -------------------------------------------------------
862
863    /// Create the root directory inode (inode 2).
864    ///
865    /// Sets `S_IFDIR | 0o755`, two links (`.` and `..`), `HUGE_FILE` flag,
866    /// and timestamps to the current wall-clock time.
867    pub fn root_inode() -> Self {
868        let (time_lo, time_extra) = timestamp_now();
869        Self {
870            mode: file_mode::S_IFDIR | 0o755,
871            links_count: 2,
872            flags: inode_flags::HUGE_FILE,
873            extra_isize: EXTRA_ISIZE,
874            atime: time_lo,
875            ctime: time_lo,
876            mtime: time_lo,
877            crtime: time_lo,
878            atime_extra: time_extra,
879            ctime_extra: time_extra,
880            mtime_extra: time_extra,
881            crtime_extra: time_extra,
882            ..Self::default()
883        }
884    }
885
886    // -- Type queries -------------------------------------------------------
887
888    /// True if this inode represents a directory.
889    #[inline]
890    pub fn is_dir(&self) -> bool {
891        is_dir(self.mode)
892    }
893
894    /// True if this inode represents a regular file.
895    #[inline]
896    pub fn is_reg(&self) -> bool {
897        is_reg(self.mode)
898    }
899
900    /// True if this inode represents a symbolic link.
901    #[inline]
902    pub fn is_link(&self) -> bool {
903        is_link(self.mode)
904    }
905
906    // -- Size helpers -------------------------------------------------------
907
908    /// Combine `size_lo` and `size_hi` into a 64-bit file size.
909    #[inline]
910    pub fn file_size(&self) -> u64 {
911        (self.size_lo as u64) | ((self.size_hi as u64) << 32)
912    }
913
914    /// Set the 64-bit file size, splitting into `size_lo` / `size_hi`.
915    #[inline]
916    pub fn set_file_size(&mut self, size: u64) {
917        self.size_lo = size as u32;
918        self.size_hi = (size >> 32) as u32;
919    }
920
921    // -- UID / GID helpers --------------------------------------------------
922
923    /// Full 32-bit UID (lo + hi).
924    #[inline]
925    pub fn uid_full(&self) -> u32 {
926        (self.uid as u32) | ((self.uid_hi as u32) << 16)
927    }
928
929    /// Full 32-bit GID (lo + hi).
930    #[inline]
931    pub fn gid_full(&self) -> u32 {
932        (self.gid as u32) | ((self.gid_hi as u32) << 16)
933    }
934
935    /// Set the full 32-bit UID, splitting into lo / hi.
936    #[inline]
937    pub fn set_uid(&mut self, uid: u32) {
938        self.uid = uid as u16;
939        self.uid_hi = (uid >> 16) as u16;
940    }
941
942    /// Set the full 32-bit GID, splitting into lo / hi.
943    #[inline]
944    pub fn set_gid(&mut self, gid: u32) {
945        self.gid = gid as u16;
946        self.gid_hi = (gid >> 16) as u16;
947    }
948}
949
950// ===========================================================================
951// 4. ExtentHeader (12 bytes)
952// ===========================================================================
953
954/// Header of an ext4 extent tree node (root, internal, or leaf).
955#[derive(Debug, Clone, Copy, Default)]
956pub struct ExtentHeader {
957    /// Magic number (`EXTENT_HEADER_MAGIC` = 0xF30A).
958    pub magic: u16,
959    /// Number of valid entries following this header.
960    pub entries: u16,
961    /// Maximum number of entries that could follow this header.
962    pub max: u16,
963    /// Depth of this node in the extent tree (0 = leaf).
964    pub depth: u16,
965    /// Generation of the tree (used by Lustre, not standard ext4).
966    pub generation: u32,
967}
968
969impl ExtentHeader {
970    pub const SIZE: usize = 12;
971
972    pub fn read_from(buf: &[u8]) -> Self {
973        debug_assert!(buf.len() >= Self::SIZE);
974        Self {
975            magic: get_u16(buf, 0),
976            entries: get_u16(buf, 2),
977            max: get_u16(buf, 4),
978            depth: get_u16(buf, 6),
979            generation: get_u32(buf, 8),
980        }
981    }
982
983    pub fn write_to(&self, buf: &mut [u8]) {
984        debug_assert!(buf.len() >= Self::SIZE);
985        put_u16(buf, 0, self.magic);
986        put_u16(buf, 2, self.entries);
987        put_u16(buf, 4, self.max);
988        put_u16(buf, 6, self.depth);
989        put_u32(buf, 8, self.generation);
990    }
991}
992
993// ===========================================================================
994// 5. ExtentLeaf (12 bytes)
995// ===========================================================================
996
997/// A leaf entry in the extent tree, mapping logical blocks to physical blocks.
998#[derive(Debug, Clone, Copy, Default)]
999pub struct ExtentLeaf {
1000    /// First logical block number that this extent covers.
1001    pub block: u32,
1002    /// Number of blocks covered by this extent.
1003    /// If the high bit is set, the extent is uninitialized (pre-allocated).
1004    pub len: u16,
1005    /// Upper 16 bits of the 48-bit physical block number.
1006    pub start_hi: u16,
1007    /// Lower 32 bits of the 48-bit physical block number.
1008    pub start_lo: u32,
1009}
1010
1011impl ExtentLeaf {
1012    pub const SIZE: usize = 12;
1013
1014    pub fn read_from(buf: &[u8]) -> Self {
1015        debug_assert!(buf.len() >= Self::SIZE);
1016        Self {
1017            block: get_u32(buf, 0),
1018            len: get_u16(buf, 4),
1019            start_hi: get_u16(buf, 6),
1020            start_lo: get_u32(buf, 8),
1021        }
1022    }
1023
1024    pub fn write_to(&self, buf: &mut [u8]) {
1025        debug_assert!(buf.len() >= Self::SIZE);
1026        put_u32(buf, 0, self.block);
1027        put_u16(buf, 4, self.len);
1028        put_u16(buf, 6, self.start_hi);
1029        put_u32(buf, 8, self.start_lo);
1030    }
1031
1032    /// Full 48-bit physical start block.
1033    #[inline]
1034    pub fn start(&self) -> u64 {
1035        (self.start_lo as u64) | ((self.start_hi as u64) << 32)
1036    }
1037}
1038
1039// ===========================================================================
1040// 6. ExtentIndex (12 bytes)
1041// ===========================================================================
1042
1043/// An internal (index) entry in the extent tree, pointing to the next level.
1044#[derive(Debug, Clone, Copy, Default)]
1045pub struct ExtentIndex {
1046    /// Logical block number -- this index covers blocks >= `block`.
1047    pub block: u32,
1048    /// Lower 32 bits of the physical block containing the child node.
1049    pub leaf_lo: u32,
1050    /// Upper 16 bits of the physical block containing the child node.
1051    pub leaf_hi: u16,
1052    pub unused: u16,
1053}
1054
1055impl ExtentIndex {
1056    pub const SIZE: usize = 12;
1057
1058    pub fn read_from(buf: &[u8]) -> Self {
1059        debug_assert!(buf.len() >= Self::SIZE);
1060        Self {
1061            block: get_u32(buf, 0),
1062            leaf_lo: get_u32(buf, 4),
1063            leaf_hi: get_u16(buf, 8),
1064            unused: get_u16(buf, 10),
1065        }
1066    }
1067
1068    pub fn write_to(&self, buf: &mut [u8]) {
1069        debug_assert!(buf.len() >= Self::SIZE);
1070        put_u32(buf, 0, self.block);
1071        put_u32(buf, 4, self.leaf_lo);
1072        put_u16(buf, 8, self.leaf_hi);
1073        put_u16(buf, 10, self.unused);
1074    }
1075
1076    /// Full 48-bit physical block of the child node.
1077    #[inline]
1078    pub fn leaf(&self) -> u64 {
1079        (self.leaf_lo as u64) | ((self.leaf_hi as u64) << 32)
1080    }
1081}
1082
1083// ===========================================================================
1084// 7. ExtentTail (4 bytes)
1085// ===========================================================================
1086
1087/// Checksum appended after the last extent entry in a tree block.
1088#[derive(Debug, Clone, Copy, Default)]
1089pub struct ExtentTail {
1090    pub checksum: u32,
1091}
1092
1093impl ExtentTail {
1094    pub const SIZE: usize = 4;
1095
1096    pub fn read_from(buf: &[u8]) -> Self {
1097        debug_assert!(buf.len() >= Self::SIZE);
1098        Self {
1099            checksum: get_u32(buf, 0),
1100        }
1101    }
1102
1103    pub fn write_to(&self, buf: &mut [u8]) {
1104        debug_assert!(buf.len() >= Self::SIZE);
1105        put_u32(buf, 0, self.checksum);
1106    }
1107}
1108
1109// ===========================================================================
1110// 8. DirectoryEntry (8-byte header + variable-length name)
1111// ===========================================================================
1112
1113/// On-disk directory entry header (the name bytes follow immediately).
1114#[derive(Debug, Clone, Default)]
1115pub struct DirectoryEntry {
1116    /// Inode number this entry refers to (0 = deleted / unused).
1117    pub inode: u32,
1118    /// Total size of this directory entry (header + name + padding).
1119    pub rec_len: u16,
1120    /// Length of the file name in bytes.
1121    pub name_len: u8,
1122    /// File type code (see `FileType`).  Only valid when `INCOMPAT_FILETYPE`
1123    /// is enabled; otherwise this byte is the high byte of the old `name_len`
1124    /// field.
1125    pub file_type: u8,
1126}
1127
1128impl DirectoryEntry {
1129    /// Size of the fixed header (name bytes are *not* included).
1130    pub const SIZE: usize = 8;
1131
1132    pub fn read_from(buf: &[u8]) -> Self {
1133        debug_assert!(buf.len() >= Self::SIZE);
1134        Self {
1135            inode: get_u32(buf, 0),
1136            rec_len: get_u16(buf, 4),
1137            name_len: get_u8(buf, 6),
1138            file_type: get_u8(buf, 7),
1139        }
1140    }
1141
1142    pub fn write_to(&self, buf: &mut [u8]) {
1143        debug_assert!(buf.len() >= Self::SIZE);
1144        put_u32(buf, 0, self.inode);
1145        put_u16(buf, 4, self.rec_len);
1146        put_u8(buf, 6, self.name_len);
1147        put_u8(buf, 7, self.file_type);
1148    }
1149}
1150
1151// ===========================================================================
1152// 9. XAttrEntry (16-byte header + variable-length name)
1153// ===========================================================================
1154
1155/// On-disk extended-attribute entry.  The name bytes follow immediately after
1156/// this header, then padding to a 4-byte boundary.
1157#[derive(Debug, Clone, Default)]
1158pub struct XAttrEntry {
1159    /// Length of the attribute name.
1160    pub name_len: u8,
1161    /// Attribute name index (e.g. 1 = "user.", 2 = "system.posix_acl_access").
1162    pub name_index: u8,
1163    /// Offset of the value within the value area (from end of entry table).
1164    pub value_offset: u16,
1165    /// Inode holding the value (0 when value is inline in this block).
1166    pub value_inum: u32,
1167    /// Size of the attribute value in bytes.
1168    pub value_size: u32,
1169    /// Hash of the attribute name and value.
1170    pub hash: u32,
1171}
1172
1173impl XAttrEntry {
1174    /// Size of the fixed header (name bytes are *not* included).
1175    pub const SIZE: usize = 16;
1176
1177    pub fn read_from(buf: &[u8]) -> Self {
1178        debug_assert!(buf.len() >= Self::SIZE);
1179        Self {
1180            name_len: get_u8(buf, 0),
1181            name_index: get_u8(buf, 1),
1182            value_offset: get_u16(buf, 2),
1183            value_inum: get_u32(buf, 4),
1184            value_size: get_u32(buf, 8),
1185            hash: get_u32(buf, 12),
1186        }
1187    }
1188
1189    pub fn write_to(&self, buf: &mut [u8]) {
1190        debug_assert!(buf.len() >= Self::SIZE);
1191        put_u8(buf, 0, self.name_len);
1192        put_u8(buf, 1, self.name_index);
1193        put_u16(buf, 2, self.value_offset);
1194        put_u32(buf, 4, self.value_inum);
1195        put_u32(buf, 8, self.value_size);
1196        put_u32(buf, 12, self.hash);
1197    }
1198}
1199
1200// ===========================================================================
1201// Tests
1202// ===========================================================================
1203
1204#[cfg(test)]
1205mod tests {
1206    use super::*;
1207
1208    #[test]
1209    fn superblock_roundtrip() {
1210        let mut sb = SuperBlock::default();
1211        sb.magic = SUPERBLOCK_MAGIC;
1212        sb.inodes_count = 1024;
1213        sb.blocks_count_lo = 4096;
1214        sb.log_block_size = 2; // 4 KiB blocks
1215        sb.uuid[0] = 0xDE;
1216        sb.uuid[15] = 0xAD;
1217        sb.checksum = 0xCAFE_BABE;
1218
1219        let mut buf = [0u8; SUPERBLOCK_SIZE];
1220        sb.write_to(&mut buf);
1221
1222        let sb2 = SuperBlock::read_from(&buf);
1223        assert_eq!(sb2.magic, SUPERBLOCK_MAGIC);
1224        assert_eq!(sb2.inodes_count, 1024);
1225        assert_eq!(sb2.blocks_count_lo, 4096);
1226        assert_eq!(sb2.log_block_size, 2);
1227        assert_eq!(sb2.uuid[0], 0xDE);
1228        assert_eq!(sb2.uuid[15], 0xAD);
1229        assert_eq!(sb2.checksum, 0xCAFE_BABE);
1230    }
1231
1232    #[test]
1233    fn group_descriptor_roundtrip() {
1234        let gd = GroupDescriptor {
1235            block_bitmap_lo: 100,
1236            inode_bitmap_lo: 101,
1237            inode_table_lo: 102,
1238            free_blocks_count_lo: 500,
1239            free_inodes_count_lo: 200,
1240            used_dirs_count_lo: 3,
1241            flags: bg_flags::INODE_ZEROED,
1242            exclude_bitmap_lo: 0,
1243            block_bitmap_csum_lo: 0x1234,
1244            inode_bitmap_csum_lo: 0x5678,
1245            itable_unused_lo: 190,
1246            checksum: 0xABCD,
1247        };
1248
1249        let mut buf = [0u8; GroupDescriptor::SIZE];
1250        gd.write_to(&mut buf);
1251
1252        let gd2 = GroupDescriptor::read_from(&buf);
1253        assert_eq!(gd2.block_bitmap_lo, 100);
1254        assert_eq!(gd2.free_blocks_count_lo, 500);
1255        assert_eq!(gd2.flags, bg_flags::INODE_ZEROED);
1256        assert_eq!(gd2.checksum, 0xABCD);
1257    }
1258
1259    #[test]
1260    fn inode_roundtrip() {
1261        let mut inode = Inode::default();
1262        inode.mode = file_mode::S_IFREG | 0o644;
1263        inode.set_file_size(0x1_DEAD_BEEF);
1264        inode.set_uid(100_000);
1265        inode.set_gid(200_000);
1266        inode.links_count = 1;
1267        inode.extra_isize = EXTRA_ISIZE;
1268        inode.block[0] = 0xFF;
1269
1270        let mut buf = [0u8; Inode::SIZE];
1271        inode.write_to(&mut buf);
1272
1273        let i2 = Inode::read_from(&buf);
1274        assert!(i2.is_reg());
1275        assert!(!i2.is_dir());
1276        assert!(!i2.is_link());
1277        assert_eq!(i2.file_size(), 0x1_DEAD_BEEF);
1278        assert_eq!(i2.uid_full(), 100_000);
1279        assert_eq!(i2.gid_full(), 200_000);
1280        assert_eq!(i2.links_count, 1);
1281        assert_eq!(i2.block[0], 0xFF);
1282    }
1283
1284    #[test]
1285    fn root_inode_has_correct_fields() {
1286        let root = Inode::root_inode();
1287        assert!(root.is_dir());
1288        assert_eq!(root.links_count, 2);
1289        assert_eq!(root.flags, inode_flags::HUGE_FILE);
1290        assert_eq!(root.extra_isize, EXTRA_ISIZE);
1291        // Timestamps should be non-zero (we just called timestamp_now).
1292        assert_ne!(root.atime, 0);
1293        assert_ne!(root.ctime, 0);
1294        assert_ne!(root.mtime, 0);
1295        assert_ne!(root.crtime, 0);
1296    }
1297
1298    #[test]
1299    fn extent_header_roundtrip() {
1300        let hdr = ExtentHeader {
1301            magic: EXTENT_HEADER_MAGIC,
1302            entries: 3,
1303            max: 4,
1304            depth: 0,
1305            generation: 42,
1306        };
1307
1308        let mut buf = [0u8; ExtentHeader::SIZE];
1309        hdr.write_to(&mut buf);
1310
1311        let hdr2 = ExtentHeader::read_from(&buf);
1312        assert_eq!(hdr2.magic, EXTENT_HEADER_MAGIC);
1313        assert_eq!(hdr2.entries, 3);
1314        assert_eq!(hdr2.max, 4);
1315        assert_eq!(hdr2.depth, 0);
1316        assert_eq!(hdr2.generation, 42);
1317    }
1318
1319    #[test]
1320    fn extent_leaf_roundtrip() {
1321        let ext = ExtentLeaf {
1322            block: 0,
1323            len: 10,
1324            start_hi: 0x00AB,
1325            start_lo: 0xCDEF_0123,
1326        };
1327
1328        let mut buf = [0u8; ExtentLeaf::SIZE];
1329        ext.write_to(&mut buf);
1330
1331        let ext2 = ExtentLeaf::read_from(&buf);
1332        assert_eq!(ext2.block, 0);
1333        assert_eq!(ext2.len, 10);
1334        assert_eq!(ext2.start(), 0x00AB_CDEF_0123);
1335    }
1336
1337    #[test]
1338    fn extent_index_roundtrip() {
1339        let idx = ExtentIndex {
1340            block: 1000,
1341            leaf_lo: 0x1234_5678,
1342            leaf_hi: 0x00FF,
1343            unused: 0,
1344        };
1345
1346        let mut buf = [0u8; ExtentIndex::SIZE];
1347        idx.write_to(&mut buf);
1348
1349        let idx2 = ExtentIndex::read_from(&buf);
1350        assert_eq!(idx2.block, 1000);
1351        assert_eq!(idx2.leaf(), 0x00FF_1234_5678);
1352    }
1353
1354    #[test]
1355    fn extent_tail_roundtrip() {
1356        let tail = ExtentTail {
1357            checksum: 0xDEAD_BEEF,
1358        };
1359
1360        let mut buf = [0u8; ExtentTail::SIZE];
1361        tail.write_to(&mut buf);
1362
1363        let tail2 = ExtentTail::read_from(&buf);
1364        assert_eq!(tail2.checksum, 0xDEAD_BEEF);
1365    }
1366
1367    #[test]
1368    fn directory_entry_roundtrip() {
1369        let de = DirectoryEntry {
1370            inode: 42,
1371            rec_len: 20,
1372            name_len: 5,
1373            file_type: FileType::Regular as u8,
1374        };
1375
1376        let mut buf = [0u8; DirectoryEntry::SIZE];
1377        de.write_to(&mut buf);
1378
1379        let de2 = DirectoryEntry::read_from(&buf);
1380        assert_eq!(de2.inode, 42);
1381        assert_eq!(de2.rec_len, 20);
1382        assert_eq!(de2.name_len, 5);
1383        assert_eq!(de2.file_type, FileType::Regular as u8);
1384    }
1385
1386    #[test]
1387    fn xattr_entry_roundtrip() {
1388        let xa = XAttrEntry {
1389            name_len: 4,
1390            name_index: 1,
1391            value_offset: 0x100,
1392            value_inum: 0,
1393            value_size: 16,
1394            hash: 0xABCD_EF01,
1395        };
1396
1397        let mut buf = [0u8; XAttrEntry::SIZE];
1398        xa.write_to(&mut buf);
1399
1400        let xa2 = XAttrEntry::read_from(&buf);
1401        assert_eq!(xa2.name_len, 4);
1402        assert_eq!(xa2.name_index, 1);
1403        assert_eq!(xa2.value_offset, 0x100);
1404        assert_eq!(xa2.value_size, 16);
1405        assert_eq!(xa2.hash, 0xABCD_EF01);
1406    }
1407
1408    #[test]
1409    fn timestamp_now_produces_sane_values() {
1410        let (lo, extra) = timestamp_now();
1411        // The low 32 bits of seconds should be well past zero (we are past 1970).
1412        assert!(lo > 1_000_000_000);
1413        // The nanosecond part (bits 2..31) should be < 1 billion.
1414        let nanos = extra >> 2;
1415        assert!(nanos < 1_000_000_000);
1416    }
1417
1418    #[test]
1419    fn inode_size_consistency() {
1420        // Make sure our offset constants are self-consistent.
1421        // The inline xattr area starts right after the 160-byte base.
1422        assert_eq!(Inode::OFF_INLINE_XATTRS, 160);
1423        // And the total inode is 256 bytes.
1424        assert_eq!(Inode::SIZE, 256);
1425    }
1426
1427    #[test]
1428    fn superblock_default_is_all_zeros() {
1429        let sb = SuperBlock::default();
1430        let mut buf = [0xFFu8; SUPERBLOCK_SIZE];
1431        sb.write_to(&mut buf);
1432        // Every byte should be zero.
1433        assert!(buf.iter().all(|&b| b == 0));
1434    }
1435}