Skip to main content

fs_ext4/
inode.rs

1//! ext4 inode parsing.
2//!
3//! Spec: docs/ext4-spec/inodes-extents.md
4//!
5//! Base inode is 128 bytes; modern ext4 with EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
6//! adds another 32 bytes (i_extra_isize) for a total of 160 bytes. All fields
7//! little-endian. The high halves of uid/gid/size/file_acl/blocks/checksum live
8//! at the end of the base 128 bytes; nanosecond timestamps + crtime live in the
9//! extra section.
10
11use crate::error::{Error, Result};
12
13/// Minimum on-disk inode size (rev 0).
14pub const INODE_BASE_SIZE: usize = 128;
15/// The size of the original ext2 inode — `EXT2_GOOD_OLD_INODE_SIZE`.
16///
17/// Everything up to here is the fixed part every ext2/3/4 inode has;
18/// anything past it is the `i_extra_isize` region, which only larger
19/// inodes carry. The inode checksum covers every byte of it, so it is the
20/// length below which `Checksummer::verify_inode` refuses. Not because a
21/// shorter buffer cannot hold `i_checksum_lo` — that field ends at 0x7E,
22/// so 126 bytes would — but because it is not the whole inode the
23/// checksum is defined over.
24///
25/// It was declared in `mkfs.rs`, unused, while `checksum.rs` wrote the
26/// bare `128` twice.
27pub const GOOD_OLD_INODE_SIZE: usize = 128;
28
29/// Offset where the i_extra_isize field begins (start of extra section).
30pub const INODE_EXTRA_OFFSET: usize = 128;
31
32// Raw inode field byte offsets (from the start of the on-disk inode, little-endian).
33// Named so build_*_inode helpers can write fields without requiring readers to
34// memorise the ext4 spec layout. Source: docs/ext4-spec/inodes-extents.md.
35pub(crate) const OFF_MODE: usize = 0x00;
36pub(crate) const OFF_SIZE_LO: usize = 0x04;
37pub(crate) const OFF_ATIME: usize = 0x08;
38pub(crate) const OFF_CTIME: usize = 0x0C;
39pub(crate) const OFF_MTIME: usize = 0x10;
40pub(crate) const OFF_LINKS_COUNT: usize = 0x1A;
41pub(crate) const OFF_BLOCKS_LO: usize = 0x1C;
42pub(crate) const OFF_FLAGS: usize = 0x20;
43pub(crate) const OFF_BLOCK: usize = 0x28; // i_block area start (60 bytes, 0x28..0x64)
44pub(crate) const OFF_GENERATION: usize = 0x64;
45pub(crate) const OFF_SIZE_HI: usize = 0x6C;
46pub(crate) const OFF_BLOCKS_HI: usize = 0x74;
47pub(crate) const OFF_CHECKSUM_LO: usize = 0x7C;
48pub(crate) const OFF_EXTRA_ISIZE: usize = 0x80;
49pub(crate) const OFF_CHECKSUM_HI: usize = 0x82;
50pub(crate) const OFF_CRTIME: usize = 0x90;
51
52/// Default i_extra_isize value written into new inodes: covers checksum_hi,
53/// nsec timestamps, and i_crtime (32 bytes beyond the 128-byte base).
54pub(crate) const EXTRA_ISIZE_DEFAULT: u16 = 32;
55/// Minimum inode buffer length for i_crtime (offset 0x90) to be present.
56#[cfg(test)]
57pub(crate) const INODE_SIZE_WITH_CRTIME: usize = 0x94;
58/// Minimum inode buffer length for i_extra_isize + i_checksum_hi.
59pub(crate) const INODE_SIZE_WITH_EXTRA: usize = 0x84;
60
61// POSIX file-type bits (high nibble of i_mode).
62pub const S_IFMT: u16 = 0xF000;
63pub const S_IFREG: u16 = 0x8000;
64pub const S_IFDIR: u16 = 0x4000;
65pub const S_IFLNK: u16 = 0xA000;
66pub const S_IFBLK: u16 = 0x6000;
67pub const S_IFCHR: u16 = 0x2000;
68pub const S_IFIFO: u16 = 0x1000;
69pub const S_IFSOCK: u16 = 0xC000;
70
71bitflags::bitflags! {
72    /// `i_flags` — per-inode behaviour flags.
73    /// Spec: kernel.org/doc/html/latest/filesystems/ext4/inodes.html
74    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
75    pub struct InodeFlags: u32 {
76        /// Secure deletion (unused).
77        const SECRM        = 0x0000_0001;
78        /// Undelete (unused).
79        const UNRM         = 0x0000_0002;
80        /// Compressed file.
81        const COMPR        = 0x0000_0004;
82        /// Synchronous writes.
83        const SYNC         = 0x0000_0008;
84        /// Immutable.
85        const IMMUTABLE    = 0x0000_0010;
86        /// Append-only.
87        const APPEND       = 0x0000_0020;
88        /// Do not dump.
89        const NODUMP       = 0x0000_0040;
90        /// Do not update access time.
91        const NOATIME      = 0x0000_0080;
92        /// fscrypt-encrypted (`EXT4_ENCRYPT_FL`): the contents, or a
93        /// directory's entry names, are ciphertext.
94        const ENCRYPT      = 0x0000_0800;
95        /// Hash-tree-indexed directory.
96        const INDEX        = 0x0000_1000;
97        /// File data stored in extended attributes.
98        const EA_INODE     = 0x0020_0000;
99        /// Inode uses extents (EXT4_EXTENTS_FL).
100        const EXTENTS      = 0x0008_0000;
101        /// Inode stores a huge file (i_blocks counted in fs blocks not 512B sectors).
102        const HUGE_FILE    = 0x0004_0000;
103        /// Inline data — file contents live inside i_block + xattrs.
104        const INLINE_DATA  = 0x1000_0000;
105        /// Alias for EXTENTS (matches kernel naming `EXT4_EXTENTS_FL`).
106        const EXTENT       = 0x0008_0000;
107        /// Inode has extra (nanosecond) timestamp fields.
108        const EXTRA_ATIME  = 0x0000_0100;
109    }
110}
111
112/// The `i_flags` bits a caller may change through `set_flags`.
113///
114/// The kernel's `EXT4_FL_USER_MODIFIABLE`, less the bits it honours only
115/// under conditions this driver does not implement: `EXTENTS` (a
116/// migration that rewrites the block map), `DAX` (a mount option and a
117/// device that supports it) and `CASEFOLD` (an empty directory on a
118/// volume with the casefold feature), and the obsolete `EOFBLOCKS`.
119/// Every other bit — `INDEX`, `HUGE_FILE`, `ENCRYPT`, `VERITY`,
120/// `INLINE_DATA`, `EA_INODE` among them — describes how the bytes the
121/// inode already holds are read, so flipping it alone makes them read
122/// wrong.
123pub const USER_MODIFIABLE_FLAGS: u32 = InodeFlags::SECRM.bits()
124    | InodeFlags::UNRM.bits()
125    | InodeFlags::COMPR.bits()
126    | InodeFlags::SYNC.bits()
127    | InodeFlags::IMMUTABLE.bits()
128    | InodeFlags::APPEND.bits()
129    | InodeFlags::NODUMP.bits()
130    | InodeFlags::NOATIME.bits()
131    | 0x0000_4000 // JOURNAL_DATA
132    | 0x0000_8000 // NOTAIL
133    | 0x0001_0000 // DIRSYNC
134    | 0x0002_0000 // TOPDIR
135    | 0x2000_0000; // PROJINHERIT
136
137/// Parsed ext4 inode.
138///
139/// Combines hi+lo halves for uid, gid, size, file_acl, blocks, and checksum so
140/// callers don't have to reassemble them. Nanosecond timestamps come from the
141/// `*_extra` fields when present (top 30 bits = nsec, low 2 bits = epoch).
142#[derive(Debug, Clone)]
143pub struct Inode {
144    pub mode: u16,
145    pub uid: u32,
146    pub gid: u32,
147    pub size: u64,
148    /// Seconds since the Unix epoch, **signed and 64-bit**.
149    ///
150    /// The on-disk base field is a signed 32-bit value, so dates before
151    /// 1970 are representable and must not be read as far-future ones.
152    /// When `i_extra_isize` is large enough, the low two bits of the
153    /// matching `*_extra` field add whole multiples of 2^32 seconds,
154    /// widening the range from 1901..2038 to 1901..2446. Both are
155    /// applied here; see `unpack_seconds`.
156    pub atime: i64,
157    pub mtime: i64,
158    pub ctime: i64,
159    pub dtime: i64,
160    pub crtime: i64,
161    pub atime_nsec: u32,
162    pub mtime_nsec: u32,
163    pub ctime_nsec: u32,
164    pub crtime_nsec: u32,
165    pub links_count: u16,
166    pub blocks: u64, // 512-byte sectors (per spec; HUGE_FILE flag changes meaning)
167    pub flags: u32,
168    /// Raw 60-byte i_block area — extent header / direct pointers / inline data.
169    /// Parsed by the extent module.
170    pub block: [u8; 60],
171    pub generation: u32,
172    pub file_acl: u64,
173    pub checksum: u32,
174}
175
176// ---------------------------------------------------------------------------
177// Timestamps
178// ---------------------------------------------------------------------------
179//
180// The format (kernel.org ext4 documentation, inodes.html, "Inode
181// Timestamps"): each of atime, ctime, mtime and crtime has a 32-bit base
182// word holding a *signed* count of seconds from 1970-01-01 UTC, and --
183// where `i_extra_isize` reaches it -- a 32-bit companion word. The
184// companion's two low bits count whole 2^32-second "eras" to add to the
185// signed base, and its upper thirty bits are nanoseconds. dtime has no
186// companion.
187//
188// The documentation tabulates the result: era 0 covers 1901-12-13 ..
189// 2038-01-19 (the signed base on its own), and each further era slides
190// that window 2^32 seconds later, so era 3 ends on 2446-05-10. The
191// windows abut, so every second in 1901..2446 has exactly one encoding.
192// A kernel in the harness VM, asked to `touch -d @N` files on a 256-byte
193// inode volume, wrote exactly those words for every boundary tested
194// (`needs_host::the_kernel_writes_and_reads_the_documented_words`), and
195// clamped anything later than 2446 to the last second of era 3.
196
197/// Seconds one era step adds: 2^32.
198const ERA_SECONDS: i64 = 1 << 32;
199
200/// The two low bits of a companion word: the era count.
201const ERA_FIELD: u32 = 0b11;
202
203/// Read a timestamp from its base word and its companion word (pass 0
204/// for the companion when the inode has none).
205///
206/// The base is signed: a base with its top bit set is a date before
207/// 1970 in era 0, and a date 2^32 seconds later than that in each
208/// further era. The nanosecond bits of `extra` are ignored here.
209fn unpack_seconds(base: u32, extra: u32) -> i64 {
210    let signed_base = i64::from(base as i32);
211    let era = i64::from(extra & ERA_FIELD);
212    signed_base + era * ERA_SECONDS
213}
214
215/// Split a count of seconds into the base word and the era bits for
216/// the low end of the companion word -- the inverse of
217/// [`unpack_seconds`] for every value in
218/// [`MIN_ENCODABLE_TIME`]`..=`[`MAX_ENCODABLE_TIME`].
219///
220/// The era is the number of 2^32-second windows the value lies past the
221/// window centred on the epoch, `[-2^31, 2^31)`. Counted in half-windows
222/// of 2^31 (an arithmetic shift, so it floors for negative values too),
223/// that is the half-window index plus one, halved. The base word is
224/// then simply the low 32 bits: the signed reading of those bits plus
225/// the era's 2^32-multiples gives the value back.
226///
227/// 2100-01-01 (4 102 444 800) shows why the era is not just "the bits
228/// above bit 31": it lies in the second window, so it is stored with
229/// era 1 and a base whose signed reading is negative (-192 522 496).
230///
231/// Values outside the encodable range have their era taken modulo four;
232/// callers clamp or refuse first.
233pub(crate) fn pack_seconds(secs: i64) -> (u32, u32) {
234    let half_windows = secs >> 31;
235    let era = (((half_windows + 1) >> 1) as u32) & ERA_FIELD;
236    (secs as u32, era)
237}
238
239/// The earliest second the base-plus-era encoding holds: era 0 with
240/// the most negative base, 1901-12-13 20:45:52 UTC.
241pub(crate) const MIN_ENCODABLE_TIME: i64 = i32::MIN as i64;
242/// The latest: era 3 with the most positive base, 2446-05-10 22:38:55
243/// UTC. The kernel clamps later times to exactly this value.
244pub(crate) const MAX_ENCODABLE_TIME: i64 = i32::MAX as i64 + 3 * ERA_SECONDS;
245
246/// One of an inode's four timestamps, by where its base and its
247/// companion word live.
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub(crate) enum InodeTime {
250    Atime,
251    Ctime,
252    Mtime,
253    Crtime,
254}
255
256impl InodeTime {
257    /// Offset of the 32-bit base.
258    fn base_offset(self) -> usize {
259        match self {
260            InodeTime::Atime => OFF_ATIME,
261            InodeTime::Ctime => OFF_CTIME,
262            InodeTime::Mtime => OFF_MTIME,
263            InodeTime::Crtime => OFF_CRTIME,
264        }
265    }
266
267    /// Offset of the companion word (nanoseconds above, era below).
268    fn extra_offset(self) -> usize {
269        match self {
270            InodeTime::Ctime => 0x84,
271            InodeTime::Mtime => 0x88,
272            InodeTime::Atime => 0x8C,
273            InodeTime::Crtime => 0x94,
274        }
275    }
276}
277
278/// Whether the four bytes at `offset` lie inside the part of the inode
279/// that `i_extra_isize` declares in use (and inside `raw`). A field
280/// past that point is not part of this inode, whatever the inode size.
281fn extra_field_fits(raw: &[u8], offset: usize) -> bool {
282    let Some(size_bytes) = raw.get(OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2) else {
283        return false;
284    };
285    let declared_end =
286        INODE_EXTRA_OFFSET + usize::from(u16::from_le_bytes([size_bytes[0], size_bytes[1]]));
287    let field_end = offset + 4;
288    field_end <= raw.len() && field_end <= declared_end
289}
290
291/// Store `secs` (whole seconds; nanoseconds zero) as `field`.
292///
293/// - Where the field's companion word is present: the base and the era
294///   bits from [`pack_seconds`], after clamping to the encodable range.
295///   The companion's nanosecond bits are cleared.
296/// - Where it is not (a 128-byte inode, or an `i_extra_isize` too small
297///   to reach it): the base alone, clamped to the signed 32-bit range,
298///   so a time after 2038 is stored as 2038-01-19 03:14:07 rather than
299///   wrapping round to 1901.
300/// - crtime's base word is itself in the extended area; if even that is
301///   out of reach, nothing is written.
302pub(crate) fn set_inode_time(raw: &mut [u8], field: InodeTime, secs: i64) {
303    let base_at = field.base_offset();
304    let extra_at = field.extra_offset();
305    let (base, extra) = if extra_field_fits(raw, extra_at) {
306        let (base, era) = pack_seconds(secs.clamp(MIN_ENCODABLE_TIME, MAX_ENCODABLE_TIME));
307        (base, Some(era))
308    } else if field != InodeTime::Crtime || extra_field_fits(raw, base_at) {
309        let clamped = secs.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
310        (clamped as u32, None)
311    } else {
312        return;
313    };
314    raw[base_at..base_at + 4].copy_from_slice(&base.to_le_bytes());
315    if let Some(era) = extra {
316        raw[extra_at..extra_at + 4].copy_from_slice(&era.to_le_bytes());
317    }
318}
319
320impl Inode {
321    /// Parse an inode from its on-disk bytes.
322    /// Accepts any length >= 128; if >= 160 and i_extra_isize >= 28, parses the
323    /// extra (nsec + crtime + checksum_hi) section as well.
324    pub fn parse(raw: &[u8]) -> Result<Self> {
325        if raw.len() < INODE_BASE_SIZE {
326            return Err(Error::Corrupt("inode buffer too small"));
327        }
328
329        let mode = u16::from_le_bytes(raw[OFF_MODE..OFF_MODE + 2].try_into().unwrap());
330        let uid_lo = u16::from_le_bytes(raw[0x02..0x04].try_into().unwrap());
331        let size_lo = u32::from_le_bytes(raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].try_into().unwrap());
332        let atime_base = u32::from_le_bytes(raw[OFF_ATIME..OFF_ATIME + 4].try_into().unwrap());
333        let ctime_base = u32::from_le_bytes(raw[OFF_CTIME..OFF_CTIME + 4].try_into().unwrap());
334        let mtime_base = u32::from_le_bytes(raw[OFF_MTIME..OFF_MTIME + 4].try_into().unwrap());
335        let dtime = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
336        let gid_lo = u16::from_le_bytes(raw[0x18..0x1A].try_into().unwrap());
337        let links_count = u16::from_le_bytes(
338            raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2]
339                .try_into()
340                .unwrap(),
341        );
342        let blocks_lo =
343            u32::from_le_bytes(raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].try_into().unwrap());
344        let flags = u32::from_le_bytes(raw[OFF_FLAGS..OFF_FLAGS + 4].try_into().unwrap());
345        // 0x24..0x28 is i_osd1 (Linux: i_version_lo) — ignored here.
346
347        let mut block = [0u8; 60];
348        block.copy_from_slice(&raw[OFF_BLOCK..OFF_BLOCK + 60]);
349
350        let generation =
351            u32::from_le_bytes(raw[OFF_GENERATION..OFF_GENERATION + 4].try_into().unwrap());
352        let file_acl_lo = u32::from_le_bytes(raw[0x68..0x6C].try_into().unwrap());
353        let size_hi = u32::from_le_bytes(raw[OFF_SIZE_HI..OFF_SIZE_HI + 4].try_into().unwrap());
354        // 0x70..0x74 obso_faddr ignored.
355        let blocks_hi =
356            u16::from_le_bytes(raw[OFF_BLOCKS_HI..OFF_BLOCKS_HI + 2].try_into().unwrap());
357        let file_acl_hi = u16::from_le_bytes(raw[0x76..0x78].try_into().unwrap());
358        let uid_hi = u16::from_le_bytes(raw[0x78..0x7A].try_into().unwrap());
359        let gid_hi = u16::from_le_bytes(raw[0x7A..0x7C].try_into().unwrap());
360        let checksum_lo = u16::from_le_bytes(
361            raw[OFF_CHECKSUM_LO..OFF_CHECKSUM_LO + 2]
362                .try_into()
363                .unwrap(),
364        );
365        // 0x7E..0x80 i_reserved2.
366
367        // Defaults (when no extra section present).
368        let mut atime_nsec = 0u32;
369        let mut mtime_nsec = 0u32;
370        let mut ctime_nsec = 0u32;
371        let mut crtime_nsec = 0u32;
372        let mut crtime_base = 0u32;
373        // The `*_extra` words, zero when i_extra_isize is too small to
374        // hold them — which correctly yields no epoch extension.
375        let mut atime_extra = 0u32;
376        let mut mtime_extra = 0u32;
377        let mut ctime_extra = 0u32;
378        let mut crtime_extra = 0u32;
379        let mut checksum_hi = 0u16;
380
381        // Extra fields — only present when on-disk inode size is >= 160 AND
382        // i_extra_isize covers them (>= 28 includes through i_projid; we read
383        // what we need at >= 24 to cover up to crtime_extra).
384        if raw.len() >= INODE_EXTRA_OFFSET + 4 {
385            let i_extra_isize = u16::from_le_bytes(
386                raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2]
387                    .try_into()
388                    .unwrap(),
389            );
390            // Sanity: i_extra_isize is the number of bytes beyond the 128-byte
391            // base that are valid. Must fit inside the on-disk inode.
392            let extra_end = INODE_EXTRA_OFFSET + i_extra_isize as usize;
393            if extra_end > raw.len() {
394                return Err(Error::Corrupt("i_extra_isize exceeds inode size"));
395            }
396
397            // Read each extra field only if i_extra_isize covers it.
398            // Layout (offset from inode start):
399            //   0x80 u16 i_extra_isize
400            //   0x82 u16 i_checksum_hi          (needs >= 4)
401            //   0x84 u32 i_ctime_extra          (needs >= 8)
402            //   0x88 u32 i_mtime_extra          (needs >= 12)
403            //   0x8C u32 i_atime_extra          (needs >= 16)
404            //   0x90 u32 i_crtime               (needs >= 20)
405            //   0x94 u32 i_crtime_extra         (needs >= 24)
406            if i_extra_isize >= 4 {
407                checksum_hi = u16::from_le_bytes(
408                    raw[OFF_CHECKSUM_HI..OFF_CHECKSUM_HI + 2]
409                        .try_into()
410                        .unwrap(),
411                );
412            }
413            if i_extra_isize >= 8 {
414                let extra = u32::from_le_bytes(raw[0x84..0x88].try_into().unwrap());
415                ctime_nsec = extra >> 2;
416                ctime_extra = extra;
417            }
418            if i_extra_isize >= 12 {
419                let extra = u32::from_le_bytes(raw[0x88..0x8C].try_into().unwrap());
420                mtime_nsec = extra >> 2;
421                mtime_extra = extra;
422            }
423            if i_extra_isize >= 16 {
424                let extra = u32::from_le_bytes(raw[0x8C..0x90].try_into().unwrap());
425                atime_nsec = extra >> 2;
426                atime_extra = extra;
427            }
428            if i_extra_isize >= 20 {
429                crtime_base =
430                    u32::from_le_bytes(raw[OFF_CRTIME..OFF_CRTIME + 4].try_into().unwrap());
431            }
432            if i_extra_isize >= 24 {
433                let extra = u32::from_le_bytes(raw[0x94..0x98].try_into().unwrap());
434                crtime_nsec = extra >> 2;
435                crtime_extra = extra;
436            }
437        }
438
439        Ok(Self {
440            mode,
441            uid: join16(uid_hi, uid_lo),
442            gid: join16(gid_hi, gid_lo),
443            size: join32(size_hi, size_lo),
444            atime: unpack_seconds(atime_base, atime_extra),
445            mtime: unpack_seconds(mtime_base, mtime_extra),
446            ctime: unpack_seconds(ctime_base, ctime_extra),
447            // dtime has no *_extra field in the format: deletion time
448            // is a plain signed 32-bit value with no epoch extension.
449            dtime: dtime as i32 as i64,
450            crtime: unpack_seconds(crtime_base, crtime_extra),
451            atime_nsec,
452            mtime_nsec,
453            ctime_nsec,
454            crtime_nsec,
455            links_count,
456            blocks: join32(blocks_hi, blocks_lo),
457            flags,
458            block,
459            generation,
460            file_acl: join32(file_acl_hi, file_acl_lo),
461            checksum: join16(checksum_hi, checksum_lo),
462        })
463    }
464
465    /// File type from i_mode.
466    pub fn file_type(&self) -> u16 {
467        self.mode & S_IFMT
468    }
469
470    pub fn is_dir(&self) -> bool {
471        self.file_type() == S_IFDIR
472    }
473
474    pub fn is_file(&self) -> bool {
475        self.file_type() == S_IFREG
476    }
477
478    pub fn is_symlink(&self) -> bool {
479        self.file_type() == S_IFLNK
480    }
481
482    /// True when EXT4_EXTENTS_FL is set in i_flags — i_block holds an extent
483    /// tree rather than legacy direct/indirect block pointers.
484    pub fn has_extents(&self) -> bool {
485        self.flags & InodeFlags::EXTENTS.bits() != 0
486    }
487
488    /// True when INLINE_DATA flag is set — file contents live inside i_block.
489    pub fn has_inline_data(&self) -> bool {
490        self.flags & InodeFlags::INLINE_DATA.bits() != 0
491    }
492
493    /// Decode i_flags into a typed bitflags value (silently drops unknown bits).
494    pub fn flag_set(&self) -> InodeFlags {
495        InodeFlags::from_bits_truncate(self.flags)
496    }
497}
498
499/// Combine two 16-bit halves into a 32-bit value (hi occupies the upper 16 bits).
500/// Used when the on-disk layout stores a 32-bit field split across two u16 words.
501#[inline]
502fn join16(hi: u16, lo: u16) -> u32 {
503    ((hi as u32) << 16) | lo as u32
504}
505
506/// Combine a hi half (any type that fits in u64) and a 32-bit lo half into a
507/// 64-bit value. Used for size, file_acl, and i_blocks whose hi halves have
508/// different widths (u16 or u32) in the on-disk layout.
509#[inline]
510fn join32<H: Into<u64>>(hi: H, lo: u32) -> u64 {
511    (hi.into() << 32) | lo as u64
512}
513
514#[cfg(test)]
515mod timestamp_tests {
516    use super::{pack_seconds, unpack_seconds, MAX_ENCODABLE_TIME, MIN_ENCODABLE_TIME};
517
518    /// `(seconds, base word, era bits)` as the kernel in the harness VM
519    /// wrote them for `touch -m -d @seconds` on a 256-byte-inode volume,
520    /// read back with `debugfs stat` (`mtime: 0xBASE:EXTRA`). The same
521    /// list drives the live check in `needs_host` below, so a change in
522    /// what the kernel writes fails there rather than going unnoticed.
523    const KERNEL_WORDS: &[(i64, u32, u32)] = &[
524        (-2_147_483_648, 0x8000_0000, 0), // 1901-12-13, the earliest
525        (-1_000, 0xFFFF_FC18, 0),         // 1969
526        (-1, 0xFFFF_FFFF, 0),             // the second before the epoch
527        (0, 0x0000_0000, 0),              // 1970-01-01
528        (1, 0x0000_0001, 0),
529        (2_147_483_647, 0x7FFF_FFFF, 0),  // 2038-01-19 03:14:07
530        (2_147_483_648, 0x8000_0000, 1),  // one second later: era 1
531        (4_294_967_295, 0xFFFF_FFFF, 1),  // 2106-02-07 06:28:15
532        (4_294_967_296, 0x0000_0000, 1),  // 2106-02-07 06:28:16
533        (6_442_450_943, 0x7FFF_FFFF, 1),  // 2174
534        (6_442_450_944, 0x8000_0000, 2),  // 2174: era 2
535        (8_589_934_592, 0x0000_0000, 2),  // 2242-03-16
536        (12_884_901_887, 0xFFFF_FFFF, 3), // 2378, already era 3
537        (12_884_901_888, 0x0000_0000, 3),
538        (15_032_385_535, 0x7FFF_FFFF, 3), // 2446-05-10 22:38:55, the last
539    ];
540
541    #[test]
542    fn every_kernel_written_word_pair_reads_back_as_its_seconds() {
543        for &(secs, base, era) in KERNEL_WORDS {
544            assert_eq!(unpack_seconds(base, era), secs, "{base:#x}:{era}");
545        }
546    }
547
548    #[test]
549    fn every_second_in_the_table_packs_to_the_kernels_words() {
550        for &(secs, base, era) in KERNEL_WORDS {
551            assert_eq!(pack_seconds(secs), (base, era), "{secs}");
552        }
553    }
554
555    /// The bounds are the table's first and last rows.
556    #[test]
557    fn the_encodable_range_is_1901_to_2446() {
558        assert_eq!(MIN_ENCODABLE_TIME, KERNEL_WORDS[0].0);
559        assert_eq!(MAX_ENCODABLE_TIME, KERNEL_WORDS[KERNEL_WORDS.len() - 1].0);
560    }
561
562    /// Nanoseconds live above the era bits and never reach the seconds.
563    #[test]
564    fn nanosecond_bits_are_not_seconds() {
565        let all_nanosecond_bits = !0b11u32;
566        assert_eq!(unpack_seconds(1_000, all_nanosecond_bits), 1_000);
567        assert_eq!(
568            unpack_seconds(1_000, all_nanosecond_bits | 0b10),
569            1_000 + (2 << 32)
570        );
571    }
572
573    /// Every window edge, and a second either side of it, survives a
574    /// pack and an unpack.
575    #[test]
576    fn window_edges_survive_a_round_trip() {
577        for era in 0..4i64 {
578            let lowest = i64::from(i32::MIN) + (era << 32);
579            let highest = i64::from(i32::MAX) + (era << 32);
580            for secs in [lowest, lowest + 1, highest - 1, highest] {
581                let (base, bits) = pack_seconds(secs);
582                assert_eq!(bits as i64, era, "{secs}");
583                assert_eq!(unpack_seconds(base, bits), secs, "{secs}");
584            }
585        }
586    }
587
588    /// Tests that ask the kernel and debugfs in the harness VM.
589    mod needs_host {
590        use super::KERNEL_WORDS;
591        use crate::block_io::FileDevice;
592        use crate::fs::Filesystem;
593        use std::sync::Arc;
594
595        /// The kernel stamps one file per row of [`KERNEL_WORDS`] (and three
596        /// past the end of the range, which it clamps); `debugfs` reports
597        /// the raw words it wrote and `stat` the seconds it reads back.
598        /// Both must match the table, and this crate must read every file's
599        /// mtime as the kernel does.
600        #[test]
601        fn the_kernel_writes_and_reads_the_documented_words() {
602            let image =
603                fs_ext4_test_support::temp_path!("fs_ext4_inode_times_{}.img", std::process::id());
604            std::fs::File::create(&image)
605                .and_then(|f| f.set_len(8 << 20))
606                .unwrap();
607            let out = fs_ext4_test_support::oracle("mkfs.ext4")
608                .args(["-q", "-F", "-I", "256", &image])
609                .output();
610            assert!(
611                out.status.success(),
612                "{}",
613                String::from_utf8_lossy(&out.stderr)
614            );
615
616            let past_the_end = [15_032_385_536i64, 17_179_869_184, 99_999_999_999];
617            let mut all: Vec<i64> = KERNEL_WORDS.iter().map(|row| row.0).collect();
618            all.extend(past_the_end);
619            let mut script = String::from("set -e\n");
620            for secs in &all {
621                script.push_str(&format!(
622                    "touch \"$MNT/t{secs}\"; touch -m -d @{secs} \"$MNT/t{secs}\"\n"
623                ));
624            }
625            for secs in &all {
626                script.push_str(&format!(
627                    "printf '%s %s\\n' {secs} \"$(stat -c %Y \"$MNT/t{secs}\")\"\n"
628                ));
629            }
630            let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
631            assert!(
632                out.status.success(),
633                "{}",
634                String::from_utf8_lossy(&out.stderr)
635            );
636            let kernel_reads: std::collections::BTreeMap<i64, i64> =
637                String::from_utf8_lossy(&out.stdout)
638                    .lines()
639                    .filter_map(|l| {
640                        let (a, b) = l.split_once(' ')?;
641                        Some((a.parse().ok()?, b.trim().parse().ok()?))
642                    })
643                    .collect();
644
645            let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
646            let last = KERNEL_WORDS[KERNEL_WORDS.len() - 1];
647            for &secs in &all {
648                let (want_secs, want_base, want_era) = KERNEL_WORDS
649                    .iter()
650                    .copied()
651                    .find(|row| row.0 == secs)
652                    .unwrap_or(last);
653                assert_eq!(
654                    kernel_reads.get(&secs),
655                    Some(&want_secs),
656                    "kernel stat of @{secs}"
657                );
658
659                let out = fs_ext4_test_support::oracle("debugfs")
660                    .args(["-R", &format!("stat /t{secs}"), &image])
661                    .output();
662                let text = String::from_utf8_lossy(&out.stdout).into_owned();
663                let words = text
664                    .lines()
665                    .find_map(|l| l.trim().strip_prefix("mtime: 0x"))
666                    .unwrap_or_else(|| panic!("no mtime line for @{secs}: {text}"));
667                let (base, extra) = words.split_once(":").unwrap();
668                let base = u32::from_str_radix(base, 16).unwrap();
669                let extra = u32::from_str_radix(&extra[..8], 16).unwrap();
670                assert_eq!(
671                    (base, extra & 3),
672                    (want_base, want_era),
673                    "debugfs words for @{secs}"
674                );
675
676                let mut lookup = |ino: u32| fs.read_inode_verified(ino).map(|(inode, _)| inode);
677                let ino =
678                    crate::path::lookup(fs.dev.as_ref(), &fs.sb, &mut lookup, &format!("/t{secs}"))
679                        .unwrap();
680                let inode = fs.read_inode_verified(ino).unwrap().0;
681                assert_eq!(inode.mtime, want_secs, "this crate's reading of @{secs}");
682            }
683            drop(fs);
684            let _ = std::fs::remove_file(&image);
685        }
686
687        /// The other direction: this crate stamps each row's seconds with
688        /// `apply_utimens`, and the kernel's `stat` must read them back.
689        #[test]
690        fn the_kernel_reads_what_this_crate_stamps() {
691            let image = fs_ext4_test_support::temp_path!(
692                "fs_ext4_inode_times_w_{}.img",
693                std::process::id()
694            );
695            std::fs::File::create(&image)
696                .and_then(|f| f.set_len(8 << 20))
697                .unwrap();
698            let out = fs_ext4_test_support::oracle("mkfs.ext4")
699                .args(["-q", "-F", "-I", "256", &image])
700                .output();
701            assert!(
702                out.status.success(),
703                "{}",
704                String::from_utf8_lossy(&out.stderr)
705            );
706            {
707                let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&image).unwrap())).unwrap();
708                for &(secs, _, _) in KERNEL_WORDS {
709                    let path = format!("/w{secs}");
710                    fs.apply_create(&path, 0o644).unwrap();
711                    fs.apply_utimens(&path, secs, 0, secs, 0).unwrap();
712                }
713            }
714            // No `e2fsck` verdict here: e2fsck 1.47 reports a time in era 3
715            // whose base is negative (2310-04-04 .. 2378-04-22) as "likely
716            // pre-1970" -- the documentation's note on old kernels that
717            // wrote era 3 for 1901..1970 -- although the kernel above wrote
718            // those same words for those same seconds. The kernel is the
719            // reader that matters.
720            let mut script = String::from("set -e\n");
721            for &(secs, _, _) in KERNEL_WORDS {
722                script.push_str(&format!(
723                    "printf '%s %s\\n' {secs} \"$(stat -c %Y \"$MNT/w{secs}\")\"\n"
724                ));
725            }
726            let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
727            assert!(
728                out.status.success(),
729                "{}",
730                String::from_utf8_lossy(&out.stderr)
731            );
732            let stdout = String::from_utf8_lossy(&out.stdout);
733            for &(secs, _, _) in KERNEL_WORDS {
734                assert!(
735                    stdout.lines().any(|l| l == format!("{secs} {secs}")),
736                    "the kernel did not read @{secs} back:\n{stdout}"
737                );
738            }
739            let _ = std::fs::remove_file(&image);
740        }
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747
748    #[test]
749    fn join16_combines_halves() {
750        assert_eq!(join16(0x0001, 0x0002), 0x0001_0002);
751        assert_eq!(join16(0xFFFF, 0x0000), 0xFFFF_0000);
752        assert_eq!(join16(0x0000, 0xFFFF), 0x0000_FFFF);
753        assert_eq!(join16(0, 0), 0);
754    }
755
756    #[test]
757    fn join32_combines_halves_u16_hi() {
758        assert_eq!(join32(0x0001u16, 0x0000_0002), 0x0000_0001_0000_0002);
759        assert_eq!(join32(0xFFFFu16, 0x0000_0000), 0x0000_FFFF_0000_0000);
760        assert_eq!(join32(0x0000u16, 0xFFFF_FFFF), 0x0000_0000_FFFF_FFFF);
761    }
762
763    #[test]
764    fn join32_combines_halves_u32_hi() {
765        assert_eq!(join32(0x0000_0001u32, 0x0000_0002), 0x0000_0001_0000_0002);
766        assert_eq!(join32(0xFFFF_FFFFu32, 0x0000_0000), 0xFFFF_FFFF_0000_0000);
767    }
768
769    #[test]
770    fn parse_rejects_short_buffer() {
771        let short = vec![0u8; 64];
772        assert!(matches!(
773            Inode::parse(&short),
774            Err(crate::error::Error::Corrupt(_))
775        ));
776    }
777
778    #[test]
779    fn parse_rejects_invalid_extra_isize() {
780        // 160-byte inode with i_extra_isize claiming 200 bytes (exceeds buffer).
781        let mut raw = vec![0u8; 160];
782        raw[0x80] = 200; // i_extra_isize lo byte — claims 200 bytes extra
783        raw[0x81] = 0;
784        assert!(matches!(
785            Inode::parse(&raw),
786            Err(crate::error::Error::Corrupt(_))
787        ));
788    }
789
790    #[test]
791    fn parse_mode_and_links_roundtrip() {
792        let mut raw = vec![0u8; 128];
793        raw[0x00..0x02].copy_from_slice(&0x81A4u16.to_le_bytes()); // S_IFREG | 0644
794        raw[0x1A..0x1C].copy_from_slice(&3u16.to_le_bytes()); // links_count
795        let inode = Inode::parse(&raw).unwrap();
796        assert_eq!(inode.mode, 0x81A4);
797        assert_eq!(inode.links_count, 3);
798        assert!(inode.is_file());
799    }
800}
801
802/// `set_inode_time` stores the era bits where the inode has room for
803/// them, and a clamp where it does not.
804#[cfg(test)]
805mod set_inode_time_tests {
806    use super::{set_inode_time, Inode, InodeTime, OFF_ATIME, OFF_CRTIME, OFF_EXTRA_ISIZE};
807
808    const PAST_2038: i64 = (1i64 << 31) + 10;
809
810    fn inode(len: usize, extra_isize: u16) -> Vec<u8> {
811        let mut raw = vec![0u8; len];
812        if len >= OFF_EXTRA_ISIZE + 2 {
813            raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2].copy_from_slice(&extra_isize.to_le_bytes());
814        }
815        raw
816    }
817
818    fn le32(raw: &[u8], off: usize) -> u32 {
819        u32::from_le_bytes(raw[off..off + 4].try_into().unwrap())
820    }
821
822    #[test]
823    fn a_large_inode_keeps_the_epoch_bits() {
824        let mut raw = inode(256, 32);
825        for field in [
826            InodeTime::Atime,
827            InodeTime::Ctime,
828            InodeTime::Mtime,
829            InodeTime::Crtime,
830        ] {
831            set_inode_time(&mut raw, field, PAST_2038);
832        }
833        let parsed = Inode::parse(&raw).unwrap();
834        assert_eq!(parsed.atime, PAST_2038);
835        assert_eq!(parsed.ctime, PAST_2038);
836        assert_eq!(parsed.mtime, PAST_2038);
837        assert_eq!(parsed.crtime, PAST_2038);
838        // Era 1 with a base whose signed reading is negative; no nsec.
839        assert_eq!(le32(&raw, OFF_ATIME), 0x8000_000A);
840        assert_eq!(le32(&raw, 0x8C), 1);
841    }
842
843    #[test]
844    fn a_new_stamp_clears_the_old_nanoseconds() {
845        let mut raw = inode(256, 32);
846        raw[0x88..0x8C].copy_from_slice(&(123u32 << 2).to_le_bytes());
847        set_inode_time(&mut raw, InodeTime::Mtime, 1_700_000_000);
848        let parsed = Inode::parse(&raw).unwrap();
849        assert_eq!(parsed.mtime, 1_700_000_000);
850        assert_eq!(parsed.mtime_nsec, 0);
851    }
852
853    #[test]
854    fn a_small_inode_clamps_instead_of_wrapping() {
855        let mut raw = inode(128, 0);
856        set_inode_time(&mut raw, InodeTime::Mtime, PAST_2038);
857        assert_eq!(Inode::parse(&raw).unwrap().mtime, i32::MAX as i64);
858        set_inode_time(&mut raw, InodeTime::Mtime, i32::MIN as i64 - 10);
859        assert_eq!(Inode::parse(&raw).unwrap().mtime, i32::MIN as i64);
860    }
861
862    #[test]
863    fn an_extra_isize_too_small_for_the_field_clamps_it() {
864        // 12 covers ctime_extra and mtime_extra, not atime_extra.
865        let mut raw = inode(256, 12);
866        set_inode_time(&mut raw, InodeTime::Mtime, PAST_2038);
867        set_inode_time(&mut raw, InodeTime::Atime, PAST_2038);
868        let parsed = Inode::parse(&raw).unwrap();
869        assert_eq!(parsed.mtime, PAST_2038);
870        assert_eq!(parsed.atime, i32::MAX as i64);
871        assert_eq!(le32(&raw, 0x8C), 0, "atime_extra is outside i_extra_isize");
872    }
873
874    #[test]
875    fn crtime_is_not_written_where_the_inode_has_no_room_for_it() {
876        let mut raw = inode(128, 0);
877        set_inode_time(&mut raw, InodeTime::Crtime, 1_700_000_000);
878        assert!(raw.iter().all(|&b| b == 0));
879        let mut raw = inode(256, 16);
880        set_inode_time(&mut raw, InodeTime::Crtime, 1_700_000_000);
881        assert_eq!(le32(&raw, OFF_CRTIME), 0);
882    }
883
884    #[test]
885    fn past_the_format_ceiling_is_clamped_to_it() {
886        let mut raw = inode(256, 32);
887        set_inode_time(&mut raw, InodeTime::Mtime, i64::MAX);
888        assert_eq!(Inode::parse(&raw).unwrap().mtime, super::MAX_ENCODABLE_TIME);
889    }
890}