Skip to main content

fs_ext4/
fs.rs

1//! Top-level filesystem handle. Composes block_io + superblock + bgd + inode + extent + dir.
2
3use crate::bgd::{self, BlockGroupDescriptor};
4use crate::block_io::BlockDevice;
5use crate::checksum::Checksummer;
6use crate::error::{Error, Result};
7use crate::features;
8use crate::inode::{set_inode_time, Inode, InodeTime};
9use crate::superblock::Superblock;
10use std::borrow::Cow;
11use std::collections::{BTreeMap, HashMap};
12use std::sync::{Arc, Mutex};
13
14/// In-memory accumulator for journaled multi-block writes. Each helper
15/// mutation reads the latest version of a block (from this buffer if
16/// already touched, else from disk via the live `Filesystem`) and writes
17/// back into the buffer. The op then commits the whole buffer atomically.
18///
19/// `BTreeMap` so the commit order is deterministic — replay applies
20/// blocks in journal-stored order, matching the kernel's expected
21/// transaction layout.
22pub(crate) struct BlockBuffer {
23    pub dirty: BTreeMap<u64, Vec<u8>>,
24    /// Uninit flags this buffer clears, and the descriptor flags they
25    /// leave behind, held until the buffer is committed.
26    ///
27    /// They cannot be published earlier. Clearing a group's uninit flag
28    /// is what tells later allocations its bitmap is real and may be
29    /// read; if the commit then fails, the bitmap on disk is still the
30    /// unspecified bytes the flag existed to license skipping, and a
31    /// planner that trusted the flag would allocate out of them.
32    pub uninit_cleared: BTreeMap<usize, u16>,
33}
34
35impl BlockBuffer {
36    /// `block_size` is taken and not stored.
37    ///
38    /// It was a field nothing read — every block this buffer holds
39    /// arrives already sized by the caller, so the buffer never needs
40    /// to know. The parameter stays because twenty-four call sites pass
41    /// it and it says at each one which filesystem's blocks these are;
42    /// dropping it would trade a dead field for twenty-four edits and a
43    /// less legible call.
44    pub fn new(_block_size: u32) -> Self {
45        Self {
46            dirty: BTreeMap::new(),
47            uninit_cleared: BTreeMap::new(),
48        }
49    }
50
51    /// Fetch a mutable handle to `block`, loading from `fs` on first
52    /// touch. Subsequent calls for the same block return the in-buffer
53    /// copy so multiple helpers can compose patches.
54    pub fn get_mut(&mut self, fs: &Filesystem, block: u64) -> Result<&mut Vec<u8>> {
55        if let std::collections::btree_map::Entry::Vacant(e) = self.dirty.entry(block) {
56            let buf = fs.read_block(block)?;
57            e.insert(buf);
58        }
59        Ok(self.dirty.get_mut(&block).unwrap())
60    }
61
62    /// Stage an already-built block image directly (no read-modify cycle).
63    /// Useful when the caller has the bytes in hand (e.g. data blocks of
64    /// a file write).
65    pub fn put(&mut self, block: u64, bytes: Vec<u8>) {
66        self.dirty.insert(block, bytes);
67    }
68}
69
70/// Patch a split u32 counter (lo: u16 + optional hi: u16) in `buf` by `delta`.
71///
72/// ext4 BGD counters are stored as a 16-bit low word at `lo_off` and an
73/// optional 16-bit high word at `hi_off` (present when desc_size >= 64). The
74/// combined 32-bit value is clamped to zero on underflow.
75fn patch_counter_u32(buf: &mut [u8], lo_off: usize, hi_off: Option<usize>, delta: i32) {
76    let cur_lo = u16::from_le_bytes(buf[lo_off..lo_off + 2].try_into().unwrap()) as u32;
77    let cur_hi = hi_off
78        .map(|h| u16::from_le_bytes(buf[h..h + 2].try_into().unwrap()) as u32)
79        .unwrap_or(0);
80    let cur = (cur_hi << 16) | cur_lo;
81    let new = (cur as i64 + delta as i64).clamp(0, u32::MAX as i64) as u32;
82    buf[lo_off..lo_off + 2].copy_from_slice(&((new & 0xFFFF) as u16).to_le_bytes());
83    if let Some(h) = hi_off {
84        buf[h..h + 2].copy_from_slice(&(((new >> 16) & 0xFFFF) as u16).to_le_bytes());
85    }
86}
87
88/// Apply counter deltas to the descriptor at `off` inside `block`: free
89/// blocks, free inodes and used directories, each a u16 low half plus, when
90/// `desc_size >= 64`, a u16 high half at the offsets `bgd::parse` reads.
91fn patch_bgd_counter_fields(
92    block: &mut [u8],
93    off: usize,
94    desc_size: u16,
95    free_blocks_delta: i32,
96    free_inodes_delta: i32,
97    used_dirs_delta: i32,
98) {
99    use crate::bgd::{
100        OFF_FREE_BLOCKS_HI, OFF_FREE_BLOCKS_LO, OFF_FREE_INODES_HI, OFF_FREE_INODES_LO,
101        OFF_USED_DIRS_HI, OFF_USED_DIRS_LO,
102    };
103    let has_hi = desc_size >= 64;
104    for (lo, hi, delta) in [
105        (OFF_FREE_BLOCKS_LO, OFF_FREE_BLOCKS_HI, free_blocks_delta),
106        (OFF_FREE_INODES_LO, OFF_FREE_INODES_HI, free_inodes_delta),
107        (OFF_USED_DIRS_LO, OFF_USED_DIRS_HI, used_dirs_delta),
108    ] {
109        patch_counter_u32(block, off + lo, has_hi.then_some(off + hi), delta);
110    }
111}
112
113/// Pack the low bits of an ext4 nanosecond timestamp field.
114///
115/// ext4 stores extra precision in a 32-bit extra field: bits [31:2] hold the
116/// low 30 bits of the nanosecond value; bits [1:0] are the 2-bit epoch
117/// extension that extends the 32-bit seconds counter beyond 2038.
118#[inline]
119fn pack_nsec_lo(nsec: u32) -> u32 {
120    (nsec & 0x3FFF_FFFF) << 2
121}
122
123/// Passed to [`Filesystem::apply_utimens`] in place of a seconds value
124/// to leave that timestamp unchanged — the equivalent of POSIX's
125/// `UTIME_OMIT`, which `utimensat(2)` spells in the nanoseconds field.
126///
127/// `i64::MIN` and not `u32::MAX`: seconds are signed and 64-bit, so
128/// `u32::MAX` is an ordinary date in 2106 and can no longer double as a
129/// sentinel. `i64::MIN` is far outside anything ext4 can store.
130pub const TIME_OMIT: i64 = i64::MIN;
131
132/// Passed to [`Filesystem::apply_utimens`] as a nanoseconds value to set
133/// that timestamp to the current time from the mount's
134/// [`Runtime`](crate::runtime::Runtime); the seconds beside it are
135/// ignored. The value `utimensat(2)` gives `UTIME_NOW` on Linux,
136/// `(1 << 30) - 1`. The clock is whole seconds, so the stored
137/// nanoseconds are zero.
138pub const UTIME_NOW: u32 = (1 << 30) - 1;
139
140/// Passed to [`Filesystem::apply_utimens`] as a nanoseconds value to
141/// leave that timestamp unchanged; the seconds beside it are ignored.
142/// The value `utimensat(2)` gives `UTIME_OMIT` on Linux, `(1 << 30) - 2`.
143/// Equivalent to [`TIME_OMIT`] in the seconds.
144pub const UTIME_OMIT: u32 = (1 << 30) - 2;
145
146/// One more than the largest nanosecond count a timestamp can carry.
147const NSEC_PER_SEC: u32 = 1_000_000_000;
148
149/// What one `(sec, nsec)` pair passed to `apply_utimens` asks for.
150#[derive(Clone, Copy)]
151enum TimeUpdate {
152    Omit,
153    Set(i64, u32),
154}
155
156impl TimeUpdate {
157    /// Resolve the `utimensat(2)` sentinels and refuse anything that is
158    /// not a storable time, before any write.
159    fn resolve(sec: i64, nsec: u32, now: i64) -> Result<Self> {
160        if sec == TIME_OMIT || nsec == UTIME_OMIT {
161            return Ok(Self::Omit);
162        }
163        if nsec == UTIME_NOW {
164            return Ok(Self::Set(now, 0));
165        }
166        if nsec >= NSEC_PER_SEC {
167            return Err(Error::InvalidArgument(
168                "nanoseconds must be below 1e9 (or UTIME_NOW / UTIME_OMIT)",
169            ));
170        }
171        if !(crate::inode::MIN_ENCODABLE_TIME..=crate::inode::MAX_ENCODABLE_TIME).contains(&sec) {
172            return Err(Error::InvalidArgument(
173                "timestamp outside the range ext4 can store (1901..2446)",
174            ));
175        }
176        Ok(Self::Set(sec, nsec))
177    }
178
179    /// [`resolve`](Self::resolve)'s refusals alone, without a clock: the
180    /// time `UTIME_NOW` stands for is always storable.
181    fn check(sec: i64, nsec: u32) -> Result<()> {
182        Self::resolve(sec, nsec, 0).map(|_| ())
183    }
184}
185
186/// An inode named by number, for the inode-addressed entry points
187/// (`lookup_at`, `stat_ino`, `apply_*_at`, `apply_*_ino`).
188///
189/// A handle-based host holds one of these per item instead of a path: a
190/// path is the wrong key for a driver, because a hard link gives one inode
191/// several, and renaming a directory changes every one beneath it.
192///
193/// `generation` is the `i_generation` the caller read alongside the number
194/// (every attribute read returns it). With it, a handle to an inode that
195/// was freed and then reused for a different file is refused as
196/// [`Error::Stale`] instead of reaching the new file. Without it (`None`,
197/// or a bare `u32` converted with `into()`) only a freed or never-used
198/// inode is refused, which is what a caller that has not read the
199/// generation yet can ask for.
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
201pub struct InodeRef {
202    /// Inode number (1-based).
203    pub ino: u32,
204    /// Expected `i_generation`, or `None` to accept any.
205    pub generation: Option<u32>,
206}
207
208impl InodeRef {
209    /// Inode `ino`, which must still carry `generation`.
210    pub const fn new(ino: u32, generation: u32) -> Self {
211        Self {
212            ino,
213            generation: Some(generation),
214        }
215    }
216
217    /// Inode `ino`, whatever its generation.
218    pub const fn any(ino: u32) -> Self {
219        Self {
220            ino,
221            generation: None,
222        }
223    }
224}
225
226impl From<u32> for InodeRef {
227    fn from(ino: u32) -> Self {
228        Self::any(ino)
229    }
230}
231
232/// Refuse a name that cannot be one directory entry: empty, or holding a
233/// `/` or a NUL. A path-addressed call never reaches these (its split
234/// cannot produce them); an inode-addressed one is handed the name as-is.
235fn check_entry_name(name: &[u8]) -> Result<()> {
236    if name.is_empty() {
237        return Err(Error::InvalidArgument("empty name"));
238    }
239    if name.contains(&b'/') {
240        return Err(Error::InvalidArgument("a name cannot contain '/'"));
241    }
242    // See `split_parent_and_base`: the format would store it, e2fsck
243    // reports it, and the kernel never files one.
244    if name.contains(&0) {
245        return Err(Error::InvalidArgument("a name cannot contain a NUL byte"));
246    }
247    Ok(())
248}
249
250/// [`check_entry_name`] for a name about to be filed, which must also fit
251/// the entry's one-byte length.
252fn check_new_entry_name(name: &[u8]) -> Result<()> {
253    check_entry_name(name)?;
254    if name.len() > 255 {
255        return Err(Error::NameTooLong);
256    }
257    Ok(())
258}
259
260/// `.` and `..` name the directory and its parent, not entries that can
261/// be removed or moved; rmdir(2) and rename(2) refuse them.
262fn is_dot_or_dotdot(name: &[u8]) -> bool {
263    name == b"." || name == b".."
264}
265
266/// Split a `/a/b/c` path into (`/a/b`, `c`). Returns an error for empty or
267/// `"/"` paths (no basename to act on).
268///
269/// Bytes, like the names they end in: a path is never decoded (#418).
270fn split_parent_and_base(path: &[u8]) -> Result<(&[u8], &[u8])> {
271    let end = path.iter().rposition(|&b| b != b'/').map_or(0, |i| i + 1);
272    let trimmed = &path[..end];
273    if trimmed.is_empty() {
274        return Err(Error::InvalidArgument("empty path"));
275    }
276    let last_slash = trimmed
277        .iter()
278        .rposition(|&b| b == b'/')
279        .ok_or(Error::InvalidArgument("relative path"))?;
280    let base = &trimmed[last_slash + 1..];
281    let parent: &[u8] = if last_slash == 0 {
282        b"/"
283    } else {
284        &trimmed[..last_slash]
285    };
286    if base.is_empty() {
287        // Trailing slash on a non-dir path is POSIX ENOTDIR, not a generic arg error.
288        return Err(Error::NotADirectory);
289    }
290    // A `&str` can hold NUL, and the entry format would store it: names are
291    // counted bytes. The kernel never files one, because its names arrive
292    // as C strings, and e2fsck reports one as an illegal character. Every
293    // operation that files a name splits it here.
294    if base.contains(&0) {
295        return Err(Error::InvalidArgument("a name cannot contain a NUL byte"));
296    }
297    Ok((parent, base))
298}
299
300/// `DeepReader` adapter that pulls extent-tree internal/leaf node blocks
301/// straight from a `Filesystem`'s underlying device (which at mount time
302/// is wrapped in a `CachedDevice`, so reads benefit from the buffer cache
303/// holding post-commit pre-checkpoint journaled writes).
304///
305/// Used by `apply_pwrite` to satisfy `plan_insert_extent_deep`'s
306/// `&dyn DeepReader` argument when the inline extent root overflows and
307/// the tree needs to be promoted to depth ≥ 1.
308pub(crate) struct FsBlockReader<'a> {
309    pub(crate) fs: &'a Filesystem,
310}
311
312impl<'a> crate::extent_mut::DeepReader for FsBlockReader<'a> {
313    fn read_block(&self, block: u64, out: &mut [u8]) -> Result<()> {
314        let bytes = self.fs.read_block(block)?;
315        if bytes.len() != out.len() {
316            return Err(Error::Corrupt(
317                "FsBlockReader: block length mismatch (callers must pass a buffer sized to fs block_size)",
318            ));
319        }
320        out.copy_from_slice(&bytes);
321        Ok(())
322    }
323}
324
325/// Extent-tree node blocks one call has planned but not yet committed, by
326/// block number, laid over what the device holds.
327///
328/// A pwrite that promotes or splits its tree goes on planning against the
329/// tree it has just changed: a later sub-run's insert descends into the
330/// nodes the earlier one wrote, and each lookup walks them. Those nodes live
331/// in the transaction's buffer and nowhere else until it commits (#389).
332/// Writing them to the device early, so a plain reader could find them, put
333/// rewrites of the file's EXISTING nodes on disk ahead of the bitmap bits,
334/// counters and `i_blocks` that justify them: a call that then failed — out
335/// of space on a later sub-run, a failed commit, a power cut — left the tree
336/// mapping blocks the bitmap calls free, and bypassed the journal.
337struct StagedTreeNodes<'a> {
338    fs: &'a Filesystem,
339    nodes: &'a std::collections::BTreeMap<u64, Vec<u8>>,
340}
341
342impl crate::extent_mut::DeepReader for StagedTreeNodes<'_> {
343    fn read_block(&self, block: u64, out: &mut [u8]) -> Result<()> {
344        match self.nodes.get(&block) {
345            Some(bytes) if bytes.len() == out.len() => {
346                out.copy_from_slice(bytes);
347                Ok(())
348            }
349            Some(_) => Err(Error::Corrupt(
350                "StagedTreeNodes: block length mismatch (callers must pass a buffer sized to fs block_size)",
351            )),
352            None => FsBlockReader { fs: self.fs }.read_block(block, out),
353        }
354    }
355}
356
357impl crate::block_io::BlockDevice for StagedTreeNodes<'_> {
358    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
359        self.fs.dev.read_at(offset, buf)?;
360        let bs = self.fs.sb.block_size() as u64;
361        let end = offset + buf.len() as u64;
362        for (&block, bytes) in self.nodes.range(offset / bs..end.div_ceil(bs)) {
363            let (lo, hi) = ((block * bs).max(offset), ((block + 1) * bs).min(end));
364            if lo < hi {
365                buf[(lo - offset) as usize..(hi - offset) as usize].copy_from_slice(
366                    &bytes[(lo - block * bs) as usize..(hi - block * bs) as usize],
367                );
368            }
369        }
370        Ok(())
371    }
372
373    fn size_bytes(&self) -> u64 {
374        self.fs.dev.size_bytes()
375    }
376}
377
378/// `EXT4_CASEFOLD_FL`: names in this directory hash casefolded.
379const EXT4_CASEFOLD_FL: u32 = 0x4000_0000;
380/// `EXT4_ENCRYPT_FL`: names in this directory are stored encrypted.
381const EXT4_ENCRYPT_FL: u32 = 0x0000_0800;
382
383/// Write atime, ctime, mtime (and crtime when the inode buffer is large
384/// enough) from `now` into the raw inode bytes.
385///
386/// Each goes through [`set_inode_time`], so a time past 2038 keeps its
387/// epoch bits where the inode has `*_extra` fields and is clamped where
388/// it has none. Call after [`write_inode_extra_isize`]: whether those
389/// fields exist is read from `i_extra_isize`.
390fn write_inode_timestamps(raw: &mut [u8], now: i64) {
391    set_inode_time(raw, InodeTime::Atime, now);
392    set_inode_time(raw, InodeTime::Ctime, now);
393    set_inode_time(raw, InodeTime::Mtime, now);
394    // i_crtime (birth time) only exists in the extra section. Without it,
395    // a birth-time reader shows 1970-01-01.
396    set_inode_time(raw, InodeTime::Crtime, now);
397}
398
399/// Write a pre-allocated generation value into the raw inode bytes.
400fn write_inode_generation(raw: &mut [u8], generation: u32) {
401    use crate::inode::OFF_GENERATION;
402    raw[OFF_GENERATION..OFF_GENERATION + 4].copy_from_slice(&generation.to_le_bytes());
403}
404
405/// Write i_extra_isize = 32 when the inode buffer is large enough.
406/// 32 covers checksum_hi, nsec timestamps, and i_crtime beyond the 128-byte base.
407fn write_inode_extra_isize(raw: &mut [u8]) {
408    use crate::inode::{EXTRA_ISIZE_DEFAULT, INODE_SIZE_WITH_EXTRA, OFF_EXTRA_ISIZE};
409    if raw.len() >= INODE_SIZE_WITH_EXTRA {
410        raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2]
411            .copy_from_slice(&EXTRA_ISIZE_DEFAULT.to_le_bytes());
412    }
413}
414
415/// The uninit flags a mount has cleared since it read its descriptors, and
416/// the descriptors with those flags applied.
417///
418/// One lock holds both, and every change to `cleared` goes through a method
419/// here that drops `overridden` with it, so the cached vector cannot outlive
420/// the state it was built from.
421#[derive(Default)]
422struct AllocationState {
423    /// Group index to the descriptor flags its clear left behind.
424    cleared: HashMap<usize, u16>,
425    /// `Filesystem::groups` with `cleared` applied. Built on first use and
426    /// shared by every read after it, until the next change.
427    overridden: Option<Arc<[BlockGroupDescriptor]>>,
428}
429
430impl AllocationState {
431    /// Take in the clears a committed buffer staged.
432    fn publish(&mut self, cleared: BTreeMap<usize, u16>) {
433        if cleared.is_empty() {
434            return;
435        }
436        for (gi, flags) in cleared {
437            self.cleared
438                .entry(gi)
439                .and_modify(|f| *f &= flags)
440                .or_insert(flags);
441        }
442        self.overridden = None;
443    }
444
445    /// Forget every clear: the descriptors were read again and carry them.
446    fn reset(&mut self) {
447        self.cleared.clear();
448        self.overridden = None;
449    }
450}
451
452/// The descriptors the allocators plan against: the mount-time snapshot
453/// itself, or the cached copy with this mount's cleared flags applied.
454pub(crate) enum AllocationGroups<'a> {
455    Snapshot(&'a [BlockGroupDescriptor]),
456    Overridden(Arc<[BlockGroupDescriptor]>),
457}
458
459impl std::ops::Deref for AllocationGroups<'_> {
460    type Target = [BlockGroupDescriptor];
461    fn deref(&self) -> &[BlockGroupDescriptor] {
462        match self {
463            Self::Snapshot(groups) => groups,
464            Self::Overridden(groups) => groups,
465        }
466    }
467}
468
469/// What one block of a directory holds (see [`Filesystem::dir_block_role`]).
470#[derive(Clone, Copy, Debug, PartialEq, Eq)]
471pub(crate) enum DirBlockRole {
472    /// Block 0 of an indexed directory: `.`, `..` and the tree's root.
473    IndexRoot,
474    /// An interior tree node, hidden behind one empty whole-block entry.
475    IndexNode,
476    /// Directory entries: a linear directory's block, or a tree leaf.
477    Entries,
478}
479
480pub struct Filesystem {
481    runtime: Arc<dyn crate::runtime::Runtime>,
482    managed_recovery: bool,
483    /// The buffer cache `dev` routes through, kept typed for its statistics.
484    cache: Arc<crate::block_cache::CachedDevice>,
485    pub dev: Arc<dyn BlockDevice>,
486    pub sb: Superblock,
487    pub groups: Vec<BlockGroupDescriptor>,
488    /// Uninit flags this mount has already taken down on disk, by group.
489    ///
490    /// `groups` is a snapshot read once at mount and every write path holds
491    /// `&self`, so the snapshot cannot be corrected in place when a group's
492    /// INODE_UNINIT / BLOCK_UNINIT is cleared. That matters because the
493    /// allocators *plan* against those flags: a group still flagged uninit is
494    /// treated as entirely free without the bitmap being read at all. Left
495    /// stale, the second allocation into a freshly-woken group hands back the
496    /// very inode or block the first one just took — in the same mount, not
497    /// merely the next one.
498    ///
499    /// Read through [`Filesystem::allocation_groups`], which is what the
500    /// planners must be given.
501    uninit_cleared: Mutex<AllocationState>,
502    pub csum: Checksummer,
503    /// Dialect detected at mount time from the superblock's feature flags.
504    /// Drives runtime dispatch where ext2 / ext3 / ext4 differ — most
505    /// notably the inode block-mapping scheme (extent vs indirect) used
506    /// when allocating new inodes.
507    pub flavor: features::FsFlavor,
508    /// Live-write journal writer, present iff the FS has a journal AND
509    /// the device is writable. `None` for read-only mounts and for ext2-
510    /// style images. Locked per-op so mutating capi calls serialize on
511    /// the JBD2 sequence cursor.
512    pub journal: Option<std::sync::Mutex<crate::journal_writer::JournalWriter>>,
513    /// Whether this mount has cleared `EXT4_VALID_FS` on disk, and so
514    /// owes the superblock its state back when it is dropped (#85).
515    marked_not_clean: std::sync::atomic::AtomicBool,
516    /// The `s_state` found on disk when this mount cleared `EXT4_VALID_FS`:
517    /// what [`Drop`] puts back. Kept apart from `sb`, which is re-read after
518    /// replay and orphan recovery and by then holds the cleared state.
519    state_found: std::sync::atomic::AtomicU16,
520    /// Set when a direct (unjournaled) commit fails part-way (#319).
521    ///
522    /// Some of its blocks reached the disk and some did not, and the mount's
523    /// own view -- `uninit_cleared` above all -- was never told about any of
524    /// them. Planning another allocation against that view can hand out a
525    /// block the disk already records as taken, so the mount stops writing:
526    /// [`Self::refuse_write`] answers `ReadOnly` from here on, and [`Drop`]
527    /// leaves the volume marked not clean for the checker.
528    direct_commit_failed: std::sync::atomic::AtomicBool,
529    /// The on-disk journal holds committed transactions this mount has not
530    /// replayed onto the device (#375): a lazy mount, or one that mounted
531    /// read-only and replayed into the cache alone.
532    ///
533    /// A commit writes its descriptor at the head of the log and then marks
534    /// the journal clean, so one made now would overwrite the unreplayed
535    /// transactions and discard them. [`Self::refuse_write`] answers
536    /// [`Error::JournalNotReplayed`] until
537    /// [`Self::replay_journal_if_dirty`] has put them on the device.
538    replay_pending: std::sync::atomic::AtomicBool,
539}
540
541/// A mount that cleared `EXT4_VALID_FS` puts the state it found back when
542/// it is dropped, which is this crate's unmount (`fs_ext4_umount` drops the
543/// handle). A volume that was not clean when mounted stays not clean. Best
544/// effort: a drop cannot report an error, and a device that refuses the
545/// write leaves the volume marked not clean, which errs the safe way.
546impl Drop for Filesystem {
547    fn drop(&mut self) {
548        // A journal whose commit failed mid-write leaves the device to the
549        // next owner's replay: nothing more is written through this handle.
550        let journal_failed = self
551            .journal
552            .as_ref()
553            .is_some_and(|w| w.lock().map_or(true, |w| !w.is_healthy()));
554        if self
555            .marked_not_clean
556            .load(std::sync::atomic::Ordering::SeqCst)
557            && self.dev.is_writable()
558            && !journal_failed
559            && !self
560                .direct_commit_failed
561                .load(std::sync::atomic::Ordering::SeqCst)
562        {
563            let state = self.state_found.load(std::sync::atomic::Ordering::SeqCst);
564            let _ = self.write_superblock_state(state);
565        }
566    }
567}
568
569/// Encapsulates the common setup for creating a new inode in a directory:
570/// resolved parent, pre-allocated inode number, and a `BlockBuffer` with the
571/// inode-bitmap + BGD + SB counter updates already staged. Produced by
572/// `Filesystem::plan_new_inode_in_dir`.
573struct NewInodePlan {
574    /// Newly allocated inode number (1-based).
575    new_ino: u32,
576    /// Inode number of the parent directory.
577    parent_ino: u32,
578    /// Parsed parent inode (for reading the directory block).
579    parent_inode: crate::inode::Inode,
580    /// Staged write buffer (bitmap + counter deltas already applied).
581    buf: BlockBuffer,
582}
583
584/// Which BGD "uninit" flag a bitmap-marking call is about — see
585/// `Filesystem::clear_bgd_uninit_flag_if_set`.
586#[derive(Clone, Copy, PartialEq, Eq)]
587pub(crate) enum BgdUninitFlag {
588    Inode,
589    Block,
590}
591
592/// The most bytes `apply_pwrite` buffers for one transaction. A chunk is
593/// held several times over while it commits (block buffer, transaction,
594/// serialised journal blocks); this bounds that, independent of the
595/// journal's size. 32 MiB is at most 32768 blocks, the longest initialised
596/// extent, at every block size, so no allocation within a chunk can outgrow
597/// one extent.
598const PWRITE_CHUNK_MAX_BYTES: u64 = 32 * 1024 * 1024;
599
600/// The longest volume label [`Filesystem::set_volume_label`] takes: the
601/// 16 bytes of `s_volume_name`.
602pub const VOLUME_LABEL_MAX: usize = 16;
603
604/// Clean blocks the buffer cache of a [`Filesystem::mount`] keeps: about
605/// 1 MiB at 4 KiB blocks. `docs/read-path-cost.md` records what it buys on a
606/// measured tree (#68).
607pub const DEFAULT_CACHE_BLOCKS: usize = 256;
608
609impl Filesystem {
610    /// Open an exclusively owned, backed-up device with checked journal recovery.
611    /// Opening this writable handle is already a mutation: retain an external
612    /// backup before calling. Only plain JBD2 (no transaction checksums, fast
613    /// commit or async commit) is currently qualified by this lifecycle.
614    pub fn mount_recovering(dev: Arc<dyn BlockDevice>) -> Result<Self> {
615        let mut fs = Self::mount_lazy(dev)?;
616        // This lifecycle replays the journal itself, below, with the live
617        // writer detached until it has: nothing commits over the log first.
618        fs.replay_pending
619            .store(false, std::sync::atomic::Ordering::SeqCst);
620        fs.refuse_write()?;
621        let jsb = crate::jbd2::read_superblock(&fs)?.ok_or(Error::Unsupported(
622            "checked recovery requires an internal journal",
623        ))?;
624        jsb.validate_plain_recovery(fs.sb.block_size(), fs.sb.blocks_count)?;
625        if fs.sb.state & crate::superblock::EXT4_ERROR_FS != 0 {
626            return Err(Error::Corrupt("filesystem records an outstanding error"));
627        }
628        fs.journal = None;
629        fs.set_recovery_marker(true)?;
630        crate::journal_apply::replay_if_dirty(&fs)?;
631        fs.refresh_metadata()?;
632        fs.refuse_write()?;
633        crate::jbd2::read_superblock(&fs)?
634            .ok_or(Error::Corrupt("journal disappeared"))?
635            .validate_plain_recovery(fs.sb.block_size(), fs.sb.blocks_count)?;
636        fs.set_recovery_marker(true)?;
637        fs.journal = crate::journal_writer::JournalWriter::open(&fs)?
638            .map(|w| Mutex::new(w.holding_needs_recovery()));
639        fs.recover_orphans()?;
640        fs.refresh_metadata()?;
641        fs.managed_recovery = true;
642        Ok(fs)
643    }
644
645    /// Finish a checked mount, propagating flush errors. Dropping a handle does
646    /// not claim a clean release: the recovery marker remains for the next owner.
647    /// After any I/O failure, release ownership and reopen rather than reusing it.
648    pub fn finish(mut self) -> Result<()> {
649        self.flush()?;
650        if self.managed_recovery {
651            self.refresh_metadata()?;
652            if self.sb.last_orphan != 0 {
653                return Err(Error::Corrupt("orphan recovery remains incomplete"));
654            }
655            // The marker is the release's commit point, so it is cleared
656            // last: a failure before it leaves the volume still flagged
657            // for recovery rather than claiming a release that did not
658            // finish (#299).
659            self.restore_state_found()?;
660            return self.set_recovery_marker(false);
661        }
662        self.restore_state_found()
663    }
664
665    /// Put back the `s_state` this mount found, as [`Drop`] would, but with
666    /// its error reported: `finish` is a release that says whether it
667    /// happened, and the drop that follows it then has nothing left to write.
668    fn restore_state_found(&self) -> Result<()> {
669        use std::sync::atomic::Ordering;
670        if !self.marked_not_clean.load(Ordering::SeqCst) {
671            return Ok(());
672        }
673        self.write_superblock_state(self.state_found.load(Ordering::SeqCst))?;
674        self.marked_not_clean.store(false, Ordering::SeqCst);
675        Ok(())
676    }
677
678    /// Flush a live mount without clearing its recovery marker or releasing it.
679    /// Checked journal transactions must already be fully checkpointed. After an
680    /// error the owner must retire the mount, just as for a failed mutation.
681    ///
682    /// Takes `&self` so a handle shared between threads can flush while other
683    /// calls run. The journal writer is held for the whole check, so a
684    /// transaction another thread is committing either finishes first or
685    /// starts after: the check never sees one half-done.
686    pub fn flush(&self) -> Result<()> {
687        if self
688            .direct_commit_failed
689            .load(std::sync::atomic::Ordering::SeqCst)
690        {
691            return Err(Error::Corrupt(
692                "direct commit failed; reopen and check the volume",
693            ));
694        }
695        let _writer = match &self.journal {
696            Some(writer) => {
697                let guard = writer
698                    .lock()
699                    .map_err(|_| Error::Corrupt("journal writer poisoned"))?;
700                if !guard.is_healthy() {
701                    return Err(Error::Corrupt(
702                        "journal operation failed; reopen for recovery",
703                    ));
704                }
705                Some(guard)
706            }
707            None => None,
708        };
709        self.dev.flush()?;
710        if !self.managed_recovery {
711            return Ok(());
712        }
713        let jsb =
714            crate::jbd2::read_superblock(self)?.ok_or(Error::Corrupt("journal disappeared"))?;
715        if !jsb.is_clean() || jsb.errno != 0 {
716            return Err(Error::Corrupt("journal is not checkpointed"));
717        }
718        Ok(())
719    }
720
721    /// How many blocks the buffer cache holds pinned: bytes not yet at their
722    /// final location on the device, which no capacity bound can evict. A
723    /// read-only mount that replayed a dirty journal pins what it replayed;
724    /// a writable mount checkpoints every commit, so it pins nothing.
725    pub fn cache_pinned_blocks(&self) -> usize {
726        self.cache.pinned_blocks()
727    }
728
729    /// Flush and discard checkpointed read caches before physical readback.
730    /// The mount remains owned and usable. This is not concurrent-writer support:
731    /// callers must serialize all filesystem access and retire on I/O failure.
732    pub fn fresh_read(&mut self) -> Result<()> {
733        self.flush()?;
734        // Unmanaged writable mounts also use the immediate-checkpoint writer.
735        // Pinned blocks are released only once the on-disk journal is proven
736        // checkpointed. Without a writer nothing was checkpointed: a read-only
737        // mount's replayed blocks exist only in the cache, so they stay pinned
738        // and `invalidate_cache` refuses rather than serve pre-replay bytes.
739        if self.journal.is_some() {
740            let jsb =
741                crate::jbd2::read_superblock(self)?.ok_or(Error::Corrupt("journal disappeared"))?;
742            if !jsb.is_clean() || jsb.errno != 0 {
743                return Err(Error::Corrupt("journal is not checkpointed"));
744            }
745            self.dev.unpin_all();
746        }
747        self.dev.invalidate_cache()?;
748        self.refresh_metadata()
749    }
750
751    fn refresh_metadata(&mut self) -> Result<()> {
752        let sb = Superblock::read(self.dev.as_ref())?;
753        if sb.block_size() != self.sb.block_size()
754            || sb.blocks_count != self.sb.blocks_count
755            || sb.uuid != self.sb.uuid
756            || sb.raw[0xd0..0xe8] != self.sb.raw[0xd0..0xe8]
757        {
758            return Err(Error::Corrupt(
759                "journal changed filesystem identity or geometry",
760            ));
761        }
762        features::check_mountable(sb.feature_incompat, sb.feature_ro_compat)?;
763        let csum = Checksummer::from_superblock(&sb);
764        if csum.enabled && !csum.verify_superblock(&sb.raw) {
765            return Err(Error::BadChecksum {
766                what: "replayed superblock",
767            });
768        }
769        let groups = bgd::read_all(self.dev.as_ref(), &sb, &csum)?;
770        self.flavor = features::FsFlavor::detect(sb.feature_compat, sb.feature_incompat);
771        self.sb = sb;
772        self.groups = groups;
773        self.csum = csum;
774        self.uninit_cleared
775            .lock()
776            .map_err(|_| Error::Corrupt("allocation state poisoned"))?
777            .reset();
778        Ok(())
779    }
780
781    fn set_recovery_marker(&mut self, needed: bool) -> Result<()> {
782        let recover = features::Incompat::RECOVER.bits();
783        let on_disk = Superblock::read(self.dev.as_ref())?.feature_incompat & recover != 0;
784        if on_disk != needed {
785            crate::journal_apply::write_needs_recovery(self.dev.as_ref(), needed)?;
786            self.dev.flush()?;
787        }
788        self.refresh_metadata()
789    }
790
791    /// Mount the ext4 filesystem on `dev`. Read-only unless the device reports
792    /// `is_writable()`, in which case a dirty journal is replayed before
793    /// returning so callers see a consistent on-disk state.
794    ///
795    /// When `RO_COMPAT_METADATA_CSUM` is set, the superblock checksum is
796    /// verified — failure aborts the mount with `Error::BadChecksum`.
797    pub fn mount(dev: Arc<dyn BlockDevice>) -> Result<Self> {
798        Self::mount_inner(
799            dev,
800            false,
801            DEFAULT_CACHE_BLOCKS,
802            Arc::new(crate::runtime::SystemRuntime),
803        )
804    }
805
806    /// [`Filesystem::mount`] with a buffer cache of `blocks` clean blocks
807    /// rather than [`DEFAULT_CACHE_BLOCKS`]. Zero keeps none, so every read
808    /// reaches the device: the baseline `tests/read_path_cost.rs` measures the
809    /// cache against (#68). Journaled blocks awaiting checkpoint are held
810    /// whatever the capacity.
811    pub fn mount_with_cache(dev: Arc<dyn BlockDevice>, blocks: usize) -> Result<Self> {
812        Self::mount_inner(dev, false, blocks, Arc::new(crate::runtime::SystemRuntime))
813    }
814
815    /// Like `mount`, but skips the mount-time journal replay even when the
816    /// device is writable. The caller is responsible for invoking
817    /// [`Filesystem::replay_journal_if_dirty`] once the underlying write
818    /// path is actually ready to service writes (e.g. in the FSKit case the
819    /// kernel-level write FD on `FSBlockDeviceResource` only becomes
820    /// writable AFTER `loadResource` returns successfully — replaying mid-
821    /// `loadResource` produces EIO).
822    ///
823    /// Until replay runs, reads observe the on-disk pre-replay state and
824    /// any write through this handle fails with
825    /// [`Error::JournalNotReplayed`] while the journal still says dirty
826    /// (#375): a commit would overwrite the unreplayed log. This is the lazy/deferred-replay sibling of `mount`; for
827    /// most callers `mount` is correct.
828    pub fn mount_lazy(dev: Arc<dyn BlockDevice>) -> Result<Self> {
829        Self::mount_inner(
830            dev,
831            true,
832            DEFAULT_CACHE_BLOCKS,
833            Arc::new(crate::runtime::SystemRuntime),
834        )
835    }
836
837    /// Mount with caller-provided wall time and inode-generation policy.
838    /// The provider must remain valid for the entire mount, including recovery.
839    pub fn mount_with_runtime(
840        dev: Arc<dyn BlockDevice>,
841        runtime: Arc<dyn crate::runtime::Runtime>,
842    ) -> Result<Self> {
843        Self::mount_inner(dev, false, DEFAULT_CACHE_BLOCKS, runtime)
844    }
845
846    fn mount_inner(
847        dev: Arc<dyn BlockDevice>,
848        defer_replay: bool,
849        cache_blocks: usize,
850        runtime: Arc<dyn crate::runtime::Runtime>,
851    ) -> Result<Self> {
852        let sb = Superblock::read(dev.as_ref())?;
853        features::check_mountable(sb.feature_incompat, sb.feature_ro_compat)?;
854        let flavor = features::FsFlavor::detect(sb.feature_compat, sb.feature_incompat);
855        let csum = Checksummer::from_superblock(&sb);
856        if csum.enabled && !csum.verify_superblock(&sb.raw) {
857            return Err(Error::BadChecksum { what: "superblock" });
858        }
859        let groups = bgd::read_all(dev.as_ref(), &sb, &csum)?;
860        sb.check_fits_device(dev.size_bytes())?;
861        // Wrap the raw device in a write-through buffer cache. All
862        // reads and writes for the rest of this mount session route
863        // through the cache. A read-only mount that replays a dirty
864        // journal pins the replayed blocks here, since they never reach
865        // the device. The clean capacity is `DEFAULT_CACHE_BLOCKS` unless
866        // the caller chose; pinned entries are unbounded until journal
867        // replay calls `unpin_all`.
868        let cache = Arc::new(crate::block_cache::CachedDevice::new(
869            dev,
870            sb.block_size(),
871            cache_blocks,
872        ));
873        let dev: Arc<dyn BlockDevice> = cache.clone();
874        let mut fs = Self {
875            runtime,
876            managed_recovery: false,
877            cache,
878            dev,
879            sb,
880            groups,
881            uninit_cleared: Mutex::new(AllocationState::default()),
882            csum,
883            flavor,
884            journal: None,
885            marked_not_clean: std::sync::atomic::AtomicBool::new(false),
886            direct_commit_failed: std::sync::atomic::AtomicBool::new(false),
887            replay_pending: std::sync::atomic::AtomicBool::new(false),
888            state_found: std::sync::atomic::AtomicU16::new(0),
889        };
890
891        // Replay a dirty journal: onto the device if it is writable (below,
892        // after the write-breaking check), into the cache if it is not. A
893        // dirty journal's transactions are committed, not pending, and a
894        // read-only view that skips them reads superseded metadata (#72).
895        // A lazy mount defers only the writes; reading the journal is no
896        // different from the rest of mount.
897        //
898        // Both the walker (`journal_block_to_physical`) and the writer
899        // (`JournalWriter::open`) now dispatch on `indirect::map_logical_any`,
900        // so ext3 (whose journal inode uses legacy indirect block pointers)
901        // works the same as ext4 (extent tree). The Phase A blanket refusal
902        // of ext3 RW is therefore lifted.
903        // MMP — Multi-Mount Protection — exists to stop two hosts
904        // mounting one filesystem read-write at the same time and
905        // destroying it. Honouring it means reading the MMP block,
906        // checking its sequence, writing our own node name, waiting,
907        // and re-checking; none of that is implemented.
908        //
909        // Ignoring the bit is defensible for a read-only mount: a
910        // reader cannot corrupt anything, and the other host's
911        // protection is unaffected. It is NOT defensible the moment we
912        // are the one writing -- which this crate does, through
913        // twenty-one apply_* entry points and a live journal writer,
914        // both reached below on exactly this condition.
915        //
916        // So the refusal is scoped to the writable case. A read-only
917        // mount of an MMP filesystem still works, which is what a user
918        // recovering data from a disk another machine has open
919        // actually wants.
920        //
921        // CASEFOLD has the same shape and is refused by the same check —
922        // see `features::WRITE_BREAKING_INCOMPAT`, which is where the
923        // reasoning for each bit now lives. It is one set rather than a
924        // chain of `if`s because a second copy of this shape is how the
925        // first one gets forgotten.
926        let write_breaking = crate::features::write_breaking_incompat(fs.sb.feature_incompat);
927        if fs.dev.is_writable() && write_breaking != 0 {
928            return Err(crate::error::Error::UnsupportedIncompat(write_breaking));
929        }
930
931        let replayed = if !fs.dev.is_writable() {
932            crate::journal_apply::replay_into_cache(&fs)?
933        } else if !defer_replay {
934            // A replay failure fails the mount: the error surfaces so the
935            // caller can decide whether to retry or proceed; we fail loud
936            // rather than silent.
937            crate::journal_apply::replay_if_dirty(&fs)?
938        } else {
939            0
940        };
941        if replayed > 0 {
942            fs.reload_geometry()?;
943        }
944
945        // Open the live-write journal writer once replay is done. Any
946        // pending transactions are now applied; the writer can take over
947        // the JBD2 cursor from a clean state. Returns None when there is
948        // no journal at all (ext2), so the if-let handles every flavor
949        // uniformly.
950        //
951        // GATED ON `refuse_write` RATHER THAN ON THE DEVICE, so a volume
952        // carrying a feature this driver does not maintain does not get
953        // a live-write journal it will never be allowed to use.
954        //
955        // Journal REPLAY above is deliberately not gated the same way.
956        // Replay is a write, but it is the one write that adds nothing:
957        // it finishes transactions the filesystem itself already
958        // committed, including whatever the last writer did to the
959        // structures behind the unmaintained bit. Refusing it would
960        // leave a volume that reads its own stale metadata, which is a
961        // worse answer than the one it prevents.
962        if fs.write_refusal().is_ok() {
963            if let Some(jw) = crate::journal_writer::JournalWriter::open(&fs)? {
964                fs.journal = Some(std::sync::Mutex::new(jw));
965            }
966        }
967
968        // A journal still dirty on disk after the replay decision above --
969        // deferred, or replayed into the cache of a read-only device -- keeps
970        // every write waiting for `replay_journal_if_dirty` (#375). Set after
971        // the writer is opened, whose own gate is `write_refusal`.
972        if !fs.dev.is_writable() || defer_replay {
973            let dirty = crate::jbd2::read_superblock(&fs)?.is_some_and(|j| !j.is_clean());
974            fs.replay_pending
975                .store(dirty, std::sync::atomic::Ordering::SeqCst);
976        }
977
978        // Phase 6.2 — orphan recovery. Runs after journal replay so any
979        // pending kernel-level transactions have already played back;
980        // any inode still on the orphan chain at this point is genuinely
981        // dead or awaiting completion of a truncate. Failure aborts the mount;
982        // never discard an error then clear the orphan list anyway.
983        if !defer_replay {
984            // `recover_orphans` consults `refuse_write` itself and
985            // returns zero when it must not write.
986            fs.recover_orphans()?;
987            fs.refresh_metadata()?;
988        }
989
990        Ok(fs)
991    }
992
993    /// Read the superblock and group descriptors again, through the cache.
994    /// Mount read both before replaying the journal, which may carry newer
995    /// copies of either (#72).
996    fn reload_geometry(&mut self) -> Result<()> {
997        let sb = Superblock::read(self.dev.as_ref())?;
998        features::check_mountable(sb.feature_incompat, sb.feature_ro_compat)?;
999        let csum = Checksummer::from_superblock(&sb);
1000        if csum.enabled && !csum.verify_superblock(&sb.raw) {
1001            return Err(Error::BadChecksum { what: "superblock" });
1002        }
1003        self.groups = bgd::read_all(self.dev.as_ref(), &sb, &csum)?;
1004        sb.check_fits_device(self.dev.size_bytes())?;
1005        // The cached override was built from the descriptors just replaced.
1006        self.uninit_cleared
1007            .get_mut()
1008            .map_err(|_| Error::Corrupt("allocation state poisoned"))?
1009            .overridden = None;
1010        self.flavor = features::FsFlavor::detect(sb.feature_compat, sb.feature_incompat);
1011        self.csum = csum;
1012        self.sb = sb;
1013        Ok(())
1014    }
1015
1016    /// Run journal replay now if the journal is dirty. Idempotent — calling
1017    /// this on a clean (or read-only) volume is a no-op that returns 0.
1018    /// Designed to pair with [`Filesystem::mount_lazy`], but safe to call
1019    /// on any handle.
1020    /// Refuse a write this driver cannot make consistently.
1021    ///
1022    /// Two reasons, and they are different in kind:
1023    ///
1024    /// - the device cannot be written at all;
1025    /// - the volume carries a `RO_COMPAT` feature bit describing
1026    ///   structures a write here would leave behind.
1027    ///
1028    /// The second is what the bit is FOR, and it was enforced nowhere.
1029    /// `check_mountable` decides what may be read -- its own comment
1030    /// says "mounted read-only" -- and this driver stopped being
1031    /// read-only long ago. So an unrecognised bit permitted reading,
1032    /// which is correct, and writing, which is the failure the bit
1033    /// exists to prevent: the write updates what this driver knows
1034    /// about and silently leaves the rest, and nothing reports it.
1035    ///
1036    /// `QUOTA` is the case to picture. It is tolerated for reading and
1037    /// nothing here maintains the quota inodes, so a create charged
1038    /// nobody for the file and left counters describing a filesystem
1039    /// that no longer exists.
1040    pub(crate) fn refuse_write(&self) -> Result<()> {
1041        self.write_refusal()?;
1042        self.mark_not_clean_once()
1043    }
1044
1045    /// [`Self::refuse_write`]'s verdict alone, without marking the volume
1046    /// not clean: for the mount path, which asks whether it could write
1047    /// before it knows whether it will.
1048    fn write_refusal(&self) -> Result<()> {
1049        if !self.dev.is_writable() {
1050            return Err(Error::ReadOnly);
1051        }
1052        // A direct commit that failed part-way left the disk and this
1053        // mount's view of it disagreeing (#319): no further write can be
1054        // planned safely, as the kernel remounts read-only on a write error.
1055        if self
1056            .direct_commit_failed
1057            .load(std::sync::atomic::Ordering::SeqCst)
1058        {
1059            return Err(Error::ReadOnly);
1060        }
1061        if self
1062            .replay_pending
1063            .load(std::sync::atomic::Ordering::SeqCst)
1064        {
1065            return Err(Error::JournalNotReplayed);
1066        }
1067        let unmaintained = features::unmaintained_ro_compat(self.sb.feature_ro_compat);
1068        if unmaintained != 0 {
1069            return Err(Error::UnsupportedRoCompat(unmaintained));
1070        }
1071        // THE MOUNT'S INCOMPAT REFUSAL, AGAIN, HERE. It runs once, against
1072        // `is_writable()` at mount time, and writability is not fixed: a
1073        // lazy mount starts read-only and the host hands it a write FD
1074        // afterwards (see `mount_lazy`). That mount passed the check by
1075        // being read-only and then wrote to a CASEFOLD or MMP volume
1076        // (#117). The mount-time check stays, so a volume that is writable
1077        // from the start still fails to mount rather than mounting and
1078        // refusing.
1079        let write_breaking = features::write_breaking_incompat(self.sb.feature_incompat);
1080        if write_breaking != 0 {
1081            return Err(Error::UnsupportedIncompat(write_breaking));
1082        }
1083        Ok(())
1084    }
1085
1086    /// Clear `EXT4_VALID_FS` in the on-disk superblock before this mount's
1087    /// first write, as the kernel does when it mounts read-write (#85).
1088    ///
1089    /// The bit is what `fsck -p` at boot, and `e2fsck`'s "clean", read as
1090    /// "cleanly unmounted". This driver never touched it, so a volume a
1091    /// crash interrupted mid-write still claimed to have been put away
1092    /// properly and the boot-time check skipped it. Cleared here, it stays
1093    /// cleared until [`Drop`] puts the mount-time state back; a crash in
1094    /// between leaves it clear, which is the point.
1095    ///
1096    /// Done on the first write rather than at mount so a read-write mount
1097    /// that never writes leaves the volume byte-for-byte as it found it.
1098    fn mark_not_clean_once(&self) -> Result<()> {
1099        use std::sync::atomic::Ordering;
1100        if self.marked_not_clean.load(Ordering::SeqCst) {
1101            return Ok(());
1102        }
1103        self.state_found.store(self.sb.state, Ordering::SeqCst);
1104        self.write_superblock_state(self.sb.state & !crate::superblock::EXT4_VALID_FS)?;
1105        self.marked_not_clean.store(true, Ordering::SeqCst);
1106        Ok(())
1107    }
1108
1109    /// Set the volume label, `s_volume_name`: the 16 bytes at superblock
1110    /// offset 0x78, NUL-padded (#447).
1111    ///
1112    /// `label` is at most [`VOLUME_LABEL_MAX`] bytes and holds no NUL,
1113    /// since the field ends at its first NUL; anything else is
1114    /// `InvalidArgument`, and nothing is written. An empty label clears it.
1115    /// The label is bytes, as the format stores it: UTF-8 is what tools
1116    /// print, not what the format requires.
1117    ///
1118    /// Written to the primary superblock and to every backup the volume's
1119    /// own layout places (the groups [`Superblock::group_has_super`]
1120    /// names), each with its checksum restamped on metadata_csum volumes,
1121    /// in one transaction. `tune2fs -L` does the same: after it, the
1122    /// backup in group 1 carries the new label too, and `e2fsck -fn` passes
1123    /// the volume (`tests/volume_label_oracle.rs`). A backup slot that does
1124    /// not hold a superblock (no magic) is left as it is.
1125    pub fn set_volume_label(&mut self, label: &[u8]) -> Result<()> {
1126        if label.len() > VOLUME_LABEL_MAX {
1127            return Err(Error::InvalidArgument(
1128                "volume label: longer than the 16 bytes s_volume_name holds",
1129            ));
1130        }
1131        if label.contains(&0) {
1132            return Err(Error::InvalidArgument(
1133                "volume label: a NUL byte would end it early",
1134            ));
1135        }
1136        self.refuse_write()?;
1137        let mut field = [0u8; VOLUME_LABEL_MAX];
1138        field[..label.len()].copy_from_slice(label);
1139
1140        let bs = u64::from(self.sb.block_size());
1141        let primary = crate::superblock::SUPERBLOCK_OFFSET;
1142        let mut buf = BlockBuffer::new(self.sb.block_size());
1143        for g in 0..self.sb.block_group_count() {
1144            if !self.sb.group_has_super(g) {
1145                continue;
1146            }
1147            // The primary is at byte 1024; a backup opens its group's
1148            // first block.
1149            let (block, at) = if g == 0 {
1150                (primary / bs, (primary % bs) as usize)
1151            } else {
1152                let start =
1153                    u64::from(self.sb.first_data_block) + g * u64::from(self.sb.blocks_per_group);
1154                (start, 0)
1155            };
1156            let bytes = buf.get_mut(self, block)?;
1157            let sb = &mut bytes[at..at + crate::superblock::SUPERBLOCK_SIZE];
1158            if u16::from_le_bytes([sb[0x38], sb[0x39]]) != crate::superblock::EXT4_MAGIC {
1159                continue;
1160            }
1161            sb[0x78..0x88].copy_from_slice(&field);
1162            if self.csum.enabled {
1163                let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
1164                sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
1165            }
1166        }
1167        self.commit_block_buffer(buf)?;
1168
1169        if self.sb.raw.len() >= 0x88 {
1170            self.sb.raw[0x78..0x88].copy_from_slice(&field);
1171        }
1172        self.sb.volume_name = String::from_utf8_lossy(label).into_owned();
1173        Ok(())
1174    }
1175
1176    /// Write `state` into the on-disk superblock's `s_state` (0x3A),
1177    /// directly rather than through a transaction -- the kernel's own
1178    /// mount and unmount writes are direct -- keeping its checksum right.
1179    fn write_superblock_state(&self, state: u16) -> Result<()> {
1180        let at = crate::superblock::SUPERBLOCK_OFFSET;
1181        let mut sb = vec![0u8; 1024];
1182        self.dev.read_at(at, &mut sb)?;
1183        sb[0x3A..0x3C].copy_from_slice(&state.to_le_bytes());
1184        if self.csum.enabled {
1185            let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
1186            sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
1187        }
1188        self.dev.write_at(at, &sb)?;
1189        self.dev.flush()
1190    }
1191
1192    ///
1193    /// Takes `&mut self` because a replay that applied anything leaves the
1194    /// mount's own view behind (#376): the superblock and descriptors were
1195    /// read before it, and a journal writer opened over the dirty log holds
1196    /// its pre-replay sequence. All three are brought up to date here, as
1197    /// an eager mount does before it opens its writer.
1198    pub fn replay_journal_if_dirty(&mut self) -> Result<usize> {
1199        let dirty = self.dev.is_writable()
1200            && crate::jbd2::read_superblock(self)?.is_some_and(|j| !j.is_clean());
1201        let n = crate::journal_apply::replay_if_dirty(self)?;
1202        // `replay_if_dirty` does nothing on a device that cannot be written,
1203        // so only a writable one has had its log put on disk.
1204        if self.dev.is_writable() {
1205            self.replay_pending
1206                .store(false, std::sync::atomic::Ordering::SeqCst);
1207        }
1208        // Replay applied every pending journaled write to the data area,
1209        // so the device-layer cache's "pinned" entries (post-commit but
1210        // pre-checkpoint) are now consistent with disk. Tell the cache
1211        // it can stop pinning them — future evictions are safe.
1212        // Skip when nothing replayed: a clean journal returns 0, and
1213        // unpinning here would demote pinned-but-still-needed entries
1214        // from a live handle's prior journaled writes, letting later
1215        // cache misses serve stale data-area bytes.
1216        if n > 0 {
1217            self.dev.unpin_all();
1218            // The replayed transactions may carry newer copies of the
1219            // superblock and descriptors. Planning against the copies read
1220            // at mount treats a group whose UNINIT flag the replay cleared
1221            // as untouched, synthesises its bitmap, and hands out what the
1222            // replay allocated. `uninit_cleared` described the old copies.
1223            self.reload_geometry()?;
1224            // `reset`, not a bare clear: the allocation state is a
1225            // struct since #341 and also caches the descriptors with the
1226            // clears applied, which must go with them.
1227            self.uninit_cleared
1228                .lock()
1229                .map_err(|_| Error::Corrupt("allocation state poisoned"))?
1230                .reset();
1231        }
1232        // The writer takes over the log cursor from the replayed journal
1233        // superblock, as mount's does, so its next commit follows the
1234        // replayed sequence rather than rewinding it. One opened over the
1235        // dirty log is replaced; a mount that was read-only when it mounted
1236        // has none, and is given one now that the device can take writes.
1237        // A healthy writer over a journal that was already clean is left
1238        // alone: it owns the cursor.
1239        // A writer whose commit failed is kept: its failure is what `flush`
1240        // and `Drop` report, and a fresh one would hide it.
1241        let failed = self
1242            .journal
1243            .as_ref()
1244            .is_some_and(|w| w.lock().map_or(true, |w| !w.is_healthy()));
1245        if (dirty || self.journal.is_none()) && !failed && self.write_refusal().is_ok() {
1246            self.journal = crate::journal_writer::JournalWriter::open(self)?.map(Mutex::new);
1247        }
1248        Ok(n)
1249    }
1250
1251    /// Phase 6.1 — walk the orphan inode chain rooted at `s_last_orphan`
1252    /// and return its members in chain order.
1253    ///
1254    /// The chain has TWO kinds of member, and they are not distinguished
1255    /// here — see [`Filesystem::recover_orphans`], which branches on
1256    /// `i_links_count` to tell them apart. An inode with
1257    /// no links is an unlink-while-open: its blocks and its inode should
1258    /// be reclaimed. An inode that still has links is a `truncate()` a
1259    /// crash interrupted: it is still named by its directory entries and
1260    /// only the blocks past `i_size` should go.
1261    ///
1262    /// The chain is encoded by overloading `i_dtime` as "next orphan
1263    /// inode number"; the chain terminates when `dtime == 0`. We cap at
1264    /// `inodes_count` to avoid runaway loops on cycle-corrupted images.
1265    ///
1266    /// Read-only (no recovery yet — that's Phase 6.2). Returns `Ok([])`
1267    /// when there are no orphans.
1268    pub fn orphan_list(&self) -> Result<Vec<u32>> {
1269        let mut out = Vec::new();
1270        let mut cur = self.sb.last_orphan;
1271        let cap = self.sb.inodes_count;
1272        let mut steps = 0u32;
1273        while cur != 0 {
1274            if steps > cap {
1275                return Err(Error::Corrupt(
1276                    "orphan_list: chain longer than inodes_count (cycle?)",
1277                ));
1278            }
1279            out.push(cur);
1280            // Read the inode's i_dtime (offset 0x14..0x18) to find the
1281            // next link. Don't go through read_inode_verified because an
1282            // orphan inode's checksum may be stale by design.
1283            let raw = self.read_inode_raw(cur)?;
1284            if raw.len() < 0x18 {
1285                return Err(Error::Corrupt("orphan_list: inode too short"));
1286            }
1287            cur = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
1288            steps += 1;
1289        }
1290        Ok(out)
1291    }
1292
1293    /// The orphan chain as recovery walks it: [`Self::orphan_list`], except
1294    /// that it stops at a member that is not allocated in the inode bitmap
1295    /// and whose `i_dtime` is no longer a link. The kernel stops at such a
1296    /// member too (a "bad orphan inode") and then clears `s_last_orphan`.
1297    ///
1298    /// Such a member is an orphan an earlier recovery already reclaimed and
1299    /// stamped with a deletion time. Following that time as an inode number
1300    /// failed every later mount's recovery on "InvalidInode", and the chain
1301    /// was never cleared (#124).
1302    ///
1303    /// A free member whose `i_dtime` is still an inode number is different:
1304    /// a recovery was cut while reclaiming it, before the superblock moved
1305    /// the head past it. [`Self::recover_orphans`] keeps that link until the
1306    /// head has moved, so the walk goes through it and on to the members
1307    /// behind it, and the retry finishes whatever the cut left.
1308    fn orphan_chain_to_recover(&self) -> Result<Vec<u32>> {
1309        let mut out = Vec::new();
1310        let mut cur = self.sb.last_orphan;
1311        while cur != 0 {
1312            if out.len() as u64 > u64::from(self.sb.inodes_count) {
1313                return Err(Error::Corrupt(
1314                    "orphan_list: chain longer than inodes_count (cycle?)",
1315                ));
1316            }
1317            let allocated = self.inode_bit_is_set(cur)?;
1318            if !allocated && (cur > self.sb.inodes_count || cur < self.sb.first_inode) {
1319                break;
1320            }
1321            let raw = self.read_inode_raw(cur)?;
1322            if raw.len() < 0x18 {
1323                return Err(Error::Corrupt("orphan_list: inode too short"));
1324            }
1325            let next = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
1326            if !allocated && next > self.sb.inodes_count {
1327                break;
1328            }
1329            out.push(cur);
1330            cur = next;
1331        }
1332        Ok(out)
1333    }
1334
1335    /// Whether `ino` is marked in use in its group's inode bitmap. An
1336    /// out-of-range number, or a group still `INODE_UNINIT`, is not.
1337    fn inode_bit_is_set(&self, ino: u32) -> Result<bool> {
1338        if ino == 0 || ino > self.sb.inodes_count || self.sb.inodes_per_group == 0 {
1339            return Ok(false);
1340        }
1341        let ipg = self.sb.inodes_per_group;
1342        let groups = self.allocation_groups();
1343        let Some(group) = groups.get(((ino - 1) / ipg) as usize) else {
1344            return Ok(false);
1345        };
1346        if group.flags().contains(crate::bgd::BgdFlags::INODE_UNINIT) {
1347            return Ok(false);
1348        }
1349        let bitmap = self.read_block(group.inode_bitmap)?;
1350        let bit = ((ino - 1) % ipg) as usize;
1351        Ok(bitmap
1352            .get(bit / 8)
1353            .is_some_and(|byte| byte & (1 << (bit % 8)) != 0))
1354    }
1355
1356    /// Phase 6.2 — orphan replay.
1357    ///
1358    /// # THE CHAIN HAS TWO KINDS OF MEMBER AND THEY GET OPPOSITE
1359    /// TREATMENT
1360    ///
1361    /// The format documentation ("Orphan file") names both reasons an
1362    /// inode is on the chain; `i_links_count` tells them apart, and this
1363    /// branches on it:
1364    ///
1365    /// - **No links** — an unlink-while-open. Nothing names it any more,
1366    ///   so its data blocks and its inode-bitmap slot are freed and its
1367    ///   body is zeroed with `i_dtime = now`.
1368    /// - **Links remaining** — a `truncate()` that a crash interrupted.
1369    ///   `i_size` was already lowered before the machine went down; the
1370    ///   blocks past it were not yet freed. The file is still named by
1371    ///   its directory entries, so recovery FINISHES THE TRUNCATE and
1372    ///   leaves the file in place: free what lies past `i_size`, rewrite
1373    ///   the extent root and `i_blocks`, clear `i_dtime`, and touch
1374    ///   nothing else.
1375    ///
1376    /// Treating the second kind as the first is what this used to do,
1377    /// and it destroyed data: the inode of a file the user never deleted
1378    /// was freed and its body — including the block pointers that were
1379    /// the only way back to its contents — zeroed, while the directory
1380    /// entries naming it were left pointing at a free inode.
1381    ///
1382    /// # ONE COMMIT PER MEMBER, AND THE LINK OUTLIVES THE MEMBER
1383    ///
1384    /// A member's only link to the next is its own `i_dtime`, which both
1385    /// treatments overwrite. So each member is its own commit, which also
1386    /// moves `s_last_orphan` on to the next member, and in that commit the
1387    /// member's `i_dtime` still holds the link. Without a journal the
1388    /// superblock is written last, so a cut anywhere in the commit leaves
1389    /// the head on this member with the link intact, and the next mount
1390    /// walks through it to the rest (see [`Self::orphan_chain_to_recover`]).
1391    /// The final `i_dtime` (a deletion time, or zero for a finished
1392    /// truncate) is written in the NEXT member's commit, once the head is
1393    /// durably past it. The last member has nothing behind it to lose and
1394    /// gets its final `i_dtime` at once.
1395    ///
1396    /// All in one commit, a cut after the head's group and before a later
1397    /// member's left the head free with a deletion time and the later
1398    /// member allocated, and the next mount cleared the chain at the head
1399    /// and stranded the rest (Greptile on #201). A cut between two commits
1400    /// can still leave one member off the chain with its link for an
1401    /// `i_dtime`, which `e2fsck -p` resets without asking.
1402    ///
1403    /// Returns the number of orphan inodes **reclaimed** — completed
1404    /// truncates are not counted, because nothing was reclaimed. No-op
1405    /// (returns 0) when the chain is empty or the device is read-only.
1406    ///
1407    /// Designed to be called from the mount path AFTER journal replay,
1408    /// so the orphans we're about to reclaim are guaranteed not still in
1409    /// use by an in-flight kernel-level transaction.
1410    pub fn recover_orphans(&self) -> Result<usize> {
1411        // NOT AN ERROR HERE, unlike the other write paths. This runs
1412        // from the mount path on every mount, and a volume that cannot
1413        // be written -- or that this driver must not write, because it
1414        // carries a feature it does not maintain -- simply keeps its
1415        // orphans. Failing the mount over it would refuse a volume that
1416        // reads perfectly well.
1417        if self.write_refusal().is_err() {
1418            return Ok(0);
1419        }
1420        if self.sb.last_orphan == 0 {
1421            return Ok(0);
1422        }
1423        // There is work, and it writes.
1424        self.mark_not_clean_once()?;
1425        let chain = self.orphan_chain_to_recover()?;
1426
1427        let bs = self.sb.block_size();
1428        let mut reclaimed = 0usize;
1429        // The member the previous commit moved the head past, and the
1430        // `i_dtime` it should end with.
1431        let mut unstamped: Option<(u32, u32)> = None;
1432
1433        if chain.is_empty() {
1434            let mut buf = BlockBuffer::new(bs);
1435            self.buffer_patch_sb_last_orphan(&mut buf, 0)?;
1436            return self.commit_block_buffer(buf).map(|()| 0);
1437        }
1438
1439        for (at, &orphan_ino) in chain.iter().enumerate() {
1440            let next = chain.get(at + 1).copied().unwrap_or(0);
1441            let mut buf = BlockBuffer::new(bs);
1442            if let Some((ino, dtime)) = unstamped.take() {
1443                self.buffer_stamp_dtime(&mut buf, ino, dtime)?;
1444            }
1445            let Some((blocks, final_dtime, deleted)) =
1446                self.buffer_recover_orphan(&mut buf, orphan_ino, next)?
1447            else {
1448                // AN ORPHAN THIS CANNOT RECLAIM STAYS AT THE HEAD (CodeRabbit
1449                // on #201). Its inode and blocks are left untouched and the
1450                // chain is not advanced past it, so nothing is freed without
1451                // its blocks and nothing leaves the chain still holding them;
1452                // the members behind it wait with it. Only the previous
1453                // member's pending `i_dtime` is written.
1454                self.commit_block_buffer(buf)?;
1455                break;
1456            };
1457            self.buffer_patch_sb_counters(&mut buf, blocks as i64, i32::from(deleted))?;
1458            reclaimed += usize::from(deleted);
1459            if next == 0 {
1460                self.buffer_stamp_dtime(&mut buf, orphan_ino, final_dtime)?;
1461            } else {
1462                unstamped = Some((orphan_ino, final_dtime));
1463            }
1464            self.buffer_patch_sb_last_orphan(&mut buf, next)?;
1465            self.commit_block_buffer(buf)?;
1466        }
1467        Ok(reclaimed)
1468    }
1469
1470    /// Set `ino`'s `i_dtime` in `buf`, keeping its checksum right.
1471    fn buffer_stamp_dtime(&self, buf: &mut BlockBuffer, ino: u32, dtime: u32) -> Result<()> {
1472        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
1473        let inode_size = self.sb.inode_size as usize;
1474        let bytes = buf.get_mut(self, block)?;
1475        let raw = bytes
1476            .get_mut(offset as usize..offset as usize + inode_size)
1477            .ok_or(Error::Corrupt("inode slice exceeds block data"))?;
1478        raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
1479        let generation = u32::from_le_bytes(raw[0x64..0x68].try_into().unwrap());
1480        let mut owned = raw.to_vec();
1481        self.finalize_inode_raw(ino, generation, &mut owned)?;
1482        raw.copy_from_slice(&owned);
1483        Ok(())
1484    }
1485
1486    /// Reclaim one chain member into `buf`, leaving `link` in its
1487    /// `i_dtime`. Returns the blocks freed, the `i_dtime` the member ends
1488    /// with, and whether the inode itself was freed; `None`, having touched
1489    /// nothing, for a member this cannot reclaim whole -- one that does not
1490    /// parse, one whose indirect tree cannot be read or points outside the
1491    /// filesystem, or one whose extent tree the truncate planner does not
1492    /// handle.
1493    fn buffer_recover_orphan(
1494        &self,
1495        buf: &mut BlockBuffer,
1496        orphan_ino: u32,
1497        link: u32,
1498    ) -> Result<Option<(u64, u32, bool)>> {
1499        let bs = self.sb.block_size();
1500        // Read the orphan's raw bytes (skip csum verify — orphan
1501        // inodes routinely carry stale csums by design).
1502        let mut raw = self.read_inode_raw(orphan_ino)?;
1503        let parsed = match Inode::parse(&raw) {
1504            Ok(i) => i,
1505            Err(_) => return Ok(None), // unparseable orphan — skip + leak rather than panic
1506        };
1507
1508        // STILL NAMED BY A DIRECTORY. This is an interrupted
1509        // truncate, not a deletion. Finish the truncate and leave
1510        // the file alone.
1511        if parsed.links_count != 0 {
1512            let Some(freed) =
1513                self.buffer_finish_interrupted_truncate(buf, orphan_ino, &parsed, raw)?
1514            else {
1515                return Ok(None);
1516            };
1517            self.buffer_stamp_dtime(buf, orphan_ino, link)?;
1518            return Ok(Some((freed, 0, false)));
1519        }
1520
1521        let mut freed = 0u64;
1522        // LEGACY INDIRECT: every ext2 and ext3 file. Its data blocks and
1523        // the indirect blocks that map them are freed with it, as far as
1524        // its size reaches -- the bound the kernel's truncate uses -- in
1525        // this same transaction. This used to free the inode alone, which
1526        // left all of those blocks allocated and named by nothing (#79).
1527        //
1528        // Only an inode whose i_block holds pointers, decided as unlink
1529        // decides it: a fast symlink's target or an inline file's data is
1530        // no block map, and `i_blocks` also counts an xattr block, so
1531        // `blocks > 0` alone freed the blocks their text named (#384).
1532        if !parsed.has_extents() && Self::holds_block_map(&parsed, bs) {
1533            let data_blocks = u32::try_from(parsed.size.div_ceil(bs as u64))
1534                .map_err(|_| Error::Corrupt("orphan: indirect file too large to map"))?;
1535            let Ok(tree) = crate::indirect_mut::collect_for_free(
1536                &parsed.block,
1537                bs,
1538                data_blocks,
1539                self.dev.as_ref(),
1540            ) else {
1541                return Ok(None);
1542            };
1543            let fs_blocks = self.sb.blocks_count;
1544            let in_range = |start: u64, len: u64| {
1545                start >= self.sb.first_data_block as u64
1546                    && start.checked_add(len).is_some_and(|end| end <= fs_blocks)
1547            };
1548            // A pointer outside the filesystem is a corrupt tree; freeing
1549            // around it would clear bits that belong to nothing. Leave the
1550            // member whole, as for any member this cannot reclaim.
1551            if !tree
1552                .data_runs
1553                .iter()
1554                .all(|r| in_range(r.start, r.len as u64))
1555                || !tree.indirect_blocks.iter().all(|&b| in_range(b, 1))
1556            {
1557                return Ok(None);
1558            }
1559            for run in &tree.data_runs {
1560                freed += self.buffer_free_block_run_and_bgd(buf, run.start, run.len as u64)?;
1561            }
1562            for &block in &tree.indirect_blocks {
1563                freed += self.buffer_free_block_run_and_bgd(buf, block, 1)?;
1564            }
1565        }
1566        if parsed.has_extents() {
1567            let Ok(runs) = self.extent_tree_runs(orphan_ino, &parsed) else {
1568                return Ok(None);
1569            };
1570            freed += self.buffer_free_runs(buf, &runs)?;
1571        }
1572        // Its xattr block: freed, or one reference fewer if shared.
1573        if parsed.file_acl != 0 {
1574            freed += self.buffer_release_xattr_block(buf, parsed.file_acl)?;
1575        }
1576        // Free the inode bitmap slot + BGD free_inodes++.
1577        self.buffer_free_inode_slot(buf, orphan_ino)?;
1578
1579        // Zero the inode body (preserve generation). i_blocks is moot
1580        // once zeroed; the freed extents are in the counters above.
1581        let inode_size = self.sb.inode_size as usize;
1582        let old_gen = parsed.generation;
1583        for b in &mut raw[..inode_size] {
1584            *b = 0;
1585        }
1586        raw[0x14..0x18].copy_from_slice(&link.to_le_bytes());
1587        raw[0x64..0x68].copy_from_slice(&old_gen.to_le_bytes());
1588        self.finalize_inode_raw(orphan_ino, old_gen, &mut raw)?;
1589        self.buffer_write_inode(buf, orphan_ino, &raw)?;
1590        Ok(Some((freed, self.dtime_now(), true)))
1591    }
1592
1593    /// Finish a `truncate()` that a crash interrupted, for an orphan that
1594    /// still has directory links.
1595    ///
1596    /// `i_size` was lowered before the machine went down and is therefore
1597    /// already the size the user asked for; what is left over is the
1598    /// blocks past it. So this frees exactly those, rewrites the extent
1599    /// root and `i_blocks` to match, and clears `i_dtime` — which was
1600    /// doing double duty as the orphan chain's "next" pointer, and which
1601    /// a live file must not carry, because a non-zero `i_dtime` is how
1602    /// every other tool reads "this inode was deleted".
1603    ///
1604    /// Returns the number of blocks freed, to be added to the
1605    /// superblock's free count by the caller's single transaction.
1606    ///
1607    /// # WHEN THE TRUNCATE CANNOT BE PLANNED, THE FILE IS STILL LEFT
1608    /// INTACT
1609    ///
1610    /// Legacy indirect mappings still cannot be planned here: leave their
1611    /// blocks allocated instead of deleting a named file. An extent tree
1612    /// the planner refuses -- a node that fails its checksum or does not
1613    /// describe a valid tree -- returns `None` having staged nothing, and
1614    /// the caller keeps the member at the head of the chain: corrupt
1615    /// metadata is neither freed around nor dropped from the orphan list.
1616    fn buffer_finish_interrupted_truncate(
1617        &self,
1618        buf: &mut BlockBuffer,
1619        ino: u32,
1620        parsed: &Inode,
1621        mut raw: Vec<u8>,
1622    ) -> Result<Option<u64>> {
1623        let bs = self.sb.block_size() as u64;
1624        let mut freed_blocks: u64 = 0;
1625        let mut freed_sectors: u64 = 0;
1626
1627        if parsed.has_extents() {
1628            // old == new: `plan_truncate_shrink` works from the logical
1629            // end that `new_size` implies, so passing i_size for both
1630            // frees precisely what lies past the file's declared end.
1631            {
1632                // Planning reads and verifies every node and stages
1633                // nothing, so a refusal here leaves `buf` as it was.
1634                let Ok((_sc, muts)) = self.plan_inode_truncate(ino, parsed, parsed.size) else {
1635                    return Ok(None);
1636                };
1637                for m in &muts {
1638                    match m {
1639                        crate::extent_mut::ExtentMutation::WriteRoot { bytes } => {
1640                            Self::patch_inode_block_area(&mut raw, bytes)?;
1641                        }
1642                        crate::extent_mut::ExtentMutation::WriteTreeBlock { block, bytes } => {
1643                            buf.put(*block, bytes.clone());
1644                        }
1645                        crate::extent_mut::ExtentMutation::FreePhysicalRun { start, len } => {
1646                            freed_blocks +=
1647                                self.buffer_free_block_run_and_bgd(buf, *start, *len as u64)?;
1648                            freed_sectors += (*len as u64) * (bs / 512);
1649                        }
1650                        _ => {
1651                            return Err(Error::Corrupt(
1652                                "orphan truncate: unexpected mutation type",
1653                            ));
1654                        }
1655                    }
1656                }
1657                let new_blocks = parsed.blocks.saturating_sub(freed_sectors);
1658                Self::patch_inode_size_and_blocks(&mut raw, parsed.size, new_blocks)?;
1659            }
1660        }
1661
1662        // Off the chain, and no longer looking deleted. This happens on
1663        // every path through here, including the ones that freed nothing,
1664        // because an inode with links and a non-zero `i_dtime` is a
1665        // contradiction that outlives the mount.
1666        raw[0x14..0x18].copy_from_slice(&0u32.to_le_bytes());
1667        self.finalize_inode_raw(ino, parsed.generation, &mut raw)?;
1668        self.buffer_write_inode(buf, ino, &raw)?;
1669        Ok(Some(freed_blocks))
1670    }
1671
1672    /// Read a whole block by its logical block number. Routes through
1673    /// `self.dev`, which at mount time is wrapped in a `CachedDevice` —
1674    /// so this single call benefits from the buffer cache that holds
1675    /// post-commit, pre-checkpoint journaled writes.
1676    pub fn read_block(&self, block_num: u64) -> Result<Vec<u8>> {
1677        let block_size = self.sb.block_size() as usize;
1678        let byte_offset = block_num
1679            .checked_mul(block_size as u64)
1680            .ok_or(Error::Corrupt("block byte offset overflow"))?;
1681        let mut buf = vec![0u8; block_size];
1682        self.dev.read_at(byte_offset, &mut buf)?;
1683        Ok(buf)
1684    }
1685
1686    /// Read raw inode bytes for a given inode number (does not parse).
1687    pub fn read_inode_raw(&self, ino: u32) -> Result<Vec<u8>> {
1688        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
1689        let block_data = self.read_block(block)?;
1690        let inode_size = self.sb.inode_size as usize;
1691        let off = offset as usize;
1692        let end = off
1693            .checked_add(inode_size)
1694            .ok_or(Error::Corrupt("inode slice end overflows usize"))?;
1695        if end > block_data.len() {
1696            return Err(Error::Corrupt("inode slice exceeds block data"));
1697        }
1698        Ok(block_data[off..end].to_vec())
1699    }
1700
1701    /// Read + parse + checksum-verify an inode in one shot.
1702    ///
1703    /// When `RO_COMPAT_METADATA_CSUM` is enabled the inode CRC32C is checked
1704    /// (salted by inode number + generation per ext4 spec). A mismatch
1705    /// returns `Error::BadChecksum { what: "inode" }`.
1706    pub fn read_inode_verified(&self, ino: u32) -> Result<(Inode, Vec<u8>)> {
1707        let raw = self.read_inode_raw(ino)?;
1708        let inode = Inode::parse(&raw)?;
1709        if self.csum.enabled && !self.csum.verify_inode(ino, inode.generation, &raw) {
1710            return Err(Error::BadChecksum { what: "inode" });
1711        }
1712        // A DIRECTORY IS NOT SPARSE.
1713        //
1714        // Every directory scan in this crate walks
1715        // `0..size.div_ceil(block_size)` and steps over a logical block
1716        // that is not mapped -- which is what the kernel does too, so
1717        // the loop is never ended by an error and never bounded by real
1718        // content. `i_size` is `join32(i_size_high, i_size_lo)` off the
1719        // disk: setting `i_size_high` on the root of a small image gave
1720        // a directory of 2^44 bytes and a lookup that was still
1721        // spinning after twenty seconds, with `MAX_DIR_ENTRIES` never
1722        // reached because no entry is ever found.
1723        //
1724        // A regular file may legitimately declare more bytes than the
1725        // filesystem holds -- that is what a sparse file is -- but a
1726        // directory's blocks are all really there.
1727        if inode.is_dir() && inode.size > self.byte_ceiling()? {
1728            return Err(Error::Corrupt(
1729                "directory inode declares more bytes than the filesystem holds",
1730            ));
1731        }
1732        Ok((inode, raw))
1733    }
1734
1735    /// The target of the symlink at `ino`, exactly as stored and without
1736    /// a terminating NUL.
1737    ///
1738    /// A target shorter than `i_block` (60 bytes) is a fast symlink, held
1739    /// inline in `i_block`; a longer one is a slow symlink, held in data
1740    /// blocks. That is the boundary the format documentation gives
1741    /// ("Symbolic Links": inline if the target is less than 60 bytes).
1742    ///
1743    /// `Error::InvalidArgument` when the inode is not a symlink,
1744    /// `Error::Corrupt` when it declares a target longer than any path,
1745    /// and `Error::Unsupported` when the target is encrypted.
1746    pub fn read_link(&self, ino: u32) -> Result<Vec<u8>> {
1747        const I_BLOCK_BYTES: u64 = 60;
1748        // No path is longer than PATH_MAX. Without this the raw `i_size`
1749        // became an allocation: `i_mode = 0xA1FF` with
1750        // `i_size = 0x2000_0000_0000_0060` aborted the process.
1751        const PATH_MAX: u64 = 4096;
1752
1753        let (inode, _raw) = self.read_inode_verified(ino)?;
1754        if !inode.is_symlink() {
1755            return Err(Error::InvalidArgument("not a symlink"));
1756        }
1757        if inode.size > PATH_MAX {
1758            return Err(Error::Corrupt("symlink target is longer than any path"));
1759        }
1760        // A fast symlink's target is ciphertext too, and never reaches
1761        // file_io's refusal (#76).
1762        crate::file_io::refuse_encrypted(&inode)?;
1763        if inode.size < I_BLOCK_BYTES {
1764            return Ok(inode.block[..inode.size as usize].to_vec());
1765        }
1766        let mut out = vec![0u8; inode.size as usize];
1767        crate::file_io::read_verified(self, &inode, ino, 0, inode.size, &mut out)?;
1768        Ok(out)
1769    }
1770
1771    /// The most bytes anything on this filesystem can really occupy:
1772    /// `blocks_count * block_size`, checked rather than saturated, and
1773    /// never more than the device (#321). Mount already refuses a
1774    /// superblock larger than its device; taking the minimum here as well
1775    /// keeps the bound honest for a caller that reaches these paths with
1776    /// a superblock mount did not vet.
1777    pub fn byte_ceiling(&self) -> Result<u64> {
1778        let bytes = self.sb.filesystem_bytes().ok_or(Error::Corrupt(
1779            "superblock: blocks_count * block_size overflows",
1780        ))?;
1781        Ok(bytes.min(self.dev.size_bytes()))
1782    }
1783
1784    // ----------------------------------------------------------------------
1785    // Inode-addressed entry points (#372)
1786    // ----------------------------------------------------------------------
1787    //
1788    // Every path-addressed operation resolves its path and then calls one
1789    // of these; they are the implementation, and the path functions are a
1790    // resolve step in front of them. A handle-based host calls them
1791    // directly with the inode numbers it holds.
1792
1793    /// Resolve `path` to an inode number, verifying directory blocks.
1794    ///
1795    /// A path is bytes, compared byte for byte against the entry names,
1796    /// which have no encoding: one that is not UTF-8 names exactly the
1797    /// file whose name it holds, and one naming nothing is
1798    /// [`Error::NotFound`]. Every path-addressed `apply_*` resolves through
1799    /// here; each `&str` form is its byte form with `str::as_bytes` (#418).
1800    pub fn lookup_path_bytes(&self, path: &[u8]) -> Result<u32> {
1801        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
1802        crate::path::lookup_bytes_with_csum(
1803            self.dev.as_ref(),
1804            &self.sb,
1805            &mut reader,
1806            path,
1807            &self.csum,
1808        )
1809    }
1810
1811    fn resolve(&self, path: &[u8]) -> Result<u32> {
1812        self.lookup_path_bytes(path)
1813    }
1814
1815    /// Resolve the parent of `path`, for an operation on its final name.
1816    fn resolve_parent<'p>(&self, path: &'p [u8]) -> Result<(u32, &'p [u8])> {
1817        let (parent, base) = split_parent_and_base(path)?;
1818        Ok((self.resolve(parent)?, base))
1819    }
1820
1821    /// [`resolve_parent`](Self::resolve_parent) for an operation that files
1822    /// the final name, which is refused as too long before anything is
1823    /// resolved.
1824    fn resolve_new_parent<'p>(&self, path: &'p [u8]) -> Result<(u32, &'p [u8])> {
1825        let (parent, base) = split_parent_and_base(path)?;
1826        if base.len() > 255 {
1827            return Err(Error::NameTooLong);
1828        }
1829        Ok((self.resolve(parent)?, base))
1830    }
1831
1832    /// Read the inode `r` names, refusing a handle that no longer names a
1833    /// file with [`Error::Stale`]: a number outside the inode table or in
1834    /// its reserved range (other than the root), an inode not marked in use
1835    /// in its bitmap or with no links or no mode (freed, or never used), or
1836    /// one whose generation is not the
1837    /// one `r` carries (freed and reused).
1838    pub(crate) fn live_inode(&self, r: InodeRef) -> Result<(Inode, Vec<u8>)> {
1839        let ino = r.ino;
1840        let reserved = ino < self.sb.first_inode && ino != crate::path::EXT4_ROOT_INODE;
1841        if ino == 0 || ino > self.sb.inodes_count || reserved {
1842            return Err(Error::Stale);
1843        }
1844        // Checked before the inode is read: a slot that was never used can
1845        // hold anything, including a checksum that does not verify.
1846        if !self.inode_bit_is_set(ino)? {
1847            return Err(Error::Stale);
1848        }
1849        let (inode, raw) = self.read_inode_verified(ino)?;
1850        if inode.links_count == 0 || inode.mode == 0 {
1851            return Err(Error::Stale);
1852        }
1853        if r.generation.is_some_and(|g| g != inode.generation) {
1854            return Err(Error::Stale);
1855        }
1856        Ok((inode, raw))
1857    }
1858
1859    /// [`live_inode`](Self::live_inode) for a directory.
1860    fn live_dir(&self, r: InodeRef) -> Result<(Inode, Vec<u8>)> {
1861        let (inode, raw) = self.live_inode(r)?;
1862        if !inode.is_dir() {
1863            return Err(Error::NotADirectory);
1864        }
1865        Ok((inode, raw))
1866    }
1867
1868    /// The attributes of the inode `r` names; [`Error::Stale`] if it no
1869    /// longer names a file (see [`InodeRef`]).
1870    pub fn stat_ino(&self, r: impl Into<InodeRef>) -> Result<Inode> {
1871        Ok(self.live_inode(r.into())?.0)
1872    }
1873
1874    /// The inode number `name` has in directory `dir` — one step of a path
1875    /// walk. `name` is bytes, compared exactly, so a name that is not UTF-8
1876    /// is found. `.` and `..` are ordinary entries here.
1877    pub fn lookup_at(&self, dir: impl Into<InodeRef>, name: &[u8]) -> Result<u32> {
1878        check_entry_name(name)?;
1879        let dir = dir.into();
1880        let (dir_inode, _) = self.live_dir(dir)?;
1881        crate::path::find_entry(
1882            self.dev.as_ref(),
1883            &self.sb,
1884            dir.ino,
1885            &dir_inode,
1886            name,
1887            &self.csum,
1888        )
1889    }
1890
1891    /// Every entry of directory `dir`, `.` and `..` included, in on-disk
1892    /// order. Directory listing by path resolves and calls this.
1893    pub fn read_dir_ino(&self, dir: impl Into<InodeRef>) -> Result<Vec<crate::dir::DirEntry>> {
1894        let dir = dir.into();
1895        let (inode, raw) = self.live_dir(dir)?;
1896        self.dir_entries(dir.ino, &inode, &raw)
1897    }
1898
1899    /// Collect the entries of the directory `inode`, number `ino`, whose
1900    /// on-disk bytes are `inode_raw`.
1901    fn dir_entries(
1902        &self,
1903        ino: u32,
1904        inode: &Inode,
1905        inode_raw: &[u8],
1906    ) -> Result<Vec<crate::dir::DirEntry>> {
1907        use crate::dir::DirBlockIter;
1908        crate::file_io::refuse_encrypted_names(inode)?;
1909        let block_size = self.sb.block_size();
1910        let has_filetype = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
1911
1912        // Inline-data dirs (tiny dirs stored inside the inode itself).
1913        // Checked before the extent test: an inline directory has no extent
1914        // tree, and was refused as a legacy one. `.` and `..` synthesised,
1915        // entries from byte 4 and then from the `system.data` continuation
1916        // (#427).
1917        if inode.has_inline_data() {
1918            return crate::inline_data::read_dir(
1919                self.dev.as_ref(),
1920                ino,
1921                inode,
1922                inode_raw,
1923                self.sb.inode_size,
1924                block_size,
1925                has_filetype,
1926            );
1927        }
1928        if !inode.has_extents() {
1929            return Err(Error::Corrupt("legacy (non-extent) dirs not yet supported"));
1930        }
1931
1932        // Bound on entries buffered per listing. A crafted image with
1933        // `inode.size` claiming gigabytes would otherwise allocate
1934        // proportionally, since the loop grows `entries` straight from
1935        // on-disk content.
1936        const MAX_DIR_ENTRIES: usize = 1_000_000;
1937        let mut entries = Vec::new();
1938        let push = |entries: &mut Vec<crate::dir::DirEntry>, e| {
1939            if entries.len() >= MAX_DIR_ENTRIES {
1940                return Err(Error::Corrupt("dir entries exceed MAX_DIR_ENTRIES"));
1941            }
1942            entries.push(e);
1943            Ok(())
1944        };
1945
1946        let total_blocks = inode.size.div_ceil(block_size as u64);
1947        let mut block_buf = vec![0u8; block_size as usize];
1948        for logical in 0..total_blocks {
1949            let Some(phys) =
1950                crate::extent::map_logical(&inode.block, self.dev.as_ref(), block_size, logical)?
1951            else {
1952                continue; // sparse hole
1953            };
1954            self.dev.read_at(phys * block_size as u64, &mut block_buf)?;
1955            for entry in DirBlockIter::new(&block_buf, has_filetype) {
1956                push(&mut entries, entry?)?;
1957            }
1958        }
1959        Ok(entries)
1960    }
1961
1962    /// Read up to `out.len()` bytes of the regular file `r` names, from
1963    /// byte `offset`. Returns the number of bytes read, short at the end
1964    /// of the file.
1965    pub fn read_ino(&self, r: impl Into<InodeRef>, offset: u64, out: &mut [u8]) -> Result<usize> {
1966        let r = r.into();
1967        let (inode, raw) = self.live_inode(r)?;
1968        if !inode.is_file() {
1969            return Err(Error::InvalidArgument("not a regular file"));
1970        }
1971        let length = (out.len() as u64).min(inode.size);
1972        crate::file_io::read_with_raw_verified(
1973            self,
1974            &inode,
1975            &raw,
1976            r.ino,
1977            offset,
1978            length,
1979            &mut out[..length as usize],
1980        )
1981        .map(|n| n as usize)
1982    }
1983
1984    /// The target of the symlink `r` names; see [`read_link`](Self::read_link).
1985    pub fn read_link_ino(&self, r: impl Into<InodeRef>) -> Result<Vec<u8>> {
1986        let r = r.into();
1987        self.live_inode(r)?;
1988        self.read_link(r.ino)
1989    }
1990
1991    /// Set the size of the regular file `r` names, freeing what a shrink
1992    /// drops; a grow is sparse. `Error::IsADirectory` for a directory and
1993    /// `Error::InvalidArgument` for any other non-regular file.
1994    pub fn apply_truncate_ino(&self, r: impl Into<InodeRef>, new_size: u64) -> Result<()> {
1995        self.refuse_write()?;
1996        let r = r.into();
1997        let (inode, _) = self.live_inode(r)?;
1998        // Truncating a directory frees its blocks and loses `.` and `..`;
1999        // POSIX truncate(2) says EISDIR. Symlinks and devices: EINVAL.
2000        if inode.is_dir() {
2001            return Err(Error::IsADirectory);
2002        }
2003        if !inode.is_file() {
2004            return Err(Error::InvalidArgument(
2005                "truncate target is not a regular file",
2006            ));
2007        }
2008        // At equality either works; grow only bumps timestamps.
2009        if new_size >= inode.size {
2010            self.apply_truncate_grow(r.ino, new_size)
2011        } else {
2012            self.apply_truncate_shrink(r.ino, new_size)
2013        }
2014    }
2015
2016    /// Map a logical block within `inode` to its physical block, choosing
2017    /// between the extent tree and the legacy direct/indirect scheme based
2018    /// on `EXT4_EXTENTS_FL`. Returns `None` for sparse holes and (for the
2019    /// extent path) uninitialised extents — callers wanting zeros there
2020    /// must handle the `None` case explicitly.
2021    ///
2022    /// This is the per-inode dispatcher every directory traversal /
2023    /// extent-walking call site should use instead of touching
2024    /// `extent::map_logical` directly — without it, an ext2/3 inode with
2025    /// raw block pointers in `i_block` gets misparsed as an extent header
2026    /// (yielding `CorruptExtentTree("bad extent header magic")`).
2027    ///
2028    /// An inline-data inode has no block map, so it is refused with
2029    /// `Error::Unsupported` (#382). The directory writers hand an inline
2030    /// directory to the inline-directory editors before they get here
2031    /// (#428); this refusal is what stops any other caller writing through
2032    /// its bytes.
2033    ///
2034    /// The indirect path internally maintains its own block cache for the
2035    /// duration of the call; sequential lookups via repeated calls don't
2036    /// share that cache (file_io's read paths build a longer-lived cache
2037    /// to amortize across blocks).
2038    pub fn map_inode_logical(&self, inode: &Inode, logical_block: u64) -> Result<Option<u64>> {
2039        // An inline-data inode's i_block holds bytes, not pointers: for a
2040        // directory, its parent's inode number and then entries; for a
2041        // file, its first 60 bytes. Read as a block map, they name blocks
2042        // belonging to something else, which every directory writer then
2043        // parsed and wrote through (#382).
2044        if inode.has_inline_data() {
2045            return Err(Error::Unsupported(
2046                "inline-data inode: i_block holds data, not a block map",
2047            ));
2048        }
2049        let bs = self.sb.block_size();
2050        if (inode.flags & crate::inode::InodeFlags::EXTENTS.bits()) != 0 {
2051            crate::extent::map_logical(&inode.block, self.dev.as_ref(), bs, logical_block)
2052        } else {
2053            let mut cache = crate::indirect::IndirectCache::new();
2054            crate::indirect::lookup(
2055                &inode.block,
2056                self.dev.as_ref(),
2057                bs,
2058                logical_block,
2059                &mut cache,
2060            )
2061        }
2062    }
2063
2064    /// Write the given raw inode bytes back to disk. Refused as every other
2065    /// write is (`refuse_write`): `Error::ReadOnly` on a read-only device,
2066    /// and a volume carrying a feature this driver must not write is left
2067    /// untouched. The first write marks the volume not clean (#323).
2068    ///
2069    /// **Not checksum-aware**: callers that update fields affecting the inode
2070    /// CRC32C (anything except `checksum_lo` / `checksum_hi`) must recompute
2071    /// + patch the checksum into `raw` before calling this. Not wrapped in a
2072    /// journal transaction — see E11 / `journal_apply` for the journaled
2073    /// version. Use only when the caller has the full write-ordering story
2074    /// under control.
2075    pub fn write_inode_raw(&self, ino: u32, raw: &[u8]) -> Result<()> {
2076        if raw.len() != self.sb.inode_size as usize {
2077            return Err(Error::Corrupt("write_inode_raw: length != inode_size"));
2078        }
2079        self.refuse_write()?;
2080        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
2081        let block_size = self.sb.block_size() as u64;
2082        let byte_offset = block * block_size + offset as u64;
2083        self.dev.write_at(byte_offset, raw)?;
2084        Ok(())
2085    }
2086
2087    /// Write `i_file_acl` — the external xattr block pointer — into a raw
2088    /// inode. `block_nr` of 0 clears it.
2089    ///
2090    /// THE HIGH HALF IS AT 0x76, NOT 0x74. `Inode::parse` reads
2091    /// `i_file_acl_hi` from `0x76..0x78`; both writers used to put it at
2092    /// `0x74..0x76`, which is `l_i_blocks_hi`. This function now writes it
2093    /// once at `0x76..0x78`. Previously,
2094    /// `patch_inode_size_and_blocks` — which owns that field — ran six
2095    /// lines later at both sites and overwrote it. So the high half was
2096    /// never written and never cleared, by either of the two functions
2097    /// that thought they were maintaining it.
2098    ///
2099    /// WHY IT LEFT NO TRACE. Below 2^32 blocks the high half is 0, the
2100    /// clobber writes 0 over 0, and the field was already 0. Above it —
2101    /// 16 TiB at 4 KiB blocks — a fresh external block keeps only its low
2102    /// 32 bits, so `Inode::parse` reads back a DIFFERENT block, which
2103    /// `xattr::list` then reads and `apply_removexattr` WRITES; and a
2104    /// freed one leaves `file_acl == old_hi << 32` pointing at a block
2105    /// already handed back to the allocator.
2106    ///
2107    /// ONE RECIPE, TWO CALLERS, which is the other half of why this
2108    /// survived: the offset was written out by hand at each site and
2109    /// nothing made the two agree with the reader.
2110    ///
2111    /// The capacity check uses `0x78`, not `0x76`: the old guard admitted a
2112    /// buffer ending exactly where the field it was about to write begins.
2113    /// It runs before either half is written, so an inode too short to hold
2114    /// the high half is REFUSED without leaving a truncated pointer behind.
2115    pub(crate) fn write_file_acl(raw: &mut [u8], block_nr: u64) -> Result<()> {
2116        if raw.len() < 0x6C {
2117            return Err(Error::Corrupt(
2118                "write_file_acl: inode buffer too small for i_file_acl_lo",
2119            ));
2120        }
2121        let (hi, lo) = crate::extent_mut::split_phys_block(block_nr);
2122        if raw.len() < 0x78 && hi != 0 {
2123            return Err(Error::Corrupt(
2124                "write_file_acl: this inode is too small to hold i_file_acl_hi and the \
2125                 external xattr block needs it",
2126            ));
2127        }
2128        raw[0x68..0x6C].copy_from_slice(&lo.to_le_bytes());
2129        if raw.len() >= 0x78 {
2130            raw[0x76..0x78].copy_from_slice(&hi.to_le_bytes());
2131        }
2132        Ok(())
2133    }
2134
2135    /// Patch fields in a raw inode image: size, blocks_count. Leaves all
2136    /// other bytes (including the extent tree header + entries in `i_block`)
2137    /// intact. `new_block_count` is in 512-byte sectors per spec (same
2138    /// convention as `Inode::blocks`).
2139    pub fn patch_inode_size_and_blocks(
2140        raw: &mut [u8],
2141        new_size: u64,
2142        new_block_count: u64,
2143    ) -> Result<()> {
2144        if raw.len() < 128 {
2145            return Err(Error::Corrupt("patch_inode: buffer too small"));
2146        }
2147        // size = size_lo (0x04..0x08) + size_hi (0x6C..0x70)
2148        let size_lo = (new_size & 0xFFFF_FFFF) as u32;
2149        let size_hi = (new_size >> 32) as u32;
2150        raw[0x04..0x08].copy_from_slice(&size_lo.to_le_bytes());
2151        raw[0x6C..0x70].copy_from_slice(&size_hi.to_le_bytes());
2152        // blocks = blocks_lo (0x1C..0x20, u32) + blocks_hi (0x74..0x76, u16)
2153        let blocks_lo = (new_block_count & 0xFFFF_FFFF) as u32;
2154        let blocks_hi = ((new_block_count >> 32) & 0xFFFF) as u16;
2155        raw[0x1C..0x20].copy_from_slice(&blocks_lo.to_le_bytes());
2156        raw[0x74..0x76].copy_from_slice(&blocks_hi.to_le_bytes());
2157        Ok(())
2158    }
2159
2160    /// Overwrite the 60-byte `i_block` area of an inode image with `new_root`.
2161    /// Used when an extent-tree mutation changes the inline root.
2162    pub fn patch_inode_block_area(raw: &mut [u8], new_root: &[u8]) -> Result<()> {
2163        if raw.len() < 128 {
2164            return Err(Error::Corrupt("patch_inode_block_area: buffer too small"));
2165        }
2166        if new_root.len() != 60 {
2167            return Err(Error::Corrupt(
2168                "patch_inode_block_area: new_root != 60 bytes",
2169            ));
2170        }
2171        raw[0x28..0x64].copy_from_slice(new_root);
2172        Ok(())
2173    }
2174
2175    /// Verify every external node before planning any tree/bitmap mutation.
2176    /// Freed tree nodes use the same block accounting as freed data blocks.
2177    fn plan_inode_truncate(
2178        &self,
2179        ino: u32,
2180        inode: &Inode,
2181        new_size: u64,
2182    ) -> Result<(
2183        crate::file_mut::SizeChange,
2184        Vec<crate::extent_mut::ExtentMutation>,
2185    )> {
2186        if !inode.has_extents() {
2187            return Err(Error::Unsupported("truncate of legacy block mappings"));
2188        }
2189        let mut read = |block| {
2190            let bytes = self.read_block(block)?;
2191            if !self.csum.verify_extent_tail(ino, inode.generation, &bytes) {
2192                return Err(Error::BadChecksum {
2193                    what: "extent block",
2194                });
2195            }
2196            Ok(bytes)
2197        };
2198        let (change, mut mutations) = crate::file_mut::plan_truncate_shrink_deep(
2199            inode.size,
2200            new_size,
2201            &inode.block,
2202            self.sb.block_size(),
2203            self.sb.blocks_count,
2204            &mut read,
2205        )?;
2206        for mutation in &mut mutations {
2207            if let crate::extent_mut::ExtentMutation::WriteTreeBlock { bytes, .. } = mutation {
2208                self.csum.patch_extent_tail(ino, inode.generation, bytes);
2209            }
2210        }
2211        Ok((change, mutations))
2212    }
2213
2214    /// Shrink a file to `new_size`. Composes `file_mut::plan_truncate_shrink`
2215    /// (extent-tree updates + freed-block ranges) with actual disk writes —
2216    /// rewrites the inode and zeros the freed bitmap bits.
2217    ///
2218    /// The inode write, the bitmap writes, the BGD and the superblock
2219    /// accumulate into one `BlockBuffer` and commit together. On a mount
2220    /// with a journal that commit is one transaction, atomic with respect
2221    /// to a crash. Without one (an ext2 volume, or ext4 formatted without
2222    /// a journal) `commit_block_buffer` writes the blocks in turn, and a
2223    /// crash part-way leaves some written and some not (#179).
2224    ///
2225    /// This said "Not journaled … safe only in a test scratch image", and
2226    /// promised the transaction as future work. The future work landed;
2227    /// the warning outlived it and was steering callers away from an API
2228    /// that is safe.
2229    pub fn apply_truncate_shrink(&self, ino: u32, new_size: u64) -> Result<()> {
2230        self.refuse_write()?;
2231        let (inode, mut raw) = self.read_inode_verified(ino)?;
2232        Self::refuse_truncate_of(&inode)?;
2233        if new_size > inode.size {
2234            return Err(Error::InvalidArgument(
2235                "truncate: new_size > old_size (grow not supported)",
2236            ));
2237        }
2238
2239        if new_size == inode.size {
2240            return Ok(());
2241        }
2242        if inode.has_inline_data() {
2243            return self.truncate_inline_file(ino, &inode, raw, new_size);
2244        }
2245        let (_size_change, muts) = self.plan_inode_truncate(ino, &inode, new_size)?;
2246
2247        let bs = self.sb.block_size() as u64;
2248        let mut freed_sectors: u64 = 0;
2249        let mut freed_blocks: u64 = 0;
2250
2251        // Multi-block transaction: accumulate inode + bitmap + BGD + SB
2252        // mutations into one buffer, commit through the journal atomically.
2253        let mut buf = BlockBuffer::new(self.sb.block_size());
2254
2255        for m in &muts {
2256            match m {
2257                crate::extent_mut::ExtentMutation::WriteRoot { bytes } => {
2258                    Self::patch_inode_block_area(&mut raw, bytes)?;
2259                }
2260                crate::extent_mut::ExtentMutation::WriteTreeBlock { block, bytes } => {
2261                    buf.put(*block, bytes.clone());
2262                }
2263                crate::extent_mut::ExtentMutation::FreePhysicalRun { start, len } => {
2264                    freed_blocks +=
2265                        self.buffer_free_block_run_and_bgd(&mut buf, *start, *len as u64)?;
2266                    freed_sectors += (*len as u64) * (bs / 512);
2267                }
2268                _ => {
2269                    return Err(Error::Corrupt(
2270                        "apply_truncate_shrink: unexpected mutation type",
2271                    ));
2272                }
2273            }
2274        }
2275
2276        // Clear the retained partial block so a later grow cannot expose
2277        // bytes beyond the new EOF. Holes and unwritten extents need no write.
2278        let tail = (new_size % bs) as usize;
2279        if tail != 0 {
2280            if let Some(block) = self.map_inode_logical(&inode, new_size / bs)? {
2281                buf.get_mut(self, block)?[tail..].fill(0);
2282            }
2283        }
2284        // Patch size + blocks_count in the inode image, finalize csum.
2285        let new_blocks = inode.blocks.saturating_sub(freed_sectors);
2286        Self::patch_inode_size_and_blocks(&mut raw, new_size, new_blocks)?;
2287        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2288        self.buffer_write_inode(&mut buf, ino, &raw)?;
2289
2290        if freed_blocks > 0 {
2291            self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 0)?;
2292        }
2293
2294        self.commit_block_buffer(buf)
2295    }
2296
2297    /// Extend a file to `new_size`. The new range is a sparse hole — ext4's
2298    /// extent tree treats unmapped logical blocks as zeros, so no extent
2299    /// mutation and no block allocation are required. Only `i_size`,
2300    /// `i_mtime`, `i_ctime`, and the inode checksum change.
2301    ///
2302    /// Caller (capi dispatch) guarantees `new_size >= inode.size`. If
2303    /// `new_size == inode.size` this is a no-op that still bumps the
2304    /// timestamps — matches `truncate(2)` semantics.
2305    /// Truncation is for regular files. A directory's size is its blocks, a
2306    /// symlink's is its target's length, and a device node has none; setting
2307    /// any of them to a size the caller chose leaves an inode e2fsck rejects.
2308    /// The kernel answers EISDIR for a directory and EINVAL for the rest.
2309    fn refuse_truncate_of(inode: &Inode) -> Result<()> {
2310        if inode.is_dir() {
2311            return Err(Error::IsADirectory);
2312        }
2313        if !inode.is_file() {
2314            return Err(Error::InvalidArgument(
2315                "truncate: only a regular file has a size to change",
2316            ));
2317        }
2318        Ok(())
2319    }
2320
2321    // ----------------------------------------------------------------------
2322    // Inline-data files (#428)
2323    // ----------------------------------------------------------------------
2324    //
2325    // An inline file's `i_block` holds its first 60 bytes and `system.data`
2326    // the rest, so neither block writer applies to it: the extent path would
2327    // parse the bytes as a tree and the block-map path would free them as
2328    // pointers (#383). A write whose result still fits in the inode is made
2329    // there; one that outgrows it converts the file to an extent-mapped one
2330    // first, IN THE SAME TRANSACTION, and the write then continues on the
2331    // converted inode. That is the kernel's contract too: the file is inline
2332    // or it is not, and no crash leaves it half of each.
2333
2334    /// Make the inline file `ino` hold `data`, in place, staged in `buf`
2335    /// with its size, times and checksum. `Ok(false)`, with nothing staged
2336    /// and `raw` unchanged, when `data` does not fit in the inode.
2337    fn buffer_store_inline_file(
2338        &self,
2339        buf: &mut BlockBuffer,
2340        ino: u32,
2341        inode: &Inode,
2342        raw: &mut [u8],
2343        data: &[u8],
2344    ) -> Result<bool> {
2345        match crate::inline_mut::store(raw, self.sb.inode_size, data) {
2346            Ok(()) => {}
2347            Err(Error::NoSpaceLeftOnDevice) => return Ok(false),
2348            Err(e) => return Err(e),
2349        }
2350        self.finalize_inode_raw_after_write(ino, raw, inode, data.len() as u64, inode.blocks)?;
2351        self.buffer_write_inode(buf, ino, raw)?;
2352        Ok(true)
2353    }
2354
2355    /// Convert the inline file `ino` to an extent-mapped file, staged in
2356    /// `buf`. With `keep`, its content moves to a newly allocated logical
2357    /// block 0 (it fits: an inode is never larger than a block); without,
2358    /// the file keeps no bytes, for a caller about to replace them.
2359    /// `system.data` goes, `EXT4_INLINE_DATA_FL` clears and
2360    /// `EXT4_EXTENTS_FL` sets; `i_size` is unchanged.
2361    ///
2362    /// Returns the converted inode and its image, NOT yet staged: the
2363    /// caller's write continues from them in the same transaction and
2364    /// stages the inode once, with its own size and times.
2365    fn buffer_convert_inline_file(
2366        &self,
2367        buf: &mut BlockBuffer,
2368        ino: u32,
2369        inode: &Inode,
2370        raw: &[u8],
2371        keep: bool,
2372    ) -> Result<(Inode, Vec<u8>)> {
2373        if self.sb.feature_incompat & features::Incompat::EXTENTS.bits() == 0 {
2374            return Err(Error::Unsupported(
2375                "converting an inline-data file out of the inode on a volume without extents",
2376            ));
2377        }
2378        let content = if keep {
2379            crate::file_io::read_inline(self, inode, raw)?
2380        } else {
2381            Vec::new()
2382        };
2383        let bs = self.sb.block_size();
2384        if content.len() > bs as usize {
2385            return Err(Error::Corrupt("inline data larger than a block"));
2386        }
2387        let mut raw = raw.to_vec();
2388        crate::inline_mut::strip(&mut raw, self.sb.inode_size)?;
2389        let flags = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap())
2390            | crate::inode::InodeFlags::EXTENTS.bits();
2391        raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
2392
2393        let mut root = vec![0u8; 60];
2394        root[0..2].copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
2395        root[4..6].copy_from_slice(&4u16.to_le_bytes()); // max entries
2396        let mut blocks = inode.blocks;
2397        if !content.is_empty() {
2398            let phys = self.buffer_allocate_block(buf, ino)?;
2399            let mut block = vec![0u8; bs as usize];
2400            block[..content.len()].copy_from_slice(&content);
2401            buf.put(phys, block);
2402            let extent = crate::extent::Extent {
2403                logical_block: 0,
2404                length: 1,
2405                physical_block: phys,
2406                uninitialized: false,
2407            };
2408            for m in crate::extent_mut::plan_insert_extent(&root, extent)? {
2409                if let crate::extent_mut::ExtentMutation::WriteRoot { bytes } = m {
2410                    root = bytes;
2411                }
2412            }
2413            blocks += u64::from(bs / 512);
2414        }
2415        Self::patch_inode_block_area(&mut raw, &root)?;
2416        Self::patch_inode_size_and_blocks(&mut raw, inode.size, blocks)?;
2417        let converted = Inode::parse(&raw)?;
2418        Ok((converted, raw))
2419    }
2420
2421    /// Set the size of the inline file `ino`: in place while it fits, and
2422    /// converted to extents, with the new size, when it does not.
2423    fn truncate_inline_file(
2424        &self,
2425        ino: u32,
2426        inode: &Inode,
2427        mut raw: Vec<u8>,
2428        new_size: u64,
2429    ) -> Result<()> {
2430        let mut buf = BlockBuffer::new(self.sb.block_size());
2431        if new_size <= crate::inline_mut::ceiling(self.sb.inode_size) as u64 {
2432            let mut content = crate::file_io::read_inline(self, inode, &raw)?;
2433            content.resize(new_size as usize, 0);
2434            if self.buffer_store_inline_file(&mut buf, ino, inode, &mut raw, &content)? {
2435                return self.commit_block_buffer(buf);
2436            }
2437        }
2438        let (converted, mut raw) =
2439            self.buffer_convert_inline_file(&mut buf, ino, inode, &raw, true)?;
2440        self.finalize_inode_raw_after_write(ino, &mut raw, &converted, new_size, converted.blocks)?;
2441        self.buffer_write_inode(&mut buf, ino, &raw)?;
2442        self.commit_block_buffer(buf)
2443    }
2444
2445    // ----------------------------------------------------------------------
2446    // Inline-data directories (#428)
2447    // ----------------------------------------------------------------------
2448    //
2449    // An inline directory's `i_block` holds its parent's inode number and
2450    // then entries, continued in `system.data`: no block map, so the block
2451    // writers below refuse it (#382). Each of them hands an inline directory
2452    // to these instead. An entry is added and removed in the inode while it
2453    // fits, and a directory whose entries outgrow it is converted to a
2454    // one-block directory holding them, in the same transaction, which is
2455    // what the kernel does with a directory it made inline (the kernel
2456    // oracle tier edits those, tests/kernel_inline_writes.rs). Every edit
2457    // reads the
2458    // directory through the open transaction, so a second edit of the same
2459    // directory in one operation (a rename within it) sees the first.
2460
2461    /// The inline directory `ino` as `buf` has it.
2462    fn buffered_inline_dir(
2463        &self,
2464        buf: &BlockBuffer,
2465        ino: u32,
2466    ) -> Result<(Inode, Vec<u8>, crate::inline_mut::InlineDir)> {
2467        let (inode, raw) = self.buffered_inode_verified(buf, ino)?;
2468        let dir = crate::inline_mut::InlineDir::load(
2469            self.dev.as_ref(),
2470            &inode,
2471            &raw,
2472            self.sb.inode_size,
2473            self.sb.block_size(),
2474        )?;
2475        Ok((inode, raw, dir))
2476    }
2477
2478    /// Stage the edited inline directory `dir` as inode `ino`.
2479    fn buffer_store_inline_dir(
2480        &self,
2481        buf: &mut BlockBuffer,
2482        ino: u32,
2483        inode: &Inode,
2484        mut raw: Vec<u8>,
2485        dir: &crate::inline_mut::InlineDir,
2486    ) -> Result<()> {
2487        dir.store(&mut raw, self.sb.inode_size)?;
2488        self.buffer_write_dir_inode(buf, ino, inode.generation, &mut raw)
2489    }
2490
2491    /// Add `name → target` to the inline directory `ino`, staged in `buf`:
2492    /// in `i_block`, then in the continuation, which is created at the size
2493    /// the inode can hold when there is none; and when it fits in neither,
2494    /// by converting the directory to a block holding every entry.
2495    fn buffer_add_inline_dir_entry(
2496        &self,
2497        buf: &mut BlockBuffer,
2498        ino: u32,
2499        name: &[u8],
2500        target: u32,
2501        file_type: crate::dir::DirEntryType,
2502    ) -> Result<()> {
2503        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
2504        let (inode, raw, mut dir) = self.buffered_inline_dir(buf, ino)?;
2505        if dir.add(target, name, file_type, has_ft)?
2506            || (dir.expand(&raw, self.sb.inode_size) && dir.add(target, name, file_type, has_ft)?)
2507        {
2508            return self.buffer_store_inline_dir(buf, ino, &inode, raw, &dir);
2509        }
2510        let mut entries = dir.entries(has_ft)?;
2511        entries.push(crate::dir::DirEntry {
2512            inode: target,
2513            name: name.to_vec(),
2514            file_type,
2515        });
2516        self.buffer_convert_inline_dir(buf, ino, &inode, raw, dir.parent, &entries)
2517    }
2518
2519    /// Convert the inline directory `ino` to a directory of one block
2520    /// holding `.`, `..` (to `parent`) and `entries`, staged in `buf`:
2521    /// the block, its allocation, and the inode with `system.data` removed,
2522    /// `EXT4_INLINE_DATA_FL` cleared and the block mapped.
2523    fn buffer_convert_inline_dir(
2524        &self,
2525        buf: &mut BlockBuffer,
2526        ino: u32,
2527        inode: &Inode,
2528        mut raw: Vec<u8>,
2529        parent: u32,
2530        entries: &[crate::dir::DirEntry],
2531    ) -> Result<()> {
2532        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
2533        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
2534        let mut block = self.seed_directory_block(ino, parent, inode.generation)?;
2535        for e in entries {
2536            crate::dir::add_entry_to_block(
2537                &mut block,
2538                e.inode,
2539                &e.name,
2540                e.file_type,
2541                has_ft,
2542                reserved_tail,
2543            )
2544            .map_err(|e| match e {
2545                Error::OutOfBounds => {
2546                    Error::Corrupt("an inline directory's entries do not fit in one block")
2547                }
2548                e => e,
2549            })?;
2550        }
2551        if self.csum.enabled {
2552            self.csum
2553                .patch_dir_entry_tail(ino, inode.generation, &mut block);
2554        }
2555        let phys = self.buffer_alloc_dir_block(buf, ino)?;
2556        buf.put(phys, block);
2557        crate::inline_mut::strip(&mut raw, self.sb.inode_size)?;
2558        self.map_one_block(&mut raw, phys)?;
2559        let bs = u64::from(self.sb.block_size());
2560        Self::patch_inode_size_and_blocks(&mut raw, bs, inode.blocks + bs / 512)?;
2561        self.buffer_write_dir_inode(buf, ino, inode.generation, &mut raw)
2562    }
2563
2564    /// Remove `name` from the inline directory `ino`, staged in `buf`.
2565    /// `Error::NotFound` when it is not there.
2566    fn buffer_remove_inline_dir_entry(
2567        &self,
2568        buf: &mut BlockBuffer,
2569        ino: u32,
2570        name: &[u8],
2571    ) -> Result<()> {
2572        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
2573        let (inode, raw, mut dir) = self.buffered_inline_dir(buf, ino)?;
2574        if !dir.remove(name, has_ft)? {
2575            return Err(Error::NotFound);
2576        }
2577        self.buffer_store_inline_dir(buf, ino, &inode, raw, &dir)
2578    }
2579
2580    /// `Error::DirectoryNotEmpty` unless the inline directory `ino`, whose
2581    /// on-disk image is `raw`, holds nothing but `.` and `..`.
2582    fn refuse_nonempty_inline_dir(&self, ino: u32, inode: &Inode, raw: &[u8]) -> Result<()> {
2583        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
2584        let entries = crate::inline_data::read_dir(
2585            self.dev.as_ref(),
2586            ino,
2587            inode,
2588            raw,
2589            self.sb.inode_size,
2590            self.sb.block_size(),
2591            has_ft,
2592        )?;
2593        if entries.iter().any(|e| !is_dot_or_dotdot(&e.name)) {
2594            return Err(Error::DirectoryNotEmpty);
2595        }
2596        Ok(())
2597    }
2598
2599    pub fn apply_truncate_grow(&self, ino: u32, new_size: u64) -> Result<()> {
2600        self.refuse_write()?;
2601        let (inode, mut raw) = self.read_inode_verified(ino)?;
2602        Self::refuse_truncate_of(&inode)?;
2603        if new_size < inode.size {
2604            return Err(Error::InvalidArgument(
2605                "apply_truncate_grow: new_size < old_size (use apply_truncate_shrink)",
2606            ));
2607        }
2608        if inode.has_inline_data() {
2609            return self.truncate_inline_file(ino, &inode, raw, new_size);
2610        }
2611        Self::patch_inode_size_and_blocks(&mut raw, new_size, inode.blocks)?;
2612
2613        let now = self.runtime.now_unix_seconds();
2614        set_inode_time(&mut raw, InodeTime::Ctime, now);
2615        set_inode_time(&mut raw, InodeTime::Mtime, now);
2616
2617        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2618        self.commit_inode_write(ino, &raw)
2619    }
2620
2621    /// Phase 2.2: `fallocate(FALLOC_FL_KEEP_SIZE)` — preallocate blocks
2622    /// in the byte range `[offset, offset+len)` as uninitialized
2623    /// extents. The blocks are reserved (count against `i_blocks`) but
2624    /// reads return zeros until they're written. `i_size` is left
2625    /// unchanged per KEEP_SIZE semantics.
2626    ///
2627    /// v1 limitations:
2628    /// - Range must be entirely unmapped — partially-overlapping ranges
2629    ///   return `Error::InvalidArgument`. (Splitting around existing
2630    ///   extents is a follow-up.)
2631    /// - Single contiguous physical allocation. If the bitmap can't
2632    ///   serve `ceil(len / block_size)` contiguous blocks, returns
2633    ///   `Error::Corrupt("no group has a contiguous free run...")`.
2634    /// - The new extent goes into the inline root while it has a free
2635    ///   slot; a full root is promoted to an index node and a deeper tree
2636    ///   is descended, with the node blocks drawn in the same transaction.
2637    pub fn apply_fallocate_keep_size(&self, ino: u32, offset: u64, len: u64) -> Result<()> {
2638        self.refuse_write()?;
2639        if len == 0 {
2640            return Ok(());
2641        }
2642        let bs = self.sb.block_size() as u64;
2643        let bs_u32 = self.sb.block_size();
2644        let first_block = offset / bs;
2645        let last_block_excl = offset
2646            .checked_add(len)
2647            .ok_or(Error::InvalidArgument("fallocate: offset+len overflow"))?
2648            .div_ceil(bs);
2649        let need_blocks_u64 = last_block_excl - first_block;
2650        if need_blocks_u64 > u32::MAX as u64 {
2651            return Err(Error::InvalidArgument(
2652                "fallocate: range exceeds u32 block count",
2653            ));
2654        }
2655        let need_blocks = need_blocks_u64 as u32;
2656
2657        let (inode, mut raw) = self.read_inode_verified(ino)?;
2658        if !inode.is_file() {
2659            return Err(Error::InvalidArgument(
2660                "fallocate: target is not a regular file",
2661            ));
2662        }
2663        if !inode.has_extents() {
2664            return Err(Error::InvalidArgument(
2665                "fallocate: legacy (non-extents) inodes not supported",
2666            ));
2667        }
2668
2669        // V1: refuse if any block in range is already mapped — handling
2670        // the partial-overlap case requires splitting existing extents
2671        // mid-range, deferred to a follow-up.
2672        for log in first_block..last_block_excl {
2673            if crate::extent::map_logical(&inode.block, self.dev.as_ref(), bs_u32, log)?.is_some() {
2674                return Err(Error::InvalidArgument(
2675                    "fallocate: range partially mapped (v1 limitation)",
2676                ));
2677            }
2678        }
2679
2680        // Allocate one contiguous physical run.
2681        let inode_group = (ino - 1) / self.sb.inodes_per_group;
2682        let mut bitmap_reader = |block: u64| self.read_block(block);
2683        let plan = crate::alloc::plan_block_allocation(
2684            &self.sb,
2685            &self.allocation_groups(),
2686            need_blocks,
2687            inode_group,
2688            &mut bitmap_reader,
2689        )?;
2690
2691        // Insert as an uninitialized extent so reads see zeros without
2692        // hitting disk. Clamp to u16 — the range check above already
2693        // bounded need_blocks, but the on-disk extent length is u16.
2694        if need_blocks > 0x7FFF {
2695            return Err(Error::InvalidArgument(
2696                "fallocate: single-extent length > 32K blocks (split needed)",
2697            ));
2698        }
2699        let new_extent = crate::extent::Extent {
2700            logical_block: first_block as u32,
2701            length: need_blocks as u16,
2702            physical_block: plan.first_block,
2703            uninitialized: true,
2704        };
2705        // Apply via BlockBuffer — atomic across bitmap, BGD, SB, inode.
2706        // The data run is staged first, so the tree nodes a promotion
2707        // allocates below are drawn around it.
2708        let mut buf = BlockBuffer::new(self.sb.block_size());
2709        self.buffer_mark_block_run_used(&mut buf, plan.first_block, need_blocks as u64)?;
2710        self.buffer_patch_bgd_counters(
2711            &mut buf,
2712            plan.bgd.group_idx as usize,
2713            plan.bgd.free_blocks_delta,
2714            plan.bgd.free_inodes_delta,
2715            plan.bgd.used_dirs_delta,
2716        )?;
2717
2718        // The inline root holds four extents. A fifth promotes it to an
2719        // index node, and a root that already is one has to be descended:
2720        // both go through the deep planner, as `apply_pwrite` does.
2721        let mut meta_blocks: u64 = 0;
2722        let new_root = match crate::extent_mut::plan_insert_extent(&inode.block, new_extent) {
2723            Ok(muts) => muts
2724                .into_iter()
2725                .find_map(|m| match m {
2726                    crate::extent_mut::ExtentMutation::WriteRoot { bytes } => Some(bytes),
2727                    _ => None,
2728                })
2729                .ok_or(Error::Corrupt("fallocate: extent insert wrote no root"))?,
2730            Err(Error::CorruptExtentTree(msg))
2731                if msg.contains("LEAF_FULL_NEEDS_PROMOTION")
2732                    || msg.contains("multi-level tree mutation") =>
2733            {
2734                let reader = FsBlockReader { fs: self };
2735                let deep_plan = {
2736                    let mut alloc_node = || -> Result<u64> {
2737                        let p = self.plan_buffered_block_allocation(&buf, 1, inode_group)?;
2738                        self.buffer_mark_block_run_used(&mut buf, p.first_block, 1)?;
2739                        self.buffer_patch_bgd_counters(
2740                            &mut buf,
2741                            p.bgd.group_idx as usize,
2742                            p.bgd.free_blocks_delta,
2743                            0,
2744                            0,
2745                        )?;
2746                        meta_blocks += 1;
2747                        Ok(p.first_block)
2748                    };
2749                    crate::extent_mut::plan_insert_extent_deep(
2750                        &inode.block,
2751                        new_extent,
2752                        bs_u32,
2753                        &reader,
2754                        &mut alloc_node,
2755                    )?
2756                };
2757                for (block, mut bytes) in deep_plan.block_writes {
2758                    if self.csum.enabled {
2759                        self.csum
2760                            .patch_extent_tail(ino, inode.generation, &mut bytes);
2761                    }
2762                    buf.put(block, bytes);
2763                }
2764                deep_plan.new_root
2765            }
2766            Err(e) => return Err(e),
2767        };
2768        self.buffer_patch_sb_counters(
2769            &mut buf,
2770            plan.sb.free_blocks_delta - meta_blocks as i64,
2771            plan.sb.free_inodes_delta,
2772        )?;
2773
2774        // Splice the new extent root into the inode image.
2775        Self::patch_inode_block_area(&mut raw, &new_root)?;
2776
2777        // Bump i_blocks (sectors). KEEP_SIZE: i_size unchanged.
2778        let sectors_per_block = bs / 512;
2779        let new_i_blocks = inode
2780            .blocks
2781            .saturating_add((need_blocks as u64 + meta_blocks) * sectors_per_block);
2782        Self::patch_inode_size_and_blocks(&mut raw, inode.size, new_i_blocks)?;
2783
2784        // POSIX: fallocate bumps mtime + ctime.
2785        let now = self.runtime.now_unix_seconds();
2786        set_inode_time(&mut raw, InodeTime::Ctime, now);
2787        set_inode_time(&mut raw, InodeTime::Mtime, now);
2788
2789        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2790        self.buffer_write_inode(&mut buf, ino, &raw)?;
2791
2792        self.commit_block_buffer(buf)
2793    }
2794
2795    /// Phase 2.3 — `fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)`.
2796    /// Frees the data blocks wholly inside `[offset, offset+len)`, splitting
2797    /// straddling extents as needed, and zeroes the part of a block at
2798    /// either end that the range covers only in part. Reads of the punched range return
2799    /// zeros (sparse hole) thereafter; `i_size` is unchanged.
2800    ///
2801    /// Extent trees of any depth. The surviving extents are laid out
2802    /// again by `extent_mut::plan_repack_tree` over the tree blocks the
2803    /// file already holds (#258): four or fewer go in the inode, more get
2804    /// as many leaf and index levels as they need, and tree blocks left
2805    /// unused are freed with the data. A punch inside a single extent
2806    /// leaves a head and a tail where there was one record, so the layout
2807    /// can be one block short; that block is allocated, and a full volume
2808    /// fails the punch with the allocator's error rather than for size.
2809    ///
2810    /// Limits:
2811    /// - Indirect-block (ext2/3) inodes return EINVAL — punch is an
2812    ///   ext4-specific kernel API.
2813    pub fn apply_fallocate_punch_hole(&self, ino: u32, offset: u64, len: u64) -> Result<()> {
2814        self.refuse_write()?;
2815        if len == 0 {
2816            return Ok(());
2817        }
2818        let bs = self.sb.block_size() as u64;
2819        let bs_u32 = self.sb.block_size();
2820        let end = offset
2821            .checked_add(len)
2822            .ok_or(Error::InvalidArgument("punch_hole: offset+len overflow"))?;
2823        // ONLY THE BLOCKS WHOLLY INSIDE THE RANGE ARE FREED (#388). A block
2824        // the range covers in part keeps its other bytes, so it stays mapped
2825        // and the covered part is zeroed in place, as the kernel does. This
2826        // rounded outward once, and `punch(100, 100)` zeroed bytes 0..4096.
2827        let punch_first = offset.div_ceil(bs);
2828        let punch_last_excl = end / bs;
2829
2830        let (inode, mut raw) = self.read_inode_verified(ino)?;
2831        if !inode.is_file() {
2832            return Err(Error::InvalidArgument("punch_hole: not a regular file"));
2833        }
2834        if !inode.has_extents() {
2835            return Err(Error::InvalidArgument(
2836                "punch_hole: legacy (non-extents) inodes not supported",
2837            ));
2838        }
2839
2840        let (extents, tree_nodes) =
2841            crate::extent::collect_all_with_nodes(&inode.block, self.dev.as_ref(), bs_u32)?;
2842        let mut new_entries: Vec<crate::extent::Extent> = Vec::new();
2843        let mut freed_blocks: u64 = 0;
2844        let mut buf = BlockBuffer::new(bs_u32);
2845
2846        // The partial edge blocks: zeroed in the same transaction. A hole or
2847        // an uninitialized extent already reads as zeros and is left alone.
2848        let (head_block, tail_block) = (offset / bs, end / bs);
2849        let edges = if head_block == tail_block {
2850            vec![head_block]
2851        } else {
2852            vec![head_block, tail_block]
2853        };
2854        for lb in edges {
2855            let lo = offset.max(lb * bs);
2856            let hi = end.min((lb + 1) * bs);
2857            if lo >= hi || hi - lo == bs {
2858                continue; // outside the range, or wholly inside it
2859            }
2860            let Some(phys) =
2861                crate::extent::map_logical(&inode.block, self.dev.as_ref(), bs_u32, lb)?
2862            else {
2863                continue;
2864            };
2865            let block = buf.get_mut(self, phys)?;
2866            let (from, to) = ((lo - lb * bs) as usize, (hi - lb * bs) as usize);
2867            block[from..to].fill(0);
2868        }
2869
2870        // A range inside one block, or across two with no whole block
2871        // between, frees nothing: the tree is left exactly as it is.
2872        let frees_blocks = punch_first < punch_last_excl;
2873        for e in extents.iter().filter(|_| frees_blocks) {
2874            let el = e.logical_block as u64;
2875            let er = el + e.length as u64;
2876
2877            if er <= punch_first || el >= punch_last_excl {
2878                // Fully outside the punch range — keep verbatim.
2879                new_entries.push(*e);
2880                continue;
2881            }
2882            if el >= punch_first && er <= punch_last_excl {
2883                // Fully inside punch — free entirely.
2884                freed_blocks += self.buffer_free_block_run_and_bgd(
2885                    &mut buf,
2886                    e.physical_block,
2887                    e.length as u64,
2888                )?;
2889                continue;
2890            }
2891            // Partial overlap. Compute the freed sub-range; emit head /
2892            // tail retains around it.
2893            let free_lo = el.max(punch_first);
2894            let free_hi = er.min(punch_last_excl);
2895            let free_offset_in_e = free_lo - el;
2896            let free_len = (free_hi - free_lo) as u32;
2897            let free_phys = e.physical_block + free_offset_in_e;
2898            freed_blocks +=
2899                self.buffer_free_block_run_and_bgd(&mut buf, free_phys, free_len as u64)?;
2900
2901            if el < punch_first {
2902                new_entries.push(crate::extent::Extent {
2903                    logical_block: el as u32,
2904                    length: (punch_first - el) as u16,
2905                    physical_block: e.physical_block,
2906                    uninitialized: e.uninitialized,
2907                });
2908            }
2909            if er > punch_last_excl {
2910                new_entries.push(crate::extent::Extent {
2911                    logical_block: punch_last_excl as u32,
2912                    length: (er - punch_last_excl) as u16,
2913                    physical_block: e.physical_block + (punch_last_excl - el),
2914                    uninitialized: e.uninitialized,
2915                });
2916            }
2917        }
2918
2919        // THE TREE IS LAID OUT AGAIN OVER THE BLOCKS IT ALREADY HELD (#258).
2920        // The survivors are a subset of the entries the tree held, so packing
2921        // them full needs no more blocks than it has, and the ones left over
2922        // go back to free space with the data blocks. Four or fewer survivors
2923        // need no blocks at all and go in the inode, which is all this used
2924        // to do: every file with more than four surviving extents — that is,
2925        // every large file, which is what a punch is for — was refused.
2926        let mut allocated_blocks = 0;
2927        if frees_blocks {
2928            let gen = u32::from_le_bytes(inode.block[8..12].try_into().unwrap());
2929            let repacked = {
2930                let mut alloc = || self.buffer_allocate_block(&mut buf, ino);
2931                crate::extent_mut::plan_repack_tree(
2932                    gen,
2933                    &new_entries,
2934                    bs_u32,
2935                    &tree_nodes,
2936                    &mut alloc,
2937                )?
2938            };
2939            allocated_blocks = repacked.allocated_blocks.len() as u64;
2940            for (block, mut bytes) in repacked.block_writes {
2941                if self.csum.enabled {
2942                    self.csum
2943                        .patch_extent_tail(ino, inode.generation, &mut bytes);
2944                }
2945                buf.put(block, bytes);
2946            }
2947            for &node in &tree_nodes {
2948                if repacked.used_nodes.contains(&node) {
2949                    continue;
2950                }
2951                freed_blocks += self.buffer_free_block_run_and_bgd(&mut buf, node, 1)?;
2952            }
2953            Self::patch_inode_block_area(&mut raw, &repacked.new_root)?;
2954        }
2955
2956        // i_blocks decreases; i_size unchanged (KEEP_SIZE semantics
2957        // built in — punch always preserves size).
2958        let sectors_per_block = bs / 512;
2959        let new_i_blocks = inode
2960            .blocks
2961            .saturating_sub(freed_blocks * sectors_per_block)
2962            + allocated_blocks * sectors_per_block;
2963        Self::patch_inode_size_and_blocks(&mut raw, inode.size, new_i_blocks)?;
2964        let now = self.runtime.now_unix_seconds();
2965        set_inode_time(&mut raw, InodeTime::Ctime, now);
2966        set_inode_time(&mut raw, InodeTime::Mtime, now);
2967        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2968        self.buffer_write_inode(&mut buf, ino, &raw)?;
2969
2970        if freed_blocks > 0 {
2971            self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 0)?;
2972        }
2973
2974        self.commit_block_buffer(buf)
2975    }
2976
2977    /// Phase 2.4 — `fallocate(FALLOC_FL_ZERO_RANGE)`. Logically zero the
2978    /// byte range `[offset, offset+len)`. Implemented as punch-hole, which
2979    /// zeroes the partial edge blocks in place, + KEEP_SIZE preallocate of
2980    /// the whole blocks between them, so reads return zeros
2981    /// (uninitialized-extent semantics) and future writes don't need an
2982    /// allocation.
2983    ///
2984    /// Two separate transactions today (punch then alloc); a future
2985    /// optimization could fold them into one.
2986    pub fn apply_fallocate_zero_range(&self, ino: u32, offset: u64, len: u64) -> Result<()> {
2987        if len == 0 {
2988            return Ok(());
2989        }
2990        self.apply_fallocate_punch_hole(ino, offset, len)?;
2991        // Only the whole blocks the punch freed are preallocated again: the
2992        // partial ones at the edges stay mapped, their range zeroed (#388).
2993        let bs = self.sb.block_size() as u64;
2994        let end = offset
2995            .checked_add(len)
2996            .ok_or(Error::InvalidArgument("zero_range: offset+len overflow"))?;
2997        let (first, last) = (offset.div_ceil(bs) * bs, end / bs * bs);
2998        if first >= last {
2999            return Ok(());
3000        }
3001        self.apply_fallocate_keep_size(ino, first, last - first)
3002    }
3003
3004    /// Change the permission bits on `path`. Only the low 12 bits of `mode`
3005    /// (`S_ISUID|S_ISGID|S_ISVTX` plus rwx/rwx/rwx) are applied; the file-type
3006    /// bits (`S_IFMT`) are preserved from the existing inode.
3007    ///
3008    /// Updates `i_ctime = now` and recomputes the inode checksum on csum-
3009    /// enabled mounts. Returns `Error::NotFound` if the path doesn't resolve,
3010    /// `Error::ReadOnly` on a RO mount.
3011    pub fn apply_chmod(&self, path: &str, mode: u16) -> Result<()> {
3012        self.apply_chmod_bytes(path.as_bytes(), mode)
3013    }
3014
3015    /// [`apply_chmod`](Self::apply_chmod) of a path given as bytes, never decoded.
3016    pub(crate) fn apply_chmod_bytes(&self, path: &[u8], mode: u16) -> Result<()> {
3017        self.refuse_write()?;
3018        let ino = self.resolve(path)?;
3019        self.apply_chmod_ino(ino, mode)
3020    }
3021
3022    /// [`apply_chmod`](Self::apply_chmod) on the inode `r` names.
3023    pub fn apply_chmod_ino(&self, r: impl Into<InodeRef>, mode: u16) -> Result<()> {
3024        self.refuse_write()?;
3025        let r = r.into();
3026        let ino = r.ino;
3027        let (inode, mut raw) = self.live_inode(r)?;
3028
3029        // Preserve file-type bits (high 4 bits of i_mode); only the low 12
3030        // permission/suid/sgid/sticky bits are user-settable.
3031        let file_type_bits = inode.mode & crate::inode::S_IFMT;
3032        let new_mode = file_type_bits | (mode & 0x0FFF);
3033        raw[0x00..0x02].copy_from_slice(&new_mode.to_le_bytes());
3034
3035        // POSIX: chmod bumps ctime (not mtime).
3036        let now = self.runtime.now_unix_seconds();
3037        set_inode_time(&mut raw, InodeTime::Ctime, now);
3038
3039        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
3040        self.commit_inode_write(ino, &raw)
3041    }
3042
3043    /// Write a single mutated inode back, routing through the journal
3044    /// writer when one is available so the change is crash-safe. Falls
3045    /// back to a direct write + flush on unjournaled mounts.
3046    ///
3047    /// Used by every operation whose only mutation is one inode block:
3048    /// chmod, chown, utimens, and the in-place xattr ops once they're
3049    /// migrated to the journaled path.
3050    fn commit_inode_write(&self, ino: u32, new_inode_raw: &[u8]) -> Result<()> {
3051        let mut buf = BlockBuffer::new(self.sb.block_size());
3052        self.buffer_write_inode(&mut buf, ino, new_inode_raw)?;
3053        self.commit_block_buffer(buf)
3054    }
3055
3056    // ----------------------------------------------------------------------
3057    // BlockBuffer helpers (Phase 5.2 multi-block transactions)
3058    // ----------------------------------------------------------------------
3059    //
3060    // These mirror the disk-touching helpers (free_block_run_and_bgd,
3061    // patch_bgd_counters, patch_sb_counters, write_inode_raw) but operate
3062    // on an in-memory BlockBuffer instead. A multi-block op accumulates
3063    // its mutations into one buffer and commits the whole thing atomically
3064    // — either through the journal writer (when present) or via a flush-
3065    // gated direct-write fallback.
3066
3067    /// Splice a freshly-built inode into the inode-table block buffer.
3068    pub(crate) fn buffer_write_inode(
3069        &self,
3070        buf: &mut BlockBuffer,
3071        ino: u32,
3072        inode_raw: &[u8],
3073    ) -> Result<()> {
3074        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
3075        let it_buf = buf.get_mut(self, block)?;
3076        let off = offset as usize;
3077        it_buf[off..off + inode_raw.len()].copy_from_slice(inode_raw);
3078        Ok(())
3079    }
3080
3081    /// Every block an extent-mapped inode holds, as runs: its data extents,
3082    /// and the index and leaf blocks below its inline root.
3083    ///
3084    /// Read from the tree, not from `i_size`. A file of size zero can hold
3085    /// blocks (a `KEEP_SIZE` preallocation), and blocks can lie past the
3086    /// size of any file. A caller that frees a whole file frees these.
3087    ///
3088    /// Every index and leaf block must pass its checksum first, or this
3089    /// is [`Error::BadChecksum`] and nothing is returned to free: a node
3090    /// that does not verify may name blocks that belong to another file,
3091    /// and the kernel refuses the removal the same way.
3092    pub(crate) fn extent_tree_runs(&self, ino: u32, inode: &Inode) -> Result<Vec<(u64, u64)>> {
3093        let (extents, nodes) = crate::extent::collect_all_with_nodes(
3094            &inode.block,
3095            self.dev.as_ref(),
3096            self.sb.block_size(),
3097        )?;
3098        for &node in &nodes {
3099            let bytes = self.read_block(node)?;
3100            if !self.csum.verify_extent_tail(ino, inode.generation, &bytes) {
3101                return Err(Error::BadChecksum {
3102                    what: "extent block",
3103                });
3104            }
3105        }
3106        Ok(extents
3107            .iter()
3108            .map(|e| (e.physical_block, e.length as u64))
3109            .chain(nodes.into_iter().map(|n| (n, 1)))
3110            .collect())
3111    }
3112
3113    /// Free every run `extent_tree_runs` lists, returning the block count.
3114    pub(crate) fn buffer_free_runs(
3115        &self,
3116        buf: &mut BlockBuffer,
3117        runs: &[(u64, u64)],
3118    ) -> Result<u64> {
3119        let mut freed = 0;
3120        for &(start, len) in runs {
3121            freed += self.buffer_free_block_run_and_bgd(buf, start, len)?;
3122        }
3123        Ok(freed)
3124    }
3125
3126    /// Buffer-side equivalent of `free_block_run_and_bgd`: clears the
3127    /// bitmap bits AND patches the BGD counters in the buffer. Returns
3128    /// `len` so callers can accumulate a running freed-block total to
3129    /// feed to `buffer_patch_sb_counters`.
3130    ///
3131    /// A run may cross a group boundary -- the kernel merges extents on
3132    /// adjacency alone, and so does `extent_mut::are_contiguous` -- so it
3133    /// is freed one group at a time (#118). It used to take the group of
3134    /// `start` alone: the bits past that group were dropped, and its
3135    /// descriptor was credited the whole length, so the next group's
3136    /// blocks stayed allocated for ever while the first group over-counted.
3137    pub(crate) fn buffer_free_block_run_and_bgd(
3138        &self,
3139        buf: &mut BlockBuffer,
3140        start: u64,
3141        len: u64,
3142    ) -> Result<u64> {
3143        for (gi, bit_start, chunk) in self.group_chunks(start, len)? {
3144            let bitmap_block = self.groups[gi].block_bitmap;
3145            {
3146                let bm = buf.get_mut(self, bitmap_block)?;
3147                for i in 0..chunk {
3148                    let bit = bit_start + i;
3149                    let byte = (bit / 8) as usize;
3150                    let mask = 1u8 << (bit % 8);
3151                    if byte < bm.len() {
3152                        bm[byte] &= !mask;
3153                    }
3154                }
3155            }
3156            self.buffer_refresh_bitmap_csum(buf, gi, false)?;
3157            self.buffer_patch_bgd_counters(buf, gi, chunk as i32, 0, 0)?;
3158        }
3159        Ok(len)
3160    }
3161
3162    /// `[start, start + len)` split at block-group boundaries, as
3163    /// `(group, bit within the group, length)` for each group it touches.
3164    ///
3165    /// Every group is validated, not only the first: a run whose tail
3166    /// leaves the last group is refused rather than truncated. A caller
3167    /// working in a `BlockBuffer` drops it uncommitted on that error.
3168    fn group_chunks(&self, start: u64, len: u64) -> Result<Vec<(usize, u64, u64)>> {
3169        let bpg = self.sb.blocks_per_group as u64;
3170        let first_data = self.sb.first_data_block as u64;
3171        let end = start.checked_add(len).ok_or(Error::InvalidBlock(start))?;
3172        if start < first_data || bpg == 0 {
3173            return Err(Error::InvalidBlock(start));
3174        }
3175        // The last group is usually short, and its nominal span past
3176        // `blocks_count` is padding: bits that stand for no block. A run
3177        // reaching into it passed the group-index check below, and freeing
3178        // it cleared padding bits and credited blocks that do not exist
3179        // (Greptile on #188).
3180        if end > self.sb.blocks_count {
3181            return Err(Error::InvalidBlock(self.sb.blocks_count));
3182        }
3183        let mut chunks = Vec::new();
3184        let mut at = start;
3185        while at < end {
3186            let gi = ((at - first_data) / bpg) as usize;
3187            if gi >= self.groups.len() {
3188                return Err(Error::InvalidBlock(at));
3189            }
3190            let group_start = first_data + gi as u64 * bpg;
3191            let chunk = end.min(group_start + bpg) - at;
3192            chunks.push((gi, at - group_start, chunk));
3193            at += chunk;
3194        }
3195        Ok(chunks)
3196    }
3197
3198    /// Rewrite the checksum of the group descriptor at `block[off..]` (group
3199    /// `gi`) under whichever scheme the volume uses: crc32c for
3200    /// `METADATA_CSUM`, crc16 for `GDT_CSUM`, nothing for neither.
3201    fn restamp_group_desc_csum(&self, block: &mut [u8], off: usize, gi: usize) {
3202        let end = off + self.sb.desc_size as usize;
3203        if let Some(c) =
3204            crate::checksum::group_desc_csum(&self.sb, &self.csum, gi as u32, &block[off..end])
3205        {
3206            block[off + 0x1E..off + 0x20].copy_from_slice(&c.to_le_bytes());
3207        }
3208    }
3209
3210    /// Buffer-side equivalent of `mark_block_run_used`: sets the bitmap
3211    /// bits for `[start, start+len)` in the buffer's bitmap block.
3212    /// If group `gi`'s BGD has the given uninit flag set, clear it in `buf`
3213    /// and return `true` (the caller must then zero the bitmap block
3214    /// itself — the flag being set is precisely the license callers had to
3215    /// leave that block's on-disk content unspecified). Returns `false`,
3216    /// no-op, if the flag was already clear.
3217    fn clear_bgd_uninit_flag_if_set(
3218        &self,
3219        buf: &mut BlockBuffer,
3220        gi: usize,
3221        which: BgdUninitFlag,
3222    ) -> Result<bool> {
3223        let flag = match which {
3224            BgdUninitFlag::Inode => bgd::BgdFlags::INODE_UNINIT.bits(),
3225            BgdUninitFlag::Block => bgd::BgdFlags::BLOCK_UNINIT.bits(),
3226        };
3227
3228        // Where the descriptor lives, META_BG or not (#73).
3229        let (bgt_block, off) = self.sb.descriptor_location(gi as u64);
3230
3231        let block = buf.get_mut(self, bgt_block)?;
3232        let flags_off = off + 0x12;
3233        let flags = u16::from_le_bytes(block[flags_off..flags_off + 2].try_into().unwrap());
3234        if flags & flag == 0 {
3235            return Ok(false);
3236        }
3237        let new_flags = flags & !flag;
3238        block[flags_off..flags_off + 2].copy_from_slice(&new_flags.to_le_bytes());
3239        // Record it against the mount-time snapshot too, or the very next
3240        // allocation plans as though the group were still untouched — but
3241        // record it on the *buffer*, so it becomes visible only when the
3242        // buffer commits.
3243        //
3244        // Publishing it here instead would survive a failed commit: the
3245        // operation returns an error, the mount carries on, and the next
3246        // allocation is told the group's bitmap is initialised while the
3247        // bytes on disk are still whatever the uninit flag licensed
3248        // leaving there.
3249        buf.uninit_cleared
3250            .entry(gi)
3251            .and_modify(|f| *f &= !flag)
3252            .or_insert(new_flags);
3253        Ok(true)
3254    }
3255
3256    /// The group descriptors the allocators must plan against: the mount-time
3257    /// snapshot, with any uninit flag this mount has since cleared taken back
3258    /// out. Borrows the snapshot untouched in the overwhelmingly common case
3259    /// where nothing has been cleared yet.
3260    ///
3261    /// Otherwise the overridden copy is built once and shared until the next
3262    /// clear is published (#333): this is called on every allocation,
3263    /// several times inside the write retry loop, and cloning every
3264    /// descriptor of a large volume each time cost more than the plan.
3265    fn allocation_groups(&self) -> AllocationGroups<'_> {
3266        let mut state = self.uninit_cleared.lock().unwrap();
3267        if state.cleared.is_empty() {
3268            return AllocationGroups::Snapshot(&self.groups);
3269        }
3270        if state.overridden.is_none() {
3271            let mut groups = self.groups.clone();
3272            for (&gi, &flags) in state.cleared.iter() {
3273                groups[gi].flags = flags;
3274            }
3275            state.overridden = Some(groups.into());
3276        }
3277        AllocationGroups::Overridden(state.overridden.clone().unwrap())
3278    }
3279
3280    /// Plan a block allocation inside an open transaction: bitmaps come from
3281    /// the buffer when it has staged them, and so do uninit flags.
3282    ///
3283    /// The second half is the one that matters. Staging an allocation into a
3284    /// BLOCK_UNINIT group clears the flag on the buffer only, so it becomes
3285    /// visible to the mount when the buffer commits. A plan that still sees
3286    /// the flag synthesises the bitmap from the group's metadata and never
3287    /// reads the staged one, so it offers the run just handed out again.
3288    fn plan_buffered_block_allocation(
3289        &self,
3290        buf: &BlockBuffer,
3291        count: u32,
3292        hint: u32,
3293    ) -> Result<crate::alloc::BlockAllocationPlan> {
3294        self.plan_buffered_block_allocation_excluding(buf, count, hint, &[])
3295    }
3296
3297    /// [`Self::plan_buffered_block_allocation`], with `reserved` blocks --
3298    /// spoken for but not yet staged as used -- treated as used.
3299    fn plan_buffered_block_allocation_excluding(
3300        &self,
3301        buf: &BlockBuffer,
3302        count: u32,
3303        hint: u32,
3304        reserved: &[u64],
3305    ) -> Result<crate::alloc::BlockAllocationPlan> {
3306        let base = self.allocation_groups();
3307        let mut groups = Cow::Borrowed(&*base);
3308        for (&gi, &flags) in &buf.uninit_cleared {
3309            if let Some(g) = groups.to_mut().get_mut(gi) {
3310                g.flags = flags;
3311            }
3312        }
3313        crate::alloc::plan_block_allocation_excluding(
3314            &self.sb,
3315            &groups,
3316            count,
3317            hint,
3318            reserved,
3319            |block| match buf.dirty.get(&block) {
3320                Some(bytes) => Ok(bytes.clone()),
3321                None => self.read_block(block),
3322            },
3323        )
3324    }
3325
3326    pub(crate) fn buffer_mark_block_run_used(
3327        &self,
3328        buf: &mut BlockBuffer,
3329        start: u64,
3330        len: u64,
3331    ) -> Result<()> {
3332        let bpg = self.sb.blocks_per_group as u64;
3333        let first_data = self.sb.first_data_block as u64;
3334        let gi = ((start - first_data) / bpg) as usize;
3335        if gi >= self.groups.len() {
3336            return Err(Error::InvalidBlock(start));
3337        }
3338        let group_start = first_data + gi as u64 * bpg;
3339        let bit_start = (start - group_start) as u32;
3340
3341        // Same staleness problem as `buffer_mark_inode_used`, for the block
3342        // bitmap this time: BLOCK_UNINIT is every reader's license to skip
3343        // the on-disk bitmap and treat the group as empty, so the *next*
3344        // mount kept proposing the same "first free" block for every new
3345        // allocation into this group — including a file's own data block
3346        // landing on top of a directory's just-created data block in the
3347        // same group. Reproduced by hand: the second file written into a
3348        // freshly-created directory corrupted the directory's own data
3349        // block ("corrupt directory entry: bad rec_len during add") because
3350        // its content block silently reused the directory's block number.
3351        //
3352        // Unlike an uninit inode bitmap, "all blocks free" is not the
3353        // starting point: the group keeps its own fixed metadata in use
3354        // (see the comment above `alloc::uninit_group_used_ranges`).
3355        let was_uninit = self.clear_bgd_uninit_flag_if_set(buf, gi, BgdUninitFlag::Block)?;
3356        // The bitmap block of a group that was BLOCK_UNINIT holds nothing
3357        // meaningful: it starts from what the flag stood for, which is the
3358        // same bitmap the planner and fsck assumed (`uninit_block_bitmap`).
3359        let implied = was_uninit
3360            .then(|| crate::alloc::uninit_block_bitmap(&self.sb, &self.groups, gi as u32));
3361        let bitmap_block = self.groups[gi].block_bitmap;
3362        let bm = buf.get_mut(self, bitmap_block)?;
3363        if let Some(implied) = implied {
3364            let n = implied.len().min(bm.len());
3365            bm[..n].copy_from_slice(&implied[..n]);
3366        }
3367        for i in 0..len {
3368            let bit = bit_start as u64 + i;
3369            let byte = (bit / 8) as usize;
3370            let mask = 1u8 << (bit % 8);
3371            if byte < bm.len() {
3372                bm[byte] |= mask;
3373            }
3374        }
3375        self.buffer_refresh_bitmap_csum(buf, gi, false)?;
3376        Ok(())
3377    }
3378
3379    /// Recompute a group's bitmap checksum (inode or block) after its bitmap
3380    /// block changed, then refresh the BGD checksum. metadata_csum stores the
3381    /// bitmap crc split lo + hi in the descriptor (inode: 0x1A/0x3A, block:
3382    /// 0x18/0x38); a stale value makes e2fsck and the kernel report "bitmap
3383    /// does not match checksum". No-op when checksums are disabled.
3384    pub(crate) fn buffer_refresh_bitmap_csum(
3385        &self,
3386        buf: &mut BlockBuffer,
3387        gi: usize,
3388        inode_bitmap: bool,
3389    ) -> Result<()> {
3390        if !self.csum.enabled {
3391            return Ok(());
3392        }
3393        let (bitmap_block, coverage, lo_off, hi_off) = if inode_bitmap {
3394            (
3395                self.groups[gi].inode_bitmap,
3396                (self.sb.inodes_per_group as usize).div_ceil(8),
3397                0x1A,
3398                0x3A,
3399            )
3400        } else {
3401            (
3402                self.groups[gi].block_bitmap,
3403                (self.sb.blocks_per_group as usize).div_ceil(8),
3404                0x18,
3405                0x38,
3406            )
3407        };
3408        let csum = {
3409            let bm = buf.get_mut(self, bitmap_block)?;
3410            let end = coverage.min(bm.len());
3411            crate::checksum::linux_crc32c(self.csum.seed, &bm[..end])
3412        };
3413
3414        let desc_size = self.sb.desc_size as u64;
3415        // Where the descriptor lives, META_BG or not (#73).
3416        let (bgt_block, off) = self.sb.descriptor_location(gi as u64);
3417        let has_hi = desc_size >= 0x40;
3418        let block = buf.get_mut(self, bgt_block)?;
3419        block[off + lo_off..off + lo_off + 2]
3420            .copy_from_slice(&((csum & 0xFFFF) as u16).to_le_bytes());
3421        if has_hi {
3422            block[off + hi_off..off + hi_off + 2]
3423                .copy_from_slice(&(((csum >> 16) & 0xFFFF) as u16).to_le_bytes());
3424        }
3425        // Refresh the BGD checksum (0x1E) so the descriptor stays consistent.
3426        self.restamp_group_desc_csum(block, off, gi);
3427        Ok(())
3428    }
3429
3430    /// Buffer-side equivalent of `free_inode_slot`: clears the inode
3431    /// bitmap bit AND patches the BGD's `bg_free_inodes_count` (+1) in
3432    /// the buffer. Matches the kernel's pairing — the SB
3433    /// `s_free_inodes_count` is the caller's responsibility (one bump
3434    /// per high-level op, via `buffer_patch_sb_counters`).
3435    pub(crate) fn buffer_free_inode_slot(&self, buf: &mut BlockBuffer, ino: u32) -> Result<()> {
3436        let ipg = self.sb.inodes_per_group;
3437        let gi = ((ino - 1) / ipg) as usize;
3438        if gi >= self.groups.len() {
3439            return Err(Error::InvalidInode(ino));
3440        }
3441        let bit = ((ino - 1) % ipg) as u64;
3442        let bitmap_block = self.groups[gi].inode_bitmap;
3443        {
3444            let bm = buf.get_mut(self, bitmap_block)?;
3445            let byte = (bit / 8) as usize;
3446            let mask = 1u8 << (bit % 8);
3447            if byte < bm.len() {
3448                bm[byte] &= !mask;
3449            }
3450        }
3451        self.buffer_refresh_bitmap_csum(buf, gi, true)?;
3452        self.buffer_patch_bgd_counters(buf, gi, 0, 1, 0)
3453    }
3454
3455    /// Buffer-side equivalent of `mark_inode_used`: sets the inode
3456    /// bitmap bit. BGD/SB counter patches are the caller's
3457    /// responsibility (different ops want different deltas — e.g.
3458    /// mkdir bumps `used_dirs_count`).
3459    pub(crate) fn buffer_mark_inode_used(&self, buf: &mut BlockBuffer, ino: u32) -> Result<()> {
3460        let ipg = self.sb.inodes_per_group;
3461        let gi = ((ino - 1) / ipg) as usize;
3462        if gi >= self.groups.len() {
3463            return Err(Error::InvalidInode(ino));
3464        }
3465        let bit = ((ino - 1) % ipg) as u64;
3466        let bitmap_block = self.groups[gi].inode_bitmap;
3467
3468        // If this group's inode bitmap is still INODE_UNINIT, every reader
3469        // (including a future mount of this same filesystem) is required to
3470        // ignore whatever bytes are actually on disk there and assume the
3471        // whole group is free — that's the entire point of the flag, and
3472        // it's why uninit groups' bitmap blocks are allowed to contain
3473        // stale/unspecified garbage from mkfs. The moment we allocate a
3474        // real inode out of such a group, that assumption becomes false, so
3475        // we must (a) zero the block ourselves before setting our bit —
3476        // group index > 0 has zero pre-reserved inodes, so "everything but
3477        // our bit is free" is exactly correct here — and (b) clear the
3478        // flag. Skipping either step means the *next* mount still treats
3479        // the group as empty and hands out the same inode number again,
3480        // silently overwriting whatever was just written here. Found by
3481        // hand: creating a file/directory whose parent lands in a
3482        // previously-untouched group corrupted the parent on the very next
3483        // allocation, every time, until this was fixed.
3484        let was_uninit = self.clear_bgd_uninit_flag_if_set(buf, gi, BgdUninitFlag::Inode)?;
3485        let bm = buf.get_mut(self, bitmap_block)?;
3486        if was_uninit {
3487            bm.iter_mut().for_each(|byte| *byte = 0);
3488            // e2fsck convention: bits beyond `inodes_per_group`, up to the
3489            // end of the bitmap block, represent no real inode and must
3490            // read as 1 ("in use"), not 0 ("free") — that's what "padding
3491            // at end of inode bitmap is not set" flags otherwise. Harmless
3492            // on its own (no inode ever maps there), but worth getting
3493            // right since we're already the one deciding this block's
3494            // entire content for the first time.
3495            let bits_per_block = (bm.len() as u64) * 8;
3496            for pad_bit in (ipg as u64)..bits_per_block {
3497                let byte = (pad_bit / 8) as usize;
3498                let mask = 1u8 << (pad_bit % 8);
3499                bm[byte] |= mask;
3500            }
3501        }
3502        let byte = (bit / 8) as usize;
3503        let mask = 1u8 << (bit % 8);
3504        if byte < bm.len() {
3505            bm[byte] |= mask;
3506        }
3507        self.buffer_refresh_bitmap_csum(buf, gi, true)?;
3508
3509        // Maintain bg_itable_unused: this inode is now in use, so the count of
3510        // never-used inodes at the END of the group's table can be no larger
3511        // than the inodes after this one. A stale value makes e2fsck and the
3512        // kernel treat freshly-allocated inodes as unused ("references inode
3513        // found in unused inodes area" / "invalid unused inodes count"). lo at
3514        // 0x1C, hi at 0x32 (desc_size >= 64). The BGD checksum is recomputed so
3515        // the change stands alone; the following counter patch recomputes it
3516        // again harmlessly.
3517        let floor = ipg.saturating_sub(bit as u32 + 1);
3518        let desc_size = self.sb.desc_size as u64;
3519        // Where the descriptor lives, META_BG or not (#73).
3520        let (bgt_block, off) = self.sb.descriptor_location(gi as u64);
3521        let has_hi = desc_size >= 0x40;
3522        let block = buf.get_mut(self, bgt_block)?;
3523        let cur_lo = u16::from_le_bytes(block[off + 0x1C..off + 0x1E].try_into().unwrap()) as u32;
3524        let cur_hi = if has_hi {
3525            u16::from_le_bytes(block[off + 0x32..off + 0x34].try_into().unwrap()) as u32
3526        } else {
3527            0
3528        };
3529        let cur = (cur_hi << 16) | cur_lo;
3530        if floor < cur {
3531            block[off + 0x1C..off + 0x1E].copy_from_slice(&((floor & 0xFFFF) as u16).to_le_bytes());
3532            if has_hi {
3533                block[off + 0x32..off + 0x34]
3534                    .copy_from_slice(&(((floor >> 16) & 0xFFFF) as u16).to_le_bytes());
3535            }
3536            self.restamp_group_desc_csum(block, off, gi);
3537        }
3538        Ok(())
3539    }
3540
3541    /// Buffer-side BGD counter patch. Mirrors `patch_bgd_counters` byte
3542    /// for byte; only the I/O target differs (the BGD block is read from
3543    /// the buffer if already touched, else from disk).
3544    pub(crate) fn buffer_patch_bgd_counters(
3545        &self,
3546        buf: &mut BlockBuffer,
3547        gi: usize,
3548        free_blocks_delta: i32,
3549        free_inodes_delta: i32,
3550        used_dirs_delta: i32,
3551    ) -> Result<()> {
3552        // Where the descriptor lives, META_BG or not (#73).
3553        let (bgt_block, off_in_block) = self.sb.descriptor_location(gi as u64);
3554
3555        let block = buf.get_mut(self, bgt_block)?;
3556        patch_bgd_counter_fields(
3557            block,
3558            off_in_block,
3559            self.sb.desc_size,
3560            free_blocks_delta,
3561            free_inodes_delta,
3562            used_dirs_delta,
3563        );
3564
3565        self.restamp_group_desc_csum(&mut block[..], off_in_block, gi);
3566        Ok(())
3567    }
3568
3569    /// Buffer-side SB counter patch. The SB lives at byte offset 1024
3570    /// inside the device; for 4 KiB blocks that's offset 1024 within fs
3571    /// block 0, for 1 KiB blocks the SB IS fs block 1. We patch the
3572    /// 1024-byte SB region in-place inside the relevant whole block, so
3573    /// the journal can transport it as a normal full-block write.
3574    pub(crate) fn buffer_patch_sb_counters(
3575        &self,
3576        buf: &mut BlockBuffer,
3577        free_blocks_delta: i64,
3578        free_inodes_delta: i32,
3579    ) -> Result<()> {
3580        let bs = self.sb.block_size() as u64;
3581        let sb_offset = crate::superblock::SUPERBLOCK_OFFSET; // 1024
3582        let sb_block = sb_offset / bs;
3583        let off_in_block = (sb_offset % bs) as usize;
3584
3585        let block = buf.get_mut(self, sb_block)?;
3586        let sb = &mut block[off_in_block..off_in_block + 1024];
3587
3588        // s_free_inodes_count at 0x10..0x14 (u32 le)
3589        let fi = u32::from_le_bytes(sb[0x10..0x14].try_into().unwrap()) as i64;
3590        let fi_new = (fi + free_inodes_delta as i64).max(0) as u32;
3591        sb[0x10..0x14].copy_from_slice(&fi_new.to_le_bytes());
3592
3593        // s_free_blocks_count split lo (0x0C..0x10, u32) + hi (0x158..0x15C, u32)
3594        let lo = u32::from_le_bytes(sb[0x0C..0x10].try_into().unwrap()) as u64;
3595        let hi = u32::from_le_bytes(sb[0x158..0x15C].try_into().unwrap()) as u64;
3596        let cur = ((hi << 32) | lo) as i64;
3597        let new = (cur + free_blocks_delta).max(0) as u64;
3598        sb[0x0C..0x10].copy_from_slice(&(new as u32).to_le_bytes());
3599        sb[0x158..0x15C].copy_from_slice(&((new >> 32) as u32).to_le_bytes());
3600
3601        if self.csum.enabled {
3602            let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
3603            sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
3604        }
3605        Ok(())
3606    }
3607
3608    /// Buffer-side OR of `bits` into the SB's `s_feature_compat` (0x5C).
3609    pub(crate) fn buffer_patch_sb_compat(&self, buf: &mut BlockBuffer, bits: u32) -> Result<()> {
3610        let bs = self.sb.block_size() as u64;
3611        let sb_offset = crate::superblock::SUPERBLOCK_OFFSET;
3612        let block = buf.get_mut(self, sb_offset / bs)?;
3613        let off = (sb_offset % bs) as usize;
3614        let sb = &mut block[off..off + 1024];
3615        let compat = u32::from_le_bytes(sb[0x5C..0x60].try_into().unwrap());
3616        sb[0x5C..0x60].copy_from_slice(&(compat | bits).to_le_bytes());
3617        if self.csum.enabled {
3618            let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
3619            sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
3620        }
3621        Ok(())
3622    }
3623
3624    /// Buffer-side patch of the SB's `s_last_orphan` field at byte
3625    /// 0xE8. Used by orphan recovery (Phase 6.2) to clear / advance the
3626    /// chain head atomically with the inode/block frees.
3627    pub(crate) fn buffer_patch_sb_last_orphan(
3628        &self,
3629        buf: &mut BlockBuffer,
3630        value: u32,
3631    ) -> Result<()> {
3632        let bs = self.sb.block_size() as u64;
3633        let sb_offset = crate::superblock::SUPERBLOCK_OFFSET;
3634        let sb_block = sb_offset / bs;
3635        let off_in_block = (sb_offset % bs) as usize;
3636        let block = buf.get_mut(self, sb_block)?;
3637        let sb = &mut block[off_in_block..off_in_block + 1024];
3638        sb[0xE8..0xEC].copy_from_slice(&value.to_le_bytes());
3639        if self.csum.enabled {
3640            let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
3641            sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
3642        }
3643        Ok(())
3644    }
3645
3646    /// Buffer-side equivalent of `remove_dir_entry`: scans `parent`'s
3647    /// dir blocks, removes the named entry, recomputes the tail csum,
3648    /// stages the modified block in `buf`. Returns `Error::NotFound`
3649    /// when the name isn't present.
3650    pub(crate) fn buffer_remove_dir_entry(
3651        &self,
3652        buf: &mut BlockBuffer,
3653        parent_ino: u32,
3654        parent_inode: &Inode,
3655        name: &[u8],
3656    ) -> Result<()> {
3657        // An inline directory, as this transaction has it: still inline, or
3658        // converted to a block by an add earlier in it (#428).
3659        if parent_inode.has_inline_data() {
3660            let (now, _) = self.buffered_inode_verified(buf, parent_ino)?;
3661            if now.has_inline_data() {
3662                return self.buffer_remove_inline_dir_entry(buf, parent_ino, name);
3663            }
3664            return self.buffer_remove_dir_entry(buf, parent_ino, &now, name);
3665        }
3666        let bs = self.sb.block_size();
3667        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
3668        let n_blocks = parent_inode.size.div_ceil(bs as u64);
3669        for logical in 0..n_blocks {
3670            let Some(phys) = self.map_inode_logical(parent_inode, logical)? else {
3671                continue;
3672            };
3673            let block = buf.get_mut(self, phys)?;
3674            // An index block holds no entries to remove, and read as one it
3675            // ends in a tail-shaped dt_reserved and a `..` spanning the rest
3676            // (#233).
3677            if Self::is_htree_index_block(parent_inode, logical, block) {
3678                continue;
3679            }
3680            let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
3681                12
3682            } else {
3683                0
3684            };
3685            if crate::dir::remove_entry_from_block(block, name, has_ft, reserved_tail)? {
3686                if self.csum.enabled && reserved_tail == 12 {
3687                    self.csum
3688                        .patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
3689                }
3690                return Ok(());
3691            }
3692        }
3693        Err(Error::NotFound)
3694    }
3695
3696    /// Buffer-side equivalent of `update_dotdot`: rewrites the `..`
3697    /// entry in `dir_inode`'s first data block (in-buffer) to point at
3698    /// `new_parent_ino`, recomputes the tail csum.
3699    pub(crate) fn buffer_update_dotdot(
3700        &self,
3701        buf: &mut BlockBuffer,
3702        dir_ino: u32,
3703        dir_inode: &Inode,
3704        new_parent_ino: u32,
3705    ) -> Result<()> {
3706        // An inline directory's `..` is the inode number in bytes 0..4 of
3707        // its `i_block` (#428).
3708        if dir_inode.has_inline_data() {
3709            let (now, _) = self.buffered_inode_verified(buf, dir_ino)?;
3710            if !now.has_inline_data() {
3711                return self.buffer_update_dotdot(buf, dir_ino, &now, new_parent_ino);
3712            }
3713            let (inode, raw, mut dir) = self.buffered_inline_dir(buf, dir_ino)?;
3714            dir.parent = new_parent_ino;
3715            return self.buffer_store_inline_dir(buf, dir_ino, &inode, raw, &dir);
3716        }
3717        let phys = self
3718            .map_inode_logical(dir_inode, 0)?
3719            .ok_or(Error::Corrupt("buffer_update_dotdot: dir block 0 missing"))?;
3720        let block = buf.get_mut(self, phys)?;
3721        if block.len() < 24 {
3722            return Err(Error::Corrupt("buffer_update_dotdot: dir block too small"));
3723        }
3724        // Checked before it is changed, linear or indexed: a fresh checksum
3725        // over a block that was already corrupt would hide the corruption.
3726        // Nothing earlier in a rename reads this block — the lookups read
3727        // the parents — so this is the only place it can be refused (#322).
3728        self.refuse_unverified_dir_block(dir_ino, dir_inode, 0, block)?;
3729        block[12..16].copy_from_slice(&new_parent_ino.to_le_bytes());
3730        if dir_inode.flags & crate::inode::InodeFlags::INDEX.bits() != 0 {
3731            // A dx_root's checksum is its dx_tail's, over a different range
3732            // by a different rule. It can end in bytes that look like a
3733            // dirent tail, and writing one there corrupted the index.
3734            self.csum
3735                .patch_dx_tail(dir_ino, dir_inode.generation, block, 32);
3736        } else if self.csum.enabled && crate::dir::has_csum_tail(block) {
3737            self.csum
3738                .patch_dir_entry_tail(dir_ino, dir_inode.generation, block);
3739        }
3740        Ok(())
3741    }
3742
3743    /// Buffer-side equivalent of `add_dir_entry` for the IN-PLACE case
3744    /// only (an existing parent block has room for the new entry). The
3745    /// dir block is read into the buffer (or reused if already touched),
3746    /// `add_entry_to_block` rewrites it, csum patched, returns Ok(()).
3747    ///
3748    /// Returns `Error::OutOfBounds` when no existing parent block has
3749    /// room — caller should then fall through to
3750    /// `buffer_extend_dir_and_add_entry` to grow the directory by one
3751    /// block (which has its own scope limits).
3752    pub(crate) fn buffer_add_dir_entry_inplace(
3753        &self,
3754        buf: &mut BlockBuffer,
3755        parent_ino: u32,
3756        parent_inode: &Inode,
3757        name: &[u8],
3758        target_ino: u32,
3759        file_type: crate::dir::DirEntryType,
3760    ) -> Result<()> {
3761        // An inline directory takes the entry in the inode, or is converted
3762        // to a block that holds it; either way it never needs extending
3763        // (#428).
3764        if parent_inode.has_inline_data() {
3765            let (now, _) = self.buffered_inode_verified(buf, parent_ino)?;
3766            if now.has_inline_data() {
3767                return self
3768                    .buffer_add_inline_dir_entry(buf, parent_ino, name, target_ino, file_type);
3769            }
3770            return self
3771                .buffer_add_dir_entry_inplace(buf, parent_ino, &now, name, target_ino, file_type);
3772        }
3773        let bs = self.sb.block_size();
3774        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
3775        if parent_inode.flags & crate::inode::InodeFlags::INDEX.bits() != 0 {
3776            return self.buffer_add_dir_entry_indexed(
3777                buf,
3778                parent_ino,
3779                parent_inode,
3780                name,
3781                target_ino,
3782                file_type,
3783                has_ft,
3784            );
3785        }
3786        let n_blocks = parent_inode.size.div_ceil(bs as u64);
3787        for logical in 0..n_blocks {
3788            let Some(phys) = self.map_inode_logical(parent_inode, logical)? else {
3789                continue;
3790            };
3791            let block = buf.get_mut(self, phys)?;
3792            let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
3793                12
3794            } else {
3795                0
3796            };
3797            match crate::dir::add_entry_to_block(
3798                block,
3799                target_ino,
3800                name,
3801                file_type,
3802                has_ft,
3803                reserved_tail,
3804            ) {
3805                Ok(()) => {
3806                    if self.csum.enabled && reserved_tail == 12 {
3807                        self.csum
3808                            .patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
3809                    }
3810                    return Ok(());
3811                }
3812                Err(Error::OutOfBounds) => continue,
3813                Err(e) => return Err(e),
3814            }
3815        }
3816        // No existing block has room — caller must extend the directory.
3817        Err(Error::OutOfBounds)
3818    }
3819
3820    /// [`Self::buffer_add_dir_entry_inplace`] for a directory with
3821    /// `EXT4_INDEX_FL`.
3822    ///
3823    /// Block 0 of such a directory is the `dx_root`, and interior nodes are
3824    /// blocks too. None of them is a place for an entry: the root's fake
3825    /// `..` spans the rest of its block, so treating it as a linear block
3826    /// wrote the new entry over `dx_root_info` and zeroed the `dx_entry`
3827    /// array (#97). The entry goes where the index says a lookup will look,
3828    /// the leaf covering its hash, or nowhere.
3829    ///
3830    /// `Error::OutOfBounds` when that leaf is full, or when the names here
3831    /// are hashed some way this crate does not (casefolded or encrypted).
3832    /// The extend path then drops the index, as the kernel does, rather than
3833    /// append a block the index does not route to.
3834    #[allow(clippy::too_many_arguments)]
3835    fn buffer_add_dir_entry_indexed(
3836        &self,
3837        buf: &mut BlockBuffer,
3838        parent_ino: u32,
3839        parent_inode: &Inode,
3840        name: &[u8],
3841        target_ino: u32,
3842        file_type: crate::dir::DirEntryType,
3843        has_ft: bool,
3844    ) -> Result<()> {
3845        if parent_inode.flags & (EXT4_CASEFOLD_FL | EXT4_ENCRYPT_FL) != 0 {
3846            return Err(Error::OutOfBounds);
3847        }
3848        let mut read_logical = |logical: u64| -> Result<Vec<u8>> {
3849            let phys = self
3850                .map_inode_logical(parent_inode, logical)?
3851                .ok_or(Error::CorruptDirEntry("htree block is not mapped"))?;
3852            Ok(buf.get_mut(self, phys)?.to_vec())
3853        };
3854        // Every index block that routes this write is checked before it
3855        // is believed: a corrupt root or node sends the entry to the
3856        // wrong leaf, which then reads as a name the index cannot find.
3857        let root = read_logical(0)?;
3858        self.check_dx_block(parent_ino, parent_inode, &root, true)?;
3859        let leaf = crate::htree::lookup_leaf_with(
3860            name,
3861            &root,
3862            &self.sb.hash_seed,
3863            self.sb.unsigned_hash(),
3864            |logical| {
3865                let node = read_logical(u64::from(logical))?;
3866                self.check_dx_block(parent_ino, parent_inode, &node, false)?;
3867                Ok(node)
3868            },
3869        )?
3870        .ok_or(Error::CorruptDirEntry("htree root has no entries"))?;
3871        if leaf == 0 {
3872            return Err(Error::CorruptDirEntry(
3873                "htree routes a name to its own root",
3874            ));
3875        }
3876        let phys = self
3877            .map_inode_logical(parent_inode, u64::from(leaf))?
3878            .ok_or(Error::CorruptDirEntry("htree leaf is not mapped"))?;
3879        let block = buf.get_mut(self, phys)?;
3880        let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
3881            12
3882        } else {
3883            0
3884        };
3885        crate::dir::add_entry_to_block(block, target_ino, name, file_type, has_ft, reserved_tail)?;
3886        if self.csum.enabled && reserved_tail == 12 {
3887            self.csum
3888                .patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
3889        }
3890        Ok(())
3891    }
3892
3893    /// Refuse an htree root (`root`) or interior node whose `dx_tail`
3894    /// checksum does not match, before it is used or rewritten.
3895    ///
3896    /// Restamping a block, or routing a write by it, without this blessed
3897    /// whatever corruption it held with a fresh checksum (Greptile on #196).
3898    /// No-op without metadata_csum, or for a block with no tail to check.
3899    fn check_dx_block(&self, dir_ino: u32, dir: &Inode, block: &[u8], root: bool) -> Result<()> {
3900        let count_offset = if root {
3901            // `dx_root_info` starts at 24; its length is the byte at 29,
3902            // and the format fixes it at 8. Anything else is a root whose
3903            // count and limit this would read from the wrong place, so it
3904            // is refused before its checksum is trusted or it is written
3905            // through, as the kernel refuses it (CodeRabbit on #196).
3906            match block.get(29) {
3907                Some(8) => 32,
3908                _ => return Err(Error::Corrupt("htree root info_length is not 8")),
3909            }
3910        } else {
3911            8
3912        };
3913        match self
3914            .csum
3915            .verify_dx_tail(dir_ino, dir.generation, block, count_offset)
3916        {
3917            Some(false) => Err(Error::BadChecksum {
3918                what: "htree index block",
3919            }),
3920            _ => Ok(()),
3921        }
3922    }
3923
3924    /// Turn an indexed directory back into a linear one, which is what the
3925    /// kernel does when it cannot insert through the index: the format keeps
3926    /// every htree block readable as a linear directory block, so dropping
3927    /// `EXT4_INDEX_FL` leaves a valid directory.
3928    ///
3929    /// Every leaf is already an ordinary directory block, and so, read
3930    /// linearly, is the root (`.`, then a `..` spanning the rest) and each
3931    /// interior node (one unused record spanning the block). Only the
3932    /// inode's flag has to go. With metadata_csum a linear block must also
3933    /// end in a dirent tail, which those blocks do not, so the spanning
3934    /// record is shortened by twelve bytes and a tail written after it.
3935    ///
3936    /// The kernel refuses this on metadata_csum volumes because it cannot
3937    /// trust a broken index; here the index is intact and the tails are
3938    /// rebuilt.
3939    ///
3940    /// Staged into `buf`, the caller's open transaction, so the drop lands
3941    /// in the same commit as the create, link or rename that forced it
3942    /// (#347). It used to be written straight to the device after an early
3943    /// commit of `buf`, and a cut inside it left a half-converted index, or
3944    /// an inode the directory never gained a name for.
3945    fn buffer_drop_htree_index(&self, buf: &mut BlockBuffer, dir_ino: u32) -> Result<()> {
3946        let (inode, mut raw) = self.buffered_inode_verified(buf, dir_ino)?;
3947        if inode.flags & crate::inode::InodeFlags::INDEX.bits() == 0 {
3948            return Ok(());
3949        }
3950        let bs = self.sb.block_size() as usize;
3951        let physical = |logical: u64| {
3952            self.map_inode_logical(&inode, logical)?
3953                .ok_or(Error::CorruptDirEntry("htree block is not mapped"))
3954        };
3955
3956        // The root and every interior node, by physical block, each
3957        // checked before it is converted and re-tailed.
3958        let root_phys = physical(0)?;
3959        let root = self.buffered_block(buf, root_phys)?;
3960        self.check_dx_block(dir_ino, &inode, &root, true)?;
3961        let mut nodes = Vec::new();
3962        if let (Ok(info), Ok((_, entries))) = (
3963            crate::htree::parse_root_info(&root),
3964            crate::htree::parse_root_entries(&root),
3965        ) {
3966            let mut level: Vec<u32> = entries.iter().map(|e| e.block).collect();
3967            for _ in 0..info.indirect_levels {
3968                let mut next = Vec::new();
3969                for logical in level {
3970                    let phys = physical(u64::from(logical))?;
3971                    let block = self.buffered_block(buf, phys)?;
3972                    self.check_dx_block(dir_ino, &inode, &block, false)?;
3973                    let (_, entries) = crate::htree::parse_node_entries(&block)?;
3974                    next.extend(entries.iter().map(|e| e.block));
3975                    nodes.push(phys);
3976                }
3977                level = next;
3978            }
3979        }
3980
3981        if self.csum.enabled {
3982            // Each block, with the offset of the record that spans to its end.
3983            let spanning =
3984                std::iter::once((root_phys, 12)).chain(nodes.into_iter().map(|p| (p, 0)));
3985            for (phys, at) in spanning {
3986                let block = buf.get_mut(self, phys)?;
3987                block[at + 4..at + 6].copy_from_slice(&((bs - at - 12) as u16).to_le_bytes());
3988                self.csum
3989                    .patch_dir_entry_tail(dir_ino, inode.generation, block);
3990            }
3991        }
3992
3993        let flags = inode.flags & !crate::inode::InodeFlags::INDEX.bits();
3994        raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
3995        self.buffer_write_dir_inode(buf, dir_ino, inode.generation, &mut raw)
3996    }
3997
3998    /// Commit a `BlockBuffer` atomically. Routes through the journal
3999    /// writer when one is available (crash-safe four-fence protocol);
4000    /// falls back to direct device writes + flush otherwise.
4001    ///
4002    /// In journaled mode, writes go to the **journal log** on disk and
4003    /// are then checkpointed to their final locations before the journal
4004    /// writer's `commit` returns, so later reads — the allocators' bitmap
4005    /// scans included — see the committed bytes from the device or from
4006    /// the cache's clean, write-through entries. No block is pinned.
4007    pub(crate) fn commit_block_buffer(&self, buf: BlockBuffer) -> Result<()> {
4008        if buf.dirty.is_empty() {
4009            return Ok(());
4010        }
4011        let cleared = buf.uninit_cleared.clone();
4012        let publish = |fs: &Self| fs.uninit_cleared.lock().unwrap().publish(cleared);
4013        if let Some(jw_mu) = &self.journal {
4014            let mut jw = jw_mu.lock().map_err(|_| {
4015                Error::Corrupt("journal writer mutex poisoned (prior write panicked)")
4016            })?;
4017            let mut tx = jw.begin();
4018            for (block, bytes) in &buf.dirty {
4019                tx.add_write(*block, bytes.clone())?;
4020            }
4021            jw.commit(self.dev.as_ref(), &tx)?;
4022            // Nothing is pinned: `commit` checkpoints every block to its
4023            // final location before it returns, through this same cache,
4024            // whose write-through leaves each one a clean LRU entry that
4025            // matches the device. Pinning them as well held every block a
4026            // write had ever touched until unmount, whatever the capacity
4027            // (#328).
4028            publish(self);
4029            Ok(())
4030        } else {
4031            let descriptors: std::collections::BTreeSet<u64> = buf
4032                .uninit_cleared
4033                .keys()
4034                .map(|&gi| self.sb.descriptor_location(gi as u64).0)
4035                .collect();
4036            if let Err(e) = self.write_direct(buf.dirty, &descriptors) {
4037                // Part of the commit may be on disk and none of it is in
4038                // `uninit_cleared`: stop this mount writing (#319).
4039                self.direct_commit_failed
4040                    .store(true, std::sync::atomic::Ordering::SeqCst);
4041                return Err(e);
4042            }
4043            publish(self);
4044            Ok(())
4045        }
4046    }
4047
4048    /// Write a committed buffer straight to the device, in three flushed
4049    /// stages, for a mount with no journal.
4050    ///
4051    /// 1. **Everything else** -- bitmaps, inode tables, extent and directory
4052    ///    blocks, data.
4053    /// 2. **The descriptor blocks that clear an uninit flag** (`descriptors`).
4054    ///    The map iterates by block number and the descriptor table sits
4055    ///    near the front of the volume, so it went out before the bitmap it
4056    ///    vouches for. A failure in between left `BLOCK_UNINIT` down over a
4057    ///    bitmap still holding whatever the flag licensed leaving there, and
4058    ///    the next mount trusted it (#319). Written after the bitmaps, a
4059    ///    failure anywhere earlier leaves the flag standing, and the flag
4060    ///    tells every reader to ignore the bitmap.
4061    /// 3. **The superblock, last.** It carries the markers that say work is
4062    ///    finished -- `s_last_orphan` cleared, the free counts credited --
4063    ///    and block 0 came first. A crash after it and before the inode
4064    ///    table left an orphan still allocated, still holding its blocks,
4065    ///    and named by nothing, so no later mount would retry it (#124).
4066    ///    Written last, a crash anywhere before it leaves the old
4067    ///    superblock, whose chain head sends the next mount back to finish.
4068    fn write_direct(
4069        &self,
4070        mut dirty: std::collections::BTreeMap<u64, Vec<u8>>,
4071        descriptors: &std::collections::BTreeSet<u64>,
4072    ) -> Result<()> {
4073        let bs = self.sb.block_size() as u64;
4074        let sb_block = crate::superblock::SUPERBLOCK_OFFSET / bs;
4075        let superblock = dirty.remove(&sb_block);
4076        let (late, early): (Vec<_>, Vec<_>) = dirty
4077            .into_iter()
4078            .partition(|(block, _)| descriptors.contains(block));
4079        for stage in [
4080            early,
4081            late,
4082            superblock.map(|b| (sb_block, b)).into_iter().collect(),
4083        ] {
4084            if stage.is_empty() {
4085                continue;
4086            }
4087            for (block, bytes) in stage {
4088                self.dev.write_at(block * bs, &bytes)?;
4089            }
4090            self.dev.flush()?;
4091        }
4092        Ok(())
4093    }
4094
4095    /// Change the owner of `path` to (`uid`, `gid`). Both values are full
4096    /// 32-bit — the inode stores them as hi+lo u16 halves at different
4097    /// offsets per the ext4 on-disk format. Passing `u32::MAX` for either
4098    /// field leaves that value untouched (Linux lchown(2) convention).
4099    ///
4100    /// Updates `i_ctime = now` and recomputes the inode checksum on
4101    /// csum-enabled mounts.
4102    pub fn apply_chown(&self, path: &str, uid: u32, gid: u32) -> Result<()> {
4103        self.apply_chown_bytes(path.as_bytes(), uid, gid)
4104    }
4105
4106    /// [`apply_chown`](Self::apply_chown) of a path given as bytes, never decoded.
4107    pub(crate) fn apply_chown_bytes(&self, path: &[u8], uid: u32, gid: u32) -> Result<()> {
4108        self.refuse_write()?;
4109        let ino = self.resolve(path)?;
4110        self.apply_chown_ino(ino, uid, gid)
4111    }
4112
4113    /// [`apply_chown`](Self::apply_chown) on the inode `r` names.
4114    pub fn apply_chown_ino(&self, r: impl Into<InodeRef>, uid: u32, gid: u32) -> Result<()> {
4115        self.refuse_write()?;
4116        let r = r.into();
4117        let ino = r.ino;
4118        let (inode, mut raw) = self.live_inode(r)?;
4119
4120        if uid != u32::MAX {
4121            let lo = (uid & 0xFFFF) as u16;
4122            let hi = ((uid >> 16) & 0xFFFF) as u16;
4123            raw[0x02..0x04].copy_from_slice(&lo.to_le_bytes());
4124            raw[0x78..0x7A].copy_from_slice(&hi.to_le_bytes());
4125        }
4126        if gid != u32::MAX {
4127            let lo = (gid & 0xFFFF) as u16;
4128            let hi = ((gid >> 16) & 0xFFFF) as u16;
4129            raw[0x18..0x1A].copy_from_slice(&lo.to_le_bytes());
4130            raw[0x7A..0x7C].copy_from_slice(&hi.to_le_bytes());
4131        }
4132
4133        let now = self.runtime.now_unix_seconds();
4134        set_inode_time(&mut raw, InodeTime::Ctime, now);
4135
4136        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
4137        self.commit_inode_write(ino, &raw)
4138    }
4139
4140    /// Set the `i_flags` field (FS_IOC_SETFLAGS) for the inode at `path`.
4141    ///
4142    /// Bumps ctime. Fails with `Error::ReadOnly` on read-only mounts, or
4143    /// `Error::InvalidArgument` if `flags` changes any bit outside
4144    /// [`crate::inode::USER_MODIFIABLE_FLAGS`] — `INDEX`, `EXTENTS`,
4145    /// `INLINE_DATA`, `HUGE_FILE`, `ENCRYPT`, `VERITY`, `CASEFOLD` and the
4146    /// like describe how the inode's existing bytes are read, so flipping
4147    /// one without rewriting them would corrupt the file. Bits outside the
4148    /// mask that are already set may be passed back unchanged.
4149    pub fn apply_set_flags(&self, path: &str, flags: u32) -> Result<()> {
4150        self.apply_set_flags_bytes(path.as_bytes(), flags)
4151    }
4152
4153    /// [`apply_set_flags`](Self::apply_set_flags) of a path given as bytes, never decoded.
4154    pub(crate) fn apply_set_flags_bytes(&self, path: &[u8], flags: u32) -> Result<()> {
4155        use crate::inode::{OFF_FLAGS, USER_MODIFIABLE_FLAGS};
4156        self.refuse_write()?;
4157        let ino = self.resolve(path)?;
4158        let (inode, mut raw) = self.read_inode_verified(ino)?;
4159
4160        if (flags ^ inode.flags) & !USER_MODIFIABLE_FLAGS != 0 {
4161            return Err(Error::InvalidArgument(
4162                "set_flags: only the user-modifiable inode flags may change \
4163                 (INDEX, EXTENTS, INLINE_DATA, HUGE_FILE, ENCRYPT, VERITY, CASEFOLD and the like are managed)",
4164            ));
4165        }
4166
4167        raw[OFF_FLAGS..OFF_FLAGS + 4].copy_from_slice(&flags.to_le_bytes());
4168
4169        let now = self.runtime.now_unix_seconds();
4170        set_inode_time(&mut raw, InodeTime::Ctime, now);
4171
4172        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
4173        self.commit_inode_write(ino, &raw)
4174    }
4175
4176    /// Remove the extended attribute named `name` from the inode at `path`.
4177    /// `name` must carry a known namespace prefix (e.g. `"user.color"`).
4178    ///
4179    /// v1 scope: **in-inode xattrs only.** The in-inode region (bytes
4180    /// between `128 + i_extra_isize` and the end of the on-disk inode)
4181    /// is decoded, the matching entry is dropped, and the region is
4182    /// re-encoded in place. External xattr blocks (pointed at by
4183    /// Search the in-inode region first, then the external xattr block. If
4184    /// the external block becomes empty after removal, free it and zero
4185    /// `i_file_acl` (matches kernel behavior — empty xattr blocks are
4186    /// reaped on the spot rather than left dangling).
4187    ///
4188    /// Returns:
4189    /// - `Ok(())` on success.
4190    /// - `Error::NotFound` if the entry isn't present in either region.
4191    /// - `Error::InvalidArgument` on namespace-prefix issues.
4192    pub fn apply_removexattr(&self, path: &str, name: &str) -> Result<()> {
4193        self.apply_removexattr_bytes(path.as_bytes(), name)
4194    }
4195
4196    /// [`apply_removexattr`](Self::apply_removexattr) of a path given as bytes, never decoded.
4197    pub(crate) fn apply_removexattr_bytes(&self, path: &[u8], name: &str) -> Result<()> {
4198        self.refuse_write()?;
4199        let ino = self.resolve(path)?;
4200        let (inode, mut raw) = self.read_inode_verified(ino)?;
4201
4202        // Locate the in-inode xattr region (starts at 128 + i_extra_isize).
4203        let inode_size = self.sb.inode_size as usize;
4204        let i_extra_isize = if raw.len() >= 0x82 {
4205            u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap()) as usize
4206        } else {
4207            0
4208        };
4209        let region_start = 128 + i_extra_isize;
4210        let region_end = inode_size.min(raw.len());
4211
4212        // FROM BOTH PLACES. A name lives in the inode or in the external
4213        // block, and a set that moved it used to leave a copy in the other
4214        // (#377); removing only the first copy found then brought the
4215        // stale one back. Every copy goes, in one transaction.
4216        let mut found = false;
4217        // `i_extra_isize = 0` means no in-inode area at all: the kernel
4218        // does not parse one there (#380).
4219        if i_extra_isize != 0 && region_start + 4 <= region_end {
4220            let region = &mut raw[region_start..region_end];
4221            if crate::xattr::plan_remove_in_inode_region(region, name)?
4222                == crate::xattr::RemoveOutcome::Removed
4223            {
4224                found = true;
4225            }
4226        }
4227        let mut buf = BlockBuffer::new(self.sb.block_size());
4228        if self.buffer_remove_from_external_block(&mut buf, ino, &inode, &mut raw, name)? {
4229            found = true;
4230        }
4231        if !found {
4232            return Err(Error::NotFound);
4233        }
4234        // POSIX stamps ctime on an attribute write. It goes through
4235        // set_inode_time so the epoch bits are written too: the clock is
4236        // an i64 since #324, and its low four bytes alone are a time that
4237        // reads back as 1901 from 2038 (#386's neighbour, #324).
4238        set_inode_time(&mut raw, InodeTime::Ctime, self.runtime.now_unix_seconds());
4239        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
4240        self.buffer_write_inode(&mut buf, ino, &raw)?;
4241        self.commit_block_buffer(buf)
4242    }
4243
4244    /// Remove `name` from `ino`'s external xattr block, staged in `buf`.
4245    /// Returns whether the name was there.
4246    ///
4247    /// `raw` is the inode image the caller will write in the same
4248    /// transaction: when the block is shared it gets a block of its own
4249    /// (`i_file_acl` moves), and when the block empties it is released
4250    /// (`i_file_acl` cleared, `i_blocks` one block lower). The caller
4251    /// re-checksums and stages the inode.
4252    fn buffer_remove_from_external_block(
4253        &self,
4254        buf: &mut BlockBuffer,
4255        ino: u32,
4256        inode: &crate::inode::Inode,
4257        raw: &mut [u8],
4258        name: &str,
4259    ) -> Result<bool> {
4260        if inode.file_acl == 0 {
4261            return Ok(false);
4262        }
4263        let block_nr = inode.file_acl;
4264        let mut block = self.buffer_read_xattr_block(buf, block_nr)?;
4265        let refs = u32::from_le_bytes(block[4..8].try_into().unwrap());
4266        match crate::xattr::plan_remove_from_external_block(&mut block, name, 1)? {
4267            crate::xattr::BlockRemoveOutcome::NotFound => Ok(false),
4268            crate::xattr::BlockRemoveOutcome::Removed if refs > 1 => {
4269                // Shared: this inode's remaining attributes move to a
4270                // block of its own, and the others keep the old one.
4271                let new_nr = self.buffer_unshare_xattr_block(buf, ino, block_nr, block)?;
4272                Self::write_file_acl(raw, new_nr)?;
4273                Ok(true)
4274            }
4275            crate::xattr::BlockRemoveOutcome::Removed => {
4276                if self.csum.enabled {
4277                    self.csum.patch_xattr_block(block_nr, &mut block);
4278                }
4279                buf.put(block_nr, block);
4280                Ok(true)
4281            }
4282            crate::xattr::BlockRemoveOutcome::RemovedNowEmpty => {
4283                // Freed only if no other inode shares it. The bitmap, the
4284                // descriptor and the superblock count move in the same
4285                // transaction as the inode that stops pointing at it.
4286                let freed = self.buffer_release_xattr_block(buf, block_nr)?;
4287                self.buffer_patch_sb_counters(buf, freed as i64, 0)?;
4288                // Both halves, at the offsets the reader uses.
4289                Self::write_file_acl(raw, 0)?;
4290                let sectors_per_block = self.sb.block_size() as u64 / 512;
4291                let new_blocks = inode.blocks.saturating_sub(sectors_per_block);
4292                Self::patch_inode_size_and_blocks(raw, inode.size, new_blocks)?;
4293                Ok(true)
4294            }
4295        }
4296    }
4297
4298    /// Set (create or replace) the extended attribute `name` with `value`
4299    /// on the inode at `path`. `name` must carry a known namespace prefix
4300    /// (e.g. `"user.com.apple.FinderInfo"`).
4301    ///
4302    /// Try-order, matching the kernel:
4303    /// 1. **In-inode region** — between `128 + i_extra_isize` and the end
4304    ///    of the on-disk inode. Cheapest; no extra block.
4305    /// 2. **External xattr block** — when in-inode is full, fall back to a
4306    ///    dedicated block referenced by `i_file_acl`. Allocates a fresh
4307    ///    block when none exists, otherwise rewrites the existing one.
4308    ///    Returns `Error::NoSpaceLeftOnDevice` if even a full block can't
4309    ///    hold the new layout.
4310    pub fn apply_setxattr(&self, path: &str, name: &str, value: &[u8]) -> Result<()> {
4311        self.apply_setxattr_bytes(path.as_bytes(), name, value)
4312    }
4313
4314    /// [`apply_setxattr`](Self::apply_setxattr) of a path given as bytes, never decoded.
4315    pub(crate) fn apply_setxattr_bytes(&self, path: &[u8], name: &str, value: &[u8]) -> Result<()> {
4316        self.refuse_write()?;
4317        let ino = self.resolve(path)?;
4318        let (inode, mut raw) = self.read_inode_verified(ino)?;
4319
4320        let inode_size = self.sb.inode_size as usize;
4321        let mut i_extra_isize = if raw.len() >= 0x82 {
4322            u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap()) as usize
4323        } else {
4324            0
4325        };
4326        // NO AREA AT 0x80 (#380). `i_extra_isize = 0` -- `ext2.ko`, older
4327        // kernels, 256-byte-inode ext2 volumes -- is an inode whose extra
4328        // fields are unused, and the kernel parses no in-inode xattr area
4329        // there: it reads 0x80.. as `i_extra_isize`, `i_checksum_hi` and the
4330        // `i_*_extra` words. An area written at 0x80 was invisible to it,
4331        // and on `metadata_csum` the checksum's high half was then stored
4332        // over the area's magic. The kernel gives such an inode its extra
4333        // fields, zeroed, before it writes an attribute; so does this, in
4334        // the same write.
4335        let extra = crate::inode::EXTRA_ISIZE_DEFAULT as usize;
4336        if i_extra_isize == 0 && inode_size.min(raw.len()) >= 128 + extra {
4337            raw[0x80..0x80 + extra].fill(0);
4338            write_inode_extra_isize(&mut raw);
4339            i_extra_isize = extra;
4340        }
4341        let region_start = 128 + i_extra_isize;
4342        let region_end = inode_size.min(raw.len());
4343        let inline_capable = i_extra_isize != 0 && region_start + 8 <= region_end;
4344
4345        // Try in-inode first; on overflow fall through to the external block.
4346        let inline_result = if inline_capable {
4347            let region = &mut raw[region_start..region_end];
4348            crate::xattr::plan_set_in_inode_region(region, name, value)
4349        } else {
4350            Err(Error::NoSpaceLeftOnDevice)
4351        };
4352
4353        // ONE COPY, WHEREVER IT LANDS (#377). The name may already live in
4354        // the other place: a value that grew past the inode, or shrank back
4355        // into it. That copy is removed in the same transaction, as the
4356        // kernel removes it (debugfs lists one copy afterwards,
4357        // tests/xattr_one_copy_oracle.rs); left behind, the reader
4358        // returned whichever copy it met first -- the in-inode one, stale
4359        // or not -- and a remove brought the other back.
4360        match inline_result {
4361            Ok(_) => {
4362                // In-inode rewrite already in `raw`.
4363                let mut buf = BlockBuffer::new(self.sb.block_size());
4364                if self.buffer_remove_from_external_block(&mut buf, ino, &inode, &mut raw, name)? {
4365                    set_inode_time(&mut raw, InodeTime::Ctime, self.runtime.now_unix_seconds());
4366                }
4367                self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
4368                self.buffer_write_inode(&mut buf, ino, &raw)?;
4369                self.commit_block_buffer(buf)
4370            }
4371            Err(Error::NoSpaceLeftOnDevice) => {
4372                // The set never re-encoded the region, so an old in-inode
4373                // copy is still there; it goes before the block gets the
4374                // new one, and `raw` carries that into the same commit.
4375                if inline_capable {
4376                    let region = &mut raw[region_start..region_end];
4377                    crate::xattr::plan_remove_in_inode_region(region, name)?;
4378                }
4379                self.apply_setxattr_external_block(ino, &inode, &mut raw, name, value)
4380            }
4381            Err(e) => Err(e),
4382        }
4383    }
4384
4385    /// Recompute the inode checksum (when enabled) and splice both halves
4386    /// back into the inode image. No-op when csum disabled.
4387    pub(crate) fn finalize_inode_raw(
4388        &self,
4389        ino: u32,
4390        generation: u32,
4391        raw: &mut [u8],
4392    ) -> Result<()> {
4393        if self.csum.enabled {
4394            self.csum.patch_inode_checksum(ino, generation, raw);
4395        }
4396        Ok(())
4397    }
4398
4399    /// Helper: route a setxattr that overflowed the in-inode region to the
4400    /// external xattr block. Either rewrites the existing block (when
4401    /// `i_file_acl != 0`) or allocates a fresh one.
4402    fn apply_setxattr_external_block(
4403        &self,
4404        ino: u32,
4405        inode: &crate::inode::Inode,
4406        raw: &mut [u8],
4407        name: &str,
4408        value: &[u8],
4409    ) -> Result<()> {
4410        let bs = self.sb.block_size();
4411        let bs_u64 = bs as u64;
4412
4413        // Multi-block transaction: xattr block bytes + (alloc-side bitmap +
4414        // BGD + SB when fresh-block) + inode body. Atomic across the op.
4415        let mut buf = BlockBuffer::new(bs);
4416        // An xattr block on a volume without COMPAT_EXT_ATTR is one e2fsck
4417        // ignores: it clears i_file_acl and frees the block. The kernel sets
4418        // the feature when it writes the first xattr; so does this, in the
4419        // same transaction (#88). This crate's
4420        // mkfs does not set it.
4421        if self.sb.feature_compat & features::Compat::EXT_ATTR.bits() == 0 {
4422            self.buffer_patch_sb_compat(&mut buf, features::Compat::EXT_ATTR.bits())?;
4423        }
4424
4425        // Path A: existing external block — rewrite in-buffer, re-checksum.
4426        if inode.file_acl != 0 {
4427            let block_nr = inode.file_acl;
4428            let mut block = self.buffer_read_xattr_block(&mut buf, block_nr)?;
4429            let refs = u32::from_le_bytes(block[4..8].try_into().unwrap());
4430            crate::xattr::plan_set_in_external_block(&mut block, name, value, 1)?;
4431            if refs > 1 {
4432                // Shared: the edit is this inode's alone, so it gets its
4433                // own block. One block before and after, so i_blocks holds.
4434                let new_nr = self.buffer_unshare_xattr_block(&mut buf, ino, block_nr, block)?;
4435                Self::write_file_acl(raw, new_nr)?;
4436            } else {
4437                if self.csum.enabled {
4438                    self.csum.patch_xattr_block(block_nr, &mut block);
4439                }
4440                buf.put(block_nr, block);
4441            }
4442            // i_blocks unchanged — only need to bump ctime.
4443            let now = self.runtime.now_unix_seconds();
4444            set_inode_time(raw, InodeTime::Ctime, now);
4445            self.finalize_inode_raw(ino, inode.generation, raw)?;
4446            self.buffer_write_inode(&mut buf, ino, raw)?;
4447            return self.commit_block_buffer(buf);
4448        }
4449
4450        // Path B: no external block yet — allocate, build, stage, then
4451        // point i_file_acl + i_blocks at it.
4452        let mut bitmap_reader = |block: u64| self.read_block(block);
4453        let inode_group = (ino - 1) / self.sb.inodes_per_group;
4454        let plan = crate::alloc::plan_block_allocation(
4455            &self.sb,
4456            &self.allocation_groups(),
4457            1,
4458            inode_group,
4459            &mut bitmap_reader,
4460        )?;
4461        let block_nr = plan.first_block;
4462
4463        let mut block = vec![0u8; bs as usize];
4464        crate::xattr::plan_set_in_external_block(&mut block, name, value, 1)?;
4465        if self.csum.enabled {
4466            self.csum.patch_xattr_block(block_nr, &mut block);
4467        }
4468        buf.put(block_nr, block);
4469
4470        // Stage allocator side-effects in the buffer.
4471        self.buffer_mark_block_run_used(&mut buf, block_nr, 1)?;
4472        self.buffer_patch_bgd_counters(
4473            &mut buf,
4474            plan.bgd.group_idx as usize,
4475            plan.bgd.free_blocks_delta,
4476            plan.bgd.free_inodes_delta,
4477            plan.bgd.used_dirs_delta,
4478        )?;
4479        self.buffer_patch_sb_counters(
4480            &mut buf,
4481            plan.sb.free_blocks_delta,
4482            plan.sb.free_inodes_delta,
4483        )?;
4484
4485        // Splice block_nr into the inode: i_file_acl_lo at 0x68..0x6C, hi at
4486        // 0x76..0x78. The comment here used to say 0x74, and so did the
4487        // code, so a reader checking one against the other agreed.
4488        Self::write_file_acl(raw, block_nr)?;
4489        // Bump i_blocks by sectors_per_block (the xattr block now belongs
4490        // to this inode for du purposes).
4491        let sectors_per_block = bs_u64 / 512;
4492        let new_blocks = inode.blocks.saturating_add(sectors_per_block);
4493        Self::patch_inode_size_and_blocks(raw, inode.size, new_blocks)?;
4494        let now = self.runtime.now_unix_seconds();
4495        set_inode_time(raw, InodeTime::Ctime, now);
4496        self.finalize_inode_raw(ino, inode.generation, raw)?;
4497        self.buffer_write_inode(&mut buf, ino, raw)?;
4498
4499        self.commit_block_buffer(buf)
4500    }
4501
4502    /// Allocate one block near `ino`'s group, staged in `buf` with its
4503    /// bitmap, descriptor and superblock counts.
4504    ///
4505    /// Planned through the buffer, uninit clears included: a punch's tree
4506    /// repack calls this more than once in one transaction, and a second
4507    /// plan that still saw the group as BLOCK_UNINIT rebuilt its bitmap from
4508    /// metadata and handed out the block the first call staged (#291).
4509    fn buffer_allocate_block(&self, buf: &mut BlockBuffer, ino: u32) -> Result<u64> {
4510        let plan =
4511            self.plan_buffered_block_allocation(buf, 1, (ino - 1) / self.sb.inodes_per_group)?;
4512        self.buffer_mark_block_run_used(buf, plan.first_block, 1)?;
4513        self.buffer_patch_bgd_counters(
4514            buf,
4515            plan.bgd.group_idx as usize,
4516            plan.bgd.free_blocks_delta,
4517            plan.bgd.free_inodes_delta,
4518            plan.bgd.used_dirs_delta,
4519        )?;
4520        self.buffer_patch_sb_counters(buf, plan.sb.free_blocks_delta, plan.sb.free_inodes_delta)?;
4521        Ok(plan.first_block)
4522    }
4523
4524    /// Drop one inode's reference to the external xattr block at `block_nr`.
4525    ///
4526    /// THE BLOCK MAY NOT BE THIS INODE'S ALONE. The kernel keeps one block
4527    /// for every inode with an identical attribute set and counts them in
4528    /// `h_refcount`, so files with the same ACL or security label share
4529    /// one. At a count of one the block is freed. Above one, the count is
4530    /// written back one lower and the block stays where the other inodes
4531    /// point. Returns the blocks freed, 0 or 1; the caller credits the
4532    /// superblock.
4533    pub(crate) fn buffer_release_xattr_block(
4534        &self,
4535        buf: &mut BlockBuffer,
4536        block_nr: u64,
4537    ) -> Result<u64> {
4538        let mut block = self.buffer_read_xattr_block(buf, block_nr)?;
4539        let refs = u32::from_le_bytes(block[4..8].try_into().unwrap());
4540        if refs <= 1 {
4541            return self.buffer_free_block_run_and_bgd(buf, block_nr, 1);
4542        }
4543        block[4..8].copy_from_slice(&(refs - 1).to_le_bytes());
4544        if self.csum.enabled {
4545            self.csum.patch_xattr_block(block_nr, &mut block);
4546        }
4547        buf.put(block_nr, block);
4548        Ok(0)
4549    }
4550
4551    /// The external xattr block at `block_nr`, read for an edit through
4552    /// `buf`, and refused when it is not one.
4553    ///
4554    /// NOTHING IS WRITTEN OVER A BLOCK THAT HAS NOT BEEN CHECKED (#378).
4555    /// Every edit used to read the block named by `i_file_acl` and go
4556    /// ahead: a block without the magic was formatted as an empty xattr
4557    /// block over whatever it held -- another file's data, a directory --
4558    /// and a block whose checksum failed was edited and restamped, which
4559    /// blessed the corruption. `h_refcount` was decremented on the same
4560    /// trust. The kernel refuses all of them with EUCLEAN, and so does
4561    /// this (e2fsck agrees in tests/xattr_block_checked_oracle.rs): see
4562    /// [`crate::xattr::check_external_block`].
4563    fn buffer_read_xattr_block(&self, buf: &mut BlockBuffer, block_nr: u64) -> Result<Vec<u8>> {
4564        let block = buf.get_mut(self, block_nr)?.clone();
4565        crate::xattr::check_external_block(&self.csum, block_nr, &block)?;
4566        Ok(block)
4567    }
4568
4569    /// Give `ino` its own copy of a shared xattr block holding `block`'s
4570    /// edited contents: a new block for this inode, and one reference fewer
4571    /// on the old one. Returns the new block's number.
4572    fn buffer_unshare_xattr_block(
4573        &self,
4574        buf: &mut BlockBuffer,
4575        ino: u32,
4576        old_nr: u64,
4577        mut block: Vec<u8>,
4578    ) -> Result<u64> {
4579        let new_nr = self.buffer_allocate_block(buf, ino)?;
4580        block[4..8].copy_from_slice(&1u32.to_le_bytes());
4581        if self.csum.enabled {
4582            self.csum.patch_xattr_block(new_nr, &mut block);
4583        }
4584        buf.put(new_nr, block);
4585        self.buffer_release_xattr_block(buf, old_nr)?;
4586        Ok(new_nr)
4587    }
4588
4589    /// `i_dtime` for an inode deleted now. It has no `*_extra` word and
4590    /// so no epoch bits: like the kernel, store the low 32 bits of the
4591    /// clock, unsigned.
4592    fn dtime_now(&self) -> u32 {
4593        self.runtime.now_unix_seconds() as u32
4594    }
4595
4596    /// Set the access + modification times on `path`. Mirrors POSIX
4597    /// `utimensat(2)`: `atime_sec/nsec` and `mtime_sec/nsec` each replace
4598    /// the inode's atime/mtime. `ctime` is bumped to now (POSIX requires
4599    /// the change-time stamp on any attribute write). The [`TIME_OMIT`]
4600    /// sentinel on either `_sec` leaves that pair unchanged (lets callers
4601    /// touch just atime or just mtime).
4602    ///
4603    /// Each `_nsec` also takes `utimensat(2)`'s sentinels: [`UTIME_OMIT`]
4604    /// leaves that pair unchanged and [`UTIME_NOW`] sets it to the
4605    /// mount's current time, the `_sec` beside either being ignored. When
4606    /// both pairs are omitted nothing is written, ctime included, as on
4607    /// Linux. Any other `_nsec` of 1e9 or more is `InvalidArgument`,
4608    /// refused before anything is written.
4609    ///
4610    /// Seconds are signed and 64-bit because that is what the format
4611    /// means: the on-disk base is a signed 32-bit count, extended by the
4612    /// low two bits of the matching `*_extra` field. A `u32` here could
4613    /// not express a pre-1970 date at all, and stored every date past
4614    /// 2038 as one in the 1900s — the base was written and the epoch
4615    /// bits left zero, so the value read back 136 years early.
4616    ///
4617    /// `nsec` values are the sub-second timestamp in nanoseconds and are
4618    /// only written when the inode's `i_extra_isize` region is large
4619    /// enough to hold them (requires ≥ 160-byte inodes — the ext4 tooling
4620    /// default). That same region holds the epoch bits, so on an inode
4621    /// too small to carry it, a time needing them is refused rather than
4622    /// silently stored as the wrong century.
4623    pub fn apply_utimens(
4624        &self,
4625        path: &str,
4626        atime_sec: i64,
4627        atime_nsec: u32,
4628        mtime_sec: i64,
4629        mtime_nsec: u32,
4630    ) -> Result<()> {
4631        self.apply_utimens_bytes(
4632            path.as_bytes(),
4633            atime_sec,
4634            atime_nsec,
4635            mtime_sec,
4636            mtime_nsec,
4637        )
4638    }
4639
4640    /// [`apply_utimens`](Self::apply_utimens) of a path given as bytes, never decoded.
4641    pub(crate) fn apply_utimens_bytes(
4642        &self,
4643        path: &[u8],
4644        atime_sec: i64,
4645        atime_nsec: u32,
4646        mtime_sec: i64,
4647        mtime_nsec: u32,
4648    ) -> Result<()> {
4649        self.refuse_write()?;
4650        // Before resolving, so an unstorable time or nanosecond count is
4651        // refused as EINVAL whether or not the path exists.
4652        TimeUpdate::check(atime_sec, atime_nsec)?;
4653        TimeUpdate::check(mtime_sec, mtime_nsec)?;
4654        let ino = self.resolve(path)?;
4655        self.apply_utimens_ino(ino, atime_sec, atime_nsec, mtime_sec, mtime_nsec)
4656    }
4657
4658    /// [`apply_utimens`](Self::apply_utimens) on the inode `r` names.
4659    pub fn apply_utimens_ino(
4660        &self,
4661        r: impl Into<InodeRef>,
4662        atime_sec: i64,
4663        atime_nsec: u32,
4664        mtime_sec: i64,
4665        mtime_nsec: u32,
4666    ) -> Result<()> {
4667        self.refuse_write()?;
4668        // One reading of the clock serves UTIME_NOW and the ctime bump.
4669        let now = self.runtime.now_unix_seconds();
4670        let atime = TimeUpdate::resolve(atime_sec, atime_nsec, now)?;
4671        let mtime = TimeUpdate::resolve(mtime_sec, mtime_nsec, now)?;
4672        let r = r.into();
4673        let ino = r.ino;
4674        let (inode, mut raw) = self.live_inode(r)?;
4675
4676        if let (TimeUpdate::Omit, TimeUpdate::Omit) = (atime, mtime) {
4677            return Ok(());
4678        }
4679
4680        // Extra-isize region carries the nsec fields AND the epoch bits.
4681        // Offsets (relative to inode start):
4682        //   0x84 i_ctime_extra  (needs i_extra_isize ≥  8)
4683        //   0x88 i_mtime_extra  (needs i_extra_isize ≥ 12)
4684        //   0x8C i_atime_extra  (needs i_extra_isize ≥ 16)
4685        // Linux packs each as `(nsec << 2) | epoch_bits`.
4686        let i_extra_isize = if raw.len() >= 0x82 {
4687            u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap())
4688        } else {
4689            0
4690        };
4691        let has_mtime_extra = i_extra_isize >= 12 && raw.len() >= 0x8C;
4692        let has_atime_extra = i_extra_isize >= 16 && raw.len() >= 0x90;
4693
4694        // (base offset, extra offset if the inode has room for it)
4695        let fields = [
4696            (atime, 0x08, has_atime_extra.then_some(0x8C)),
4697            (mtime, 0x10, has_mtime_extra.then_some(0x88)),
4698        ];
4699
4700        // Refuse before writing anything, so a rejected call leaves the
4701        // inode exactly as it was rather than half-updated.
4702        for (update, _, extra) in fields {
4703            if let TimeUpdate::Set(sec, _) = update {
4704                if crate::inode::pack_seconds(sec).1 != 0 && extra.is_none() {
4705                    return Err(Error::InvalidArgument(
4706                        "timestamp past 2038 needs an *_extra field this inode is too small to hold",
4707                    ));
4708                }
4709            }
4710        }
4711
4712        for (update, base_off, extra) in fields {
4713            let TimeUpdate::Set(sec, nsec) = update else {
4714                continue;
4715            };
4716            let (base, epoch) = crate::inode::pack_seconds(sec);
4717            raw[base_off..base_off + 4].copy_from_slice(&base.to_le_bytes());
4718            if let Some(off) = extra {
4719                let packed = pack_nsec_lo(nsec) | epoch;
4720                raw[off..off + 4].copy_from_slice(&packed.to_le_bytes());
4721            }
4722        }
4723
4724        // POSIX: any attribute write bumps ctime.
4725        // Its nanoseconds go to zero with it: `now` is whole seconds.
4726        set_inode_time(&mut raw, InodeTime::Ctime, now);
4727
4728        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
4729        self.commit_inode_write(ino, &raw)
4730    }
4731
4732    /// Unlink a regular file / symlink / special file at `path`.
4733    ///
4734    /// Semantics:
4735    /// - Refuses to unlink a directory (use a future `apply_rmdir`).
4736    /// - Decrements the target inode's `i_links_count`. When that reaches
4737    ///   zero, frees every data block via `plan_truncate_shrink(size → 0)`,
4738    ///   clears the inode bitmap bit, zeroes the inode body, and sets
4739    ///   `i_dtime = now`. When `links_count > 1` we only drop the dir entry
4740    ///   and decrement — matches POSIX unlink semantics for hard-linked files.
4741    /// - Mutates: parent-dir block (entry removal), target inode, block +
4742    ///   inode bitmaps, BGD counters, SB counters. No journaling yet —
4743    ///   safe only on scratch images (same caveat as `apply_truncate_shrink`).
4744    ///
4745    /// Returns `Error::NotFound` if the path doesn't exist,
4746    /// `Error::NotADirectory` if the parent isn't a directory, and
4747    /// `Error::IsADirectory` (POSIX EISDIR) if the target is a directory.
4748    pub fn apply_unlink(&self, path: &str) -> Result<()> {
4749        self.apply_unlink_bytes(path.as_bytes())
4750    }
4751
4752    /// [`apply_unlink`](Self::apply_unlink) of a path given as bytes, never decoded.
4753    pub(crate) fn apply_unlink_bytes(&self, path: &[u8]) -> Result<()> {
4754        self.refuse_write()?;
4755        // POSIX: a trailing slash asserts the path refers to a directory,
4756        // which is incompatible with `unlink(2)` no matter what kind of file
4757        // the path resolves to. `split_parent_and_base` swallows the slash,
4758        // so snapshot the flag first.
4759        let trailing_slash = path.len() > 1 && path.ends_with(b"/");
4760        let (parent_path, base_name) = split_parent_and_base(path)?;
4761        let parent_ino = self.resolve(parent_path)?;
4762        if trailing_slash {
4763            // The call fails either way; only the errno depends on what the
4764            // name is: a directory is EISDIR, as unlink(2) says of any
4765            // directory, and anything else ENOTDIR, the slash having
4766            // asserted a directory.
4767            let (parent_inode, _) = self.live_dir(parent_ino.into())?;
4768            let target_ino = self.find_entry_in_dir(parent_ino, &parent_inode, base_name)?;
4769            let (target_inode, _) = self.live_inode(target_ino.into())?;
4770            return Err(if target_inode.is_dir() {
4771                Error::IsADirectory
4772            } else {
4773                Error::NotADirectory
4774            });
4775        }
4776        self.apply_unlink_at(parent_ino, base_name)
4777    }
4778
4779    /// [`apply_unlink`](Self::apply_unlink) of entry `name` in directory
4780    /// `dir`.
4781    pub fn apply_unlink_at(&self, dir: impl Into<InodeRef>, name: &[u8]) -> Result<()> {
4782        self.refuse_write()?;
4783        check_entry_name(name)?;
4784        let dir = dir.into();
4785        let parent_ino_num = dir.ino;
4786        let (parent_inode, _parent_raw) = self.live_dir(dir)?;
4787
4788        let target_ino = self.find_entry_in_dir(parent_ino_num, &parent_inode, name)?;
4789        let (target_inode, mut target_raw) = self.live_inode(target_ino.into())?;
4790        if target_inode.is_dir() {
4791            // POSIX: unlink(2) on a directory must fail with EISDIR; the
4792            // caller should use rmdir(2) instead.
4793            return Err(Error::IsADirectory);
4794        }
4795
4796        // All mutations land in this buffer and commit as one transaction.
4797        let mut buf = BlockBuffer::new(self.sb.block_size());
4798
4799        // Remove the dir entry from the parent: a block directory's first
4800        // block holding it, or an inline directory's inode (#428).
4801        let bs = self.sb.block_size();
4802        self.buffer_remove_dir_entry(&mut buf, parent_ino_num, &parent_inode, name)?;
4803
4804        // Decrement link count. Non-zero after → just persist the new count.
4805        let new_links = target_inode.links_count.saturating_sub(1);
4806        target_raw[0x1A..0x1C].copy_from_slice(&new_links.to_le_bytes());
4807
4808        if new_links > 0 {
4809            self.finalize_inode_raw(target_ino, target_inode.generation, &mut target_raw)?;
4810            self.buffer_write_inode(&mut buf, target_ino, &target_raw)?;
4811            return self.commit_block_buffer(buf);
4812        }
4813
4814        // Last link gone — free data blocks + inode slot, all into the same
4815        // transaction so a crash either keeps everything or undoes everything.
4816        let mut freed_sectors: u64 = 0;
4817        let sectors_per_block = bs as u64 / 512;
4818        if target_inode.has_extents() {
4819            let runs = self.extent_tree_runs(target_ino, &target_inode)?;
4820            freed_sectors += self.buffer_free_runs(&mut buf, &runs)? * sectors_per_block;
4821        } else {
4822            // A BLOCK-MAPPED FILE HAS BLOCKS TOO. Only extents were freed,
4823            // so every ext2/ext3-style file left its data and indirect
4824            // blocks allocated with nothing pointing at them.
4825            freed_sectors +=
4826                self.buffer_free_block_map(&mut buf, &target_inode)? * sectors_per_block;
4827        }
4828
4829        // The xattr block goes with the inode, or loses one reference if
4830        // another inode shares it. It was never released at all.
4831        if target_inode.file_acl != 0 {
4832            freed_sectors += self.buffer_release_xattr_block(&mut buf, target_inode.file_acl)?
4833                * sectors_per_block;
4834        }
4835
4836        // Inode bitmap + BGD free_inodes_count; SB counter for both
4837        // freed_blocks AND +1 inode goes via one buffer_patch_sb_counters
4838        // call below.
4839        self.buffer_free_inode_slot(&mut buf, target_ino)?;
4840
4841        let freed_blocks = freed_sectors.checked_div(sectors_per_block).unwrap_or(0);
4842        self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 1)?;
4843
4844        // Zero the inode body. Kernel sets dtime = now, mode = 0, and
4845        // leaves the generation intact (helps tooling detect the dead slot).
4846        let inode_size = self.sb.inode_size as usize;
4847        let old_gen = target_inode.generation;
4848        for b in &mut target_raw[..inode_size] {
4849            *b = 0;
4850        }
4851        let dtime = self.dtime_now();
4852        target_raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes()); // dtime
4853        target_raw[0x64..0x68].copy_from_slice(&old_gen.to_le_bytes()); // generation
4854        self.finalize_inode_raw(target_ino, old_gen, &mut target_raw)?;
4855        self.buffer_write_inode(&mut buf, target_ino, &target_raw)?;
4856
4857        self.commit_block_buffer(buf)
4858    }
4859
4860    /// Common setup for creating a new inode inside a directory: resolves
4861    /// the parent, checks preconditions, allocates an inode, and stages the
4862    /// bitmap + counter updates into a fresh `BlockBuffer`. The caller then
4863    /// builds the inode bytes and adds the dir entry.
4864    fn plan_new_inode_in_dir(&self, dir: InodeRef, name: &[u8]) -> Result<NewInodePlan> {
4865        check_new_entry_name(name)?;
4866        let parent_ino = dir.ino;
4867        let (parent_inode, _) = self.live_dir(dir)?;
4868        if self.entry_exists(parent_ino, &parent_inode, name)? {
4869            return Err(Error::AlreadyExists);
4870        }
4871
4872        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
4873        let bs = self.sb.block_size();
4874        let mut bitmap_reader = |block: u64| self.read_block(block);
4875        let plan = crate::alloc::plan_inode_allocation(
4876            &self.sb,
4877            &self.allocation_groups(),
4878            false,
4879            parent_group,
4880            &mut bitmap_reader,
4881        )?;
4882        let new_ino = plan.inode;
4883
4884        let mut buf = BlockBuffer::new(bs);
4885        self.buffer_mark_inode_used(&mut buf, new_ino)?;
4886        self.buffer_patch_bgd_counters(
4887            &mut buf,
4888            plan.bgd.group_idx as usize,
4889            plan.bgd.free_blocks_delta,
4890            plan.bgd.free_inodes_delta,
4891            plan.bgd.used_dirs_delta,
4892        )?;
4893        self.buffer_patch_sb_counters(
4894            &mut buf,
4895            plan.sb.free_blocks_delta,
4896            plan.sb.free_inodes_delta,
4897        )?;
4898
4899        Ok(NewInodePlan {
4900            new_ino,
4901            parent_ino,
4902            parent_inode,
4903            buf,
4904        })
4905    }
4906
4907    /// Create a new regular file at `path` with permission bits `mode`
4908    /// (e.g. `0o644`). Returns the allocated inode number on success.
4909    ///
4910    /// Semantics:
4911    /// - Parent must exist and be a directory.
4912    /// - Refuses if `path` already exists.
4913    /// - Allocates an inode via `plan_inode_allocation` (hints to the
4914    ///   parent's group), marks the bitmap, bumps BGD + SB counters.
4915    /// - Initialises the inode as a regular file with EXTENTS flag and an
4916    ///   empty extent tree (size=0, blocks=0). Timestamps set to `now`.
4917    /// - Adds the directory entry into the first parent block with room
4918    ///   (linear; htree-extending dirs are a follow-up).
4919    /// - Not journaled — scratch-image safe, same caveat as other Phase-4
4920    ///   applies.
4921    pub fn apply_create(&self, path: &str, mode: u16) -> Result<u32> {
4922        self.apply_create_bytes(path.as_bytes(), mode)
4923    }
4924
4925    /// [`apply_create`](Self::apply_create) of a path given as bytes, never decoded.
4926    pub(crate) fn apply_create_bytes(&self, path: &[u8], mode: u16) -> Result<u32> {
4927        self.refuse_write()?;
4928        let (parent, name) = self.resolve_new_parent(path)?;
4929        self.apply_create_at(parent, name, mode)
4930    }
4931
4932    /// [`apply_create`](Self::apply_create) of entry `name` in directory
4933    /// `dir`. `name` is bytes and need not be UTF-8.
4934    pub fn apply_create_at(&self, dir: impl Into<InodeRef>, name: &[u8], mode: u16) -> Result<u32> {
4935        self.refuse_write()?;
4936        let dir = dir.into();
4937        let NewInodePlan {
4938            new_ino,
4939            parent_ino,
4940            parent_inode,
4941            mut buf,
4942        } = self.plan_new_inode_in_dir(dir, name)?;
4943
4944        let raw = self.build_regular_file_inode(new_ino, mode)?;
4945        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
4946
4947        // Multi-block transaction: inode bitmap + BGD + SB + new inode +
4948        // parent dir entry, all atomic, and the parent's growth with them
4949        // when it has no room (see `extend_dir_and_add_entry` for the one
4950        // case that commits early).
4951        match self.buffer_add_dir_entry_inplace(
4952            &mut buf,
4953            parent_ino,
4954            &parent_inode,
4955            name,
4956            new_ino,
4957            crate::dir::DirEntryType::RegFile,
4958        ) {
4959            Ok(()) => {
4960                self.commit_block_buffer(buf)?;
4961                Ok(new_ino)
4962            }
4963            Err(Error::OutOfBounds) => {
4964                // Parent dir is full → grow it in the same transaction as
4965                // the inode allocation.
4966                self.extend_dir_and_add_entry(
4967                    &mut buf,
4968                    parent_ino,
4969                    name,
4970                    new_ino,
4971                    crate::dir::DirEntryType::RegFile,
4972                )?;
4973                self.commit_block_buffer(buf)?;
4974                Ok(new_ino)
4975            }
4976            Err(e) => Err(e),
4977        }
4978    }
4979
4980    /// Create a special file (FIFO, socket, char device, block device).
4981    /// `mode` must include the type bits (`S_IFIFO`, `S_IFSOCK`, `S_IFCHR`,
4982    /// or `S_IFBLK`) plus the permission bits. `major` and `minor` are the
4983    /// device numbers (both 0 for FIFOs and sockets). Mirrors POSIX `mknod`.
4984    pub fn apply_mknod(&self, path: &str, mode: u16, major: u32, minor: u32) -> Result<u32> {
4985        self.apply_mknod_bytes(path.as_bytes(), mode, major, minor)
4986    }
4987
4988    /// [`apply_mknod`](Self::apply_mknod) of a path given as bytes, never decoded.
4989    pub(crate) fn apply_mknod_bytes(
4990        &self,
4991        path: &[u8],
4992        mode: u16,
4993        major: u32,
4994        minor: u32,
4995    ) -> Result<u32> {
4996        self.refuse_write()?;
4997        Self::mknod_entry_type(mode)?;
4998        let (parent, name) = self.resolve_new_parent(path)?;
4999        self.apply_mknod_at(parent, name, mode, major, minor)
5000    }
5001
5002    /// The directory-entry type for a special file of `mode`, or the
5003    /// refusal of a mode that is not one.
5004    fn mknod_entry_type(mode: u16) -> Result<crate::dir::DirEntryType> {
5005        let file_type = mode & crate::inode::S_IFMT;
5006        match file_type {
5007            crate::inode::S_IFCHR
5008            | crate::inode::S_IFBLK
5009            | crate::inode::S_IFIFO
5010            | crate::inode::S_IFSOCK => Ok(crate::dir::DirEntryType::from_mode(file_type)),
5011            _ => Err(Error::InvalidArgument(
5012                "mknod: unsupported type; use create/mkdir for reg/dir",
5013            )),
5014        }
5015    }
5016
5017    /// [`apply_mknod`](Self::apply_mknod) of entry `name` in directory
5018    /// `dir`.
5019    pub fn apply_mknod_at(
5020        &self,
5021        dir: impl Into<InodeRef>,
5022        name: &[u8],
5023        mode: u16,
5024        major: u32,
5025        minor: u32,
5026    ) -> Result<u32> {
5027        self.refuse_write()?;
5028        let dir = dir.into();
5029        let dir_entry_type = Self::mknod_entry_type(mode)?;
5030        let NewInodePlan {
5031            new_ino,
5032            parent_ino,
5033            parent_inode,
5034            mut buf,
5035        } = self.plan_new_inode_in_dir(dir, name)?;
5036
5037        let raw = self.build_special_file_inode(new_ino, mode, major, minor)?;
5038        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
5039
5040        match self.buffer_add_dir_entry_inplace(
5041            &mut buf,
5042            parent_ino,
5043            &parent_inode,
5044            name,
5045            new_ino,
5046            dir_entry_type,
5047        ) {
5048            Ok(()) => {
5049                self.commit_block_buffer(buf)?;
5050                Ok(new_ino)
5051            }
5052            Err(Error::OutOfBounds) => {
5053                self.extend_dir_and_add_entry(&mut buf, parent_ino, name, new_ino, dir_entry_type)?;
5054                self.commit_block_buffer(buf)?;
5055                Ok(new_ino)
5056            }
5057            Err(e) => Err(e),
5058        }
5059    }
5060
5061    /// Write inode checksum fields (lo at OFF_CHECKSUM_LO, hi at OFF_CHECKSUM_HI)
5062    /// when metadata checksums are enabled for this filesystem.
5063    fn stamp_inode_checksum(&self, raw: &mut [u8], ino: u32, generation: u32) {
5064        if self.csum.enabled {
5065            self.csum.patch_inode_checksum(ino, generation, raw);
5066        }
5067    }
5068
5069    fn build_special_file_inode(
5070        &self,
5071        ino: u32,
5072        mode: u16,
5073        major: u32,
5074        minor: u32,
5075    ) -> Result<Vec<u8>> {
5076        use crate::inode::{OFF_BLOCK, OFF_LINKS_COUNT, OFF_MODE};
5077        let inode_size = self.sb.inode_size as usize;
5078        let mut raw = vec![0u8; inode_size];
5079
5080        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode.to_le_bytes());
5081        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
5082
5083        // Device files: store encoded device number in i_block (no EXTENTS).
5084        // Linux stores old (i_block[0]) and new (i_block[1]) formats.
5085        let file_type = mode & crate::inode::S_IFMT;
5086        if file_type == crate::inode::S_IFBLK || file_type == crate::inode::S_IFCHR {
5087            let old_dev = (major << 8) | (minor & 0xff);
5088            raw[OFF_BLOCK..OFF_BLOCK + 4].copy_from_slice(&old_dev.to_le_bytes());
5089            let new_dev = (minor & 0xff) | (major << 8) | ((minor & !0xff) << 12);
5090            raw[OFF_BLOCK + 4..OFF_BLOCK + 8].copy_from_slice(&new_dev.to_le_bytes());
5091        }
5092
5093        // i_extra_isize first: it says whether the epoch bits have a home.
5094        write_inode_extra_isize(&mut raw);
5095        let now = self.runtime.now_unix_seconds();
5096        write_inode_timestamps(&mut raw, now);
5097        let generation = self.runtime.next_inode_generation();
5098        write_inode_generation(&mut raw, generation);
5099        self.stamp_inode_checksum(&mut raw, ino, generation);
5100        Ok(raw)
5101    }
5102
5103    /// Create a symbolic link at `linkpath` whose target is `target`.
5104    /// Mirrors POSIX `symlink(target, linkpath)`: allocates a fresh inode
5105    /// with mode S_IFLNK, installs the target bytes, and adds a dir entry
5106    /// at the link path.
5107    ///
5108    /// Two storage paths:
5109    /// - **Fast symlink** (`target.len() <= 60`): target stored inline in
5110    ///   the 60-byte `i_block` area; no data-block allocation.
5111    /// - **Slow symlink** (`61..=255` bytes): one filesystem block is
5112    ///   allocated and the target is written there, with an EXTENTS
5113    ///   i_block pointing at it.
5114    ///
5115    /// POSIX caps symlink targets at SYMLINK_MAX (255 bytes on Linux +
5116    /// macOS). Longer returns `Error::NameTooLong` → ENAMETOOLONG.
5117    pub fn apply_symlink(&self, target: &str, linkpath: &str) -> Result<u32> {
5118        self.apply_symlink_bytes(target.as_bytes(), linkpath.as_bytes())
5119    }
5120
5121    /// [`apply_symlink`](Self::apply_symlink) of a path given as bytes, never decoded.
5122    pub(crate) fn apply_symlink_bytes(&self, target: &[u8], linkpath: &[u8]) -> Result<u32> {
5123        self.refuse_write()?;
5124        self.check_symlink_target(target)?;
5125        let (parent, name) = self.resolve_new_parent(linkpath)?;
5126        self.apply_symlink_at(parent, name, target)
5127    }
5128
5129    fn check_symlink_target(&self, target: &[u8]) -> Result<()> {
5130        if target.is_empty() {
5131            return Err(Error::InvalidArgument("symlink target is empty"));
5132        }
5133        // A C string cannot carry one, and readlink(2) would stop at it.
5134        if target.contains(&0) {
5135            return Err(Error::InvalidArgument(
5136                "a symlink target cannot contain a NUL byte",
5137            ));
5138        }
5139        // PATH_MAX cap (matches Linux). Slow path allocates exactly one fs
5140        // block, so we additionally require target.len() <= block_size — the
5141        // 4096 ceiling matches the typical ext4 block size and Linux PATH_MAX.
5142        let max_target = 4096usize.min(self.sb.block_size() as usize);
5143        if target.len() > max_target {
5144            return Err(Error::NameTooLong);
5145        }
5146        Ok(())
5147    }
5148
5149    /// [`apply_symlink`](Self::apply_symlink): entry `name` in directory
5150    /// `dir`, pointing at `target`. Both are bytes and need not be UTF-8.
5151    pub fn apply_symlink_at(
5152        &self,
5153        dir: impl Into<InodeRef>,
5154        name: &[u8],
5155        target: &[u8],
5156    ) -> Result<u32> {
5157        self.refuse_write()?;
5158        let dir = dir.into();
5159        self.check_symlink_target(target)?;
5160
5161        let NewInodePlan {
5162            new_ino,
5163            parent_ino,
5164            parent_inode,
5165            mut buf,
5166        } = self.plan_new_inode_in_dir(dir, name)?;
5167
5168        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
5169        let bs = self.sb.block_size();
5170
5171        // Fast-symlink if target strictly fits inline (i_block is 60 bytes);
5172        // otherwise allocate a block and stage its bytes into the buffer.
5173        // The format stores a target inline only when it is less than 60
5174        // bytes (the size of i_block; format documentation, "Symbolic
5175        // Links"), and our readlink path uses that boundary, so we match
5176        // here.
5177        let raw = if target.len() < 60 {
5178            self.build_fast_symlink_inode(new_ino, target)?
5179        } else {
5180            let mut bitmap_reader = |block: u64| self.read_block(block);
5181            let bplan = crate::alloc::plan_block_allocation(
5182                &self.sb,
5183                &self.allocation_groups(),
5184                1,
5185                parent_group,
5186                &mut bitmap_reader,
5187            )?;
5188            let data_phys = bplan.first_block;
5189
5190            self.buffer_mark_block_run_used(&mut buf, data_phys, 1)?;
5191            self.buffer_patch_bgd_counters(
5192                &mut buf,
5193                bplan.bgd.group_idx as usize,
5194                bplan.bgd.free_blocks_delta,
5195                bplan.bgd.free_inodes_delta,
5196                bplan.bgd.used_dirs_delta,
5197            )?;
5198            self.buffer_patch_sb_counters(
5199                &mut buf,
5200                bplan.sb.free_blocks_delta,
5201                bplan.sb.free_inodes_delta,
5202            )?;
5203
5204            let mut block = vec![0u8; bs as usize];
5205            block[..target.len()].copy_from_slice(target);
5206            buf.put(data_phys, block);
5207
5208            self.build_slow_symlink_inode(new_ino, target, data_phys)?
5209        };
5210        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
5211
5212        match self.buffer_add_dir_entry_inplace(
5213            &mut buf,
5214            parent_ino,
5215            &parent_inode,
5216            name,
5217            new_ino,
5218            crate::dir::DirEntryType::Symlink,
5219        ) {
5220            Ok(()) => {
5221                self.commit_block_buffer(buf)?;
5222                Ok(new_ino)
5223            }
5224            Err(Error::OutOfBounds) => {
5225                self.extend_dir_and_add_entry(
5226                    &mut buf,
5227                    parent_ino,
5228                    name,
5229                    new_ino,
5230                    crate::dir::DirEntryType::Symlink,
5231                )?;
5232                self.commit_block_buffer(buf)?;
5233                Ok(new_ino)
5234            }
5235            Err(e) => Err(e),
5236        }
5237    }
5238
5239    /// Compose a fresh fast-symlink inode image: `S_IFLNK | 0o777`, 1 link,
5240    /// `i_size = target.len()`, 0 blocks, NO EXTENTS flag (fast symlinks
5241    /// store their target directly in the 60-byte `i_block` area — no
5242    /// extent tree).
5243    fn build_fast_symlink_inode(&self, ino: u32, target: &[u8]) -> Result<Vec<u8>> {
5244        use crate::inode::{OFF_BLOCK, OFF_FLAGS, OFF_LINKS_COUNT, OFF_MODE, OFF_SIZE_LO};
5245        debug_assert!(target.len() < 60);
5246        let mut raw = vec![0u8; self.sb.inode_size as usize];
5247
5248        // Symlinks are traditionally rwxrwxrwx — the OS enforces access on
5249        // the *target*, not the symlink itself.
5250        let mode_bits = crate::inode::S_IFLNK | 0o0777;
5251        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
5252        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&(target.len() as u32).to_le_bytes());
5253        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
5254        // Fast symlinks store the target inline in the i_block area — no extent tree.
5255        raw[OFF_FLAGS..OFF_FLAGS + 4].copy_from_slice(&0u32.to_le_bytes());
5256        let inline_target_off = OFF_BLOCK;
5257        raw[inline_target_off..inline_target_off + target.len()].copy_from_slice(target);
5258
5259        // i_extra_isize first: it says whether the epoch bits have a home.
5260        write_inode_extra_isize(&mut raw);
5261        let now = self.runtime.now_unix_seconds();
5262        write_inode_timestamps(&mut raw, now);
5263        let generation = self.runtime.next_inode_generation();
5264        write_inode_generation(&mut raw, generation);
5265        self.stamp_inode_checksum(&mut raw, ino, generation);
5266        Ok(raw)
5267    }
5268
5269    /// Map logical block 0 of a new inode image to `phys`, in the volume's
5270    /// dialect: an EXTENTS_FL leaf with one extent on ext4, direct pointer 0
5271    /// on ext2/ext3. e2fsck calls an extent-mapped inode on a volume without
5272    /// the EXTENTS feature corrupt, and the new directory and slow symlink
5273    /// were extent-mapped whatever the volume (#89).
5274    fn map_one_block(&self, raw: &mut [u8], phys: u64) -> Result<()> {
5275        use crate::inode::{OFF_BLOCK, OFF_FLAGS};
5276        if !self.flavor.uses_extents() {
5277            let direct =
5278                u32::try_from(phys).map_err(|_| Error::Corrupt("block past a 32-bit block map"))?;
5279            raw[OFF_BLOCK..OFF_BLOCK + 4].copy_from_slice(&direct.to_le_bytes());
5280            return Ok(());
5281        }
5282        let flags = u32::from_le_bytes(raw[OFF_FLAGS..OFF_FLAGS + 4].try_into().unwrap());
5283        raw[OFF_FLAGS..OFF_FLAGS + 4]
5284            .copy_from_slice(&(flags | crate::inode::InodeFlags::EXTENTS.bits()).to_le_bytes());
5285
5286        // i_block: extent leaf header (1 entry, max 4, depth 0) + one extent,
5287        // logical block 0, length 1.
5288        let header = OFF_BLOCK;
5289        raw[header..header + 2].copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
5290        raw[header + 2..header + 4].copy_from_slice(&1u16.to_le_bytes());
5291        raw[header + 4..header + 6].copy_from_slice(&4u16.to_le_bytes());
5292        raw[header + 6..header + 8].copy_from_slice(&0u16.to_le_bytes());
5293        let entry = header + 12;
5294        raw[entry..entry + 4].copy_from_slice(&0u32.to_le_bytes());
5295        raw[entry + 4..entry + 6].copy_from_slice(&1u16.to_le_bytes());
5296        let (phys_hi, phys_lo) = crate::extent_mut::split_phys_block(phys);
5297        raw[entry + 6..entry + 8].copy_from_slice(&phys_hi.to_le_bytes());
5298        raw[entry + 8..entry + 12].copy_from_slice(&phys_lo.to_le_bytes());
5299        Ok(())
5300    }
5301
5302    /// Compose a slow-symlink inode image: `S_IFLNK | 0o777`, 1 link,
5303    /// `i_size = target.len()`, logical block 0 mapped to `data_phys` by
5304    /// [`Self::map_one_block`]. One fs
5305    /// block worth of 512-byte sectors charged to `i_blocks`.
5306    ///
5307    /// Caller must have already written the target bytes (zero-padded) to
5308    /// `data_phys * block_size`.
5309    fn build_slow_symlink_inode(&self, ino: u32, target: &[u8], data_phys: u64) -> Result<Vec<u8>> {
5310        use crate::inode::{OFF_BLOCKS_LO, OFF_LINKS_COUNT, OFF_MODE, OFF_SIZE_LO};
5311        debug_assert!(target.len() >= 60 && target.len() <= 4096);
5312        let mut raw = vec![0u8; self.sb.inode_size as usize];
5313
5314        let mode_bits = crate::inode::S_IFLNK | 0o0777;
5315        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
5316        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&(target.len() as u32).to_le_bytes());
5317        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
5318        let bs = self.sb.block_size() as u64;
5319        let sectors = bs / 512;
5320        raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].copy_from_slice(&(sectors as u32).to_le_bytes());
5321        self.map_one_block(&mut raw, data_phys)?;
5322
5323        // i_extra_isize first: it says whether the epoch bits have a home.
5324        write_inode_extra_isize(&mut raw);
5325        let now = self.runtime.now_unix_seconds();
5326        write_inode_timestamps(&mut raw, now);
5327        let generation = self.runtime.next_inode_generation();
5328        write_inode_generation(&mut raw, generation);
5329        self.stamp_inode_checksum(&mut raw, ino, generation);
5330        Ok(raw)
5331    }
5332
5333    /// Compose a fresh regular-file inode image: `S_IFREG | mode`, 1 link,
5334    /// 0 size, 0 blocks, EXTENTS flag set with an empty 4-entry leaf root,
5335    /// timestamps = now, generation = process-id-derived counter, extra_isize
5336    /// = 32 so the inode has room for nsec timestamps + checksum_hi.
5337    fn build_regular_file_inode(&self, ino: u32, mode: u16) -> Result<Vec<u8>> {
5338        use crate::inode::{OFF_BLOCK, OFF_FLAGS, OFF_LINKS_COUNT, OFF_MODE};
5339        let mut raw = vec![0u8; self.sb.inode_size as usize];
5340
5341        let mode_bits = crate::inode::S_IFREG | (mode & 0x0FFF);
5342        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
5343        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
5344
5345        // i_flags + i_block layout depend on the FS dialect:
5346        // - ext4 (FsFlavor::Ext4): EXTENTS_FL set, i_block holds an empty
5347        //   extent leaf header (magic + entries=0 + max=4 + depth=0).
5348        // - ext2 / ext3: no flag, i_block stays all-zero (no direct or
5349        //   indirect pointers — file is empty so there's nothing to map).
5350        if self.flavor.uses_extents() {
5351            raw[OFF_FLAGS..OFF_FLAGS + 4]
5352                .copy_from_slice(&crate::inode::InodeFlags::EXTENTS.bits().to_le_bytes());
5353
5354            let extent_header_off = OFF_BLOCK;
5355            raw[extent_header_off..extent_header_off + 2]
5356                .copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
5357            raw[extent_header_off + 2..extent_header_off + 4].copy_from_slice(&0u16.to_le_bytes());
5358            raw[extent_header_off + 4..extent_header_off + 6].copy_from_slice(&4u16.to_le_bytes());
5359            raw[extent_header_off + 6..extent_header_off + 8].copy_from_slice(&0u16.to_le_bytes());
5360        }
5361
5362        // i_extra_isize first: it says whether the epoch bits have a home.
5363        write_inode_extra_isize(&mut raw);
5364        let now = self.runtime.now_unix_seconds();
5365        write_inode_timestamps(&mut raw, now);
5366        let generation = self.runtime.next_inode_generation();
5367        write_inode_generation(&mut raw, generation);
5368        self.stamp_inode_checksum(&mut raw, ino, generation);
5369        Ok(raw)
5370    }
5371
5372    /// Replace the content of `path` with `data`. The file must already
5373    /// exist. Frees every existing extent, allocates a single contiguous run
5374    /// of blocks large enough for `data`, writes the bytes (zero-padding the
5375    /// tail of the last block), then inserts one extent into the inode.
5376    ///
5377    /// This is the "Finder just saved a document" path — complete rewrite of
5378    /// a file. Piecewise writes / appends / sparse writes come later.
5379    ///
5380    /// For an extent-mapped inode on a mount with a journal, atomic across
5381    /// the whole replace: freeing the old data, allocating the new run, the
5382    /// bitmap, BGD and superblock updates, the new block contents and the
5383    /// inode all commit as one transaction. Two cases are not (#179):
5384    ///
5385    /// - without a journal, the same blocks are written in turn, and a
5386    ///   crash part-way leaves some written and some not;
5387    /// - an inode that is not extent-mapped goes to
5388    ///   `apply_replace_file_content_indirect`, which builds no transaction
5389    ///   at all, on ext3 with a journal too.
5390    ///
5391    /// Returns the new file size on success.
5392    pub fn apply_replace_file_content(&self, path: &str, data: &[u8]) -> Result<u64> {
5393        self.apply_replace_file_content_bytes(path.as_bytes(), data)
5394    }
5395
5396    /// [`apply_replace_file_content`](Self::apply_replace_file_content) of a path given as bytes, never decoded.
5397    pub(crate) fn apply_replace_file_content_bytes(&self, path: &[u8], data: &[u8]) -> Result<u64> {
5398        self.refuse_write()?;
5399        let ino = self.resolve(path)?;
5400        let (inode, mut raw) = self.read_inode_verified(ino)?;
5401        if !inode.is_file() {
5402            return Err(Error::InvalidArgument(
5403                "write_file target is not a regular file",
5404            ));
5405        }
5406        let bs = self.sb.block_size();
5407        // Multi-block transaction: free existing data + alloc new run +
5408        // bitmap + BGD + SB + new data block contents + inode update.
5409        // Atomic across the whole replace.
5410        let mut buf = BlockBuffer::new(bs);
5411        // An inline file keeps content that fits where it is, and is
5412        // converted out of the inode, with no bytes, in this transaction
5413        // when the new content does not (#428).
5414        let (inode, mut raw) = if inode.has_inline_data() {
5415            if self.buffer_store_inline_file(&mut buf, ino, &inode, &mut raw, data)? {
5416                self.commit_block_buffer(buf)?;
5417                return Ok(data.len() as u64);
5418            }
5419            self.buffer_convert_inline_file(&mut buf, ino, &inode, &raw, false)?
5420        } else {
5421            (inode, raw)
5422        };
5423        if !inode.has_extents() {
5424            // ext2 / ext3 (or ext4 inode without EXTENTS_FL): legacy
5425            // direct/indirect block-pointer scheme. Same overall shape as
5426            // the extent path below — free old → allocate → write data →
5427            // patch inode — but the i_block tree comes from `indirect_mut`
5428            // and any indirect-tree blocks are co-allocated with the data
5429            // run (one bitmap call covers both).
5430            return self.apply_replace_file_content_indirect(ino, inode, raw, data);
5431        }
5432
5433        let sectors_per_block = bs as u64 / 512;
5434        let group_idx_of_inode = ((ino - 1) / self.sb.inodes_per_group) as usize;
5435
5436        // Phase 1: free existing data blocks. Each freed run credits its
5437        // own group's BGD via `buffer_free_block_run_and_bgd`.
5438        let runs = self.extent_tree_runs(ino, &inode)?;
5439        let freed_fs_blocks = self.buffer_free_runs(&mut buf, &runs)?;
5440
5441        // Reset the inode's extent root to an empty leaf.
5442        let mut root = vec![0u8; 60];
5443        root[0..2].copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
5444        root[4..6].copy_from_slice(&4u16.to_le_bytes()); // max entries
5445        Self::patch_inode_block_area(&mut raw, &root)?;
5446
5447        // Empty write: BGDs already credited per-run above; only SB needs
5448        // a single update + inode rewrite.
5449        if data.is_empty() {
5450            self.finalize_inode_raw_after_write(
5451                ino,
5452                &mut raw,
5453                &inode,
5454                0,
5455                Self::xattr_block_sectors(&inode, bs),
5456            )?;
5457            if freed_fs_blocks > 0 {
5458                self.buffer_patch_sb_counters(&mut buf, freed_fs_blocks as i64, 0)?;
5459            }
5460            self.buffer_write_inode(&mut buf, ino, &raw)?;
5461            self.commit_block_buffer(buf)?;
5462            return Ok(0);
5463        }
5464
5465        // Phase 2: allocate one contiguous run for the whole payload.
5466        let needed_blocks: u32 = data.len().div_ceil(bs as usize) as u32;
5467        let mut bitmap_reader = |block: u64| self.read_block(block);
5468        let plan = crate::alloc::plan_block_allocation(
5469            &self.sb,
5470            &self.allocation_groups(),
5471            needed_blocks,
5472            group_idx_of_inode as u32,
5473            &mut bitmap_reader,
5474        )?;
5475
5476        // Phase 3: mark allocated bitmap + patch destination BGD; SB nets
5477        // the alloc delta against the freed total computed above.
5478        self.buffer_mark_block_run_used(&mut buf, plan.first_block, needed_blocks as u64)?;
5479        self.buffer_patch_bgd_counters(
5480            &mut buf,
5481            plan.bgd.group_idx as usize,
5482            plan.bgd.free_blocks_delta,
5483            plan.bgd.free_inodes_delta,
5484            plan.bgd.used_dirs_delta,
5485        )?;
5486        let net_block_delta = freed_fs_blocks as i64 - needed_blocks as i64;
5487        self.buffer_patch_sb_counters(&mut buf, net_block_delta, 0)?;
5488
5489        // Phase 4: stage the payload into the allocated physical run.
5490        for i in 0..needed_blocks as u64 {
5491            let off_in_data = (i as usize) * bs as usize;
5492            let chunk_end = ((i as usize + 1) * bs as usize).min(data.len());
5493            let mut block = vec![0u8; bs as usize];
5494            block[..chunk_end - off_in_data].copy_from_slice(&data[off_in_data..chunk_end]);
5495            buf.put(plan.first_block + i, block);
5496        }
5497
5498        // Phase 5: insert the single extent into the (now-empty) inline
5499        // root and stage the inode.
5500        let new_extent = crate::extent::Extent {
5501            logical_block: 0,
5502            length: needed_blocks as u16,
5503            physical_block: plan.first_block,
5504            uninitialized: false,
5505        };
5506        let muts = crate::extent_mut::plan_insert_extent(&root, new_extent)?;
5507        for m in &muts {
5508            if let crate::extent_mut::ExtentMutation::WriteRoot { bytes } = m {
5509                Self::patch_inode_block_area(&mut raw, bytes)?;
5510            }
5511        }
5512        let new_size = data.len() as u64;
5513        let new_sectors =
5514            needed_blocks as u64 * sectors_per_block + Self::xattr_block_sectors(&inode, bs);
5515        self.finalize_inode_raw_after_write(ino, &mut raw, &inode, new_size, new_sectors)?;
5516        self.buffer_write_inode(&mut buf, ino, &raw)?;
5517
5518        self.commit_block_buffer(buf)?;
5519        Ok(new_size)
5520    }
5521
5522    /// ext2/ext3 sibling of `apply_replace_file_content`'s extent path.
5523    /// Frees the inode's existing direct/indirect tree, allocates one
5524    /// contiguous run sized for both the data payload AND the indirect-tree
5525    /// metadata blocks, builds the new tree via `indirect_mut::plan_contiguous`,
5526    /// then persists everything (data → indirect blocks → inode).
5527    ///
5528    /// No journal interaction, on any mount. The writer can address an
5529    /// indirect-mapped journal inode (see `mount_inner`), so an ext3 mount
5530    /// has one, but this path frees, allocates and writes block by block
5531    /// and never builds a `BlockBuffer` for it. A crash after the old runs
5532    /// are freed and before the inode is rewritten leaves the inode
5533    /// mapping blocks the bitmap already calls free (#179).
5534    /// Whether a block-mapped inode's `i_block` holds block pointers.
5535    ///
5536    /// Not every inode's does: a fast symlink keeps its target there and a
5537    /// device node its numbers, with no data blocks at all. `i_blocks`
5538    /// tells them apart, less the external xattr block it also counts.
5539    fn holds_block_map(inode: &Inode, block_size: u32) -> bool {
5540        let xattr_sectors = if inode.file_acl != 0 {
5541            u64::from(block_size) / 512
5542        } else {
5543            0
5544        };
5545        inode.flags & crate::inode::InodeFlags::INLINE_DATA.bits() == 0
5546            && inode.blocks > xattr_sectors
5547    }
5548
5549    /// Free every data and indirect block a block-mapped inode holds,
5550    /// staged in `buf`. Returns the blocks freed. Does nothing for an inode
5551    /// whose `i_block` holds no pointers ([`Self::holds_block_map`]).
5552    fn buffer_free_block_map(&self, buf: &mut BlockBuffer, inode: &Inode) -> Result<u64> {
5553        let bs = self.sb.block_size();
5554        if !Self::holds_block_map(inode, bs) {
5555            return Ok(0);
5556        }
5557        let freed = crate::indirect_mut::collect_for_free(
5558            &inode.block,
5559            bs,
5560            inode.size.div_ceil(u64::from(bs)) as u32,
5561            self.dev.as_ref(),
5562        )?;
5563        let mut count = 0;
5564        for run in &freed.data_runs {
5565            count += self.buffer_free_block_run_and_bgd(buf, run.start, run.len as u64)?;
5566        }
5567        for &iblk in &freed.indirect_blocks {
5568            count += self.buffer_free_block_run_and_bgd(buf, iblk, 1)?;
5569        }
5570        Ok(count)
5571    }
5572
5573    /// The sectors an inode's external xattr block adds to `i_blocks`.
5574    ///
5575    /// `i_blocks` counts every block the inode holds, and the xattr block is
5576    /// one of them. Replacing a file's content replaces its data, not its
5577    /// attributes, so the count set afterwards keeps this.
5578    fn xattr_block_sectors(inode: &Inode, block_size: u32) -> u64 {
5579        if inode.file_acl != 0 {
5580            u64::from(block_size) / 512
5581        } else {
5582            0
5583        }
5584    }
5585
5586    fn apply_replace_file_content_indirect(
5587        &self,
5588        ino: u32,
5589        inode: Inode,
5590        mut raw: Vec<u8>,
5591        data: &[u8],
5592    ) -> Result<u64> {
5593        let bs = self.sb.block_size();
5594        let sectors_per_block = bs as u64 / 512;
5595        let group_idx_of_inode = ((ino - 1) / self.sb.inodes_per_group) as usize;
5596
5597        // Phase 1: free existing data + indirect-tree blocks. `collect_for_free`
5598        // walks the tree and returns coalesced data runs + individual indirect
5599        // blocks, so cross-group fragmented files are accounted for correctly.
5600        // THROUGH THE BUFFER, as the extent path goes. The unbuffered
5601        // helpers wrote the block bitmap without restamping its checksum,
5602        // and outside the journal.
5603        let mut buf = BlockBuffer::new(bs);
5604        let mut freed_fs_blocks: u64 = 0;
5605        if inode.size > 0 {
5606            let block_count = inode.size.div_ceil(bs as u64) as u32;
5607            let freed = crate::indirect_mut::collect_for_free(
5608                &inode.block,
5609                bs,
5610                block_count,
5611                self.dev.as_ref(),
5612            )?;
5613            for run in &freed.data_runs {
5614                freed_fs_blocks +=
5615                    self.buffer_free_block_run_and_bgd(&mut buf, run.start, run.len as u64)?;
5616            }
5617            for &iblk in &freed.indirect_blocks {
5618                freed_fs_blocks += self.buffer_free_block_run_and_bgd(&mut buf, iblk, 1)?;
5619            }
5620        }
5621        // Reset i_block to all zeros — no extent magic for legacy inodes.
5622        let zero_iblock = [0u8; 60];
5623        Self::patch_inode_block_area(&mut raw, &zero_iblock)?;
5624
5625        if data.is_empty() {
5626            self.finalize_inode_raw_after_write(
5627                ino,
5628                &mut raw,
5629                &inode,
5630                0,
5631                Self::xattr_block_sectors(&inode, bs),
5632            )?;
5633            if freed_fs_blocks > 0 {
5634                self.buffer_patch_sb_counters(&mut buf, freed_fs_blocks as i64, 0)?;
5635            }
5636            self.buffer_write_inode(&mut buf, ino, &raw)?;
5637            self.commit_block_buffer(buf)?;
5638            return Ok(0);
5639        }
5640
5641        // Phase 2: allocate one contiguous run sized for data + indirect tree.
5642        // Indirect blocks live at the head of the run, data at the tail.
5643        // `count_indirect_blocks` is exactly the number of allocator pulls
5644        // `plan_contiguous` will make, so the budget is tight (verified by
5645        // the `count_indirect_blocks_matches_plan_contiguous` unit test).
5646        let needed_data_blocks: u32 = data.len().div_ceil(bs as usize) as u32;
5647        let n_indirect: u32 = crate::indirect_mut::count_indirect_blocks(needed_data_blocks, bs)
5648            .try_into()
5649            .map_err(|_| Error::Corrupt("indirect_mut: indirect block count overflow"))?;
5650        let total_run = needed_data_blocks
5651            .checked_add(n_indirect)
5652            .ok_or(Error::Corrupt("indirect_mut: total run count overflow"))?;
5653
5654        let mut bitmap_reader = |block: u64| self.read_block(block);
5655        let plan = crate::alloc::plan_block_allocation(
5656            &self.sb,
5657            &self.allocation_groups(),
5658            total_run,
5659            group_idx_of_inode as u32,
5660            &mut bitmap_reader,
5661        )?;
5662        let first_indirect = plan.first_block;
5663        let first_data = plan.first_block + n_indirect as u64;
5664
5665        // Phase 3: build the indirect tree. The closure hands out blocks
5666        // sequentially from `first_indirect` — `plan_contiguous` doesn't
5667        // care about address ordering, so any allocation order is fine.
5668        let mut next_indirect = first_indirect;
5669        let i_plan =
5670            crate::indirect_mut::plan_contiguous(needed_data_blocks, first_data, bs, || {
5671                let v = next_indirect;
5672                next_indirect += 1;
5673                Ok(v)
5674            })?;
5675
5676        // Phase 4: bitmap + BGD + SB counters cover the whole run in one
5677        // mark-used + one BGD-credit + one SB-update.
5678        self.buffer_mark_block_run_used(&mut buf, plan.first_block, total_run as u64)?;
5679        self.buffer_patch_bgd_counters(
5680            &mut buf,
5681            plan.bgd.group_idx as usize,
5682            plan.bgd.free_blocks_delta,
5683            plan.bgd.free_inodes_delta,
5684            plan.bgd.used_dirs_delta,
5685        )?;
5686        let net_block_delta = freed_fs_blocks as i64 - total_run as i64;
5687        self.buffer_patch_sb_counters(&mut buf, net_block_delta, 0)?;
5688
5689        // Phase 5: write the data payload into the data-portion of the run.
5690        for i in 0..needed_data_blocks as u64 {
5691            let off_in_data = (i as usize) * bs as usize;
5692            let chunk_end = ((i as usize + 1) * bs as usize).min(data.len());
5693            let mut block = vec![0u8; bs as usize];
5694            block[..chunk_end - off_in_data].copy_from_slice(&data[off_in_data..chunk_end]);
5695            buf.put(first_data + i, block);
5696        }
5697
5698        // Phase 6: write the indirect-tree blocks.
5699        for (blk, bytes) in &i_plan.block_writes {
5700            buf.put(*blk, bytes.clone());
5701        }
5702
5703        // Phase 7: patch i_block region with the new tree root.
5704        Self::patch_inode_block_area(&mut raw, &i_plan.i_block)?;
5705
5706        // Phase 8: finalize. ext2/3 i_blocks counts BOTH data AND indirect
5707        // blocks (in 512-byte sectors) — extent metadata blocks count the
5708        // same way for ext4 so the rule is consistent across flavors.
5709        let new_size = data.len() as u64;
5710        let new_sectors = (needed_data_blocks as u64 + n_indirect as u64) * sectors_per_block
5711            + Self::xattr_block_sectors(&inode, bs);
5712        self.finalize_inode_raw_after_write(ino, &mut raw, &inode, new_size, new_sectors)?;
5713        self.buffer_write_inode(&mut buf, ino, &raw)?;
5714        self.commit_block_buffer(buf)?;
5715        Ok(new_size)
5716    }
5717
5718    /// The largest write, in data blocks, that `apply_pwrite` puts in one
5719    /// transaction.
5720    ///
5721    /// Two bounds, the smaller winning. MEMORY: a chunk is held several
5722    /// times over while it is committed (the block buffer, the transaction,
5723    /// the serialised journal blocks), so no chunk exceeds
5724    /// [`PWRITE_CHUNK_MAX_BYTES`]. THE JOURNAL: the chunk's data blocks,
5725    /// the metadata it dirties, the descriptor blocks tagging all of them
5726    /// and the commit block must fit in the journal's usable length.
5727    ///
5728    /// The metadata is bounded for the worst case, free space fragmented
5729    /// into single blocks: every data block its own allocation, so every
5730    /// block group touched has its bitmap and descriptor block dirtied, and
5731    /// every block its own extent, so the extent tree grows by a leaf per
5732    /// half-leaf of blocks and by index nodes above those. Descriptor blocks
5733    /// are counted at the widest tag with a checksum tail, the fewest tags a
5734    /// descriptor can hold.
5735    fn pwrite_chunk_blocks(&self) -> u64 {
5736        let bs = self.sb.block_size() as u64;
5737        let memory_bound = (PWRITE_CHUNK_MAX_BYTES / bs).max(1);
5738        let Some(journal) = &self.journal else {
5739            return memory_bound;
5740        };
5741        let capacity = match journal.lock() {
5742            Ok(jw) => jw.max_blocks_per_transaction() as u64,
5743            // The commit refuses a poisoned writer anyway; any chunk will do.
5744            Err(_) => return memory_bound,
5745        };
5746        // Widest JBD2 tag (16 bytes, CSUM_V3) and a 4-byte checksum tail
5747        // after the 12-byte header: the fewest tags a descriptor holds.
5748        let tags_per_desc = ((bs - 12 - 4) / 16).max(1);
5749        // Extents per tree node (12-byte entries after a 12-byte header),
5750        // halved: a split leaves both halves this full.
5751        let half_node = (((bs - 12) / 12) / 2).max(1);
5752        let groups = self.groups.len() as u64;
5753        let gdt_blocks = (groups * u64::from(self.sb.desc_size)).div_ceil(bs);
5754        let fits = |data: u64| {
5755            // Extent-tree blocks: leaves for `data` single-block extents, and
5756            // index levels above them, up to the format's depth limit of 5.
5757            let mut tree = 0u64;
5758            let mut level = data;
5759            for _ in 0..5 {
5760                level = level.div_ceil(half_node) + 1;
5761                tree += level;
5762            }
5763            let allocations = data + tree;
5764            // Inode table block + superblock, then a bitmap per group and the
5765            // descriptor-table blocks covering them.
5766            let metadata = 2 + allocations.min(groups) + allocations.min(gdt_blocks) + tree;
5767            let tagged = data + metadata;
5768            let descriptors = tagged.div_ceil(tags_per_desc);
5769            let commit_block = 1;
5770            tagged + descriptors + commit_block <= capacity
5771        };
5772        // The largest `data` that fits, by bisection (`fits` is monotone).
5773        let (mut lo, mut hi) = (1u64, memory_bound);
5774        if fits(hi) {
5775            return hi;
5776        }
5777        while lo + 1 < hi {
5778            let mid = lo + (hi - lo) / 2;
5779            if fits(mid) {
5780                lo = mid;
5781            } else {
5782                hi = mid;
5783            }
5784        }
5785        lo
5786    }
5787
5788    /// Positional write: splice `data` into the file at byte `offset`,
5789    /// allocating new physical blocks for any logical blocks that aren't
5790    /// yet mapped (sparse holes, or blocks past EOF). Existing mapped
5791    /// blocks are read-modify-written for partial overlap; full-block
5792    /// writes go in fresh.
5793    ///
5794    /// This is the primitive needed by streaming write paths
5795    /// (FUSE/WinFsp/FSKit cache-manager dispatches) — `apply_replace_file_content`
5796    /// is "save-as", `apply_pwrite` is `pwrite(2)`.
5797    ///
5798    /// Returns the new file size on success.
5799    ///
5800    /// Allocation behaviour:
5801    /// - Each unmapped logical run is satisfied by one or more physical
5802    ///   runs. If `plan_block_allocation` can't find a single contiguous
5803    ///   group-local run sized for the whole logical run, the request is
5804    ///   halved and retried — each successful sub-run becomes its own
5805    ///   extent. True ENOSPC (single-block allocation also fails)
5806    ///   surfaces as `Error::NoSpaceLeftOnDevice`.
5807    /// - Extent inserts try the inline-root path first; on
5808    ///   `LEAF_FULL_NEEDS_PROMOTION` they fall back to
5809    ///   `plan_insert_extent_deep`, which promotes the tree to depth ≥ 1
5810    ///   and allocates the additional internal/leaf node blocks via the
5811    ///   same buffer-aware allocator. Tail checksums on tree blocks are
5812    ///   patched when `metadata_csum` is on.
5813    ///
5814    /// v1 limitations:
5815    /// - Extent-tree inodes only. Legacy ext2/3 (direct/indirect blocks)
5816    ///   returns `Error::InvalidArgument`. The streaming-copy use case
5817    ///   for this path is on freshly-mkfs'd ext4 volumes that always
5818    ///   have `EXTENTS_FL`.
5819    /// - Pre-existing uninitialised extents (from `fallocate`) in the
5820    ///   write range: not handled — the unmapped-run walk treats them
5821    ///   the same as holes and tries to insert a fresh extent that
5822    ///   would overlap, hitting `CorruptExtentTree("extent overlaps
5823    ///   existing")`. Skipping fallocate-then-write, the streaming
5824    ///   copy path doesn't trigger this.
5825    pub fn apply_pwrite(&self, path: &str, offset: u64, data: &[u8]) -> Result<u64> {
5826        self.apply_pwrite_bytes(path.as_bytes(), offset, data)
5827    }
5828
5829    /// [`apply_pwrite`](Self::apply_pwrite) of a path given as bytes, never decoded.
5830    pub(crate) fn apply_pwrite_bytes(&self, path: &[u8], offset: u64, data: &[u8]) -> Result<u64> {
5831        self.refuse_write()?;
5832        let ino = self.resolve(path)?;
5833        self.apply_pwrite_ino(ino, offset, data)
5834    }
5835
5836    /// [`apply_pwrite`](Self::apply_pwrite) on the inode `r` names.
5837    pub fn apply_pwrite_ino(
5838        &self,
5839        r: impl Into<InodeRef>,
5840        offset: u64,
5841        data: &[u8],
5842    ) -> Result<u64> {
5843        self.refuse_write()?;
5844        let r = r.into();
5845        let ino = r.ino;
5846        let (inode, mut raw) = self.live_inode(r)?;
5847        if !inode.is_file() {
5848            return Err(Error::InvalidArgument(
5849                "pwrite target is not a regular file",
5850            ));
5851        }
5852        if !inode.has_extents() && !inode.has_inline_data() {
5853            return Err(Error::InvalidArgument(
5854                "pwrite: legacy (non-extents) inodes not supported in v1",
5855            ));
5856        }
5857
5858        if data.is_empty() {
5859            // No-op (no size change either — a zero-length pwrite at any
5860            // offset is a no-op per POSIX `pwrite(2)`).
5861            return Ok(inode.size);
5862        }
5863
5864        let bs = self.sb.block_size() as u64;
5865        let bs_usize = bs as usize;
5866        let sectors_per_block = bs / 512;
5867        let len = data.len() as u64;
5868        let end = offset
5869            .checked_add(len)
5870            .ok_or(Error::InvalidArgument("pwrite: offset+len overflow"))?;
5871        let first_lb = offset / bs;
5872        let last_lb_excl = end.div_ceil(bs);
5873
5874        // A pwrite journals its data blocks and its metadata in ONE
5875        // transaction, and a transaction has to fit in the journal: the
5876        // writer checkpoints every commit, so the whole log is free for each
5877        // one, and it refuses a transaction longer than that. A write larger
5878        // than the chunk the journal can take is cut into block-aligned
5879        // chunks, each its own transaction and each atomic (POSIX pwrite is
5880        // not atomic across a large range anyway). A transaction carries as
5881        // many descriptor blocks as it needs (#147), so descriptor capacity
5882        // is not the bound; it only counts toward the journal's length (#293).
5883        let max_data_blocks = self.pwrite_chunk_blocks();
5884        let max_chunk = max_data_blocks * bs;
5885        if len > max_chunk {
5886            let mut chunk_off = 0u64;
5887            while chunk_off < len {
5888                let take = max_chunk.min(len - chunk_off);
5889                let s = chunk_off as usize;
5890                let e = (chunk_off + take) as usize;
5891                self.apply_pwrite_ino(r, offset + chunk_off, &data[s..e])?;
5892                chunk_off += take;
5893            }
5894            let (after, _) = self.read_inode_verified(ino)?;
5895            return Ok(after.size);
5896        }
5897
5898        let mut buf = BlockBuffer::new(self.sb.block_size());
5899        // An inline file takes the write in place while the result fits in
5900        // the inode, and is converted to extents, its content in block 0,
5901        // in this transaction when it does not; the write then continues
5902        // on the converted inode (#428).
5903        let (inode, mut raw) = if inode.has_inline_data() {
5904            if end <= crate::inline_mut::ceiling(self.sb.inode_size) as u64 {
5905                let mut content = crate::file_io::read_inline(self, &inode, &raw)?;
5906                if content.len() < end as usize {
5907                    content.resize(end as usize, 0);
5908                }
5909                content[offset as usize..end as usize].copy_from_slice(data);
5910                if self.buffer_store_inline_file(&mut buf, ino, &inode, &mut raw, &content)? {
5911                    self.commit_block_buffer(buf)?;
5912                    return Ok(content.len() as u64);
5913                }
5914            }
5915            self.buffer_convert_inline_file(&mut buf, ino, &inode, &raw, true)?
5916        } else {
5917            (inode, raw)
5918        };
5919
5920        // Working copy of the 60-byte inline extent root. Updated in place
5921        // as we insert extents for each unmapped run; patched into `raw`
5922        // once at the end.
5923        let mut root_bytes: Vec<u8> = inode.block.to_vec();
5924        // The tree nodes this call has planned, staged here and in `buf`
5925        // until the commit; every lookup below reads through them (#389).
5926        let mut tree_nodes: std::collections::BTreeMap<u64, Vec<u8>> =
5927            std::collections::BTreeMap::new();
5928
5929        let group_idx_of_inode = (ino - 1) / self.sb.inodes_per_group;
5930
5931        // Track which logical blocks were freshly allocated by this call.
5932        // Phase-2 writes for these MUST NOT read from disk (the prior
5933        // contents of those physical blocks are stale junk from whoever
5934        // freed them last); they get a zero-init buffer instead.
5935        let mut newly_alloc: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
5936        let mut alloc_total_blocks: u64 = 0;
5937
5938        // A PREALLOCATED BLOCK IS NOT A HOLE. `map_logical` answers `None`
5939        // for an uninitialized extent, so reads see zeros, and Phase 1
5940        // below took that for a hole: it allocated a fresh block and was
5941        // refused inserting an extent over the preallocated one. The
5942        // blocks already belong to the file, so the write goes into them
5943        // and the range it covers becomes initialized. They count as new
5944        // in Phase 2: a zero-filled block holds the data, so whatever the
5945        // preallocated block held never becomes readable.
5946        let mut preallocated = false;
5947        for lb in first_lb..last_lb_excl.min(u64::from(u32::MAX)) {
5948            if let Some(e) = crate::extent::lookup(
5949                &root_bytes,
5950                &StagedTreeNodes {
5951                    fs: self,
5952                    nodes: &tree_nodes,
5953                },
5954                self.sb.block_size(),
5955                lb,
5956            )? {
5957                if e.uninitialized {
5958                    preallocated = true;
5959                    break;
5960                }
5961            }
5962        }
5963        if preallocated {
5964            let (new_root, converted) = crate::extent_mut::plan_initialize_range(
5965                &root_bytes,
5966                first_lb as u32,
5967                last_lb_excl.min(u64::from(u32::MAX)) as u32,
5968            )
5969            .map_err(|e| match e {
5970                Error::CorruptExtentTree(msg)
5971                    if msg.contains("multi-level") || msg.contains("LEAF_FULL") =>
5972                {
5973                    Error::Unsupported(
5974                        "pwrite: writing into a preallocated range needs its extent split \
5975                         in a tree deeper than the inode's inline root",
5976                    )
5977                }
5978                e => e,
5979            })?;
5980            root_bytes = new_root;
5981            newly_alloc.extend(converted.into_iter().map(u64::from));
5982        }
5983
5984        // Phase 1: walk affected logical blocks; allocate each contiguous
5985        // unmapped run as one physical extent and stage the bitmap/BGD
5986        // updates. Repeated `map_logical` calls re-parse `root_bytes` each
5987        // time, so the in-progress inserts are visible to subsequent
5988        // lookups in the same loop.
5989        let mut lb = first_lb;
5990        while lb < last_lb_excl {
5991            let mapped = crate::extent::map_logical(
5992                &root_bytes,
5993                &StagedTreeNodes {
5994                    fs: self,
5995                    nodes: &tree_nodes,
5996                },
5997                self.sb.block_size(),
5998                lb,
5999            )?;
6000            if mapped.is_some() {
6001                lb += 1;
6002                continue;
6003            }
6004            // Find the end of this unmapped run.
6005            let mut run_end = lb + 1;
6006            while run_end < last_lb_excl {
6007                let p = crate::extent::map_logical(
6008                    &root_bytes,
6009                    &StagedTreeNodes {
6010                        fs: self,
6011                        nodes: &tree_nodes,
6012                    },
6013                    self.sb.block_size(),
6014                    run_end,
6015                )?;
6016                if p.is_some() {
6017                    break;
6018                }
6019                run_end += 1;
6020            }
6021            let run_len_u64 = run_end - lb;
6022            if run_len_u64 > u32::MAX as u64 {
6023                return Err(Error::InvalidArgument(
6024                    "pwrite: unmapped run exceeds u32 block count",
6025                ));
6026            }
6027
6028            // Allocate physical blocks for this logical run, splitting
6029            // across smaller contiguous physical runs when no single
6030            // group has a free run that size. Each sub-allocation is
6031            // staged into the buffer (bitmap + BGD) and inserted as its
6032            // own extent. plan_insert_extent auto-merges adjacent extents
6033            // so the *common* sequential-write case still produces one
6034            // extent overall.
6035            let mut remaining_in_run = run_len_u64 as u32;
6036            let mut sub_lb = lb;
6037            while remaining_in_run > 0 {
6038                let mut want = remaining_in_run;
6039                let plan = loop {
6040                    let plan_result =
6041                        self.plan_buffered_block_allocation(&buf, want, group_idx_of_inode);
6042                    match plan_result {
6043                        Ok(p) => break p,
6044                        Err(Error::Corrupt(msg)) if msg.contains("contiguous free run") => {
6045                            if want == 1 {
6046                                // Even a single block isn't available
6047                                // anywhere — true ENOSPC.
6048                                return Err(Error::NoSpaceLeftOnDevice);
6049                            }
6050                            // Fragmented: halve the request and retry.
6051                            // Each successful sub-run becomes its own
6052                            // extent; the outer while loop keeps drawing
6053                            // until the whole logical run is covered.
6054                            want /= 2;
6055                        }
6056                        Err(e) => return Err(e),
6057                    }
6058                };
6059
6060                let got = want;
6061                let got_u64 = got as u64;
6062
6063                self.buffer_mark_block_run_used(&mut buf, plan.first_block, got_u64)?;
6064                self.buffer_patch_bgd_counters(
6065                    &mut buf,
6066                    plan.bgd.group_idx as usize,
6067                    plan.bgd.free_blocks_delta,
6068                    plan.bgd.free_inodes_delta,
6069                    plan.bgd.used_dirs_delta,
6070                )?;
6071                alloc_total_blocks += got_u64;
6072
6073                let new_extent = crate::extent::Extent {
6074                    logical_block: sub_lb as u32,
6075                    length: got as u16,
6076                    physical_block: plan.first_block,
6077                    uninitialized: false,
6078                };
6079
6080                // Try the inline-root insert first; on overflow fall back
6081                // to the depth-promoting deep insert. Both paths produce a
6082                // new 60-byte root that we splice into `raw` at the end.
6083                match crate::extent_mut::plan_insert_extent(&root_bytes, new_extent) {
6084                    Ok(muts) => {
6085                        for m in &muts {
6086                            if let crate::extent_mut::ExtentMutation::WriteRoot { bytes } = m {
6087                                root_bytes = bytes.clone();
6088                            }
6089                        }
6090                    }
6091                    Err(Error::CorruptExtentTree(msg))
6092                        if msg.contains("LEAF_FULL_NEEDS_PROMOTION")
6093                            || msg.contains("multi-level tree mutation") =>
6094                    {
6095                        // Two distinct failures both route to the deep path:
6096                        // 1. Inline leaf root has 4 entries already
6097                        //    (LEAF_FULL_NEEDS_PROMOTION) → promote to depth 1.
6098                        // 2. Root has *already* been promoted on a prior
6099                        //    insert in this same call → root is an index
6100                        //    node, so the inline-leaf-only `plan_insert_extent`
6101                        //    bails with "multi-level tree mutation". The
6102                        //    deep planner descends correctly.
6103                        // Allocate tree-meta blocks one at a time via the
6104                        // same buffer-aware allocator. Each call stages a
6105                        // bitmap + BGD update so subsequent allocations
6106                        // see the just-claimed bits.
6107                        let reader = StagedTreeNodes {
6108                            fs: self,
6109                            nodes: &tree_nodes,
6110                        };
6111                        let mut meta_blocks_alloc: u64 = 0;
6112                        let inode_generation = inode.generation;
6113                        let deep_plan = {
6114                            let mut alloc_closure = || -> Result<u64> {
6115                                let p = self.plan_buffered_block_allocation(
6116                                    &buf,
6117                                    1,
6118                                    group_idx_of_inode,
6119                                )?;
6120                                self.buffer_mark_block_run_used(&mut buf, p.first_block, 1)?;
6121                                self.buffer_patch_bgd_counters(
6122                                    &mut buf,
6123                                    p.bgd.group_idx as usize,
6124                                    p.bgd.free_blocks_delta,
6125                                    0,
6126                                    0,
6127                                )?;
6128                                meta_blocks_alloc += 1;
6129                                Ok(p.first_block)
6130                            };
6131                            crate::extent_mut::plan_insert_extent_deep(
6132                                &root_bytes,
6133                                new_extent,
6134                                self.sb.block_size(),
6135                                &reader,
6136                                &mut alloc_closure,
6137                            )?
6138                        };
6139                        root_bytes = deep_plan.new_root;
6140                        for (block, bytes) in deep_plan.block_writes {
6141                            let mut bytes = bytes;
6142                            if self.csum.enabled {
6143                                self.csum
6144                                    .patch_extent_tail(ino, inode_generation, &mut bytes);
6145                            }
6146                            // Staged, never written ahead of the commit: a
6147                            // later sub-run's plan and every lookup read it
6148                            // back through `tree_nodes` (#389).
6149                            tree_nodes.insert(block, bytes.clone());
6150                            buf.put(block, bytes);
6151                        }
6152                        alloc_total_blocks += meta_blocks_alloc;
6153                    }
6154                    Err(e) => return Err(e),
6155                }
6156
6157                // Mark these logical blocks as freshly-allocated so Phase 2
6158                // writes use put() (zero-init) instead of get_mut()
6159                // (read-from-disk-and-modify).
6160                for x in sub_lb..(sub_lb + got_u64) {
6161                    newly_alloc.insert(x);
6162                }
6163
6164                sub_lb += got_u64;
6165                remaining_in_run -= got;
6166            }
6167
6168            lb = run_end;
6169        }
6170
6171        // Phase 2: splice the chunk into each affected block.
6172        let mut data_off: usize = 0;
6173        for cur_lb in first_lb..last_lb_excl {
6174            let block_byte_start = cur_lb * bs;
6175            let block_byte_end = block_byte_start + bs;
6176            let chunk_start = offset.max(block_byte_start);
6177            let chunk_end = end.min(block_byte_end);
6178            let in_block_off = (chunk_start - block_byte_start) as usize;
6179            let chunk_len = (chunk_end - chunk_start) as usize;
6180
6181            let phys = crate::extent::map_logical(
6182                &root_bytes,
6183                &StagedTreeNodes { fs: self, nodes: &tree_nodes },
6184                self.sb.block_size(),
6185                cur_lb,
6186            )?
6187            .ok_or(Error::Corrupt(
6188                "pwrite Phase 2: logical block unmapped after Phase 1 (allocator/extent insert mismatch)",
6189            ))?;
6190
6191            if newly_alloc.contains(&cur_lb) {
6192                // Fresh block: zero-init then splice. Avoids reading stale
6193                // bytes from a previously-freed extent.
6194                let mut block = vec![0u8; bs_usize];
6195                block[in_block_off..in_block_off + chunk_len]
6196                    .copy_from_slice(&data[data_off..data_off + chunk_len]);
6197                buf.put(phys, block);
6198            } else {
6199                // Existing block: read-modify-write to preserve untouched
6200                // bytes (head before `chunk_start`, tail after `chunk_end`).
6201                let block = buf.get_mut(self, phys)?;
6202                if block.len() != bs_usize {
6203                    return Err(Error::Corrupt(
6204                        "pwrite Phase 2: existing block has wrong size",
6205                    ));
6206                }
6207                block[in_block_off..in_block_off + chunk_len]
6208                    .copy_from_slice(&data[data_off..data_off + chunk_len]);
6209            }
6210
6211            data_off += chunk_len;
6212        }
6213        debug_assert_eq!(data_off, data.len());
6214
6215        // Phase 3: patch the extent root onto `raw`, update size + sectors,
6216        // recompute the inode checksum, stage the inode write.
6217        Self::patch_inode_block_area(&mut raw, &root_bytes)?;
6218        let new_size = inode.size.max(end);
6219        let new_sectors = inode
6220            .blocks
6221            .checked_add(alloc_total_blocks * sectors_per_block)
6222            .ok_or(Error::Corrupt("pwrite: i_blocks overflow"))?;
6223        self.finalize_inode_raw_after_write(ino, &mut raw, &inode, new_size, new_sectors)?;
6224        self.buffer_write_inode(&mut buf, ino, &raw)?;
6225
6226        // Phase 4: SB delta for the newly-allocated blocks.
6227        if alloc_total_blocks > 0 {
6228            self.buffer_patch_sb_counters(&mut buf, -(alloc_total_blocks as i64), 0)?;
6229        }
6230
6231        // Phase 5: commit everything atomically (journaled if available).
6232        self.commit_block_buffer(buf)?;
6233        Ok(new_size)
6234    }
6235
6236    /// Buffer-friendly variant of `finalize_inode_after_write`: patches
6237    /// size, blocks, ctime, mtime, and checksum on `raw` IN PLACE without
6238    /// writing to disk. Caller stages the result via `buffer_write_inode`
6239    /// so the inode update is atomic with the surrounding multi-block tx.
6240    fn finalize_inode_raw_after_write(
6241        &self,
6242        ino: u32,
6243        raw: &mut [u8],
6244        orig: &Inode,
6245        new_size: u64,
6246        new_sectors: u64,
6247    ) -> Result<()> {
6248        Self::patch_inode_size_and_blocks(raw, new_size, new_sectors)?;
6249        let now = self.runtime.now_unix_seconds();
6250        set_inode_time(raw, InodeTime::Ctime, now);
6251        set_inode_time(raw, InodeTime::Mtime, now);
6252        if self.csum.enabled {
6253            self.csum.patch_inode_checksum(ino, orig.generation, raw);
6254        }
6255        Ok(())
6256    }
6257
6258    /// What logical block `logical` of directory `dir` holds: part of the
6259    /// hash-tree index, or directory entries.
6260    ///
6261    /// directory.html ("Hash Tree Directories"): in a directory with the
6262    /// INDEX flag the root of the tree always occupies the first data
6263    /// block, and every interior node is disguised as one unused entry
6264    /// (inode 0) whose record length is the whole block, so a reader
6265    /// that scans entries sees nothing there. Leaves are ordinary entry
6266    /// blocks. Nothing at the end of a block takes part: a root the kernel
6267    /// grew from a linear directory keeps the old block's final twelve
6268    /// bytes, which look exactly like an entry-block checksum tail (#233).
6269    pub(crate) fn dir_block_role(dir: &Inode, logical: u64, block: &[u8]) -> DirBlockRole {
6270        let indexed = dir.flags & crate::inode::InodeFlags::INDEX.bits() != 0;
6271        if !indexed || block.len() < 8 {
6272            return DirBlockRole::Entries;
6273        }
6274        if logical == 0 {
6275            return DirBlockRole::IndexRoot;
6276        }
6277        let leading_inode = u32::from_le_bytes([block[0], block[1], block[2], block[3]]);
6278        let leading_span = usize::from(u16::from_le_bytes([block[4], block[5]]));
6279        if leading_inode == 0 && leading_span == block.len() {
6280            DirBlockRole::IndexNode
6281        } else {
6282            DirBlockRole::Entries
6283        }
6284    }
6285
6286    /// Whether [`Self::dir_block_role`] puts the block in the index.
6287    pub(crate) fn is_htree_index_block(dir: &Inode, logical: u64, block: &[u8]) -> bool {
6288        Self::dir_block_role(dir, logical, block) != DirBlockRole::Entries
6289    }
6290
6291    /// Refuse a directory block whose checksum does not verify, before
6292    /// anything reads entries out of it to edit them.
6293    ///
6294    /// The write paths walk blocks with `DirBlockIter`, which knows nothing
6295    /// of checksums, and then re-stamp the block's tail after the edit; a
6296    /// corrupt block verified nowhere would come out of `unlink`, `rename`,
6297    /// `mkdir`, `link` and the rest with a fresh, valid checksum over the
6298    /// damage. This is the check they make first. It verifies exactly
6299    /// what `dir::parse_block_verified` does on the read path.
6300    ///
6301    /// By role:
6302    /// - the index root carries the index's own checksum, and is verified
6303    ///   as an index (#233: read as an entry block, a kernel-grown root's
6304    ///   leftover tail bytes made every create in it fail);
6305    /// - an interior node is not verified here. Its shape is also the
6306    ///   shape of a tail-less leaf that begins with one empty entry, so it
6307    ///   is left to the tree walk, which verifies real nodes as it
6308    ///   descends;
6309    /// - an entry block is verified when the volume has `metadata_csum` and
6310    ///   the block carries a tail. A block without one is left as it is.
6311    fn refuse_unverified_dir_block(
6312        &self,
6313        ino: u32,
6314        dir: &Inode,
6315        logical: u64,
6316        block: &[u8],
6317    ) -> Result<()> {
6318        match Self::dir_block_role(dir, logical, block) {
6319            DirBlockRole::IndexRoot => return self.check_dx_block(ino, dir, block, true),
6320            DirBlockRole::IndexNode => return Ok(()),
6321            DirBlockRole::Entries => {}
6322        }
6323        if self.csum.enabled
6324            && crate::dir::has_csum_tail(block)
6325            && !self.csum.verify_dir_entry_tail(ino, dir.generation, block)
6326        {
6327            return Err(Error::BadChecksum {
6328                what: "directory block",
6329            });
6330        }
6331        Ok(())
6332    }
6333
6334    /// Does `name` already exist in `dir_inode`?
6335    ///
6336    /// NOT `find_entry_in_dir(..).is_ok()`. That spelling maps EVERY error to
6337    /// "absent", including the `BadChecksum` this scan now raises — so
6338    /// `mkdir` on a directory whose block was corrupt concluded the name was
6339    /// free, added an entry to the block it had just failed to verify, and
6340    /// re-stamped a valid checksum over it. Verifying the block and then
6341    /// discarding the verdict is worse than not verifying, because it reads
6342    /// as protection.
6343    ///
6344    /// FOUR SITES DID IT, AND A GREP FINDS THREE. `plan_new_inode_in_dir`,
6345    /// `apply_mkdir` and `apply_link` wrote `.is_ok()`; `apply_rename` wrote
6346    /// `.ok()` on the destination check, which discards identically. The
6347    /// fourth is handled where it is, because rename needs the inode number
6348    /// rather than a yes/no, but it is the same defect and it is why this
6349    /// doc comment names the count instead of leaving it to a search.
6350    ///
6351    /// Only `NotFound` means absent. Everything else propagates.
6352    fn entry_exists(&self, dir_ino: u32, dir_inode: &Inode, name: &[u8]) -> Result<bool> {
6353        match self.find_entry_in_dir(dir_ino, dir_inode, name) {
6354            Ok(_) => Ok(true),
6355            Err(Error::NotFound) => Ok(false),
6356            Err(e) => Err(e),
6357        }
6358    }
6359
6360    /// Find `name` in directory `dir_inode` — scans each data block. Returns
6361    /// the inode number or `Error::NotFound`.
6362    ///
6363    /// TAKES THE INODE NUMBER as well as the inode, and only because the
6364    /// checksum seed needs it: the tail is `crc32c(seed, ino || generation ||
6365    /// block)`, so a scan that has only the `Inode` cannot verify what it is
6366    /// reading. Every caller already had the number in scope.
6367    fn find_entry_in_dir(&self, dir_ino: u32, dir_inode: &Inode, name: &[u8]) -> Result<u32> {
6368        if dir_inode.has_inline_data() {
6369            return crate::path::find_entry(
6370                self.dev.as_ref(),
6371                &self.sb,
6372                dir_ino,
6373                dir_inode,
6374                name,
6375                &self.csum,
6376            );
6377        }
6378        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
6379        let bs = self.sb.block_size();
6380        let n_blocks = dir_inode.size.div_ceil(bs as u64);
6381        for logical in 0..n_blocks {
6382            let Some(phys) = self.map_inode_logical(dir_inode, logical)? else {
6383                continue;
6384            };
6385            let block = self.read_block(phys)?;
6386            self.refuse_unverified_dir_block(dir_ino, dir_inode, logical, &block)?;
6387            for entry in crate::dir::DirBlockIter::new(&block, has_ft) {
6388                let e = entry?;
6389                if e.name == name {
6390                    return Ok(e.inode);
6391                }
6392            }
6393        }
6394        Err(Error::NotFound)
6395    }
6396
6397    /// Apply per-group counter deltas on disk for group `gi`. Positive deltas
6398    /// increase the corresponding `bg_free_*` / `bg_used_dirs` counter,
6399    /// negative deltas decrease. Recomputes the BGD csum when `metadata_csum`
6400    /// is enabled. The in-memory `self.groups` copy is NOT updated — callers
6401    /// doing a sequence of allocations should `Filesystem::mount` fresh.
6402    pub(crate) fn patch_bgd_counters(
6403        &self,
6404        gi: usize,
6405        free_blocks_delta: i32,
6406        free_inodes_delta: i32,
6407        used_dirs_delta: i32,
6408    ) -> Result<()> {
6409        let bs = self.sb.block_size() as u64;
6410        // Where the descriptor lives, META_BG or not (#73).
6411        let (bgt_block, off_in_block) = self.sb.descriptor_location(gi as u64);
6412
6413        let mut block = self.read_block(bgt_block)?;
6414        patch_bgd_counter_fields(
6415            &mut block,
6416            off_in_block,
6417            self.sb.desc_size,
6418            free_blocks_delta,
6419            free_inodes_delta,
6420            used_dirs_delta,
6421        );
6422
6423        self.restamp_group_desc_csum(&mut block[..], off_in_block, gi);
6424        self.dev.write_at(bgt_block * bs, &block)?;
6425        Ok(())
6426    }
6427
6428    /// Apply deltas to SB `s_free_blocks_count` and `s_free_inodes_count`.
6429    /// Recomputes the SB checksum when enabled. Does not mutate `self.sb`.
6430    pub(crate) fn patch_sb_counters(
6431        &self,
6432        free_blocks_delta: i64,
6433        free_inodes_delta: i32,
6434    ) -> Result<()> {
6435        // Route through the cache-coherent buffer path (which reads the SB via
6436        // read_block) so consecutive ops accumulate against the CURRENT
6437        // on-disk superblock. The old body re-read the immutable mount-time
6438        // snapshot `self.sb.raw` every call, so within a single mount each
6439        // call rewrote the SB from mount-time values — a sequence of
6440        // frees/allocs clobbered each other (e.g. directory growth froze
6441        // free_blocks at mount-1 and reset free_inodes, which e2fsck flags as
6442        // "Free blocks/inodes count wrong").
6443        let mut buf = BlockBuffer::new(self.sb.block_size());
6444        self.buffer_patch_sb_counters(&mut buf, free_blocks_delta, free_inodes_delta)?;
6445        self.commit_block_buffer(buf)?;
6446        Ok(())
6447    }
6448
6449    // -----------------------------------------------------------------------
6450    // mkdir / rmdir
6451    // -----------------------------------------------------------------------
6452
6453    /// Build an on-disk inode image for a freshly-created directory. Sets
6454    /// `S_IFDIR | mode`, `i_links_count = 2` (for `.` and the dir entry in
6455    /// the parent), `i_size = block_size` (one data block), EXTENTS flag
6456    /// with a single leaf extent mapping logical 0 → `data_phys_block`,
6457    /// timestamps = now.
6458    fn build_directory_inode(&self, ino: u32, mode: u16, data_phys_block: u64) -> Result<Vec<u8>> {
6459        use crate::inode::{
6460            OFF_BLOCKS_HI, OFF_BLOCKS_LO, OFF_LINKS_COUNT, OFF_MODE, OFF_SIZE_HI, OFF_SIZE_LO,
6461        };
6462        let mut raw = vec![0u8; self.sb.inode_size as usize];
6463
6464        let mode_bits = crate::inode::S_IFDIR | (mode & 0x0FFF);
6465        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
6466        // 2 hard links: one for "." and one for the parent's entry naming this dir.
6467        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&2u16.to_le_bytes());
6468        self.map_one_block(&mut raw, data_phys_block)?;
6469
6470        // Size = block_size (the single data block fills the dir).
6471        let bs = self.sb.block_size() as u64;
6472        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4]
6473            .copy_from_slice(&((bs & 0xFFFF_FFFF) as u32).to_le_bytes());
6474        raw[OFF_SIZE_HI..OFF_SIZE_HI + 4].copy_from_slice(&((bs >> 32) as u32).to_le_bytes());
6475
6476        let sectors = bs / 512;
6477        raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].copy_from_slice(&(sectors as u32).to_le_bytes());
6478        raw[OFF_BLOCKS_HI..OFF_BLOCKS_HI + 2]
6479            .copy_from_slice(&(((sectors >> 32) & 0xFFFF) as u16).to_le_bytes());
6480
6481        // i_extra_isize first: it says whether the epoch bits have a home.
6482        write_inode_extra_isize(&mut raw);
6483        let now = self.runtime.now_unix_seconds();
6484        write_inode_timestamps(&mut raw, now);
6485        let generation = self.runtime.next_inode_generation();
6486        write_inode_generation(&mut raw, generation);
6487        self.stamp_inode_checksum(&mut raw, ino, generation);
6488        Ok(raw)
6489    }
6490
6491    /// Seed a freshly-allocated dir block with the two canonical entries
6492    /// `.` (→ new_ino) and `..` (→ parent_ino). Handles the metadata-csum
6493    /// tail when required: the last 12 bytes are reserved, and the CRC is
6494    /// computed over everything before them.
6495    fn seed_directory_block(
6496        &self,
6497        new_ino: u32,
6498        parent_ino: u32,
6499        new_generation: u32,
6500    ) -> Result<Vec<u8>> {
6501        let bs = self.sb.block_size() as usize;
6502        let mut block = vec![0u8; bs];
6503        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
6504        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
6505        let usable = bs - reserved_tail;
6506
6507        // "." entry: rec_len = 12
6508        block[0..4].copy_from_slice(&new_ino.to_le_bytes());
6509        block[4..6].copy_from_slice(&12u16.to_le_bytes());
6510        block[6] = 1; // name_len
6511        block[7] = if has_ft {
6512            crate::dir::DirEntryType::Directory as u8
6513        } else {
6514            0
6515        };
6516        block[8] = b'.';
6517
6518        // ".." entry: rec_len absorbs the rest of the usable region.
6519        let off = 12;
6520        block[off..off + 4].copy_from_slice(&parent_ino.to_le_bytes());
6521        let rec_len = (usable - off) as u16;
6522        block[off + 4..off + 6].copy_from_slice(&rec_len.to_le_bytes());
6523        block[off + 6] = 2;
6524        block[off + 7] = if has_ft {
6525            crate::dir::DirEntryType::Directory as u8
6526        } else {
6527            0
6528        };
6529        block[off + 8] = b'.';
6530        block[off + 9] = b'.';
6531
6532        // Tail (when metadata_csum enabled): fake inode=0, rec_len=12,
6533        // name_len=0, file_type=0xDE, u32 checksum.
6534        if reserved_tail == 12 {
6535            self.csum
6536                .patch_dir_entry_tail(new_ino, new_generation, &mut block);
6537        }
6538
6539        Ok(block)
6540    }
6541
6542    /// Whether the volume carries RO_COMPAT `DIR_NLINK`, under which a
6543    /// directory `i_links_count` of 1 means "more subdirectories than the
6544    /// field can count".
6545    fn has_dir_nlink(&self) -> bool {
6546        self.sb.feature_ro_compat & features::RoCompat::DIR_NLINK.bits() != 0
6547    }
6548
6549    /// Refuse, with [`Error::TooManyLinks`], a new link to `inode` that the
6550    /// link count has no room for: [`MAX_LINKS`] for a file, and for a
6551    /// directory gaining a subdirectory the same limit unless `DIR_NLINK`
6552    /// lets the count go to 1 (see [`next_links_count`]).
6553    ///
6554    /// Called before anything is staged, so a refusal writes nothing.
6555    fn check_link_room(&self, inode: &Inode) -> Result<()> {
6556        next_links_count(inode.is_dir(), inode.links_count, 1, self.has_dir_nlink()).map(|_| ())
6557    }
6558
6559    /// Adjust `i_links_count` on a raw inode image. Recomputes CSUM.
6560    ///
6561    /// The count moves as [`next_links_count`] says: it never wraps, and a
6562    /// directory
6563    /// count of 1 stays 1.
6564    fn patch_inode_nlink(&self, ino: u32, raw: &mut [u8], inode: &Inode, delta: i32) -> Result<()> {
6565        let new_count = next_links_count(
6566            inode.is_dir(),
6567            inode.links_count,
6568            delta,
6569            self.has_dir_nlink(),
6570        )?;
6571        raw[0x1A..0x1C].copy_from_slice(&new_count.to_le_bytes());
6572        if self.csum.enabled {
6573            self.csum.patch_inode_checksum(ino, inode.generation, raw);
6574        }
6575        Ok(())
6576    }
6577
6578    /// Create a subdirectory at `path` with POSIX mode bits (low 12 bits of
6579    /// `mode`). Returns the new directory's inode number. Steps: allocate
6580    /// inode (Orlov-hinted) → allocate one data block → seed it with `.` / `..`
6581    /// → build dir inode → write inode + data block → add dir entry in parent
6582    /// → bump parent's `i_links_count` → commit BGD/SB counters.
6583    ///
6584    /// Not journaled — safe only in scratch-image contexts until transaction
6585    /// wrapping lands.
6586    pub fn apply_mkdir(&self, path: &str, mode: u16) -> Result<u32> {
6587        self.apply_mkdir_bytes(path.as_bytes(), mode)
6588    }
6589
6590    /// [`apply_mkdir`](Self::apply_mkdir) of a path given as bytes, never decoded.
6591    pub(crate) fn apply_mkdir_bytes(&self, path: &[u8], mode: u16) -> Result<u32> {
6592        self.refuse_write()?;
6593        let (parent, name) = self.resolve_new_parent(path)?;
6594        self.apply_mkdir_at(parent, name, mode)
6595    }
6596
6597    /// [`apply_mkdir`](Self::apply_mkdir) of entry `name` in directory
6598    /// `dir`. `name` is bytes and need not be UTF-8.
6599    pub fn apply_mkdir_at(&self, dir: impl Into<InodeRef>, name: &[u8], mode: u16) -> Result<u32> {
6600        self.refuse_write()?;
6601        check_new_entry_name(name)?;
6602        let dir = dir.into();
6603        let parent_ino = dir.ino;
6604        let (parent_inode, _) = self.live_dir(dir)?;
6605        if self.entry_exists(parent_ino, &parent_inode, name)? {
6606            return Err(Error::AlreadyExists);
6607        }
6608        // The new subdirectory's `..` is a link to the parent.
6609        self.check_link_room(&parent_inode)?;
6610
6611        let bs = self.sb.block_size();
6612        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
6613        let mut bitmap_reader = |block: u64| self.read_block(block);
6614
6615        // 1. Allocate inode (is_dir = true so Orlov picks a dir-friendly group).
6616        let iplan = crate::alloc::plan_inode_allocation(
6617            &self.sb,
6618            &self.allocation_groups(),
6619            true,
6620            parent_group,
6621            &mut bitmap_reader,
6622        )?;
6623        let new_ino = iplan.inode;
6624
6625        // 2. Allocate one data block for the dir contents.
6626        let bplan = crate::alloc::plan_block_allocation(
6627            &self.sb,
6628            &self.allocation_groups(),
6629            1,
6630            iplan.bgd.group_idx,
6631            &mut bitmap_reader,
6632        )?;
6633        let data_block = bplan.first_block;
6634
6635        // Multi-block transaction: inode bitmap + block bitmap + counters
6636        // + new dir inode + seeded data block + parent dir entry +
6637        // parent nlink bump, all atomic.
6638        let mut buf = BlockBuffer::new(bs);
6639        self.buffer_mark_inode_used(&mut buf, new_ino)?;
6640        self.buffer_patch_bgd_counters(
6641            &mut buf,
6642            iplan.bgd.group_idx as usize,
6643            iplan.bgd.free_blocks_delta,
6644            iplan.bgd.free_inodes_delta,
6645            iplan.bgd.used_dirs_delta,
6646        )?;
6647        self.buffer_patch_sb_counters(
6648            &mut buf,
6649            iplan.sb.free_blocks_delta,
6650            iplan.sb.free_inodes_delta,
6651        )?;
6652
6653        self.buffer_mark_block_run_used(&mut buf, data_block, 1)?;
6654        self.buffer_patch_bgd_counters(
6655            &mut buf,
6656            bplan.bgd.group_idx as usize,
6657            bplan.bgd.free_blocks_delta,
6658            bplan.bgd.free_inodes_delta,
6659            bplan.bgd.used_dirs_delta,
6660        )?;
6661        self.buffer_patch_sb_counters(
6662            &mut buf,
6663            bplan.sb.free_blocks_delta,
6664            bplan.sb.free_inodes_delta,
6665        )?;
6666
6667        let raw = self.build_directory_inode(new_ino, mode, data_block)?;
6668        let gen = u32::from_le_bytes(raw[0x64..0x68].try_into().unwrap());
6669        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
6670
6671        // Seed the data block (`.` and `..` entries) and stage it.
6672        let seed = self.seed_directory_block(new_ino, parent_ino, gen)?;
6673        buf.put(data_block, seed);
6674
6675        // Try to install the dir entry in the parent in-place first.
6676        let parent_extends = match self.buffer_add_dir_entry_inplace(
6677            &mut buf,
6678            parent_ino,
6679            &parent_inode,
6680            name,
6681            new_ino,
6682            crate::dir::DirEntryType::Directory,
6683        ) {
6684            Ok(()) => false,
6685            Err(Error::OutOfBounds) => true,
6686            Err(e) => return Err(e),
6687        };
6688
6689        if !parent_extends {
6690            // In-place add succeeded — bump parent's nlink in the same
6691            // buffer, read through it: an inline parent's new entry is in
6692            // its inode, staged there (#428).
6693            let (parent_now, mut parent_raw) = self.buffered_inode_verified(&buf, parent_ino)?;
6694            self.patch_inode_nlink(parent_ino, &mut parent_raw, &parent_now, 1)?;
6695            self.buffer_write_inode(&mut buf, parent_ino, &parent_raw)?;
6696            self.commit_block_buffer(buf)?;
6697        } else {
6698            // Parent dir is full → grow it, and bump its nlink, in the
6699            // same transaction. The parent is re-read from the buffer,
6700            // where the growth restaged it.
6701            self.extend_dir_and_add_entry(
6702                &mut buf,
6703                parent_ino,
6704                name,
6705                new_ino,
6706                crate::dir::DirEntryType::Directory,
6707            )?;
6708            let (grown_parent, mut grown_raw) = self.buffered_inode_verified(&buf, parent_ino)?;
6709            self.patch_inode_nlink(parent_ino, &mut grown_raw, &grown_parent, 1)?;
6710            self.buffer_write_inode(&mut buf, parent_ino, &grown_raw)?;
6711            self.commit_block_buffer(buf)?;
6712        }
6713
6714        Ok(new_ino)
6715    }
6716
6717    /// Create a hard link at `dst` pointing to the same inode as `src`.
6718    ///
6719    /// Semantics:
6720    /// - `src` must exist and must NOT be a directory (POSIX forbids
6721    ///   directory hardlinks to avoid reference cycles).
6722    /// - `dst`'s parent must exist and be a directory.
6723    /// - `dst` must not already exist.
6724    /// - On success the shared inode's `i_links_count` is incremented by 1.
6725    ///
6726    /// Not journaled — same caveat as other Phase-4 ops.
6727    pub fn apply_link(&self, src: &str, dst: &str) -> Result<()> {
6728        self.apply_link_bytes(src.as_bytes(), dst.as_bytes())
6729    }
6730
6731    /// [`apply_link`](Self::apply_link) of a path given as bytes, never decoded.
6732    pub(crate) fn apply_link_bytes(&self, src: &[u8], dst: &[u8]) -> Result<()> {
6733        self.refuse_write()?;
6734        let (dst_parent_path, dst_name) = split_parent_and_base(dst)?;
6735        if dst_name.len() > 255 {
6736            return Err(Error::NameTooLong);
6737        }
6738        let src_ino = self.resolve(src)?;
6739        // Before the destination is resolved, as it always was: linking a
6740        // directory is EISDIR wherever it was to go.
6741        if self.live_inode(src_ino.into())?.0.is_dir() {
6742            return Err(Error::IsADirectory);
6743        }
6744        let dst_parent = self.resolve(dst_parent_path)?;
6745        self.apply_link_at(src_ino, dst_parent, dst_name)
6746    }
6747
6748    /// [`apply_link`](Self::apply_link): a new entry `dst_name` in
6749    /// directory `dir` for the inode `target` names.
6750    pub fn apply_link_at(
6751        &self,
6752        target: impl Into<InodeRef>,
6753        dir: impl Into<InodeRef>,
6754        dst_name: &[u8],
6755    ) -> Result<()> {
6756        self.refuse_write()?;
6757        check_new_entry_name(dst_name)?;
6758        let target = target.into();
6759        let src_ino = target.ino;
6760        let (src_inode, mut src_raw) = self.live_inode(target)?;
6761        if src_inode.is_dir() {
6762            // POSIX: hard-linking a directory is forbidden. Map to EISDIR
6763            // (rather than EPERM) — matches our IsADirectory convention.
6764            return Err(Error::IsADirectory);
6765        }
6766        self.check_link_room(&src_inode)?;
6767
6768        let dir = dir.into();
6769        let dst_parent_ino = dir.ino;
6770        let (dst_parent_inode, _) = self.live_dir(dir)?;
6771        if self.entry_exists(dst_parent_ino, &dst_parent_inode, dst_name)? {
6772            return Err(Error::AlreadyExists);
6773        }
6774
6775        let dir_type = crate::dir::DirEntryType::from_mode(src_inode.mode);
6776
6777        // Build the multi-block transaction: bump nlink + add dir entry,
6778        // both staged into one buffer so a crash either applies both or
6779        // neither.
6780        let mut buf = BlockBuffer::new(self.sb.block_size());
6781        self.patch_inode_nlink(src_ino, &mut src_raw, &src_inode, 1)?;
6782        self.buffer_write_inode(&mut buf, src_ino, &src_raw)?;
6783
6784        match self.buffer_add_dir_entry_inplace(
6785            &mut buf,
6786            dst_parent_ino,
6787            &dst_parent_inode,
6788            dst_name,
6789            src_ino,
6790            dir_type,
6791        ) {
6792            Ok(()) => self.commit_block_buffer(buf),
6793            Err(Error::OutOfBounds) => {
6794                // Parent dir is full → grow it in the same transaction as
6795                // the nlink bump.
6796                self.extend_dir_and_add_entry(
6797                    &mut buf,
6798                    dst_parent_ino,
6799                    dst_name,
6800                    src_ino,
6801                    dir_type,
6802                )?;
6803                self.commit_block_buffer(buf)
6804            }
6805            Err(e) => Err(e),
6806        }
6807    }
6808
6809    /// Rename `src` → `dst` within the same filesystem.
6810    ///
6811    /// Semantics:
6812    /// - Both endpoints are within this mount.
6813    /// - Works for files and directories.
6814    /// - Cross-parent moves update the moved dir's `..` entry + bump /
6815    ///   decrement both parents' `i_links_count`.
6816    /// - Refuses to move a directory into its own subtree (cycle check).
6817    /// - Same source and dest: no-op success.
6818    /// - When dst already exists:
6819    ///     - `replace_if_exists = false` → returns `Error::AlreadyExists`.
6820    ///     - `replace_if_exists = true` → overwrites dst. See
6821    ///       "Atomicity" below for exactly how far that holds.
6822    ///       Type-compatibility rules (POSIX rename(2)):
6823    ///         * file→dir   → `Error::IsADirectory`
6824    ///         * dir→file   → `Error::NotADirectory`
6825    ///         * non-empty-dir overwrite → `Error::DirectoryNotEmpty`
6826    ///         * src and dst resolve to the same inode (hardlink) →
6827    ///           no-op success.
6828    ///       Otherwise the previous dst inode's link count is decremented
6829    ///       in the same buffer; if that drops it to zero the inode's
6830    ///       extents and slot are freed in the same atomic commit.
6831    ///
6832    /// # Atomicity
6833    ///
6834    /// Both paths stage their work into a single [`BlockBuffer`] and
6835    /// commit it through the journal, so a crash either applies the
6836    /// whole rename or none of it. That includes growing the destination
6837    /// directory, splitting a full leaf of its htree index (#302), and
6838    /// dropping that index when it has no room to route a new leaf (#347).
6839    pub fn apply_rename(&self, src: &str, dst: &str, replace_if_exists: bool) -> Result<()> {
6840        self.apply_rename_bytes(src.as_bytes(), dst.as_bytes(), replace_if_exists)
6841    }
6842
6843    /// [`apply_rename`](Self::apply_rename) of a path given as bytes, never decoded.
6844    pub(crate) fn apply_rename_bytes(
6845        &self,
6846        src: &[u8],
6847        dst: &[u8],
6848        replace_if_exists: bool,
6849    ) -> Result<()> {
6850        // The verdict only: the volume is marked not clean once the rename
6851        // is known to write (#303), in `apply_rename_at`.
6852        self.write_refusal()?;
6853        let (src_parent_path, src_name) = split_parent_and_base(src)?;
6854        let (dst_parent_path, dst_name) = split_parent_and_base(dst)?;
6855        if dst_name.len() > 255 {
6856            return Err(Error::NameTooLong);
6857        }
6858        let src_parent = self.resolve(src_parent_path)?;
6859        let dst_parent = self.resolve(dst_parent_path)?;
6860        self.apply_rename_at(
6861            src_parent,
6862            src_name,
6863            dst_parent,
6864            dst_name,
6865            replace_if_exists,
6866        )
6867    }
6868
6869    /// [`apply_rename`](Self::apply_rename) of entry `src_name` in directory
6870    /// `src_dir` to entry `dst_name` in directory `dst_dir`. Renaming an
6871    /// entry to itself succeeds and writes nothing; `.` and `..` cannot be
6872    /// renamed or replaced (`Error::InvalidArgument`).
6873    pub fn apply_rename_at(
6874        &self,
6875        src_dir: impl Into<InodeRef>,
6876        src_name: &[u8],
6877        dst_dir: impl Into<InodeRef>,
6878        dst_name: &[u8],
6879        replace_if_exists: bool,
6880    ) -> Result<()> {
6881        // The verdict only; the volume is marked not clean below, once the
6882        // rename is known to write (#303).
6883        self.write_refusal()?;
6884        check_entry_name(src_name)?;
6885        check_new_entry_name(dst_name)?;
6886        // Renaming `.` removed the directory's own entry for itself and
6887        // filed the directory a second time elsewhere.
6888        if is_dot_or_dotdot(src_name) || is_dot_or_dotdot(dst_name) {
6889            return Err(Error::InvalidArgument("rename: cannot rename . or .."));
6890        }
6891
6892        let (src_dir, dst_dir) = (src_dir.into(), dst_dir.into());
6893        let (src_parent_ino, dst_parent_ino) = (src_dir.ino, dst_dir.ino);
6894        let (src_parent_inode, _) = self.live_dir(src_dir)?;
6895        let (dst_parent_inode, _) = self.live_dir(dst_dir)?;
6896
6897        let src_ino = self.find_entry_in_dir(src_parent_ino, &src_parent_inode, src_name)?;
6898        // rename(2) of an existing name onto itself succeeds and changes
6899        // nothing. Only after both names are validated and the source is
6900        // found: a NUL name is still refused and a missing one is still
6901        // ENOENT (#303).
6902        if src_parent_ino == dst_parent_ino && src_name == dst_name {
6903            return Ok(());
6904        }
6905        self.mark_not_clean_once()?;
6906        // `.ok()` here for the same reason as `entry_exists` above: it turned
6907        // a refusal to read the block into "dst does not exist", and rename
6908        // then created it and re-stamped the block.
6909        let existing_dst_ino =
6910            match self.find_entry_in_dir(dst_parent_ino, &dst_parent_inode, dst_name) {
6911                Ok(ino) => Some(ino),
6912                Err(Error::NotFound) => None,
6913                Err(e) => return Err(e),
6914            };
6915        if existing_dst_ino.is_some() && !replace_if_exists {
6916            return Err(Error::AlreadyExists);
6917        }
6918
6919        let (src_inode, _) = self.live_inode(src_ino.into())?;
6920        let src_is_dir = src_inode.is_dir();
6921
6922        // Cycle check: moving a directory into its own subtree would cut
6923        // that subtree off from the root. Walked up the destination's `..`
6924        // chain, so it holds however the caller named the two directories.
6925        if src_is_dir && self.is_ancestor_or_self(src_ino, dst_parent_ino)? {
6926            return Err(Error::InvalidArgument(
6927                "rename: cannot move directory into its own subtree",
6928            ));
6929        }
6930
6931        // A directory arriving in a new parent adds a `..` link to it; one
6932        // that replaces a directory there leaves the count where it was.
6933        if src_is_dir && existing_dst_ino.is_none() && src_parent_ino != dst_parent_ino {
6934            self.check_link_room(&dst_parent_inode)?;
6935        }
6936
6937        // Map POSIX mode bits to the directory-entry file-type byte: every
6938        // type, not only the three this once knew, which filed a renamed
6939        // FIFO, socket or device node under type 0 (#386).
6940        let dir_type = crate::dir::DirEntryType::from_mode(src_inode.mode);
6941
6942        // ===================================================================
6943        // Replace-overwrite branch — dst already exists and caller opted in.
6944        // ===================================================================
6945        if let Some(dst_old_ino) = existing_dst_ino {
6946            // Hardlink case: src and dst already share an inode. POSIX
6947            // rename(2) requires this to be a no-op success — entry count
6948            // is unchanged, and removing src would unconditionally drop the
6949            // shared link count by one which is wrong.
6950            if dst_old_ino == src_ino {
6951                return Ok(());
6952            }
6953
6954            let (dst_old_inode, mut dst_old_raw) = self.read_inode_verified(dst_old_ino)?;
6955            let dst_is_dir = dst_old_inode.is_dir();
6956
6957            // Type compatibility — rename(2) forbids crossing the
6958            // file/directory boundary.
6959            if !src_is_dir && dst_is_dir {
6960                return Err(Error::IsADirectory);
6961            }
6962            if src_is_dir && !dst_is_dir {
6963                return Err(Error::NotADirectory);
6964            }
6965
6966            // Non-empty-dir overwrite is forbidden by POSIX. Walk every
6967            // block of dst and reject any entry that isn't `.` / `..`.
6968            if dst_is_dir && dst_old_inode.has_inline_data() {
6969                // Its entries are in its inode (#428).
6970                self.refuse_nonempty_inline_dir(dst_old_ino, &dst_old_inode, &dst_old_raw)?;
6971            } else if dst_is_dir {
6972                let bs = self.sb.block_size();
6973                let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
6974                let blocks = dst_old_inode.size.div_ceil(bs as u64);
6975                for logical in 0..blocks {
6976                    // Either mapping, as rmdir's emptiness check.
6977                    let Some(phys) = self.map_inode_logical(&dst_old_inode, logical)? else {
6978                        continue;
6979                    };
6980                    let block = self.read_block(phys)?;
6981                    // The block this branch is about to overwrite. Unverified
6982                    // here, it would be emptied and re-stamped valid.
6983                    self.refuse_unverified_dir_block(dst_old_ino, &dst_old_inode, logical, &block)?;
6984                    for entry in crate::dir::DirBlockIter::new(&block, has_ft) {
6985                        let e = entry?;
6986                        if e.name != b"." && e.name != b".." {
6987                            return Err(Error::DirectoryNotEmpty);
6988                        }
6989                    }
6990                }
6991            }
6992
6993            // Validate a destination that will be freed before any directory
6994            // growth can commit part of the rename.
6995            let destination_runs =
6996                if (dst_is_dir || dst_old_inode.links_count <= 1) && dst_old_inode.has_extents() {
6997                    self.extent_tree_runs(dst_old_ino, &dst_old_inode)?
6998                } else {
6999                    Vec::new()
7000                };
7001
7002            // Stage the whole overwrite into a single buffer so a crash
7003            // either fully replaces dst or leaves the FS in its prior
7004            // state — UNLESS the destination's htree index has to be
7005            // dropped, which commits this buffer early. See the
7006            // "Atomicity" section on this function for what that costs.
7007            let mut buf = BlockBuffer::new(self.sb.block_size());
7008
7009            // Parent link-count changes are ACCUMULATED rather than
7010            // applied where they are discovered.
7011            //
7012            // Each site used to read its parent inode back from disk and
7013            // stage a write of it. Two such sites naming the same inode
7014            // in one buffer would have the second read stale bytes and
7015            // overwrite the first's change — and the only thing
7016            // preventing that was that their branch conditions happened
7017            // to be mutually exclusive, which nothing said and nothing
7018            // enforced.
7019            //
7020            // Summing deltas and applying them once removes the hazard
7021            // instead of relying on it not being reached: every parent
7022            // is read exactly once, after every delta is known, and
7023            // written exactly once. It also turns the dir-replaces-dir
7024            // "these two cancel out" reasoning into arithmetic that
7025            // cancels, rather than a suppressed branch that has to be
7026            // kept in step with the branch it suppresses.
7027            let mut parent_nlink: BTreeMap<u32, i32> = BTreeMap::new();
7028
7029            // 1. Pop the existing dst entry from dst_parent so the
7030            //    in-place add below has somewhere to land.
7031            self.buffer_remove_dir_entry(&mut buf, dst_parent_ino, &dst_parent_inode, dst_name)?;
7032
7033            // 2. Add the new dst entry pointing at src_ino. Try in-place
7034            //    first; if no block has room, mirror the dst_extends
7035            //    fall-back from the non-replace path.
7036            let dst_extends = match self.buffer_add_dir_entry_inplace(
7037                &mut buf,
7038                dst_parent_ino,
7039                &dst_parent_inode,
7040                dst_name,
7041                src_ino,
7042                dir_type,
7043            ) {
7044                Ok(()) => false,
7045                Err(Error::OutOfBounds) => true,
7046                Err(e) => return Err(e),
7047            };
7048            if dst_extends {
7049                self.extend_dir_and_add_entry(
7050                    &mut buf,
7051                    dst_parent_ino,
7052                    dst_name,
7053                    src_ino,
7054                    dir_type,
7055                )?;
7056            }
7057
7058            // 3. Remove src entry from its parent, as step 2 left it: read
7059            //    through the buffer, for the same reason as below (#392).
7060            let (src_parent_now, _) = self.buffered_inode_verified(&buf, src_parent_ino)?;
7061            self.buffer_remove_dir_entry(&mut buf, src_parent_ino, &src_parent_now, src_name)?;
7062
7063            // 4. Cross-parent dir move: fix `..` + parent nlinks.
7064            //    For dir-replaces-dir the dst_parent gains the moved
7065            //    subdir and loses the dropped one; both deltas are
7066            //    recorded and cancel in the sum.
7067            if src_is_dir && src_parent_ino != dst_parent_ino {
7068                self.buffer_update_dotdot(&mut buf, src_ino, &src_inode, dst_parent_ino)?;
7069                *parent_nlink.entry(src_parent_ino).or_default() -= 1;
7070                *parent_nlink.entry(dst_parent_ino).or_default() += 1;
7071            }
7072
7073            // 5. Decrement dst_old_ino's link count. If it hits zero,
7074            //    free its data extents + inode slot in this same buffer.
7075            //    Directories always reap (they only ever have one external
7076            //    name in our v1 — directory hardlinks aren't supported).
7077            let new_links = dst_old_inode.links_count.saturating_sub(1);
7078            if new_links > 0 && !dst_is_dir {
7079                // Hardlinked file overwrite — just persist the new count.
7080                dst_old_raw[0x1A..0x1C].copy_from_slice(&new_links.to_le_bytes());
7081                self.finalize_inode_raw(dst_old_ino, dst_old_inode.generation, &mut dst_old_raw)?;
7082                self.buffer_write_inode(&mut buf, dst_old_ino, &dst_old_raw)?;
7083            } else {
7084                let bs = self.sb.block_size();
7085                let sectors_per_block = bs as u64 / 512;
7086                let mut freed_sectors: u64 = 0;
7087                if dst_old_inode.has_extents() {
7088                    freed_sectors +=
7089                        self.buffer_free_runs(&mut buf, &destination_runs)? * sectors_per_block;
7090                } else {
7091                    // A block-mapped file or directory being replaced: its
7092                    // blocks were left allocated.
7093                    freed_sectors +=
7094                        self.buffer_free_block_map(&mut buf, &dst_old_inode)? * sectors_per_block;
7095                }
7096                if dst_old_inode.file_acl != 0 {
7097                    freed_sectors += self
7098                        .buffer_release_xattr_block(&mut buf, dst_old_inode.file_acl)?
7099                        * sectors_per_block;
7100                }
7101
7102                self.buffer_free_inode_slot(&mut buf, dst_old_ino)?;
7103                if dst_is_dir {
7104                    // Reaped a directory → bg_used_dirs_count -= 1.
7105                    let dst_old_gi = ((dst_old_ino - 1) / self.sb.inodes_per_group) as usize;
7106                    self.buffer_patch_bgd_counters(&mut buf, dst_old_gi, 0, 0, -1)?;
7107                }
7108                let freed_blocks = freed_sectors.checked_div(sectors_per_block).unwrap_or(0);
7109                self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 1)?;
7110
7111                // Zero the inode body, set dtime = now, preserve generation.
7112                let inode_size = self.sb.inode_size as usize;
7113                let old_gen = dst_old_inode.generation;
7114                for b in &mut dst_old_raw[..inode_size] {
7115                    *b = 0;
7116                }
7117                let dtime = self.dtime_now();
7118                dst_old_raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
7119                dst_old_raw[0x64..0x68].copy_from_slice(&old_gen.to_le_bytes());
7120                self.finalize_inode_raw(dst_old_ino, old_gen, &mut dst_old_raw)?;
7121                self.buffer_write_inode(&mut buf, dst_old_ino, &dst_old_raw)?;
7122
7123                // Dir-replaces-dir: dst_parent loses the removed subdir's
7124                // `..` reference → -1 nlink. Recorded unconditionally;
7125                // when a cross-parent dir move already recorded a +1 for
7126                // the same parent, the sum is what cancels them.
7127                if dst_is_dir {
7128                    *parent_nlink.entry(dst_parent_ino).or_default() -= 1;
7129                }
7130            }
7131
7132            self.apply_parent_nlink_deltas(&mut buf, &parent_nlink)?;
7133            return self.commit_block_buffer(buf);
7134        }
7135
7136        // ===================================================================
7137        // No-overwrite path — dst doesn't exist. Mirrors the v1 behaviour.
7138        // ===================================================================
7139        // Multi-block transaction: insert dst entry + remove src entry +
7140        // (cross-parent dir) update .. + adjust parent nlinks. Atomic so
7141        // a crash either fully renames or leaves the original, including
7142        // when the destination's htree index has to be dropped (#347).
7143        let mut buf = BlockBuffer::new(self.sb.block_size());
7144        let mut parent_nlink: BTreeMap<u32, i32> = BTreeMap::new();
7145
7146        let dst_extends = match self.buffer_add_dir_entry_inplace(
7147            &mut buf,
7148            dst_parent_ino,
7149            &dst_parent_inode,
7150            dst_name,
7151            src_ino,
7152            dir_type,
7153        ) {
7154            Ok(()) => false,
7155            Err(Error::OutOfBounds) => true,
7156            Err(e) => return Err(e),
7157        };
7158
7159        if dst_extends {
7160            // Dest parent full → grow it in this transaction.
7161            self.extend_dir_and_add_entry(&mut buf, dst_parent_ino, dst_name, src_ino, dir_type)?;
7162        }
7163
7164        // The source parent as the add above left it, read through the
7165        // buffer. When it is also the destination and the add split a full
7166        // htree leaf, the upper half of that leaf -- the source entry among
7167        // it, perhaps -- now lives in a block past the size read before the
7168        // add, and removing through that stale inode missed it: a legal
7169        // rename failed with NotFound (#392). When the add dropped the index
7170        // instead (#347), the stale inode still called it indexed.
7171        let (src_parent_now, _) = self.buffered_inode_verified(&buf, src_parent_ino)?;
7172        self.buffer_remove_dir_entry(&mut buf, src_parent_ino, &src_parent_now, src_name)?;
7173
7174        if src_is_dir && src_parent_ino != dst_parent_ino {
7175            self.buffer_update_dotdot(&mut buf, src_ino, &src_inode, dst_parent_ino)?;
7176            *parent_nlink.entry(src_parent_ino).or_default() -= 1;
7177            *parent_nlink.entry(dst_parent_ino).or_default() += 1;
7178        }
7179
7180        // Read after the extend above, if there was one, so the counts
7181        // come from what is actually on disk now.
7182        self.apply_parent_nlink_deltas(&mut buf, &parent_nlink)?;
7183        self.commit_block_buffer(buf)
7184    }
7185
7186    /// True if directory `ancestor` is `dir` or lies on its `..` chain to
7187    /// the root.
7188    fn is_ancestor_or_self(&self, ancestor: u32, dir: u32) -> Result<bool> {
7189        let mut cur = dir;
7190        // A `..` chain longer than the inode table is a loop, not a tree.
7191        for _ in 0..=self.sb.inodes_count {
7192            if cur == ancestor {
7193                return Ok(true);
7194            }
7195            if cur == crate::path::EXT4_ROOT_INODE {
7196                return Ok(false);
7197            }
7198            let (inode, _) = self.read_inode_verified(cur)?;
7199            cur = crate::path::find_entry(
7200                self.dev.as_ref(),
7201                &self.sb,
7202                cur,
7203                &inode,
7204                b"..",
7205                &self.csum,
7206            )?;
7207        }
7208        Err(Error::Corrupt(
7209            "directory `..` chain does not reach the root",
7210        ))
7211    }
7212
7213    /// Apply accumulated `i_links_count` deltas, one read and one write
7214    /// per inode.
7215    ///
7216    /// The point is the "one read" half. Patching a link count means
7217    /// reading the inode, changing the field and staging the whole
7218    /// record — so two patches of the same inode staged into one buffer
7219    /// would have the second read the *pre-buffer* bytes from disk and
7220    /// write them back over the first. Summing first makes that
7221    /// impossible rather than merely unreached.
7222    ///
7223    /// A delta of zero writes nothing. That is what makes the
7224    /// dir-replaces-dir case (+1 for the arriving subdirectory, -1 for
7225    /// the departing one) come out as no write at all, without a branch
7226    /// anywhere having to know about the other.
7227    fn apply_parent_nlink_deltas(
7228        &self,
7229        buf: &mut BlockBuffer,
7230        deltas: &BTreeMap<u32, i32>,
7231    ) -> Result<()> {
7232        for (&ino, &delta) in deltas {
7233            if delta == 0 {
7234                continue;
7235            }
7236            let (inode, mut raw) = self.buffered_inode_verified(buf, ino)?;
7237            self.patch_inode_nlink(ino, &mut raw, &inode, delta)?;
7238            self.buffer_write_inode(buf, ino, &raw)?;
7239        }
7240        Ok(())
7241    }
7242
7243    /// Stage one freshly planned directory block as used: its bitmap bit
7244    /// (through `buffer_mark_block_run_used`, which also refreshes the
7245    /// block-bitmap checksum -- the bare `mark_block_run_used` +
7246    /// `patch_*_counters` sequence the directory-grow path used to run left
7247    /// that csum stale, so e2fsck reported "block bitmap does not match
7248    /// checksum" once a directory grew a block) and its BGD + SB free-count
7249    /// deltas.
7250    ///
7251    /// The block is the plan's own, and a plan for anything but one block is
7252    /// refused whole, rather than marking one block of it and applying the
7253    /// counter deltas of all of them (#333).
7254    fn buffer_dir_block_alloc(
7255        &self,
7256        buf: &mut BlockBuffer,
7257        plan: &crate::alloc::BlockAllocationPlan,
7258    ) -> Result<()> {
7259        if plan.count != 1 {
7260            return Err(Error::Corrupt(
7261                "buffer_dir_block_alloc: a directory block plan must allocate exactly one block",
7262            ));
7263        }
7264        self.buffer_mark_block_run_used(buf, plan.first_block, 1)?;
7265        self.buffer_patch_bgd_counters(
7266            buf,
7267            plan.bgd.group_idx as usize,
7268            plan.bgd.free_blocks_delta,
7269            plan.bgd.free_inodes_delta,
7270            plan.bgd.used_dirs_delta,
7271        )?;
7272        self.buffer_patch_sb_counters(buf, plan.sb.free_blocks_delta, plan.sb.free_inodes_delta)
7273    }
7274
7275    /// Plan one block for directory `parent_ino` against the bitmaps `buf`
7276    /// has staged, and stage it as used, so the next plan in the same
7277    /// transaction picks a different one.
7278    fn buffer_alloc_dir_block(&self, buf: &mut BlockBuffer, parent_ino: u32) -> Result<u64> {
7279        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
7280        let plan = self.plan_buffered_block_allocation(buf, 1, parent_group)?;
7281        self.buffer_dir_block_alloc(buf, &plan)?;
7282        Ok(plan.first_block)
7283    }
7284
7285    /// `block`, as `buf` has it staged, or as the device has it.
7286    fn buffered_block(&self, buf: &BlockBuffer, block: u64) -> Result<Vec<u8>> {
7287        match buf.dirty.get(&block) {
7288            Some(bytes) => Ok(bytes.clone()),
7289            None => self.read_block(block),
7290        }
7291    }
7292
7293    /// [`Self::read_inode_verified`] through `buf`: the inode as the open
7294    /// transaction has staged it, when it has staged its inode-table block.
7295    ///
7296    /// A directory grown inside a caller's transaction is read and restaged
7297    /// in it, and restaging the device's copy of an inode the caller has
7298    /// already patched in the buffer would put the old bytes back.
7299    fn buffered_inode_verified(&self, buf: &BlockBuffer, ino: u32) -> Result<(Inode, Vec<u8>)> {
7300        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
7301        let Some(staged) = buf.dirty.get(&block) else {
7302            return self.read_inode_verified(ino);
7303        };
7304        let off = offset as usize;
7305        let raw = staged
7306            .get(off..off + self.sb.inode_size as usize)
7307            .ok_or(Error::Corrupt("inode slice exceeds block data"))?
7308            .to_vec();
7309        let inode = Inode::parse(&raw)?;
7310        if self.csum.enabled && !self.csum.verify_inode(ino, inode.generation, &raw) {
7311            return Err(Error::BadChecksum { what: "inode" });
7312        }
7313        Ok((inode, raw))
7314    }
7315
7316    /// Recompute `raw`'s inode checksum and stage it into `buf`.
7317    fn buffer_write_dir_inode(
7318        &self,
7319        buf: &mut BlockBuffer,
7320        ino: u32,
7321        generation: u32,
7322        raw: &mut [u8],
7323    ) -> Result<()> {
7324        if self.csum.enabled {
7325            self.csum.patch_inode_checksum(ino, generation, raw);
7326        }
7327        self.buffer_write_inode(buf, ino, raw)
7328    }
7329
7330    /// Add `(name → target_ino)` to `parent_ino` by growing it, staged into
7331    /// `buf`, the caller's open transaction, which the caller then commits:
7332    /// the new entry, its inode and the growth land together.
7333    ///
7334    /// An indexed directory's full leaf is split (#195), and the split is
7335    /// staged whole (#302). Where the index cannot take the new leaf, the
7336    /// index is dropped instead, staged in `buf` too (#347), and the
7337    /// directory grows as a linear one.
7338    fn extend_dir_and_add_entry(
7339        &self,
7340        buf: &mut BlockBuffer,
7341        parent_ino: u32,
7342        name: &[u8],
7343        target_ino: u32,
7344        file_type: crate::dir::DirEntryType,
7345    ) -> Result<()> {
7346        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
7347
7348        // An indexed directory reaches here when the leaf its index picks
7349        // is full. Split that leaf where the index has room for another
7350        // entry, as the kernel does for a directory it indexed (#195); only
7351        // where it does not is the index dropped, so the block appended
7352        // below is one a linear scan finds.
7353        if self.buffer_split_htree_leaf_and_add_entry(
7354            buf, parent_ino, name, target_ino, file_type, has_ft,
7355        )? {
7356            return Ok(());
7357        }
7358        self.buffer_drop_htree_index(buf, parent_ino)?;
7359
7360        let (parent_inode, _) = self.buffered_inode_verified(buf, parent_ino)?;
7361        let block = self.seeded_dir_block(
7362            &parent_inode,
7363            parent_ino,
7364            name,
7365            target_ino,
7366            file_type,
7367            has_ft,
7368        )?;
7369        self.buffer_append_dir_block(buf, parent_ino, block)
7370    }
7371
7372    /// Split the full htree leaf that `name` routes to, add the entry to the
7373    /// half its hash belongs in, and route the new half from the parent index
7374    /// block, so e2fsck and the kernel find every name through it (#195).
7375    ///
7376    /// `Ok(false)` when this is not a split it makes, and the caller drops the
7377    /// index instead, leaving a valid linear directory: the directory is not
7378    /// indexed, or is casefolded or encrypted (hashed some way this crate does
7379    /// not), the index has more than one interior level, the parent index
7380    /// block is full, every name in the leaf has one hash, or the new entry
7381    /// does not fit the half it belongs in. Nothing is staged then.
7382    ///
7383    /// Otherwise everything is staged into `buf`: the new leaf's allocation,
7384    /// its extent, the directory's new size, the new leaf, the halved leaf
7385    /// and the routing entry. They land in one commit, so a cut anywhere
7386    /// leaves either the full leaf or both halves routed (#302). The new
7387    /// leaf used to be appended and written first, outside the transaction,
7388    /// and a cut before the commit left a block the index never referenced.
7389    fn buffer_split_htree_leaf_and_add_entry(
7390        &self,
7391        buf: &mut BlockBuffer,
7392        dir_ino: u32,
7393        name: &[u8],
7394        target_ino: u32,
7395        file_type: crate::dir::DirEntryType,
7396        has_ft: bool,
7397    ) -> Result<bool> {
7398        let (dir, _) = self.buffered_inode_verified(buf, dir_ino)?;
7399        if dir.flags & crate::inode::InodeFlags::INDEX.bits() == 0
7400            || dir.flags & (EXT4_CASEFOLD_FL | EXT4_ENCRYPT_FL) != 0
7401        {
7402            return Ok(false);
7403        }
7404        let bs = self.sb.block_size() as usize;
7405        let physical = |logical: u32| {
7406            self.map_inode_logical(&dir, u64::from(logical))?
7407                .ok_or(Error::CorruptDirEntry("htree block is not mapped"))
7408        };
7409
7410        let root_phys = physical(0)?;
7411        let root = self.buffered_block(buf, root_phys)?;
7412        self.check_dx_block(dir_ino, &dir, &root, true)?;
7413        let info = crate::htree::parse_root_info(&root)?;
7414        if info.indirect_levels > 1 {
7415            return Ok(false);
7416        }
7417        let version = crate::hash::effective_version(info.hash_version, self.sb.unsigned_hash());
7418        let hash = crate::hash::name_hash(name, version, &self.sb.hash_seed).major;
7419
7420        let (_, root_entries) = crate::htree::parse_root_entries(&root)?;
7421        let routed = crate::htree::find_entry_for_hash(&root_entries, hash).block;
7422        let (parent_phys, parent, count_offset, leaf_logical) = if info.indirect_levels == 0 {
7423            (root_phys, root, 32, routed)
7424        } else {
7425            let node_phys = physical(routed)?;
7426            let node = self.buffered_block(buf, node_phys)?;
7427            self.check_dx_block(dir_ino, &dir, &node, false)?;
7428            let (_, entries) = crate::htree::parse_node_entries(&node)?;
7429            let leaf = crate::htree::find_entry_for_hash(&entries, hash).block;
7430            (node_phys, node, 8, leaf)
7431        };
7432        if leaf_logical == 0 {
7433            return Err(Error::CorruptDirEntry(
7434                "htree routes a name to its own root",
7435            ));
7436        }
7437
7438        let leaf_phys = physical(leaf_logical)?;
7439        let leaf = self.buffered_block(buf, leaf_phys)?;
7440        let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(&leaf) {
7441            if !self
7442                .csum
7443                .verify_dir_entry_tail(dir_ino, dir.generation, &leaf)
7444            {
7445                return Err(Error::BadChecksum {
7446                    what: "directory block",
7447                });
7448            }
7449            12
7450        } else {
7451            0
7452        };
7453        let Ok(split) = crate::htree_mut::plan_leaf_split(
7454            &leaf,
7455            version,
7456            &self.sb.hash_seed,
7457            has_ft,
7458            bs,
7459            reserved_tail,
7460        ) else {
7461            return Ok(false);
7462        };
7463
7464        // Where `buffer_append_dir_block` puts the new leaf: a directory's
7465        // size is whole blocks.
7466        let new_logical = u32::try_from(dir.size.div_ceil(bs as u64))
7467            .map_err(|_| Error::Corrupt("directory too large to index another block"))?;
7468        let parent = match if count_offset == 32 {
7469            crate::htree_mut::plan_insert_dx_entry_root(
7470                &parent,
7471                32,
7472                split.split_out_hash,
7473                new_logical,
7474            )
7475        } else {
7476            crate::htree_mut::plan_insert_dx_entry_node(&parent, split.split_out_hash, new_logical)
7477        } {
7478            Ok(bytes) => bytes,
7479            // A full parent, or a bound the parent already routes.
7480            Err(Error::CorruptExtentTree(_)) => return Ok(false),
7481            Err(e) => return Err(e),
7482        };
7483
7484        let (mut left, mut right) = (split.left_bytes, split.right_bytes);
7485        let into = if hash >= split.split_out_hash {
7486            &mut right
7487        } else {
7488            &mut left
7489        };
7490        match crate::dir::add_entry_to_block(
7491            into,
7492            target_ino,
7493            name,
7494            file_type,
7495            has_ft,
7496            reserved_tail,
7497        ) {
7498            Ok(()) => {}
7499            Err(Error::OutOfBounds) => return Ok(false),
7500            Err(e) => return Err(e),
7501        }
7502        if reserved_tail == 12 {
7503            for block in [&mut left, &mut right] {
7504                self.csum
7505                    .patch_dir_entry_tail(dir_ino, dir.generation, block);
7506            }
7507        }
7508        let mut parent = parent;
7509        self.csum
7510            .patch_dx_tail(dir_ino, dir.generation, &mut parent, count_offset);
7511
7512        // The new leaf, its allocation and extent and the grown inode; then
7513        // the halved leaf and the routing entry. One transaction.
7514        self.buffer_append_dir_block(buf, dir_ino, right)?;
7515        buf.put(leaf_phys, left);
7516        buf.put(parent_phys, parent);
7517        Ok(true)
7518    }
7519
7520    /// A fresh directory block holding one entry, with its checksum tail.
7521    fn seeded_dir_block(
7522        &self,
7523        parent_inode: &Inode,
7524        parent_ino: u32,
7525        name: &[u8],
7526        target_ino: u32,
7527        file_type: crate::dir::DirEntryType,
7528        has_ft: bool,
7529    ) -> Result<Vec<u8>> {
7530        let bs = self.sb.block_size() as usize;
7531        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
7532        let mut block = vec![0u8; bs];
7533        block[4..6].copy_from_slice(&((bs - reserved_tail) as u16).to_le_bytes());
7534        crate::dir::add_entry_to_block(
7535            &mut block,
7536            target_ino,
7537            name,
7538            file_type,
7539            has_ft,
7540            reserved_tail,
7541        )?;
7542        if self.csum.enabled {
7543            self.csum
7544                .patch_dir_entry_tail(parent_ino, parent_inode.generation, &mut block);
7545        }
7546        Ok(block)
7547    }
7548
7549    /// Grow `parent_ino`'s directory by one block holding `block`, at logical
7550    /// block `size / block_size`, staged into `buf`: the allocation, the
7551    /// mapping, the block itself and the grown inode. Nothing is written
7552    /// until the caller commits `buf`, so a cut leaves all of it or none.
7553    /// Leaves any htree index alone.
7554    fn buffer_append_dir_block(
7555        &self,
7556        buf: &mut BlockBuffer,
7557        parent_ino: u32,
7558        block: Vec<u8>,
7559    ) -> Result<()> {
7560        let bs = self.sb.block_size();
7561        let bs_u64 = bs as u64;
7562
7563        let (parent_inode, mut parent_raw) = self.buffered_inode_verified(buf, parent_ino)?;
7564        if !parent_inode.is_dir() {
7565            return Err(Error::NotADirectory);
7566        }
7567        let new_logical_block = parent_inode.size.div_ceil(bs_u64);
7568        if parent_inode.flags & crate::inode::InodeFlags::EXTENTS.bits() == 0 {
7569            return self.buffer_extend_mapped_dir(
7570                buf,
7571                parent_ino,
7572                &parent_inode,
7573                &mut parent_raw,
7574                new_logical_block,
7575                block,
7576            );
7577        }
7578
7579        // 1. Allocate one fs block. Hint to parent's group.
7580        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
7581        let plan = self.plan_buffered_block_allocation(buf, 1, parent_group)?;
7582        let new_phys = plan.first_block;
7583
7584        // 2. Insert extent into parent's inline extent root. If the root is
7585        //    saturated at depth 0, promote to depth 1 by allocating a fresh
7586        //    leaf block, moving all entries into it, and writing a single
7587        //    index entry into the inline root.
7588        let new_extent = crate::extent::Extent {
7589            logical_block: new_logical_block as u32,
7590            length: 1,
7591            physical_block: new_phys,
7592            uninitialized: false,
7593        };
7594        // If the parent root is already promoted (depth ≥ 1), operate on the
7595        // leaf block directly instead of the 60-byte inline root. This keeps
7596        // the inode.block area unchanged; only the leaf-node physical block
7597        // gets rewritten.
7598        let root_header = crate::extent::ExtentHeader::parse(&parent_inode.block)?;
7599        if root_header.depth == 1 {
7600            return self.buffer_extend_dir_depth1(
7601                buf,
7602                parent_ino,
7603                &parent_inode,
7604                &mut parent_raw,
7605                block,
7606                new_extent,
7607                plan,
7608            );
7609        }
7610        if root_header.depth > 1 {
7611            return self.buffer_extend_dir_deep(
7612                buf,
7613                parent_ino,
7614                &parent_inode,
7615                &mut parent_raw,
7616                block,
7617                new_extent,
7618                plan,
7619            );
7620        }
7621
7622        // The data block is staged as used now, so the leaf-node plan on
7623        // the promotion path below picks a different one.
7624        self.buffer_dir_block_alloc(buf, &plan)?;
7625        let (new_root, promoted) =
7626            match crate::extent_mut::plan_insert_extent(&parent_inode.block, new_extent) {
7627                Ok(muts) => {
7628                    let root = muts
7629                        .into_iter()
7630                        .find_map(|m| match m {
7631                            crate::extent_mut::ExtentMutation::WriteRoot { bytes } => Some(bytes),
7632                            _ => None,
7633                        })
7634                        .ok_or(Error::Corrupt(
7635                            "extend_dir_and_add_entry: plan produced no WriteRoot",
7636                        ))?;
7637                    (root, false)
7638                }
7639                Err(Error::CorruptExtentTree(msg)) if msg.contains("LEAF_FULL_NEEDS_PROMOTION") => {
7640                    // Second allocation: the leaf node block.
7641                    let leaf_meta_phys = self.buffer_alloc_dir_block(buf, parent_ino)?;
7642                    let promo = crate::extent_mut::plan_promote_leaf(
7643                        &parent_inode.block,
7644                        new_extent,
7645                        bs as usize,
7646                        leaf_meta_phys,
7647                        self.csum.enabled,
7648                    )?;
7649                    let mut leaf = promo.leaf_bytes;
7650                    if self.csum.enabled {
7651                        self.csum
7652                            .patch_extent_tail(parent_ino, parent_inode.generation, &mut leaf);
7653                    }
7654                    buf.put(leaf_meta_phys, leaf);
7655                    (promo.new_root_bytes, true)
7656                }
7657                Err(e) => return Err(e),
7658            };
7659        Self::patch_inode_block_area(&mut parent_raw, &new_root)?;
7660
7661        // 3. Patch size (+= block_size) and i_blocks. On the promotion path
7662        //    the inode claims both the data block AND the leaf-node block.
7663        let blocks_consumed: u64 = 1 + u64::from(promoted);
7664        let new_size = parent_inode.size + bs_u64;
7665        let new_blocks = parent_inode.blocks + (bs_u64 / 512) * blocks_consumed;
7666        Self::patch_inode_size_and_blocks(&mut parent_raw, new_size, new_blocks)?;
7667
7668        // 4. The inode, with its checksum, and the block.
7669        self.buffer_write_dir_inode(buf, parent_ino, parent_inode.generation, &mut parent_raw)?;
7670        buf.put(new_phys, block);
7671        Ok(())
7672    }
7673
7674    /// [`Self::buffer_append_dir_block`] for an ext2/ext3 directory, whose
7675    /// blocks are named by `i_block`'s twelve direct pointers and then its
7676    /// single-indirect block. It read that array as an extent header and
7677    /// refused, so such a directory never grew past its first block (#89).
7678    /// A directory needing the double-indirect block is refused.
7679    fn buffer_extend_mapped_dir(
7680        &self,
7681        buf: &mut BlockBuffer,
7682        parent_ino: u32,
7683        parent_inode: &Inode,
7684        parent_raw: &mut [u8],
7685        new_logical_block: u64,
7686        block: Vec<u8>,
7687    ) -> Result<()> {
7688        use crate::inode::OFF_BLOCK;
7689        const DIRECT: u64 = 12;
7690        let bs = self.sb.block_size();
7691        let bs_u64 = bs as u64;
7692        let per_block = bs_u64 / 4;
7693        if new_logical_block >= DIRECT + per_block {
7694            return Err(Error::Unsupported(
7695                "growing an ext2/ext3 directory past its single-indirect block",
7696            ));
7697        }
7698        // Each allocation is staged as used, so the next picks another.
7699        let allocate = |buf: &mut BlockBuffer| -> Result<u32> {
7700            let phys = self.buffer_alloc_dir_block(buf, parent_ino)?;
7701            u32::try_from(phys)
7702                .map_err(|_| Error::Corrupt("directory block past a 32-bit block map"))
7703        };
7704
7705        let new_phys = allocate(buf)?;
7706        let mut blocks_consumed = 1u64;
7707        if new_logical_block < DIRECT {
7708            let at = OFF_BLOCK + 4 * new_logical_block as usize;
7709            parent_raw[at..at + 4].copy_from_slice(&new_phys.to_le_bytes());
7710        } else {
7711            let slot = OFF_BLOCK + 4 * DIRECT as usize;
7712            let mut indirect_phys =
7713                u32::from_le_bytes(parent_raw[slot..slot + 4].try_into().unwrap());
7714            let mut indirect = if indirect_phys == 0 {
7715                indirect_phys = allocate(buf)?;
7716                blocks_consumed += 1;
7717                parent_raw[slot..slot + 4].copy_from_slice(&indirect_phys.to_le_bytes());
7718                vec![0u8; bs as usize]
7719            } else {
7720                self.buffered_block(buf, u64::from(indirect_phys))?
7721            };
7722            let at = 4 * (new_logical_block - DIRECT) as usize;
7723            indirect[at..at + 4].copy_from_slice(&new_phys.to_le_bytes());
7724            buf.put(u64::from(indirect_phys), indirect);
7725        }
7726
7727        let new_size = parent_inode.size + bs_u64;
7728        let new_blocks = parent_inode.blocks + (bs_u64 / 512) * blocks_consumed;
7729        Self::patch_inode_size_and_blocks(parent_raw, new_size, new_blocks)?;
7730        self.buffer_write_dir_inode(buf, parent_ino, parent_inode.generation, parent_raw)?;
7731        buf.put(u64::from(new_phys), block);
7732        Ok(())
7733    }
7734
7735    /// Grow a directory whose extent tree is already at depth ≥ 2.
7736    /// Uses `plan_insert_extent_deep` to navigate and split the tree,
7737    /// allocating index-node blocks on demand via
7738    /// `plan_block_allocation_excluding`, which is told about the data
7739    /// block and about every meta block already handed out -- none of
7740    /// which is staged as used until the plan has succeeded.
7741    #[allow(clippy::too_many_arguments)]
7742    fn buffer_extend_dir_deep(
7743        &self,
7744        buf: &mut BlockBuffer,
7745        parent_ino: u32,
7746        parent_inode: &Inode,
7747        parent_raw: &mut [u8],
7748        block: Vec<u8>,
7749        new_extent: crate::extent::Extent,
7750        data_plan: crate::alloc::BlockAllocationPlan,
7751    ) -> Result<()> {
7752        let bs = self.sb.block_size();
7753        let bs_u64 = bs as u64;
7754        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
7755
7756        // NOTHING HERE IS STAGED AS USED UNTIL THE PLAN SUCCEEDS, SO THE
7757        // PLANNER HAS TO BE TOLD WHAT IS ALREADY SPOKEN FOR.
7758        //
7759        // The planner reads the bitmaps `buf` holds, and this function marks
7760        // nothing in them until the tree plan is done. So every call sees
7761        // the same bytes and returns the same block: measured on a fresh
7762        // 64 MiB image, three consecutive plans gave `517 517 517`.
7763        //
7764        // This used to defend itself with one equality test against
7765        // `data_block`, and the comment above it claimed the closure "skips
7766        // that block and retries once", which it never did -- it returned
7767        // `NoSpaceLeftOnDevice`. Both halves were wrong:
7768        //
7769        //   - when the planner did return `data_block`, which it does on the
7770        //     FIRST call because that is what it returned for the data page
7771        //     moments earlier, the directory grow failed with
7772        //     `NoSpaceLeftOnDevice` on a nearly empty filesystem;
7773        //   - when it did not, the test passed, the block went into
7774        //     `pending_meta`, and the NEXT call returned the same block,
7775        //     passed the same test and went in again -- two extent-tree
7776        //     nodes on one physical block, which is silent corruption.
7777        //
7778        // Adding `pending_meta` to that test would only turn the second case
7779        // into more of the first. The reservations go into the bitmap the
7780        // scan reads instead, via `plan_block_allocation_excluding`, and the
7781        // equality test is then unnecessary rather than insufficient.
7782        let mut pending_meta: Vec<crate::alloc::BlockAllocationPlan> = Vec::new();
7783
7784        let reader = FsBlockReader { fs: self };
7785        let staged: &BlockBuffer = buf;
7786        let mut alloc_fn = || -> Result<u64> {
7787            // RECOMPUTED PER CALL rather than accumulated, so the list
7788            // handed to the planner is a function of the plans that
7789            // exist -- one expression to test, and no state to get out
7790            // of step with `pending_meta`.
7791            let reserved = crate::alloc::reserved_blocks(&data_plan, &pending_meta);
7792            let meta_plan =
7793                self.plan_buffered_block_allocation_excluding(staged, 1, parent_group, &reserved)?;
7794            pending_meta.push(meta_plan);
7795            Ok(pending_meta.last().unwrap().first_block)
7796        };
7797
7798        let deep_plan = crate::extent_mut::plan_insert_extent_deep(
7799            &parent_inode.block,
7800            new_extent,
7801            bs,
7802            &reader,
7803            &mut alloc_fn,
7804        )?;
7805
7806        // The plan holds: every block it handed out is staged as used.
7807        self.buffer_dir_block_alloc(buf, &data_plan)?;
7808        for plan in &pending_meta {
7809            self.buffer_dir_block_alloc(buf, plan)?;
7810        }
7811
7812        // Tree-meta blocks (rewritten leaves + any new index nodes).
7813        for (block, mut bytes) in deep_plan.block_writes {
7814            if self.csum.enabled {
7815                self.csum
7816                    .patch_extent_tail(parent_ino, parent_inode.generation, &mut bytes);
7817            }
7818            buf.put(block, bytes);
7819        }
7820
7821        // Patch inode: root bytes, size (+1 data block), i_blocks.
7822        Self::patch_inode_block_area(parent_raw, &deep_plan.new_root)?;
7823        let new_size = parent_inode.size + bs_u64;
7824        let new_blocks = parent_inode.blocks + (bs_u64 / 512) * (1 + pending_meta.len() as u64);
7825        Self::patch_inode_size_and_blocks(parent_raw, new_size, new_blocks)?;
7826        self.buffer_write_dir_inode(buf, parent_ino, parent_inode.generation, parent_raw)?;
7827        buf.put(data_plan.first_block, block);
7828        Ok(())
7829    }
7830
7831    /// Grow a directory whose extent tree is already at depth 1 (i.e. has
7832    /// been promoted). The inline root holds a single index entry → one leaf
7833    /// block. The mutation happens entirely inside the leaf block; the inode
7834    /// root is unchanged.
7835    ///
7836    /// Leaf overflow (>340 entries in a 4 KiB block with csum) falls back to
7837    /// `buffer_extend_dir_deep`.
7838    #[allow(clippy::too_many_arguments)]
7839    fn buffer_extend_dir_depth1(
7840        &self,
7841        buf: &mut BlockBuffer,
7842        parent_ino: u32,
7843        parent_inode: &Inode,
7844        parent_raw: &mut [u8],
7845        block: Vec<u8>,
7846        new_extent: crate::extent::Extent,
7847        plan: crate::alloc::BlockAllocationPlan,
7848    ) -> Result<()> {
7849        let bs = self.sb.block_size();
7850        let bs_u64 = bs as u64;
7851
7852        // ONLY A ROOT WITH ONE INDEX ENTRY HAS A SINGLE LEAF. Once the deep
7853        // path has split that leaf the root indexes two or more, and the
7854        // new extent belongs in the last of them; appending it to the
7855        // first put logical blocks past the split where no lookup
7856        // descends, and the entry just added was not found.
7857        if crate::extent::ExtentHeader::parse(&parent_inode.block)?.entries != 1 {
7858            return self.buffer_extend_dir_deep(
7859                buf,
7860                parent_ino,
7861                parent_inode,
7862                parent_raw,
7863                block,
7864                new_extent,
7865                plan,
7866            );
7867        }
7868
7869        // Resolve the single index entry in the 60-byte inline root.
7870        let idx = crate::extent::ExtentIdx::parse(
7871            &parent_inode.block
7872                [crate::extent::EXT4_EXT_NODE_SIZE..2 * crate::extent::EXT4_EXT_NODE_SIZE],
7873        )?;
7874        let leaf_phys = idx.leaf_block;
7875
7876        // Read the leaf block + run plan_insert_extent on its 4 KiB buffer.
7877        // `plan_insert_extent` operates on any depth-0 root — it uses
7878        // `header.max` for capacity, which was set to (bs-12-4)/12 = 340
7879        // when the leaf was built by `plan_promote_leaf`.
7880        let leaf = self.buffered_block(buf, leaf_phys)?;
7881        // CRC-verify before mutating — if the leaf's tail is corrupt we'd
7882        // write a false "fixed" version back.
7883        if self.csum.enabled
7884            && !self
7885                .csum
7886                .verify_extent_tail(parent_ino, parent_inode.generation, &leaf)
7887        {
7888            return Err(Error::BadChecksum {
7889                what: "extent block",
7890            });
7891        }
7892
7893        let muts = match crate::extent_mut::plan_insert_extent(&leaf, new_extent) {
7894            Ok(muts) => muts,
7895            Err(Error::CorruptExtentTree(msg)) if msg.contains("LEAF_FULL_NEEDS_PROMOTION") => {
7896                // The single depth-1 leaf is full (≥340 extents in a 4 KiB block
7897                // with csum). Fall back to the deep path, which handles adding a
7898                // sibling leaf or promoting to depth 2. The data block hasn't
7899                // been staged yet, so pass `plan` unchanged.
7900                return self.buffer_extend_dir_deep(
7901                    buf,
7902                    parent_ino,
7903                    parent_inode,
7904                    parent_raw,
7905                    block,
7906                    new_extent,
7907                    plan,
7908                );
7909            }
7910            Err(e) => return Err(e),
7911        };
7912        let mut new_leaf = muts
7913            .into_iter()
7914            .find_map(|m| match m {
7915                crate::extent_mut::ExtentMutation::WriteRoot { bytes } => Some(bytes),
7916                _ => None,
7917            })
7918            .ok_or(Error::Corrupt(
7919                "buffer_extend_dir_depth1: plan produced no WriteRoot",
7920            ))?;
7921        if self.csum.enabled {
7922            self.csum
7923                .patch_extent_tail(parent_ino, parent_inode.generation, &mut new_leaf);
7924        }
7925        self.buffer_dir_block_alloc(buf, &plan)?;
7926        buf.put(leaf_phys, new_leaf);
7927
7928        // Inode root is unchanged — just grow size + blocks by one data block.
7929        let new_size = parent_inode.size + bs_u64;
7930        let new_blocks = parent_inode.blocks + (bs_u64 / 512);
7931        Self::patch_inode_size_and_blocks(parent_raw, new_size, new_blocks)?;
7932        self.buffer_write_dir_inode(buf, parent_ino, parent_inode.generation, parent_raw)?;
7933        buf.put(plan.first_block, block);
7934        Ok(())
7935    }
7936
7937    /// Remove an empty directory at `path`. Requires the target to contain
7938    /// only `.` and `..`. Frees the data block(s) + inode, removes the
7939    /// entry from the parent, decrements parent's `i_links_count`.
7940    pub fn apply_rmdir(&self, path: &str) -> Result<()> {
7941        self.apply_rmdir_bytes(path.as_bytes())
7942    }
7943
7944    /// [`apply_rmdir`](Self::apply_rmdir) of a path given as bytes, never decoded.
7945    pub(crate) fn apply_rmdir_bytes(&self, path: &[u8]) -> Result<()> {
7946        self.refuse_write()?;
7947        let (parent, name) = self.resolve_parent(path)?;
7948        self.apply_rmdir_at(parent, name)
7949    }
7950
7951    /// [`apply_rmdir`](Self::apply_rmdir) of entry `name` in directory
7952    /// `dir`. `.` is refused with `Error::InvalidArgument` and `..` with
7953    /// `Error::DirectoryNotEmpty`, as rmdir(2) does.
7954    pub fn apply_rmdir_at(&self, dir: impl Into<InodeRef>, name: &[u8]) -> Result<()> {
7955        self.refuse_write()?;
7956        check_entry_name(name)?;
7957        // `.` is the directory itself: removing it freed the directory
7958        // while its parent's entry still pointed at it.
7959        if name == b"." {
7960            return Err(Error::InvalidArgument("rmdir: cannot remove ."));
7961        }
7962        if name == b".." {
7963            return Err(Error::DirectoryNotEmpty);
7964        }
7965        let dir = dir.into();
7966        let parent_ino = dir.ino;
7967        let (parent_inode, _) = self.live_dir(dir)?;
7968        let target_ino = self.find_entry_in_dir(parent_ino, &parent_inode, name)?;
7969        let (target_inode, target_raw) = self.live_inode(target_ino.into())?;
7970        if !target_inode.is_dir() {
7971            return Err(Error::NotADirectory);
7972        }
7973
7974        // Empty-check: walk every block, reject if any entry is not "." or
7975        // "..". An inline directory's entries are in its inode (#428).
7976        let bs = self.sb.block_size();
7977        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
7978        let blocks = if target_inode.has_inline_data() {
7979            self.refuse_nonempty_inline_dir(target_ino, &target_inode, &target_raw)?;
7980            0
7981        } else {
7982            target_inode.size.div_ceil(bs as u64)
7983        };
7984        for logical in 0..blocks {
7985            // Either mapping: a block-mapped directory's i_block is not an
7986            // extent header.
7987            let Some(phys) = self.map_inode_logical(&target_inode, logical)? else {
7988                continue;
7989            };
7990            let block = self.read_block(phys)?;
7991            // The emptiness decision is made from this block's contents, and
7992            // the block is then freed. Unverified, a corrupt one reads as
7993            // empty or not-empty by accident.
7994            self.refuse_unverified_dir_block(target_ino, &target_inode, logical, &block)?;
7995            for entry in crate::dir::DirBlockIter::new(&block, has_ft) {
7996                let e = entry?;
7997                if e.name != b"." && e.name != b".." {
7998                    return Err(Error::DirectoryNotEmpty);
7999                }
8000            }
8001        }
8002
8003        // Multi-block transaction: free target data blocks + free inode +
8004        // remove parent's dir entry + decrement parent nlink, all atomic.
8005        let mut buf = BlockBuffer::new(bs);
8006
8007        // Free target's data blocks. Each freed run credits its own group's
8008        // BGD; SB credit accumulates and lands once below.
8009        let mut freed_blocks = if target_inode.has_extents() {
8010            let runs = self.extent_tree_runs(target_ino, &target_inode)?;
8011            self.buffer_free_runs(&mut buf, &runs)?
8012        } else {
8013            // A block-mapped directory: read as an extent header, it was
8014            // refused as a corrupt tree on a valid volume.
8015            self.buffer_free_block_map(&mut buf, &target_inode)?
8016        };
8017
8018        if target_inode.file_acl != 0 {
8019            freed_blocks += self.buffer_release_xattr_block(&mut buf, target_inode.file_acl)?;
8020        }
8021
8022        // Free the inode slot. A removed dir decrements `bg_used_dirs_count`
8023        // — buffer_free_inode_slot already credits free_inodes by +1, so we
8024        // separately patch used_dirs by -1 here.
8025        self.buffer_free_inode_slot(&mut buf, target_ino)?;
8026        let target_gi = ((target_ino - 1) / self.sb.inodes_per_group) as usize;
8027        self.buffer_patch_bgd_counters(&mut buf, target_gi, 0, 0, -1)?;
8028        // SB: free_blocks_count += freed, free_inodes_count += 1.
8029        self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 1)?;
8030
8031        // Zero the freed directory inode body (mode/links -> 0, set dtime, keep
8032        // the generation) so the slot no longer reads as a live directory.
8033        // Without this the freed inode keeps S_IFDIR + its "." / ".." and
8034        // e2fsck reports "unconnected directory inode", a stale ".." and bad
8035        // refcounts — the same cleanup apply_unlink already does for files.
8036        let inode_size = self.sb.inode_size as usize;
8037        let mut target_raw = vec![0u8; inode_size];
8038        let dtime = self.dtime_now();
8039        target_raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
8040        target_raw[0x64..0x68].copy_from_slice(&target_inode.generation.to_le_bytes());
8041        self.finalize_inode_raw(target_ino, target_inode.generation, &mut target_raw)?;
8042        self.buffer_write_inode(&mut buf, target_ino, &target_raw)?;
8043
8044        // Remove the entry from the parent directory, a block one or an
8045        // inline one (#428).
8046        match self.buffer_remove_dir_entry(&mut buf, parent_ino, &parent_inode, name) {
8047            Err(Error::NotFound) => {
8048                return Err(Error::Corrupt(
8049                    "apply_rmdir: entry disappeared mid-operation",
8050                ))
8051            }
8052            r => r?,
8053        }
8054
8055        // Parent loses the ".." reference from the removed child → nlink -1.
8056        // Read through the buffer: an inline parent's entries were just
8057        // staged in its inode.
8058        let (parent_now, mut parent_raw) = self.buffered_inode_verified(&buf, parent_ino)?;
8059        self.patch_inode_nlink(parent_ino, &mut parent_raw, &parent_now, -1)?;
8060        self.buffer_write_inode(&mut buf, parent_ino, &parent_raw)?;
8061
8062        self.commit_block_buffer(buf)
8063    }
8064}
8065
8066/// The most links one inode may have: 65,000 (inodes.html, `i_links_count`).
8067pub(crate) const MAX_LINKS: u16 = 65000;
8068
8069/// The link count `count` becomes after `delta` links are added (positive)
8070/// or dropped (negative), one at a time. The rules are the `i_links_count`
8071/// entry of the format documentation's inode table (65,000 at most; with
8072/// `DIR_NLINK` a directory past that is 1, "not known"), as the kernel
8073/// applies them:
8074///
8075/// - adding: a count already at [`MAX_LINKS`] takes no more
8076///   links ([`Error::TooManyLinks`]) -- except a directory on a `DIR_NLINK`
8077///   volume, whose count is pinned at 1, "too many to count". A directory
8078///   already at 1 stays at 1.
8079/// - dropping: a directory's count only drops while it is above 2,
8080///   so a pinned 1 is never taken to 0 (which Linux refuses to load); a
8081///   file's count stops at 0.
8082pub(crate) fn next_links_count(
8083    is_dir: bool,
8084    count: u16,
8085    delta: i32,
8086    dir_nlink: bool,
8087) -> Result<u16> {
8088    let mut count = count;
8089    for _ in 0..delta.unsigned_abs() {
8090        count = if delta > 0 {
8091            if is_dir && count == 1 {
8092                1
8093            } else if count >= MAX_LINKS {
8094                if is_dir && dir_nlink {
8095                    1
8096                } else {
8097                    return Err(Error::TooManyLinks);
8098                }
8099            } else {
8100                count + 1
8101            }
8102        } else if is_dir {
8103            if count > 2 {
8104                count - 1
8105            } else {
8106                count
8107            }
8108        } else {
8109            count.saturating_sub(1)
8110        };
8111    }
8112    Ok(count)
8113}
8114
8115#[cfg(test)]
8116mod tests {
8117    use super::*;
8118
8119    use crate::inode::{
8120        EXTRA_ISIZE_DEFAULT, INODE_SIZE_WITH_CRTIME, INODE_SIZE_WITH_EXTRA, OFF_ATIME, OFF_CRTIME,
8121        OFF_CTIME, OFF_EXTRA_ISIZE, OFF_GENERATION, OFF_MTIME,
8122    };
8123
8124    fn read_le32(buf: &[u8], off: usize) -> u32 {
8125        u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
8126    }
8127    fn read_le16(buf: &[u8], off: usize) -> u16 {
8128        u16::from_le_bytes(buf[off..off + 2].try_into().unwrap())
8129    }
8130
8131    // --- write_inode_timestamps ---
8132
8133    #[test]
8134    fn write_inode_timestamps_sets_atime_ctime_mtime() {
8135        let mut raw = vec![0u8; 256];
8136        write_inode_timestamps(&mut raw, 0x5EAD_BEEF);
8137        assert_eq!(read_le32(&raw, OFF_ATIME), 0x5EAD_BEEF);
8138        assert_eq!(read_le32(&raw, OFF_CTIME), 0x5EAD_BEEF);
8139        assert_eq!(read_le32(&raw, OFF_MTIME), 0x5EAD_BEEF);
8140    }
8141
8142    /// With no `*_extra` fields (i_extra_isize 0) a time past 2038 is
8143    /// clamped, as the kernel does, not wrapped to a negative base.
8144    #[test]
8145    fn write_inode_timestamps_clamps_past_2038_without_extra_fields() {
8146        let mut raw = vec![0u8; 256];
8147        write_inode_timestamps(&mut raw, 0xDEAD_BEEF);
8148        assert_eq!(read_le32(&raw, OFF_ATIME), i32::MAX as u32);
8149        assert_eq!(read_le32(&raw, OFF_CTIME), i32::MAX as u32);
8150        assert_eq!(read_le32(&raw, OFF_MTIME), i32::MAX as u32);
8151    }
8152
8153    /// With them, the same time keeps its epoch bits and reads back whole.
8154    #[test]
8155    fn write_inode_timestamps_keeps_epoch_bits_with_extra_fields() {
8156        let mut raw = vec![0u8; 256];
8157        write_inode_extra_isize(&mut raw);
8158        write_inode_timestamps(&mut raw, 0xDEAD_BEEF);
8159        let inode = Inode::parse(&raw).unwrap();
8160        assert_eq!(inode.atime, 0xDEAD_BEEF);
8161        assert_eq!(inode.ctime, 0xDEAD_BEEF);
8162        assert_eq!(inode.mtime, 0xDEAD_BEEF);
8163        assert_eq!(inode.crtime, 0xDEAD_BEEF);
8164    }
8165
8166    #[test]
8167    fn write_inode_timestamps_sets_crtime_when_large_enough() {
8168        let mut raw = vec![0u8; INODE_SIZE_WITH_CRTIME + 4];
8169        write_inode_extra_isize(&mut raw);
8170        write_inode_timestamps(&mut raw, 0x1234_5678);
8171        assert_eq!(read_le32(&raw, OFF_CRTIME), 0x1234_5678);
8172    }
8173
8174    #[test]
8175    fn write_inode_timestamps_skips_crtime_when_too_small() {
8176        let mut raw = vec![0xAAu8; INODE_SIZE_WITH_CRTIME - 1];
8177        write_inode_timestamps(&mut raw, 0x1234_5678);
8178        // Buffer too small for crtime — no write, no panic.
8179        // atime/ctime/mtime still set.
8180        assert_eq!(read_le32(&raw, OFF_ATIME), 0x1234_5678);
8181    }
8182
8183    #[test]
8184    fn write_inode_timestamps_zero_now() {
8185        let mut raw = vec![0xFFu8; 256];
8186        write_inode_timestamps(&mut raw, 0);
8187        assert_eq!(read_le32(&raw, OFF_ATIME), 0);
8188        assert_eq!(read_le32(&raw, OFF_CTIME), 0);
8189        assert_eq!(read_le32(&raw, OFF_MTIME), 0);
8190        assert_eq!(read_le32(&raw, OFF_CRTIME), 0);
8191    }
8192
8193    // --- write_inode_generation ---
8194
8195    #[test]
8196    fn write_inode_generation_writes_at_correct_offset() {
8197        let mut raw = vec![0u8; 256];
8198        write_inode_generation(&mut raw, 0xCAFE_BABE);
8199        assert_eq!(read_le32(&raw, OFF_GENERATION), 0xCAFE_BABE);
8200    }
8201
8202    #[test]
8203    fn write_inode_generation_overwrites_existing() {
8204        let mut raw = vec![0xFFu8; 256];
8205        write_inode_generation(&mut raw, 0);
8206        assert_eq!(read_le32(&raw, OFF_GENERATION), 0);
8207    }
8208
8209    // --- write_inode_extra_isize ---
8210
8211    #[test]
8212    fn write_inode_extra_isize_sets_default_when_large_enough() {
8213        let mut raw = vec![0u8; INODE_SIZE_WITH_EXTRA + 4];
8214        write_inode_extra_isize(&mut raw);
8215        assert_eq!(read_le16(&raw, OFF_EXTRA_ISIZE), EXTRA_ISIZE_DEFAULT);
8216    }
8217
8218    #[test]
8219    fn write_inode_extra_isize_skips_when_too_small() {
8220        let mut raw = vec![0u8; INODE_SIZE_WITH_EXTRA - 1];
8221        write_inode_extra_isize(&mut raw); // must not panic
8222                                           // No bytes should have been written — buffer too small.
8223    }
8224
8225    // --- alloc_inode_generation ---
8226
8227    #[test]
8228    fn alloc_inode_generation_produces_unique_values() {
8229        let g1 = crate::runtime::Runtime::next_inode_generation(&crate::runtime::SystemRuntime);
8230        let g2 = crate::runtime::Runtime::next_inode_generation(&crate::runtime::SystemRuntime);
8231        assert_ne!(g1, g2, "successive calls must produce distinct values");
8232    }
8233
8234    // ---------------------------------------------------------------
8235    // Orphan recovery: the two kinds of orphan
8236    // ---------------------------------------------------------------
8237    //
8238    // The kernel puts an inode on the `s_last_orphan` chain for two
8239    // different reasons (format documentation, "Orphan file"), and
8240    // `i_links_count` tells them apart. Zero means unlinked-while-open:
8241    // really delete it. Non-zero means a `truncate()` that a crash
8242    // interrupted: the file is still named by its directory entries, and
8243    // recovery is supposed to finish the truncate and leave it in place.
8244    //
8245    // These tests build both shapes on a formatted in-memory volume and
8246    // pin the two outcomes against each other.
8247
8248    struct MemDev {
8249        bytes: std::sync::Mutex<Vec<u8>>,
8250        size: u64,
8251    }
8252
8253    impl MemDev {
8254        fn new(size: u64) -> std::sync::Arc<Self> {
8255            std::sync::Arc::new(Self {
8256                bytes: std::sync::Mutex::new(vec![0u8; size as usize]),
8257                size,
8258            })
8259        }
8260    }
8261
8262    impl crate::block_io::BlockDevice for MemDev {
8263        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
8264            let b = self.bytes.lock().unwrap();
8265            let start = offset as usize;
8266            let end = start + buf.len();
8267            if end > b.len() {
8268                return Err(Error::Corrupt("MemDev: read past end"));
8269            }
8270            buf.copy_from_slice(&b[start..end]);
8271            Ok(())
8272        }
8273        fn size_bytes(&self) -> u64 {
8274            self.size
8275        }
8276        fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
8277            let mut b = self.bytes.lock().unwrap();
8278            let start = offset as usize;
8279            let end = start + buf.len();
8280            if end > b.len() {
8281                return Err(Error::Corrupt("MemDev: write past end"));
8282            }
8283            b[start..end].copy_from_slice(buf);
8284            Ok(())
8285        }
8286        fn flush(&self) -> Result<()> {
8287            Ok(())
8288        }
8289        fn is_writable(&self) -> bool {
8290            true
8291        }
8292    }
8293
8294    const BS: u32 = 4096;
8295    const VOL: u64 = 32 * 1024 * 1024;
8296
8297    fn formatted() -> std::sync::Arc<MemDev> {
8298        let dev = MemDev::new(VOL);
8299        crate::mkfs::format_filesystem(dev.as_ref(), Some("orphan"), None, VOL, BS)
8300            .expect("format");
8301        dev
8302    }
8303
8304    fn mount(dev: &std::sync::Arc<MemDev>) -> Filesystem {
8305        Filesystem::mount(dev.clone()).expect("mount")
8306    }
8307
8308    /// `/f` holding `len` bytes of 0xAA; returns its inode number.
8309    fn file_of_aa(fs: &Filesystem, len: usize) -> u32 {
8310        let ino = fs.apply_create("/f", 0o644).unwrap();
8311        fs.apply_replace_file_content("/f", &vec![0xAAu8; len])
8312            .unwrap();
8313        ino
8314    }
8315
8316    /// The file's bytes, and how many of them disagree with "0xAA outside
8317    /// `zeroed`, 0 inside it".
8318    fn bytes_wrong_after_zeroing(
8319        fs: &Filesystem,
8320        ino: u32,
8321        zeroed: std::ops::Range<usize>,
8322    ) -> usize {
8323        let (inode, _) = fs.read_inode_verified(ino).unwrap();
8324        let data = crate::file_io::read_all(fs, &inode).unwrap();
8325        data.iter()
8326            .enumerate()
8327            .filter(|(i, b)| **b != if zeroed.contains(i) { 0 } else { 0xAA })
8328            .count()
8329    }
8330
8331    /// An unaligned punch keeps the bytes outside its range (#388): the
8332    /// edge blocks are zeroed in part, not freed whole.
8333    #[test]
8334    fn an_unaligned_punch_keeps_the_bytes_around_it() {
8335        let dev = formatted();
8336        let fs = mount(&dev);
8337        let ino = file_of_aa(&fs, 8192);
8338        fs.apply_fallocate_punch_hole(ino, 100, 100).unwrap();
8339        assert_eq!(bytes_wrong_after_zeroing(&fs, ino, 100..200), 0);
8340        drop(fs);
8341        let fs = mount(&dev);
8342        assert_eq!(
8343            bytes_wrong_after_zeroing(&fs, ino, 100..200),
8344            0,
8345            "after a remount"
8346        );
8347    }
8348
8349    /// A punch spanning whole blocks frees those and zeroes the partial
8350    /// ones at each end in place (#388).
8351    #[test]
8352    fn a_punch_frees_the_whole_blocks_and_zeroes_the_edges() {
8353        let dev = formatted();
8354        let fs = mount(&dev);
8355        let ino = file_of_aa(&fs, 3 * BS as usize);
8356        drop(fs);
8357        let fs = mount(&dev); // `fs.sb` is the superblock as mounted
8358        let free_before = fs.sb.free_blocks_count;
8359        let (lo, hi) = (100usize, 2 * BS as usize + 100);
8360        fs.apply_fallocate_punch_hole(ino, lo as u64, (hi - lo) as u64)
8361            .unwrap();
8362        drop(fs);
8363        let fs = mount(&dev);
8364        assert_eq!(bytes_wrong_after_zeroing(&fs, ino, lo..hi), 0);
8365        let (inode, _) = fs.read_inode_verified(ino).unwrap();
8366        let mapped: Vec<bool> = (0..3)
8367            .map(|lb| fs.map_inode_logical(&inode, lb).unwrap().is_some())
8368            .collect();
8369        assert_eq!(
8370            mapped,
8371            [true, false, true],
8372            "only the middle block is freed"
8373        );
8374        assert_eq!(fs.sb.free_blocks_count, free_before + 1, "one block freed");
8375    }
8376
8377    /// A punch inside one block frees nothing.
8378    #[test]
8379    fn a_punch_inside_one_block_frees_nothing() {
8380        let dev = formatted();
8381        let fs = mount(&dev);
8382        let ino = file_of_aa(&fs, 2 * BS as usize);
8383        drop(fs);
8384        let fs = mount(&dev); // `fs.sb` is the superblock as mounted
8385        let free_before = fs.sb.free_blocks_count;
8386        fs.apply_fallocate_punch_hole(ino, BS as u64 + 1, BS as u64 - 2)
8387            .unwrap();
8388        drop(fs);
8389        let fs = mount(&dev);
8390        let (lo, hi) = (BS as usize + 1, 2 * BS as usize - 1);
8391        assert_eq!(bytes_wrong_after_zeroing(&fs, ino, lo..hi), 0);
8392        assert_eq!(fs.sb.free_blocks_count, free_before);
8393    }
8394
8395    /// An unaligned zero-range zeroes exactly its range (#388): it is a
8396    /// punch plus a preallocation, and inherited the punch's rounding.
8397    #[test]
8398    fn an_unaligned_zero_range_keeps_the_bytes_around_it() {
8399        let dev = formatted();
8400        let fs = mount(&dev);
8401        let ino = file_of_aa(&fs, 3 * BS as usize);
8402        let (lo, hi) = (100usize, 2 * BS as usize + 100);
8403        fs.apply_fallocate_zero_range(ino, lo as u64, (hi - lo) as u64)
8404            .unwrap();
8405        drop(fs);
8406        let fs = mount(&dev);
8407        assert_eq!(bytes_wrong_after_zeroing(&fs, ino, lo..hi), 0);
8408    }
8409
8410    /// `/f` with one block written at every other logical block from 0 to
8411    /// 10: six extents, so its tree is depth 1. Returns its inode number.
8412    fn striped_file(fs: &Filesystem) -> u32 {
8413        let ino = fs.apply_create("/f", 0o644).unwrap();
8414        for lb in [0u64, 2, 4, 6, 8, 10] {
8415            fs.apply_pwrite("/f", lb * BS as u64, &[1u8; BS as usize])
8416                .unwrap();
8417        }
8418        let (inode, _) = fs.read_inode_verified(ino).unwrap();
8419        let depth = u16::from_le_bytes(inode.block[6..8].try_into().unwrap());
8420        assert!(depth >= 1, "precondition: depth >= 1, got {depth}");
8421        ino
8422    }
8423
8424    /// Fill the volume with `/fill`, leaving no more than a few blocks free.
8425    fn fill_leaving_a_few_free(fs: &Filesystem) {
8426        let mut n = fs.sb.free_blocks_count.saturating_sub(4);
8427        loop {
8428            let r = fs.apply_create("/fill", 0o644).and_then(|_| {
8429                fs.apply_replace_file_content("/fill", &vec![0u8; (n * BS as u64) as usize])
8430            });
8431            match r {
8432                Ok(_) => break,
8433                Err(_) => {
8434                    let _ = fs.apply_unlink("/fill");
8435                    n -= 1;
8436                }
8437            }
8438        }
8439    }
8440
8441    /// A pwrite that fails with ENOSPC part-way leaves no extent on disk
8442    /// that maps blocks the bitmap still calls free (#389): the tree nodes
8443    /// it rewrote were written straight to the device, ahead of the
8444    /// transaction that never committed.
8445    #[test]
8446    fn a_pwrite_that_runs_out_of_space_leaves_the_tree_as_it_was() {
8447        let dev = formatted();
8448        let fs = mount(&dev);
8449        let ino = striped_file(&fs);
8450        drop(fs);
8451        let fs = mount(&dev);
8452        fill_leaving_a_few_free(&fs);
8453        drop(fs);
8454        let fs = mount(&dev);
8455        let left = fs.sb.free_blocks_count;
8456        let bs = BS as u64;
8457        let r = fs.apply_pwrite("/f", 20 * bs, &vec![2u8; ((left + 8) * bs) as usize]);
8458        assert!(r.is_err(), "precondition: ENOSPC ({left} free)");
8459        drop(fs);
8460        let fs = mount(&dev);
8461        let (inode, _) = fs.read_inode_verified(ino).unwrap();
8462        let mapped = fs.map_inode_logical(&inode, 20).unwrap();
8463        assert_eq!(
8464            mapped, None,
8465            "a failed pwrite left logical block 20 mapped ({left} were free)"
8466        );
8467        let data = crate::file_io::read_all(&fs, &inode).unwrap();
8468        assert_eq!(data.len(), 11 * BS as usize, "the size is unchanged");
8469    }
8470
8471    /// Two plans in one open transaction must not hand out the same blocks
8472    /// of a BLOCK_UNINIT group. The first plan's staging clears the group's
8473    /// flag only on the buffer, so a second plan that still sees the flag
8474    /// re-synthesises the bitmap from metadata alone and ignores the staged
8475    /// bits -- a data run's second sub-allocation, or an extent-tree block,
8476    /// then lands on top of the first.
8477    /// A formatted volume whose group 0 is flagged BLOCK_UNINIT. Only the
8478    /// planners are run on it: a plan synthesises the group's bitmap from its
8479    /// metadata, which is what the flag says to do.
8480    fn formatted_with_group_zero_block_uninit() -> std::sync::Arc<MemDev> {
8481        let dev = formatted();
8482        let fs = mount(&dev);
8483        let (bgt_block, off) = fs.sb.descriptor_location(0);
8484        let ds = fs.sb.desc_size as usize;
8485        let mut raw = fs.read_block(bgt_block).unwrap();
8486        let flags = u16::from_le_bytes(raw[off + 0x12..off + 0x14].try_into().unwrap())
8487            | crate::bgd::BgdFlags::BLOCK_UNINIT.bits();
8488        raw[off + 0x12..off + 0x14].copy_from_slice(&flags.to_le_bytes());
8489        let c = crate::checksum::group_desc_csum(&fs.sb, &fs.csum, 0, &raw[off..off + ds])
8490            .expect("the formatted volume checksums its descriptors");
8491        raw[off + 0x1e..off + 0x20].copy_from_slice(&c.to_le_bytes());
8492        dev.write_at(bgt_block * u64::from(BS), &raw).unwrap();
8493        drop(fs);
8494        dev
8495    }
8496
8497    #[test]
8498    fn buffered_allocations_do_not_reuse_an_uninitialized_groups_first_run() {
8499        let dev = formatted_with_group_zero_block_uninit();
8500        let fs = mount(&dev);
8501        assert!(fs.allocation_groups()[0]
8502            .flags()
8503            .contains(crate::bgd::BgdFlags::BLOCK_UNINIT));
8504        let before = dev.bytes.lock().unwrap().clone();
8505
8506        let mut buf = BlockBuffer::new(BS);
8507        let first = fs.plan_buffered_block_allocation(&buf, 4, 0).unwrap();
8508        fs.buffer_mark_block_run_used(&mut buf, first.first_block, 4)
8509            .unwrap();
8510        let next = fs.plan_buffered_block_allocation(&buf, 1, 0).unwrap();
8511        assert!(
8512            next.first_block < first.first_block || next.first_block >= first.first_block + 4,
8513            "second plan {} overlaps the staged run {}..{}",
8514            next.first_block,
8515            first.first_block,
8516            first.first_block + 4
8517        );
8518
8519        // Nothing committed: the mount still sees the group as uninit, the
8520        // device is untouched, and a fresh transaction plans as before.
8521        assert!(fs.allocation_groups()[0]
8522            .flags()
8523            .contains(crate::bgd::BgdFlags::BLOCK_UNINIT));
8524        assert!(
8525            *dev.bytes.lock().unwrap() == before,
8526            "planning wrote to the device"
8527        );
8528        drop(buf);
8529        let again = fs
8530            .plan_buffered_block_allocation(&BlockBuffer::new(BS), 4, 0)
8531            .unwrap();
8532        assert_eq!(again.first_block, first.first_block);
8533    }
8534
8535    /// The one-block allocator a punch's tree repack and an xattr unshare
8536    /// draw from must see the uninit clear its own first call staged (#291).
8537    /// A punch that splits an extent in a tree of full leaves calls it twice
8538    /// in one transaction, for a new leaf and an index node above it; the
8539    /// second call planned without the buffer's pending clear, rebuilt the
8540    /// group's bitmap from metadata and returned the leaf's block again.
8541    #[test]
8542    fn one_block_allocations_in_a_transaction_do_not_reuse_an_uninitialized_groups_block() {
8543        let dev = formatted_with_group_zero_block_uninit();
8544        let fs = mount(&dev);
8545        let mut buf = BlockBuffer::new(BS);
8546        let first = fs.buffer_allocate_block(&mut buf, 1).unwrap();
8547        let second = fs.buffer_allocate_block(&mut buf, 1).unwrap();
8548        assert_ne!(
8549            first, second,
8550            "the second allocation handed out block {first} again"
8551        );
8552    }
8553
8554    /// Set `flag` in group `gi`'s on-disk descriptor, restamping its
8555    /// checksum, behind the back of any mount.
8556    fn set_group_flag(dev: &std::sync::Arc<MemDev>, gi: u64, flag: crate::bgd::BgdFlags) {
8557        let fs = mount(dev);
8558        let bs = u64::from(fs.sb.block_size());
8559        let (bgt_block, off) = fs.sb.descriptor_location(gi);
8560        let ds = fs.sb.desc_size as usize;
8561        let mut raw = fs.read_block(bgt_block).unwrap();
8562        let flags =
8563            u16::from_le_bytes(raw[off + 0x12..off + 0x14].try_into().unwrap()) | flag.bits();
8564        raw[off + 0x12..off + 0x14].copy_from_slice(&flags.to_le_bytes());
8565        if let Some(c) =
8566            crate::checksum::group_desc_csum(&fs.sb, &fs.csum, gi as u32, &raw[off..off + ds])
8567        {
8568            raw[off + 0x1e..off + 0x20].copy_from_slice(&c.to_le_bytes());
8569        }
8570        drop(fs);
8571        dev.write_at(bgt_block * bs, &raw).unwrap();
8572    }
8573
8574    fn block_uninit(fs: &Filesystem, gi: usize) -> bool {
8575        fs.allocation_groups()[gi]
8576            .flags()
8577            .contains(crate::bgd::BgdFlags::BLOCK_UNINIT)
8578    }
8579
8580    /// Clear group `gi`'s BLOCK_UNINIT in a transaction of its own and
8581    /// commit it, which is what publishes the change to the mount.
8582    fn clear_and_publish(fs: &Filesystem, gi: usize) {
8583        let mut buf = BlockBuffer::new(fs.sb.block_size());
8584        assert!(fs
8585            .clear_bgd_uninit_flag_if_set(&mut buf, gi, BgdUninitFlag::Block)
8586            .unwrap());
8587        // No counter moves; this is what restamps the descriptor checksum.
8588        fs.buffer_patch_bgd_counters(&mut buf, gi, 0, 0, 0).unwrap();
8589        fs.commit_block_buffer(buf).unwrap();
8590    }
8591
8592    /// The descriptors the planners are handed are cached once any uninit
8593    /// flag has been cleared, rather than cloned on every call (#333). A
8594    /// cache read before a transaction publishes must not survive it: the
8595    /// planner would go on treating the group the transaction woke as
8596    /// untouched, and hand out its blocks again without reading its bitmap.
8597    #[test]
8598    fn a_published_uninit_clear_is_seen_through_the_cached_descriptors() {
8599        // 2 KiB blocks put 16,384 in a group, so 128 MiB is four groups:
8600        // more than one to wake.
8601        const MULTI: u64 = 128 * 1024 * 1024;
8602        let dev = MemDev::new(MULTI);
8603        crate::mkfs::format_filesystem(dev.as_ref(), Some("cache"), None, MULTI, 2048)
8604            .expect("format");
8605        for gi in [1, 2, 3] {
8606            set_group_flag(&dev, gi, crate::bgd::BgdFlags::BLOCK_UNINIT);
8607        }
8608        let mut fs = mount(&dev);
8609        assert!(fs.groups.len() >= 4, "{} groups", fs.groups.len());
8610        assert!(block_uninit(&fs, 1) && block_uninit(&fs, 2) && block_uninit(&fs, 3));
8611
8612        // First publish: from here on the descriptors come from the cache,
8613        // and this read fills it.
8614        clear_and_publish(&fs, 1);
8615        assert!(!block_uninit(&fs, 1));
8616        assert!(block_uninit(&fs, 2));
8617
8618        // Second publish, with the cache full.
8619        clear_and_publish(&fs, 2);
8620        assert!(!block_uninit(&fs, 1), "an earlier clear was lost");
8621        assert!(
8622            !block_uninit(&fs, 2),
8623            "a transaction published and the planners still see the group it woke as uninit"
8624        );
8625        assert!(block_uninit(&fs, 3));
8626
8627        // Re-reading the descriptors (`fresh_read`) resets what the mount
8628        // has cleared, and must drop the cache with it: the next publish
8629        // builds on the descriptors as read now.
8630        fs.fresh_read().unwrap();
8631        assert!(!block_uninit(&fs, 1) && !block_uninit(&fs, 2) && block_uninit(&fs, 3));
8632        clear_and_publish(&fs, 3);
8633        assert!(!block_uninit(&fs, 1) && !block_uninit(&fs, 2) && !block_uninit(&fs, 3));
8634    }
8635
8636    /// A directory block is one block. A plan for any other count is refused
8637    /// whole, rather than marking one block of it and applying the plan's
8638    /// counter deltas for all of them (#333).
8639    #[test]
8640    fn a_dir_block_commit_refuses_a_plan_for_more_than_one_block() {
8641        let dev = formatted();
8642        let fs = mount(&dev);
8643        let plan = fs
8644            .plan_buffered_block_allocation(&BlockBuffer::new(BS), 2, 0)
8645            .unwrap();
8646        assert_eq!(plan.count, 2);
8647        let before = dev.bytes.lock().unwrap().clone();
8648        let mut buf = BlockBuffer::new(BS);
8649        let got = fs.buffer_dir_block_alloc(&mut buf, &plan);
8650        assert!(
8651            matches!(got, Err(Error::Corrupt(_))),
8652            "a count-2 plan was staged as a directory block: {got:?}"
8653        );
8654        assert!(
8655            buf.dirty.is_empty() && buf.uninit_cleared.is_empty(),
8656            "the refused plan staged {} blocks",
8657            buf.dirty.len()
8658        );
8659        assert!(
8660            *dev.bytes.lock().unwrap() == before,
8661            "the refused plan wrote to the device"
8662        );
8663    }
8664
8665    /// #381: set_flags must not let a caller set INDEX_FL on a linear
8666    /// directory -- the driver then reads block 0 as a dx_root.
8667    #[test]
8668    fn set_flags_refuses_index_on_a_linear_directory() {
8669        let dev = formatted();
8670        let fs = mount(&dev);
8671        let d = fs.apply_mkdir("/d", 0o755).unwrap();
8672        let (i, _) = fs.read_inode_verified(d).unwrap();
8673        let r = fs.apply_set_flags("/d", i.flags | crate::inode::InodeFlags::INDEX.bits());
8674        assert!(r.is_err(), "INDEX_FL accepted on a linear directory");
8675        let (after, _) = fs.read_inode_verified(d).unwrap();
8676        assert_eq!(after.flags, i.flags, "a refused set_flags changed i_flags");
8677    }
8678
8679    /// #381: every bit outside the kernel's user-modifiable mask that
8680    /// changes how the inode's existing bytes are read is refused, set or
8681    /// cleared; the ones a caller may change still go through.
8682    #[test]
8683    fn set_flags_changes_only_user_modifiable_bits() {
8684        let dev = formatted();
8685        let fs = mount(&dev);
8686        let f = fs.apply_create("/f", 0o644).unwrap();
8687        let (i, _) = fs.read_inode_verified(f).unwrap();
8688        for (bit, name) in [
8689            (0x0000_0800u32, "ENCRYPT"),
8690            (0x0000_1000, "INDEX"),
8691            (0x0004_0000, "HUGE_FILE"),
8692            (0x0010_0000, "VERITY"),
8693            (0x0200_0000, "DAX"),
8694            (0x4000_0000, "CASEFOLD"),
8695            (0x8000_0000, "RESERVED"),
8696        ] {
8697            assert!(
8698                fs.apply_set_flags("/f", i.flags | bit).is_err(),
8699                "{name} ({bit:#x}) accepted"
8700            );
8701        }
8702        // A bit already set that the caller cannot change passes unchanged.
8703        let wanted = i.flags
8704            | crate::inode::InodeFlags::IMMUTABLE.bits()
8705            | crate::inode::InodeFlags::NOATIME.bits()
8706            | crate::inode::InodeFlags::NODUMP.bits();
8707        fs.apply_set_flags("/f", wanted).unwrap();
8708        let (after, _) = fs.read_inode_verified(f).unwrap();
8709        assert_eq!(after.flags, wanted);
8710        fs.apply_set_flags("/f", i.flags).unwrap();
8711        let (after, _) = fs.read_inode_verified(f).unwrap();
8712        assert_eq!(after.flags, i.flags);
8713    }
8714
8715    /// `Filesystem::mount_recovering` / `finish` on a journalled volume.
8716    mod checked_recovery {
8717        use super::*;
8718        use std::sync::atomic::{AtomicUsize, Ordering};
8719
8720        fn journalled() -> std::sync::Arc<MemDev> {
8721            let dev = MemDev::new(VOL);
8722            crate::mkfs::format_filesystem_with_flavor(
8723                dev.as_ref(),
8724                Some("checked"),
8725                None,
8726                VOL,
8727                BS,
8728                features::FsFlavor::Ext3,
8729            )
8730            .expect("format");
8731            dev
8732        }
8733
8734        fn on_disk(dev: &MemDev) -> Superblock {
8735            Superblock::read(dev).expect("superblock")
8736        }
8737
8738        fn needs_recovery(dev: &MemDev) -> bool {
8739            on_disk(dev).feature_incompat & features::Incompat::RECOVER.bits() != 0
8740        }
8741
8742        /// The marker is the lifecycle's promise: while the handle is
8743        /// owned, the next owner must treat the volume as in recovery.
8744        #[test]
8745        fn needs_recovery_stays_set_across_commits_until_finish() {
8746            let dev = journalled();
8747            let fs = Filesystem::mount_recovering(dev.clone()).expect("checked mount");
8748            fs.apply_create("/a", 0o644).expect("create");
8749            assert!(
8750                needs_recovery(&dev),
8751                "a commit on a checked mount cleared needs_recovery before finish"
8752            );
8753            fs.finish().expect("finish");
8754            assert!(!needs_recovery(&dev), "finish left needs_recovery set");
8755        }
8756
8757        /// A checked mount marks the volume not clean on the way in; its
8758        /// release must put back the state it found, not the state it made.
8759        #[test]
8760        fn finish_puts_back_the_clean_state_it_found() {
8761            let dev = journalled();
8762            assert_ne!(on_disk(&dev).state & crate::superblock::EXT4_VALID_FS, 0);
8763            let fs = Filesystem::mount_recovering(dev.clone()).expect("checked mount");
8764            fs.apply_create("/a", 0o644).expect("create");
8765            fs.finish().expect("finish");
8766            assert_ne!(
8767                on_disk(&dev).state & crate::superblock::EXT4_VALID_FS,
8768                0,
8769                "a finished checked mount left the volume marked not clean"
8770            );
8771        }
8772
8773        /// A MemDev whose every write and flush after `fail_at` fails.
8774        struct Failing {
8775            inner: std::sync::Arc<MemDev>,
8776            events: AtomicUsize,
8777            fail_at: AtomicUsize,
8778        }
8779
8780        impl Failing {
8781            fn gate(&self) -> Result<()> {
8782                if self.events.fetch_add(1, Ordering::SeqCst) >= self.fail_at.load(Ordering::SeqCst)
8783                {
8784                    return Err(Error::Corrupt("injected I/O failure"));
8785                }
8786                Ok(())
8787            }
8788        }
8789
8790        impl crate::block_io::BlockDevice for Failing {
8791            fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
8792                self.inner.read_at(offset, buf)
8793            }
8794            fn size_bytes(&self) -> u64 {
8795                self.inner.size_bytes()
8796            }
8797            fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
8798                self.gate()?;
8799                self.inner.write_at(offset, buf)
8800            }
8801            fn flush(&self) -> Result<()> {
8802                self.gate()
8803            }
8804            fn is_writable(&self) -> bool {
8805                true
8806            }
8807        }
8808
8809        /// Every device write and flush of a checked mount, create and
8810        /// finish, the drop included, is one `finish` answers for: a failure
8811        /// at any of them must not be reported as a clean release.
8812        #[test]
8813        fn a_failure_at_any_release_write_is_not_a_clean_finish() {
8814            let run = |fail_at: usize| {
8815                let dev = std::sync::Arc::new(Failing {
8816                    inner: journalled(),
8817                    events: AtomicUsize::new(0),
8818                    fail_at: AtomicUsize::new(fail_at),
8819                });
8820                let finished = Filesystem::mount_recovering(dev.clone())
8821                    .and_then(|fs| fs.apply_create("/a", 0o644).map(|_| fs))
8822                    .and_then(|fs| fs.finish());
8823                (finished.is_ok(), dev.events.load(Ordering::SeqCst))
8824            };
8825            let (ok, events) = run(usize::MAX);
8826            assert!(ok, "the uninterrupted run must finish");
8827            for fail_at in 0..events {
8828                assert!(
8829                    !run(fail_at).0,
8830                    "a failure at device event {fail_at} of {events} was reported as a clean finish"
8831                );
8832            }
8833        }
8834
8835        /// Once a journal commit failed mid-write, neither `finish` nor the
8836        /// drop that follows it may touch the device again.
8837        #[test]
8838        fn a_failed_commit_leaves_finish_and_drop_without_device_io() {
8839            let dev = std::sync::Arc::new(Failing {
8840                inner: journalled(),
8841                events: AtomicUsize::new(0),
8842                fail_at: AtomicUsize::new(usize::MAX),
8843            });
8844            let fs = Filesystem::mount_recovering(dev.clone()).expect("checked mount");
8845            let first = dev.events.load(Ordering::SeqCst) + 2;
8846            dev.fail_at.store(first, Ordering::SeqCst);
8847            assert!(
8848                fs.apply_create("/a", 0o644).is_err(),
8849                "the commit must fail"
8850            );
8851            let after_failure = dev.events.load(Ordering::SeqCst);
8852            assert!(
8853                fs.finish().is_err(),
8854                "a failed journal claimed a clean finish"
8855            );
8856            assert_eq!(
8857                dev.events.load(Ordering::SeqCst),
8858                after_failure,
8859                "finish or drop wrote to the device after the journal failed"
8860            );
8861        }
8862    }
8863
8864    fn resolve(fs: &Filesystem, path: &str) -> Result<u32> {
8865        let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
8866        crate::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, path)
8867    }
8868
8869    /// Put `ino` on the orphan chain with the given link count and size.
8870    ///
8871    /// `links == 0` is the unlinked-while-open shape. A non-zero `links`
8872    /// with a lowered `size` is the shape a crash during `truncate()`
8873    /// leaves: `i_size` already reduced, the extents still covering the
8874    /// old range, the inode still named by its directory entries.
8875    fn plant_orphan(fs: &Filesystem, ino: u32, links: u16, new_size: Option<u64>) {
8876        let mut buf = BlockBuffer::new(fs.sb.block_size());
8877        let (inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
8878        if let Some(size) = new_size {
8879            Filesystem::patch_inode_size_and_blocks(&mut raw, size, inode.blocks)
8880                .expect("patch size");
8881        }
8882        raw[0x1A..0x1C].copy_from_slice(&links.to_le_bytes());
8883        // dtime doubles as the "next orphan" link; zero terminates.
8884        raw[0x14..0x18].copy_from_slice(&0u32.to_le_bytes());
8885        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
8886            .expect("finalize");
8887        fs.buffer_write_inode(&mut buf, ino, &raw)
8888            .expect("write inode");
8889        fs.buffer_patch_sb_last_orphan(&mut buf, ino)
8890            .expect("patch s_last_orphan");
8891        fs.commit_block_buffer(buf).expect("commit");
8892    }
8893
8894    /// Give `ino` an external xattr block the way a security label that does
8895    /// not fit in the inode would, counted in `i_blocks`. Returns the block.
8896    fn give_xattr_block(fs: &Filesystem, ino: u32) -> u64 {
8897        let mut buf = BlockBuffer::new(BS);
8898        let xb = fs.buffer_allocate_block(&mut buf, ino).unwrap();
8899        let mut blk = vec![0u8; BS as usize];
8900        crate::xattr::plan_set_in_external_block(&mut blk, "security.selinux", &[b'z'; 32], 1)
8901            .unwrap();
8902        fs.csum.patch_xattr_block(xb, &mut blk);
8903        buf.put(xb, blk);
8904        let (inode, mut raw) = fs.read_inode_verified(ino).unwrap();
8905        Filesystem::write_file_acl(&mut raw, xb).unwrap();
8906        Filesystem::patch_inode_size_and_blocks(&mut raw, inode.size, inode.blocks + 8).unwrap();
8907        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
8908            .unwrap();
8909        fs.buffer_write_inode(&mut buf, ino, &raw).unwrap();
8910        fs.commit_block_buffer(buf).unwrap();
8911        xb
8912    }
8913
8914    /// Whether `block` is marked in use in its group's bitmap.
8915    fn block_bit_set(fs: &Filesystem, block: u64) -> bool {
8916        let rel = block - fs.sb.first_data_block as u64;
8917        let gi = (rel / fs.sb.blocks_per_group as u64) as usize;
8918        let bit = (rel % fs.sb.blocks_per_group as u64) as usize;
8919        let bm = fs.read_block(fs.groups[gi].block_bitmap).unwrap();
8920        bm[bit / 8] & (1 << (bit % 8)) != 0
8921    }
8922
8923    /// #384: orphan recovery must not read a fast symlink's target as a
8924    /// block map because it also holds an xattr block. The member is still
8925    /// reclaimed, its xattr block with it.
8926    #[test]
8927    fn recovering_an_orphaned_fast_symlink_frees_no_block_named_by_its_target() {
8928        let dev = formatted();
8929        let fs = mount(&dev);
8930        let (root, _) = fs.read_inode_verified(2).unwrap();
8931        let victim = fs.map_inode_logical(&root, 0).unwrap().unwrap();
8932        let mut tb: Vec<u8> = (victim as u32).to_le_bytes().to_vec();
8933        while tb.last() == Some(&0) {
8934            tb.pop();
8935        }
8936        assert!(
8937            tb.iter().all(|&b| b > 0 && b < 0x80),
8938            "block {victim} not expressible as ASCII"
8939        );
8940        let t = String::from_utf8(tb).unwrap();
8941        let ino = fs.apply_symlink(&t, "/l").unwrap();
8942        let xb = give_xattr_block(&fs, ino);
8943        plant_orphan(&fs, ino, 0, None);
8944        drop(fs);
8945        let fs = mount(&dev); // recovers orphans
8946        assert!(
8947            block_bit_set(&fs, victim),
8948            "block {victim} (the root directory's) was freed by orphan recovery"
8949        );
8950        assert!(!block_bit_set(&fs, xb), "the xattr block {xb} was kept");
8951        assert_eq!(fs.sb.last_orphan, 0, "the orphan chain was not drained");
8952    }
8953
8954    /// #384: a fast symlink with an xattr block whose target, read as block
8955    /// pointers, lies outside the volume is still reclaimed. It used to be
8956    /// left on the chain, and every orphan behind it with it.
8957    #[test]
8958    fn an_orphaned_fast_symlink_whose_target_is_no_block_does_not_stall_the_chain() {
8959        let dev = formatted();
8960        let fs = mount(&dev);
8961        let behind = fs.apply_create("/behind", 0o644).unwrap();
8962        plant_orphan(&fs, behind, 0, None);
8963        let ino = fs.apply_symlink("zzzz", "/l").unwrap();
8964        let xb = give_xattr_block(&fs, ino);
8965        // The symlink heads the chain; `behind` follows it.
8966        let (inode, mut raw) = fs.read_inode_verified(ino).unwrap();
8967        raw[0x1A..0x1C].copy_from_slice(&0u16.to_le_bytes());
8968        raw[0x14..0x18].copy_from_slice(&behind.to_le_bytes());
8969        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
8970            .unwrap();
8971        let mut buf = BlockBuffer::new(BS);
8972        fs.buffer_write_inode(&mut buf, ino, &raw).unwrap();
8973        fs.buffer_patch_sb_last_orphan(&mut buf, ino).unwrap();
8974        fs.commit_block_buffer(buf).unwrap();
8975        drop(fs);
8976        let fs = mount(&dev); // recovers orphans
8977        assert_eq!(fs.sb.last_orphan, 0, "the orphan chain was not drained");
8978        assert!(!block_bit_set(&fs, xb), "the xattr block {xb} was kept");
8979        for gone in [ino, behind] {
8980            let (i, _) = fs.read_inode_verified(gone).unwrap();
8981            assert_eq!(i.mode, 0, "orphan {gone} was not reclaimed");
8982        }
8983    }
8984
8985    /// #384: the same for an inline-data file holding an xattr block: its
8986    /// data bytes are not block pointers.
8987    #[test]
8988    fn recovering_an_orphaned_inline_file_frees_no_block_named_by_its_data() {
8989        let dev = formatted();
8990        set_incompat_bit(&dev, crate::features::Incompat::INLINE_DATA.bits());
8991        let fs = mount(&dev);
8992        let (root, _) = fs.read_inode_verified(2).unwrap();
8993        let victim = fs.map_inode_logical(&root, 0).unwrap().unwrap();
8994        let ino = fs.apply_create("/f", 0o644).unwrap();
8995        let (inode, mut raw) = fs.read_inode_verified(ino).unwrap();
8996        let mut flags = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
8997        flags &= !crate::inode::InodeFlags::EXTENTS.bits();
8998        flags |= crate::inode::InodeFlags::INLINE_DATA.bits();
8999        raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
9000        raw[0x28..0x64].fill(0);
9001        raw[0x28..0x2C].copy_from_slice(&(victim as u32).to_le_bytes());
9002        raw[0x04..0x08].copy_from_slice(&4u32.to_le_bytes());
9003        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
9004            .unwrap();
9005        fs.write_inode_raw(ino, &raw).unwrap();
9006        let xb = give_xattr_block(&fs, ino);
9007        plant_orphan(&fs, ino, 0, None);
9008        drop(fs);
9009        let fs = mount(&dev); // recovers orphans
9010        assert!(
9011            block_bit_set(&fs, victim),
9012            "block {victim} (the root directory's) was freed by orphan recovery"
9013        );
9014        assert!(!block_bit_set(&fs, xb), "the xattr block {xb} was kept");
9015        assert_eq!(fs.sb.last_orphan, 0, "the orphan chain was not drained");
9016    }
9017
9018    /// `s_state` straight off the device, as another handle -- or `e2fsck`
9019    /// -- would read it.
9020    fn on_disk_state(dev: &std::sync::Arc<MemDev>) -> u16 {
9021        let bytes = dev.bytes.lock().unwrap();
9022        u16::from_le_bytes([bytes[1024 + 0x3A], bytes[1024 + 0x3B]])
9023    }
9024
9025    /// A read-write mount that writes marks the volume not clean while it
9026    /// is mounted, and a drop puts it back (#85). The superblock checksum
9027    /// stays right through both writes: each remount verifies it.
9028    #[test]
9029    fn a_writing_mount_marks_the_volume_not_clean_until_it_is_dropped() {
9030        use crate::superblock::EXT4_VALID_FS;
9031        let dev = formatted();
9032        assert_ne!(
9033            on_disk_state(&dev) & EXT4_VALID_FS,
9034            0,
9035            "fixture: formatted clean"
9036        );
9037
9038        let fs = mount(&dev);
9039        assert_ne!(
9040            on_disk_state(&dev) & EXT4_VALID_FS,
9041            0,
9042            "mounting alone must not touch the superblock"
9043        );
9044        fs.apply_create("/a.txt", 0o644).expect("create");
9045        assert_eq!(
9046            on_disk_state(&dev) & EXT4_VALID_FS,
9047            0,
9048            "a volume being written must not read as cleanly unmounted"
9049        );
9050        drop(mount(&dev)); // the superblock checksum still verifies
9051        drop(fs);
9052        assert_ne!(
9053            on_disk_state(&dev) & EXT4_VALID_FS,
9054            0,
9055            "the unmount must mark the volume clean again"
9056        );
9057        drop(mount(&dev));
9058    }
9059
9060    /// A read-write mount that never writes leaves the superblock exactly
9061    /// as it found it.
9062    #[test]
9063    fn a_mount_that_does_not_write_leaves_the_superblock_alone() {
9064        let dev = formatted();
9065        let before = dev.bytes.lock().unwrap()[1024..2048].to_vec();
9066        let fs = mount(&dev);
9067        fs.read_inode_verified(2).expect("read the root");
9068        drop(fs);
9069        assert!(dev.bytes.lock().unwrap()[1024..2048] == before[..]);
9070    }
9071
9072    /// A volume that was already not clean -- a crash before this mount --
9073    /// is not declared clean by this mount's unmount. The kernel restores
9074    /// the state it mounted with, and so does this.
9075    #[test]
9076    fn an_unclean_volume_stays_unclean_after_a_writing_mount() {
9077        use crate::superblock::EXT4_VALID_FS;
9078        let dev = formatted();
9079        {
9080            let fs = mount(&dev);
9081            fs.write_superblock_state(fs.sb.state & !EXT4_VALID_FS)
9082                .expect("mark not clean, as a crash would leave it");
9083        }
9084        let fs = mount(&dev);
9085        assert!(!fs.sb.is_clean(), "fixture: mounted not clean");
9086        fs.apply_create("/a.txt", 0o644).expect("create");
9087        drop(fs);
9088        assert_eq!(on_disk_state(&dev) & EXT4_VALID_FS, 0);
9089    }
9090
9091    /// Renaming a path onto itself answers as rename(2) does -- a missing
9092    /// path is ENOENT, a NUL in the name is refused, an existing path is a
9093    /// success that changes nothing -- and none of the three marks the
9094    /// volume not clean, because none of them writes (#303).
9095    #[test]
9096    fn a_rename_onto_itself_resolves_the_path_and_writes_nothing() {
9097        use crate::superblock::EXT4_VALID_FS;
9098        let dev = formatted();
9099        {
9100            let fs = mount(&dev);
9101            fs.apply_create("/f", 0o644).expect("create");
9102        }
9103        assert_ne!(on_disk_state(&dev) & EXT4_VALID_FS, 0, "fixture: clean");
9104
9105        let fs = mount(&dev);
9106        let mut wrong = Vec::new();
9107        let got = fs.apply_rename("/missing", "/missing", false);
9108        if !matches!(got, Err(Error::NotFound)) {
9109            wrong.push(format!("a missing path onto itself: {got:?}"));
9110        }
9111        let got = fs.apply_rename("/a\0b", "/a\0b", false);
9112        if !matches!(
9113            got,
9114            Err(Error::InvalidArgument("a name cannot contain a NUL byte"))
9115        ) {
9116            wrong.push(format!("a NUL name onto itself: {got:?}"));
9117        }
9118        if on_disk_state(&dev) & EXT4_VALID_FS == 0 {
9119            wrong.push("a refused rename marked the volume not clean".into());
9120        }
9121        let before = dev.bytes.lock().unwrap().clone();
9122        let got = fs.apply_rename("/f", "/f", false);
9123        if got.is_err() {
9124            wrong.push(format!("an existing path onto itself: {got:?}"));
9125        }
9126        if *dev.bytes.lock().unwrap() != before {
9127            wrong.push("renaming an existing path onto itself wrote".into());
9128        }
9129        assert!(wrong.is_empty(), "{}", wrong.join("\n"));
9130        resolve(&fs, "/f").expect("/f is still there");
9131    }
9132
9133    /// The case this crate already handled: nothing names the inode any
9134    /// more, so recovery really does delete it. Kept as the other half of
9135    /// the pair, so the fix for the truncate case cannot be a blanket
9136    /// "leave every orphan alone".
9137    #[test]
9138    fn an_orphan_with_no_links_is_still_reclaimed() {
9139        let dev = formatted();
9140        let ino = {
9141            let fs = mount(&dev);
9142            let ino = fs.apply_create("/gone.txt", 0o644).expect("create");
9143            fs.apply_pwrite("/gone.txt", 0, &[0xAB; 4 * BS as usize])
9144                .expect("write");
9145            ino
9146        };
9147        {
9148            let fs = mount(&dev);
9149            plant_orphan(&fs, ino, 0, None);
9150        }
9151        // Recovery runs on this mount; the next one observes the result.
9152        drop(mount(&dev));
9153
9154        let fs = mount(&dev);
9155        assert!(
9156            fs.orphan_list().expect("orphan_list").is_empty(),
9157            "recovery must empty the chain"
9158        );
9159        let (inode, _) = fs.read_inode_verified(ino).expect("read inode");
9160        assert_eq!(inode.links_count, 0, "an unlinked orphan stays unlinked");
9161        assert_ne!(inode.dtime, 0, "and is stamped as deleted");
9162        assert_eq!(inode.size, 0, "its body is gone");
9163    }
9164
9165    /// A device that drops every write after the first `budget`, as power
9166    /// loss would, and reports success for them.
9167    struct CrashDev {
9168        inner: std::sync::Arc<MemDev>,
9169        budget: usize,
9170        writes: std::sync::atomic::AtomicUsize,
9171    }
9172
9173    impl crate::block_io::BlockDevice for CrashDev {
9174        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
9175            self.inner.read_at(offset, buf)
9176        }
9177        fn size_bytes(&self) -> u64 {
9178            self.inner.size_bytes()
9179        }
9180        fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
9181            let n = self
9182                .writes
9183                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
9184            if n < self.budget {
9185                self.inner.write_at(offset, buf)?;
9186            }
9187            Ok(())
9188        }
9189        fn flush(&self) -> Result<()> {
9190            Ok(())
9191        }
9192        fn is_writable(&self) -> bool {
9193            true
9194        }
9195    }
9196
9197    /// Unjournaled orphan recovery, cut after every write, never disarms
9198    /// itself before its work is on disk (#124).
9199    ///
9200    /// The unjournaled commit wrote its blocks in ascending block order, so
9201    /// the superblock -- whose cleared `s_last_orphan` says the work is done
9202    /// -- went first and the inode table last. A cut in between left an
9203    /// orphan still allocated, still holding its blocks, and named by
9204    /// nothing, so no mount would retry it; one cut later its blocks were
9205    /// free in the bitmap while the inode still mapped them.
9206    ///
9207    /// Each cut is judged twice. Straight after the crash, read-only: a
9208    /// cleared chain head must mean the inode really is deleted. Then after
9209    /// the next writable mount has had its chance to retry: the orphan must
9210    /// be gone and fsck must find nothing.
9211    #[test]
9212    fn unjournaled_orphan_recovery_survives_a_cut_after_every_write() {
9213        let dev = formatted();
9214        let planted = {
9215            let fs = mount(&dev);
9216            assert!(
9217                fs.journal.is_none(),
9218                "fixture: this path is the unjournaled one"
9219            );
9220            let ino = fs.apply_create("/gone.txt", 0o644).expect("create");
9221            fs.apply_pwrite("/gone.txt", 0, &[0xAB; 4 * BS as usize])
9222                .expect("write");
9223            ino
9224        };
9225        {
9226            // Unlinked while open: the name is gone, and only the chain
9227            // holds the inode and its blocks.
9228            let fs = mount(&dev);
9229            let (root, _) = fs.read_inode_verified(2).expect("root");
9230            let mut buf = BlockBuffer::new(fs.sb.block_size());
9231            fs.buffer_remove_dir_entry(&mut buf, 2, &root, b"gone.txt")
9232                .expect("remove the name");
9233            fs.commit_block_buffer(buf).expect("commit");
9234            plant_orphan(&fs, planted, 0, None);
9235        }
9236        let snapshot = dev.bytes.lock().unwrap().clone();
9237
9238        let mut total = None;
9239        for cut in 0.. {
9240            let image = MemDev::new(VOL);
9241            *image.bytes.lock().unwrap() = snapshot.clone();
9242            let crash = std::sync::Arc::new(CrashDev {
9243                inner: image.clone(),
9244                budget: cut,
9245                writes: std::sync::atomic::AtomicUsize::new(0),
9246            });
9247            drop(Filesystem::mount(crash.clone()).expect("mount through the cut"));
9248            let written = crash.writes.load(std::sync::atomic::Ordering::SeqCst);
9249            total.get_or_insert(written);
9250
9251            {
9252                let ro = std::sync::Arc::new(RoDev(image.clone()));
9253                let fs = Filesystem::mount(ro).expect("read-only mount of the cut image");
9254                let (inode, _) = fs.read_inode_verified(planted).expect("read orphan");
9255                if fs.sb.last_orphan == 0 {
9256                    assert!(
9257                        inode.dtime != 0 && inode.size == 0,
9258                        "cut {cut}: the chain head is cleared but the orphan is not deleted \
9259                         (dtime {}, size {}); no mount will retry it",
9260                        inode.dtime,
9261                        inode.size
9262                    );
9263                }
9264            }
9265            {
9266                // This mount retries recovery; the next one observes it,
9267                // since `orphan_list` reads the superblock as mounted.
9268                drop(mount(&image));
9269                let fs = mount(&image);
9270                assert!(
9271                    fs.orphan_list()
9272                        .unwrap_or_else(|e| panic!("cut {cut}: orphan_list: {e:?}"))
9273                        .is_empty(),
9274                    "cut {cut}: the next mount left the orphan on the chain"
9275                );
9276                // Free-count drift is allowed: a retry credits the counts a
9277                // crashed attempt already credited, and `e2fsck -p` fixes
9278                // "free blocks count wrong" silently. Anything else is not.
9279                let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).expect("audit");
9280                let serious: Vec<_> = report
9281                    .anomalies
9282                    .iter()
9283                    .filter(|a| {
9284                        !matches!(
9285                            a,
9286                            crate::fsck::Anomaly::BlockGroupFreeCountDrift { .. }
9287                                | crate::fsck::Anomaly::SuperblockFreeCountDrift { .. }
9288                        )
9289                    })
9290                    .collect();
9291                assert!(
9292                    serious.is_empty(),
9293                    "cut {cut}: after the retry, fsck finds {serious:?}"
9294                );
9295                // Reclaimed means free in the inode bitmap. A crash between
9296                // that bit and the inode table can leave the body unzeroed
9297                // with no deletion time, which `e2fsck -p` stamps silently;
9298                // with the bit clear nothing treats it as live.
9299                assert!(
9300                    !fs.inode_bit_is_set(planted).expect("inode bitmap"),
9301                    "cut {cut}: after the retry the orphan is still allocated"
9302                );
9303            }
9304            if cut >= written {
9305                break;
9306            }
9307        }
9308        assert!(
9309            total.unwrap_or(0) > 1,
9310            "the recovery commit must span several writes to cut"
9311        );
9312    }
9313
9314    /// Journaled orphan recovery, cut after every write, leaves an image
9315    /// whose next mount finishes the job (#125).
9316    ///
9317    /// `tests/orphan_recovery_crash_safety.rs`, the file this replaces, cut
9318    /// a no-op: its fixture had no orphans, so every budget interrupted a
9319    /// recovery that wrote nothing, and it skipped when the fixture was
9320    /// missing. Here the chain holds a
9321    /// real orphan on a volume with a journal, so the recovery is one
9322    /// journaled transaction, and every cut lands inside it or its
9323    /// checkpoint. Whatever the cut, the next writable mount -- replaying
9324    /// the journal and retrying the chain -- must end with the orphan
9325    /// reclaimed, fsck clean, and the volume still taking writes.
9326    ///
9327    /// The volume is this crate's ext3 flavour, the one it formats with a
9328    /// journal. Without extents, recovery reclaims an orphan's inode but
9329    /// declines to free indirect-mapped blocks (#124), so the orphan is
9330    /// planted empty.
9331    #[test]
9332    fn journaled_orphan_recovery_survives_a_cut_after_every_write() {
9333        const JVOL: u64 = 64 * 1024 * 1024;
9334        let dev = MemDev::new(JVOL);
9335        crate::mkfs::format_filesystem_with_flavor(
9336            dev.as_ref(),
9337            Some("jorphan"),
9338            None,
9339            JVOL,
9340            BS,
9341            crate::features::FsFlavor::Ext3,
9342        )
9343        .expect("format");
9344        let planted = {
9345            let fs = mount(&dev);
9346            assert!(
9347                fs.journal.is_some(),
9348                "fixture: this path is the journaled one"
9349            );
9350            let ino = fs.apply_create("/gone.txt", 0o644).expect("create");
9351            let (root, _) = fs.read_inode_verified(2).expect("root");
9352            let mut buf = BlockBuffer::new(fs.sb.block_size());
9353            fs.buffer_remove_dir_entry(&mut buf, 2, &root, b"gone.txt")
9354                .expect("remove the name");
9355            fs.commit_block_buffer(buf).expect("commit");
9356            plant_orphan(&fs, ino, 0, None);
9357            ino
9358        };
9359        let snapshot = dev.bytes.lock().unwrap().clone();
9360
9361        let mut total = None;
9362        for cut in 0.. {
9363            let image = MemDev::new(JVOL);
9364            *image.bytes.lock().unwrap() = snapshot.clone();
9365            let crash = std::sync::Arc::new(CrashDev {
9366                inner: image.clone(),
9367                budget: cut,
9368                writes: std::sync::atomic::AtomicUsize::new(0),
9369            });
9370            drop(Filesystem::mount(crash.clone()).expect("mount through the cut"));
9371            let written = crash.writes.load(std::sync::atomic::Ordering::SeqCst);
9372            total.get_or_insert(written);
9373
9374            // The retry, then a mount that observes it.
9375            drop(mount(&image));
9376            let fs = mount(&image);
9377            assert!(
9378                fs.orphan_list()
9379                    .unwrap_or_else(|e| panic!("cut {cut}: orphan_list: {e:?}"))
9380                    .is_empty(),
9381                "cut {cut}: the next mount left the orphan on the chain"
9382            );
9383            assert!(
9384                !fs.inode_bit_is_set(planted).expect("inode bitmap"),
9385                "cut {cut}: after the retry the orphan is still allocated"
9386            );
9387            let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).expect("audit");
9388            let serious: Vec<_> = report
9389                .anomalies
9390                .iter()
9391                .filter(|a| {
9392                    !matches!(
9393                        a,
9394                        crate::fsck::Anomaly::BlockGroupFreeCountDrift { .. }
9395                            | crate::fsck::Anomaly::SuperblockFreeCountDrift { .. }
9396                    )
9397                })
9398                .collect();
9399            assert!(
9400                serious.is_empty(),
9401                "cut {cut}: after the retry, fsck finds {serious:?}"
9402            );
9403            // And the journal still takes a transaction after recovery.
9404            fs.apply_create("/after.txt", 0o644)
9405                .unwrap_or_else(|e| panic!("cut {cut}: a write after recovery failed: {e:?}"));
9406            if cut >= written {
9407                break;
9408            }
9409        }
9410        assert!(
9411            total.unwrap_or(0) > 1,
9412            "the recovery commit must span several writes to cut"
9413        );
9414    }
9415
9416    /// A chain whose members sit in different block groups, cut after
9417    /// every write, strands none of them (Greptile on #201).
9418    ///
9419    /// The chain's only link from one member to the next is the member's
9420    /// own `i_dtime`, and reclaiming a member overwrites it. With the
9421    /// whole chain in one commit, a cut after the head's group was written
9422    /// and before the second member's left the head free with a deletion
9423    /// time and the second still allocated; the next mount stopped at the
9424    /// free head and cleared `s_last_orphan`, and the second member and its
9425    /// blocks were named by nothing, for good.
9426    #[test]
9427    fn unjournaled_recovery_of_a_chain_across_groups_survives_a_cut_after_every_write() {
9428        // Two groups: 32768 blocks per group at 4 KiB, and a second
9429        // group this driver's mkfs only makes at a 4 KiB block size.
9430        const TWO_GROUPS: u64 = 160 * 1024 * 1024;
9431        let dev = MemDev::new(TWO_GROUPS);
9432        crate::mkfs::format_filesystem(dev.as_ref(), Some("orphans"), None, TWO_GROUPS, BS)
9433            .expect("format");
9434        let members = {
9435            let fs = mount(&dev);
9436            assert!(fs.journal.is_none(), "fixture: the unjournaled path");
9437            let ipg = fs.sb.inodes_per_group;
9438            let mut by_group = std::collections::BTreeMap::new();
9439            for d in 0..8 {
9440                // Remounted each time, so the spread sees the counts the
9441                // last directory left.
9442                let fs = mount(&dev);
9443                fs.apply_mkdir(&format!("/d{d}"), 0o755).expect("mkdir");
9444                let path = format!("/d{d}/gone");
9445                let ino = fs.apply_create(&path, 0o644).expect("create");
9446                fs.apply_pwrite(&path, 0, &[0xCD; 3000]).expect("write");
9447                by_group.entry((ino - 1) / ipg).or_insert((d, ino));
9448            }
9449            let members: Vec<(usize, u32)> = by_group.into_values().take(3).collect();
9450            assert!(
9451                members.len() >= 2,
9452                "fixture: the orphans must sit in different groups, got {by_group_len}",
9453                by_group_len = members.len()
9454            );
9455            members
9456        };
9457        {
9458            // Unlink each while open, and chain them: the head's i_dtime
9459            // names the second, and so on.
9460            let fs = mount(&dev);
9461            for &(d, _) in &members {
9462                let dir = resolve_ino(&fs, &format!("/d{d}"));
9463                let (parent, _) = fs.read_inode_verified(dir).expect("dir");
9464                let mut buf = BlockBuffer::new(fs.sb.block_size());
9465                fs.buffer_remove_dir_entry(&mut buf, dir, &parent, b"gone")
9466                    .expect("remove the name");
9467                fs.commit_block_buffer(buf).expect("commit");
9468            }
9469            let mut buf = BlockBuffer::new(fs.sb.block_size());
9470            for (i, &(_, ino)) in members.iter().enumerate() {
9471                let next = members.get(i + 1).map_or(0, |&(_, n)| n);
9472                let (inode, mut raw) = fs.read_inode_verified(ino).expect("read");
9473                raw[0x1A..0x1C].copy_from_slice(&0u16.to_le_bytes());
9474                raw[0x14..0x18].copy_from_slice(&next.to_le_bytes());
9475                fs.finalize_inode_raw(ino, inode.generation, &mut raw)
9476                    .expect("finalize");
9477                fs.buffer_write_inode(&mut buf, ino, &raw).expect("write");
9478            }
9479            fs.buffer_patch_sb_last_orphan(&mut buf, members[0].1)
9480                .expect("head");
9481            fs.commit_block_buffer(buf).expect("commit");
9482        }
9483        let snapshot = dev.bytes.lock().unwrap().clone();
9484
9485        let mut total = None;
9486        for cut in 0.. {
9487            let image = MemDev::new(TWO_GROUPS);
9488            *image.bytes.lock().unwrap() = snapshot.clone();
9489            let crash = std::sync::Arc::new(CrashDev {
9490                inner: image.clone(),
9491                budget: cut,
9492                writes: std::sync::atomic::AtomicUsize::new(0),
9493            });
9494            drop(Filesystem::mount(crash.clone()).expect("mount through the cut"));
9495            let written = crash.writes.load(std::sync::atomic::Ordering::SeqCst);
9496            total.get_or_insert(written);
9497
9498            // The next writable mount retries; the one after observes.
9499            drop(mount(&image));
9500            let fs = mount(&image);
9501            assert!(
9502                fs.orphan_list()
9503                    .unwrap_or_else(|e| panic!("cut {cut}: orphan_list: {e:?}"))
9504                    .is_empty(),
9505                "cut {cut}: the chain was not cleared"
9506            );
9507            for &(_, ino) in &members {
9508                assert!(
9509                    !fs.inode_bit_is_set(ino).expect("inode bitmap"),
9510                    "cut {cut}: orphan {ino} is still allocated and named by nothing"
9511                );
9512            }
9513            let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).expect("audit");
9514            let serious: Vec<_> = report
9515                .anomalies
9516                .iter()
9517                .filter(|a| {
9518                    !matches!(
9519                        a,
9520                        crate::fsck::Anomaly::BlockGroupFreeCountDrift { .. }
9521                            | crate::fsck::Anomaly::SuperblockFreeCountDrift { .. }
9522                    )
9523                })
9524                .collect();
9525            assert!(
9526                serious.is_empty(),
9527                "cut {cut}: after the retry, fsck finds {serious:?}"
9528            );
9529            if cut >= written {
9530                break;
9531            }
9532        }
9533        assert!(total.unwrap_or(0) > 2, "recovery must span several writes");
9534    }
9535
9536    /// An orphan whose blocks this cannot reclaim -- mapped the legacy
9537    /// indirect way, as every ext3 file is -- is left whole and at the head
9538    /// of the chain (CodeRabbit on #201). Freeing its inode without its
9539    /// blocks, and moving the head past it, left the blocks allocated and
9540    /// named by nothing, for good.
9541    #[test]
9542    fn an_orphan_whose_blocks_cannot_be_reclaimed_stays_whole_at_the_head() {
9543        let dev = formatted();
9544        let ino = {
9545            let fs = mount(&dev);
9546            let ino = fs.apply_create("/gone.txt", 0o644).expect("create");
9547            fs.apply_pwrite("/gone.txt", 0, &[0xAB; 3 * BS as usize])
9548                .expect("write");
9549            ino
9550        };
9551        {
9552            let fs = mount(&dev);
9553            let (root, _) = fs.read_inode_verified(2).expect("root");
9554            let mut buf = BlockBuffer::new(fs.sb.block_size());
9555            fs.buffer_remove_dir_entry(&mut buf, 2, &root, b"gone.txt")
9556                .expect("remove the name");
9557            // Mapped the legacy way, as far as recovery can tell: the
9558            // driver cannot write an indirect file to make one.
9559            let (inode, mut raw) = fs.read_inode_verified(ino).expect("read");
9560            let flags = inode.flags & !crate::inode::InodeFlags::EXTENTS.bits();
9561            raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
9562            fs.finalize_inode_raw(ino, inode.generation, &mut raw)
9563                .expect("finalize");
9564            fs.buffer_write_inode(&mut buf, ino, &raw)
9565                .expect("write inode");
9566            fs.commit_block_buffer(buf).expect("commit");
9567            plant_orphan(&fs, ino, 0, None);
9568            let (inode, _) = fs.read_inode_verified(ino).expect("read");
9569            assert!(
9570                !inode.has_extents() && inode.blocks > 0,
9571                "fixture: a non-extent orphan holding blocks"
9572            );
9573        }
9574        // This mount runs recovery; the next one observes what it left.
9575        drop(mount(&dev));
9576        let fs = mount(&dev);
9577        assert_eq!(fs.sb.last_orphan, ino, "the orphan left the chain");
9578        assert!(
9579            fs.inode_bit_is_set(ino).expect("bitmap"),
9580            "the orphan's inode was freed without its blocks"
9581        );
9582        let (inode, _) = fs.read_inode_verified(ino).expect("read");
9583        assert!(inode.blocks > 0, "the orphan's body was zeroed");
9584    }
9585
9586    fn resolve_ino(fs: &Filesystem, path: &str) -> u32 {
9587        let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(inode, _)| inode);
9588        crate::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, path).expect("resolve")
9589    }
9590
9591    // ---------------------------------------------------------------
9592    // Features that may be read and may not be written
9593    // ---------------------------------------------------------------
9594
9595    /// A device that reads and writes like `MemDev` but reports itself
9596    /// read-only, so a mount takes the read-only path.
9597    struct RoDev(std::sync::Arc<MemDev>);
9598
9599    impl crate::block_io::BlockDevice for RoDev {
9600        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
9601            self.0.read_at(offset, buf)
9602        }
9603        fn size_bytes(&self) -> u64 {
9604            self.0.size_bytes()
9605        }
9606        fn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()> {
9607            Err(Error::ReadOnly)
9608        }
9609        fn flush(&self) -> Result<()> {
9610            Ok(())
9611        }
9612        fn is_writable(&self) -> bool {
9613            false
9614        }
9615    }
9616
9617    /// Set an INCOMPAT bit on a formatted volume, fixing the superblock
9618    /// checksum so the result still mounts.
9619    fn set_incompat_bit(dev: &std::sync::Arc<MemDev>, bit: u32) {
9620        let mut sb = vec![0u8; 1024];
9621        dev.read_at(crate::superblock::SUPERBLOCK_OFFSET, &mut sb)
9622            .expect("read sb");
9623        let cur = u32::from_le_bytes(sb[0x60..0x64].try_into().unwrap());
9624        sb[0x60..0x64].copy_from_slice(&(cur | bit).to_le_bytes());
9625        let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
9626        sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
9627        dev.write_at(crate::superblock::SUPERBLOCK_OFFSET, &sb)
9628            .expect("write sb");
9629    }
9630
9631    /// Turn the directory `dir` (child of `parent`) into an inline-data
9632    /// directory the way the kernel lays one out: `i_block` holds the
9633    /// parent's inode number, then entries; `i_size` is 60. `entry`, if
9634    /// given, is `(inode, name, file_type)` placed as its one entry.
9635    fn make_inline_dir(fs: &Filesystem, dir: u32, parent: u32, entry: Option<(u32, &[u8], u8)>) {
9636        let (inode, mut raw) = fs.read_inode_verified(dir).unwrap();
9637        let mut flags = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
9638        flags &= !crate::inode::InodeFlags::EXTENTS.bits();
9639        flags |= crate::inode::InodeFlags::INLINE_DATA.bits();
9640        raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
9641        raw[0x28..0x64].fill(0);
9642        raw[0x28..0x2C].copy_from_slice(&parent.to_le_bytes());
9643        let (ino, name, ft) = entry.unwrap_or((0, b"", 0));
9644        raw[0x2C..0x30].copy_from_slice(&ino.to_le_bytes());
9645        raw[0x30..0x32].copy_from_slice(&56u16.to_le_bytes());
9646        raw[0x32] = name.len() as u8;
9647        raw[0x33] = ft;
9648        raw[0x34..0x34 + name.len()].copy_from_slice(name);
9649        raw[0x04..0x08].copy_from_slice(&60u32.to_le_bytes());
9650        fs.finalize_inode_raw(dir, inode.generation, &mut raw)
9651            .unwrap();
9652        fs.write_inode_raw(dir, &raw).unwrap();
9653    }
9654
9655    /// The image outside the primary superblock, which a mount's first write
9656    /// marks not clean and its drop marks clean again.
9657    fn outside_superblock(dev: &std::sync::Arc<MemDev>) -> Vec<u8> {
9658        let mut bytes = dev.bytes.lock().unwrap().clone();
9659        bytes[1024..2048].fill(0);
9660        bytes
9661    }
9662
9663    /// The inline-directory fixture of the #382 and #428 tests: `/a` and
9664    /// `/b` block directories, `/b/y` a file; `/a/sub` inline holding `f`,
9665    /// a second link to `/f2`; `/a/empty` inline and empty.
9666    fn inline_dir_volume() -> std::sync::Arc<MemDev> {
9667        let dev = formatted();
9668        set_incompat_bit(&dev, crate::features::Incompat::INLINE_DATA.bits());
9669        let fs = mount(&dev);
9670        fs.apply_mkdir("/a", 0o755).unwrap();
9671        fs.apply_mkdir("/b", 0o755).unwrap();
9672        fs.apply_mkdir("/a/sub", 0o755).unwrap();
9673        fs.apply_mkdir("/a/empty", 0o755).unwrap();
9674        fs.apply_create("/f2", 0o644).unwrap();
9675        fs.apply_create("/b/y", 0o644).unwrap();
9676        let a = resolve(&fs, "/a").unwrap();
9677        let sub = resolve(&fs, "/a/sub").unwrap();
9678        let empty = resolve(&fs, "/a/empty").unwrap();
9679        let f2 = resolve(&fs, "/f2").unwrap();
9680        make_inline_dir(&fs, sub, a, Some((f2, b"f", 1)));
9681        make_inline_dir(&fs, empty, a, None);
9682        let (i, raw) = fs.read_inode_verified(f2).unwrap();
9683        let mut raw = raw.clone();
9684        raw[0x1A..0x1C].copy_from_slice(&2u16.to_le_bytes());
9685        fs.finalize_inode_raw(f2, i.generation, &mut raw).unwrap();
9686        fs.write_inode_raw(f2, &raw).unwrap();
9687        drop(fs);
9688        dev
9689    }
9690
9691    /// The names in directory `path`, `.` and `..` excluded, sorted, and
9692    /// whether it is inline.
9693    fn names_in(fs: &Filesystem, path: &str) -> (Vec<String>, bool) {
9694        let ino = resolve(fs, path).unwrap();
9695        let mut names: Vec<String> = fs
9696            .read_dir_ino(ino)
9697            .unwrap()
9698            .into_iter()
9699            .map(|e| String::from_utf8_lossy(&e.name).into_owned())
9700            .filter(|n| n != "." && n != "..")
9701            .collect();
9702        names.sort();
9703        (names, fs.stat_ino(ino).unwrap().has_inline_data())
9704    }
9705
9706    fn links(fs: &Filesystem, path: &str) -> u16 {
9707        fs.stat_ino(resolve(fs, path).unwrap()).unwrap().links_count
9708    }
9709
9710    /// #382, #428: renaming an inline-data directory across parents must
9711    /// not treat its i_block (parent inode number, then entries) as a block
9712    /// map; it rewrites the parent number there.
9713    #[test]
9714    fn renaming_an_inline_directory_across_parents_rewrites_its_parent_number() {
9715        let dev = inline_dir_volume();
9716        let fs = mount(&dev);
9717        let (a, b) = (links(&fs, "/a"), links(&fs, "/b"));
9718        fs.apply_rename("/a/sub", "/b/sub", false).unwrap();
9719        drop(fs);
9720        let fs = mount(&dev);
9721        assert_eq!(
9722            resolve(&fs, "/b/sub/..").unwrap(),
9723            resolve(&fs, "/b").unwrap()
9724        );
9725        assert_eq!(names_in(&fs, "/b/sub"), (vec!["f".into()], true));
9726        assert_eq!((links(&fs, "/a"), links(&fs, "/b")), (a - 1, b + 1));
9727        let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).unwrap();
9728        assert!(report.anomalies.is_empty(), "{:?}", report.anomalies);
9729    }
9730
9731    /// #428: every directory mutation whose parent or target is an
9732    /// inline-data directory goes through (they were refused, #382), by
9733    /// path and by inode number; the entries read back and the volume's
9734    /// audit is clean after each.
9735    #[test]
9736    fn every_mutation_of_an_inline_directory_succeeds() {
9737        type Op = fn(&Filesystem) -> Result<()>;
9738        type Check = fn(&Filesystem);
9739        fn ino(fs: &Filesystem, path: &str) -> u32 {
9740            resolve(fs, path).unwrap()
9741        }
9742        let ops: [(&str, Op, Check); 16] = [
9743            (
9744                "create inside",
9745                |fs| fs.apply_create("/a/sub/g", 0o644).map(drop),
9746                |fs| assert_eq!(names_in(fs, "/a/sub"), (vec!["f".into(), "g".into()], true)),
9747            ),
9748            (
9749                "create_at inside",
9750                |fs| fs.apply_create_at(ino(fs, "/a/sub"), b"g", 0o644).map(drop),
9751                |fs| assert_eq!(names_in(fs, "/a/sub"), (vec!["f".into(), "g".into()], true)),
9752            ),
9753            (
9754                "mkdir inside",
9755                |fs| fs.apply_mkdir("/a/sub/h", 0o755).map(drop),
9756                |fs| {
9757                    assert_eq!(names_in(fs, "/a/sub").0, ["f", "h"]);
9758                    assert_eq!(links(fs, "/a/sub"), 3);
9759                    assert_eq!(ino(fs, "/a/sub/h/.."), ino(fs, "/a/sub"));
9760                },
9761            ),
9762            (
9763                "mkdir_at inside an empty one",
9764                |fs| {
9765                    fs.apply_mkdir_at(ino(fs, "/a/empty"), b"h", 0o755)
9766                        .map(drop)
9767                },
9768                |fs| {
9769                    assert_eq!(names_in(fs, "/a/empty"), (vec!["h".into()], true));
9770                    assert_eq!(links(fs, "/a/empty"), 3);
9771                },
9772            ),
9773            (
9774                "symlink inside",
9775                |fs| fs.apply_symlink("t", "/a/sub/s").map(drop),
9776                |fs| assert_eq!(fs.read_link(ino(fs, "/a/sub/s")).unwrap(), b"t"),
9777            ),
9778            (
9779                "link into",
9780                |fs| fs.apply_link("/f2", "/a/sub/l"),
9781                |fs| {
9782                    assert_eq!(ino(fs, "/a/sub/l"), ino(fs, "/f2"));
9783                    assert_eq!(links(fs, "/f2"), 3);
9784                },
9785            ),
9786            (
9787                "unlink inside",
9788                |fs| fs.apply_unlink("/a/sub/f"),
9789                |fs| {
9790                    assert_eq!(names_in(fs, "/a/sub"), (vec![], true));
9791                    assert_eq!(links(fs, "/f2"), 1);
9792                },
9793            ),
9794            (
9795                "unlink_at inside",
9796                |fs| fs.apply_unlink_at(ino(fs, "/a/sub"), b"f"),
9797                |fs| assert_eq!(names_in(fs, "/a/sub"), (vec![], true)),
9798            ),
9799            (
9800                "rename out of",
9801                |fs| fs.apply_rename("/a/sub/f", "/b/f", false),
9802                |fs| {
9803                    assert_eq!(names_in(fs, "/a/sub"), (vec![], true));
9804                    assert_eq!(ino(fs, "/b/f"), ino(fs, "/f2"));
9805                },
9806            ),
9807            (
9808                "rename into",
9809                |fs| fs.apply_rename("/b/y", "/a/sub/y", false),
9810                |fs| assert_eq!(names_in(fs, "/a/sub").0, ["f", "y"]),
9811            ),
9812            (
9813                "rename within",
9814                |fs| {
9815                    let sub = ino(fs, "/a/sub");
9816                    fs.apply_rename_at(sub, b"f", sub, b"renamed", false)
9817                },
9818                |fs| assert_eq!(names_in(fs, "/a/sub"), (vec!["renamed".into()], true)),
9819            ),
9820            (
9821                "rename over an empty one",
9822                |fs| fs.apply_rename("/b", "/a/empty", true),
9823                |fs| {
9824                    assert_eq!(names_in(fs, "/a/empty"), (vec!["y".into()], false));
9825                    assert!(matches!(resolve(fs, "/b"), Err(Error::NotFound)));
9826                },
9827            ),
9828            (
9829                "rmdir",
9830                |fs| fs.apply_rmdir("/a/empty"),
9831                |fs| assert_eq!(names_in(fs, "/a").0, ["sub"]),
9832            ),
9833            (
9834                "rmdir_at inside",
9835                |fs| {
9836                    fs.apply_mkdir("/a/sub/h", 0o755)?;
9837                    fs.apply_rmdir_at(ino(fs, "/a/sub"), b"h")
9838                },
9839                |fs| {
9840                    assert_eq!(names_in(fs, "/a/sub"), (vec!["f".into()], true));
9841                    assert_eq!(links(fs, "/a/sub"), 2);
9842                },
9843            ),
9844            (
9845                "entries past what the inode holds",
9846                |fs| {
9847                    for i in 0..12 {
9848                        fs.apply_create(&format!("/a/sub/file-number-{i:02}"), 0o644)?;
9849                    }
9850                    Ok(())
9851                },
9852                |fs| {
9853                    let (names, inline) = names_in(fs, "/a/sub");
9854                    assert!(!inline, "twelve more entries converted it to a block");
9855                    assert_eq!(names.len(), 13);
9856                    assert_eq!(ino(fs, "/a/sub/f"), ino(fs, "/f2"));
9857                    assert_eq!(ino(fs, "/a/sub/.."), ino(fs, "/a"));
9858                },
9859            ),
9860            (
9861                "rename within, converting",
9862                |fs| {
9863                    let sub = ino(fs, "/a/sub");
9864                    fs.apply_rename_at(sub, b"f", sub, &[b'n'; 200], false)
9865                },
9866                |fs| {
9867                    let (names, inline) = names_in(fs, "/a/sub");
9868                    assert_eq!((names, inline), (vec!["n".repeat(200)], false));
9869                },
9870            ),
9871        ];
9872        for (name, op, check) in ops {
9873            let dev = inline_dir_volume();
9874            let fs = mount(&dev);
9875            op(&fs).unwrap_or_else(|e| panic!("{name}: {e:?}"));
9876            drop(fs);
9877            let fs = mount(&dev);
9878            check(&fs);
9879            let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).unwrap();
9880            assert!(
9881                report.anomalies.is_empty(),
9882                "{name}: {:?}",
9883                report.anomalies
9884            );
9885        }
9886    }
9887
9888    /// #428: an inline directory that is not empty is still not removed,
9889    /// and the refusal writes nothing.
9890    #[test]
9891    fn a_non_empty_inline_directory_is_not_removed() {
9892        let dev = inline_dir_volume();
9893        let before = outside_superblock(&dev);
9894        let fs = mount(&dev);
9895        assert!(matches!(
9896            fs.apply_rmdir("/a/sub"),
9897            Err(Error::DirectoryNotEmpty)
9898        ));
9899        assert!(matches!(
9900            fs.apply_rename("/b", "/a/sub", true),
9901            Err(Error::DirectoryNotEmpty)
9902        ));
9903        drop(fs);
9904        assert!(outside_superblock(&dev) == before, "a refusal wrote");
9905        let fs = mount(&dev);
9906        assert_eq!(names_in(&fs, "/a/sub"), (vec!["f".into()], true));
9907    }
9908
9909    /// `fs_ext4_listxattr` into a short buffer writes whole names only,
9910    /// as `include/fs_ext4.h` now says; it said "as much as fits".
9911    #[test]
9912    fn listxattr_into_a_short_buffer_writes_whole_names_only() {
9913        let dev = formatted();
9914        {
9915            let fs = mount(&dev);
9916            fs.apply_create("/x.txt", 0o644).expect("create");
9917            fs.apply_setxattr("/x.txt", "user.first", b"1")
9918                .expect("set");
9919            fs.apply_setxattr("/x.txt", "user.second", b"2")
9920                .expect("set");
9921        }
9922        let image = fs_ext4_test_support::temp_path!("fs_ext4_lx_whole_{}.img", std::process::id());
9923        std::fs::write(&image, &*dev.bytes.lock().unwrap()).expect("write image");
9924        let c_image = std::ffi::CString::new(image.clone()).unwrap();
9925        let c_path = std::ffi::CString::new("/x.txt").unwrap();
9926        unsafe {
9927            let handle = crate::capi::fs_ext4_mount(c_image.as_ptr());
9928            assert!(!handle.is_null(), "C mount");
9929            let needed =
9930                crate::capi::fs_ext4_listxattr(handle, c_path.as_ptr(), std::ptr::null_mut(), 0);
9931            let mut full = vec![0u8; needed as usize];
9932            crate::capi::fs_ext4_listxattr(
9933                handle,
9934                c_path.as_ptr(),
9935                full.as_mut_ptr().cast(),
9936                full.len(),
9937            );
9938            let first_len = full.iter().position(|&b| b == 0).unwrap() + 1;
9939            assert!((first_len as i64) < needed, "fixture: two names");
9940
9941            // Room for the first name and three bytes of the second.
9942            let mut short = vec![0xEEu8; first_len + 3];
9943            let got = crate::capi::fs_ext4_listxattr(
9944                handle,
9945                c_path.as_ptr(),
9946                short.as_mut_ptr().cast(),
9947                short.len(),
9948            );
9949            assert_eq!(got, needed, "the required size is still returned");
9950            assert_eq!(
9951                &short[..first_len],
9952                &full[..first_len],
9953                "the first name, whole"
9954            );
9955            assert_eq!(
9956                &short[first_len..],
9957                &[0xEE; 3],
9958                "no part of the second name"
9959            );
9960            crate::capi::fs_ext4_umount(handle);
9961        }
9962        let _ = std::fs::remove_file(&image);
9963    }
9964
9965    /// fsck repair is a write, and refuses what every other write refuses
9966    /// (#119).
9967    ///
9968    /// The repair gate checked only that the device was writable. The
9969    /// control is the point: the same volume, in the same process, refuses
9970    /// a create, and an audit without repair still runs.
9971    #[test]
9972    fn fsck_repair_refuses_a_volume_whose_features_refuse_writes() {
9973        let dev = formatted();
9974        let mut sb = vec![0u8; 1024];
9975        dev.read_at(crate::superblock::SUPERBLOCK_OFFSET, &mut sb)
9976            .unwrap();
9977        let cur = u32::from_le_bytes(sb[0x64..0x68].try_into().unwrap());
9978        let quota = crate::features::RoCompat::QUOTA.bits();
9979        sb[0x64..0x68].copy_from_slice(&(cur | quota).to_le_bytes());
9980        let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
9981        sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
9982        dev.write_at(crate::superblock::SUPERBLOCK_OFFSET, &sb)
9983            .unwrap();
9984
9985        let fs = mount(&dev);
9986        assert!(
9987            matches!(
9988                fs.apply_create("/x", 0o644),
9989                Err(Error::UnsupportedRoCompat(_))
9990            ),
9991            "control: an ordinary write is refused on this volume"
9992        );
9993        fs.audit_repair(u32::MAX, u32::MAX, false)
9994            .expect("an audit without repair still runs");
9995        match fs.audit_repair(u32::MAX, u32::MAX, true) {
9996            Err(Error::UnsupportedRoCompat(bits)) => assert_eq!(bits & quota, quota),
9997            other => panic!("repair must be refused, got {:?}", other.map(|_| ())),
9998        }
9999    }
10000
10001    /// And an ordinary volume still repairs.
10002    #[test]
10003    fn fsck_repair_still_runs_on_an_ordinary_volume() {
10004        let dev = formatted();
10005        let fs = mount(&dev);
10006        fs.audit_repair(u32::MAX, u32::MAX, true)
10007            .expect("repair on an ordinary volume");
10008    }
10009
10010    /// A CASEFOLD volume must not be mounted writable.
10011    ///
10012    /// The kernel files a directory entry into the htree leaf that the
10013    /// hash of the case-folded name selects; this driver's write path
10014    /// hashes the raw bytes. Reads survive on the linear-scan fallback. A write does
10015    /// not: the entry lands in the wrong leaf, stays findable here and
10016    /// stops being findable on Linux.
10017    #[test]
10018    fn a_casefold_volume_is_not_mounted_writable() {
10019        let dev = formatted();
10020        set_incompat_bit(&dev, crate::features::Incompat::CASEFOLD.bits());
10021
10022        let err = match Filesystem::mount(dev.clone()) {
10023            Ok(_) => panic!("a writable mount of a CASEFOLD volume must be refused"),
10024            Err(e) => e,
10025        };
10026        match err {
10027            Error::UnsupportedIncompat(bits) => assert_eq!(
10028                bits,
10029                crate::features::Incompat::CASEFOLD.bits(),
10030                "the refusal must name the bit responsible"
10031            ),
10032            other => panic!("expected UnsupportedIncompat, got {other:?}"),
10033        }
10034    }
10035
10036    /// And it must still be READABLE, which is the whole reason the
10037    /// refusal is scoped to writable mounts rather than to the volume.
10038    #[test]
10039    fn a_casefold_volume_still_mounts_read_only() {
10040        let dev = formatted();
10041        {
10042            let fs = mount(&dev);
10043            fs.apply_create("/before.txt", 0o644).expect("create");
10044        }
10045        set_incompat_bit(&dev, crate::features::Incompat::CASEFOLD.bits());
10046
10047        let ro = std::sync::Arc::new(RoDev(dev.clone()));
10048        let fs = Filesystem::mount(ro).expect("a read-only mount must still work");
10049        assert_eq!(
10050            resolve(&fs, "/before.txt").expect("the directory is still readable"),
10051            resolve(&fs, "/before.txt").expect("stable"),
10052        );
10053    }
10054
10055    /// A device that turns writable after the mount, as the FSKit write FD
10056    /// does for a lazy mount.
10057    struct LaterWritable {
10058        inner: std::sync::Arc<MemDev>,
10059        writable: std::sync::atomic::AtomicBool,
10060    }
10061
10062    impl crate::block_io::BlockDevice for LaterWritable {
10063        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
10064            self.inner.read_at(offset, buf)
10065        }
10066        fn size_bytes(&self) -> u64 {
10067            self.inner.size_bytes()
10068        }
10069        fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
10070            if !self.is_writable() {
10071                return Err(Error::ReadOnly);
10072            }
10073            self.inner.write_at(offset, buf)
10074        }
10075        fn flush(&self) -> Result<()> {
10076            Ok(())
10077        }
10078        fn is_writable(&self) -> bool {
10079            self.writable.load(std::sync::atomic::Ordering::SeqCst)
10080        }
10081    }
10082
10083    /// A lazy mount that was read-only when it mounted and writable later
10084    /// still refuses to write a write-breaking INCOMPAT volume (#117).
10085    ///
10086    /// The refusal ran once at mount, and passed because the device was
10087    /// not yet writable; every later write checked only RO_COMPAT.
10088    #[test]
10089    fn a_volume_that_becomes_writable_after_a_lazy_mount_still_refuses_write_breaking_features() {
10090        for bit in [
10091            crate::features::Incompat::CASEFOLD.bits(),
10092            crate::features::Incompat::MMP.bits(),
10093        ] {
10094            let dev = formatted();
10095            set_incompat_bit(&dev, bit);
10096            let later = std::sync::Arc::new(LaterWritable {
10097                inner: dev.clone(),
10098                writable: std::sync::atomic::AtomicBool::new(false),
10099            });
10100            let fs = Filesystem::mount_lazy(later.clone()).expect("a read-only lazy mount");
10101            later
10102                .writable
10103                .store(true, std::sync::atomic::Ordering::SeqCst);
10104            match fs.apply_create("/late.txt", 0o644) {
10105                Err(Error::UnsupportedIncompat(b)) => assert_eq!(b, bit),
10106                other => panic!("{bit:#x}: a write went through: {:?}", other.map(|_| ())),
10107            }
10108        }
10109    }
10110
10111    /// The control: the same late-writable device over an ordinary volume
10112    /// does write, so the refusal above is the feature and not the device.
10113    #[test]
10114    fn an_ordinary_volume_that_becomes_writable_after_a_lazy_mount_writes() {
10115        let dev = formatted();
10116        let later = std::sync::Arc::new(LaterWritable {
10117            inner: dev,
10118            writable: std::sync::atomic::AtomicBool::new(false),
10119        });
10120        let fs = Filesystem::mount_lazy(later.clone()).expect("a read-only lazy mount");
10121        later
10122            .writable
10123            .store(true, std::sync::atomic::Ordering::SeqCst);
10124        fs.apply_create("/late.txt", 0o644)
10125            .expect("an ordinary volume must accept the write");
10126    }
10127
10128    /// The MMP case this generalised, so folding the two into one set
10129    /// cannot have dropped the original.
10130    #[test]
10131    fn an_mmp_volume_is_still_not_mounted_writable() {
10132        let dev = formatted();
10133        set_incompat_bit(&dev, crate::features::Incompat::MMP.bits());
10134        let err = match Filesystem::mount(dev.clone()) {
10135            Ok(_) => panic!("a writable mount of an MMP volume must be refused"),
10136            Err(e) => e,
10137        };
10138        assert!(matches!(
10139            err,
10140            Error::UnsupportedIncompat(b) if b == crate::features::Incompat::MMP.bits()
10141        ));
10142    }
10143
10144    /// The control: an ordinary volume still mounts writable and still
10145    /// accepts a create, so none of the above can be satisfied by
10146    /// refusing every writable mount.
10147    #[test]
10148    fn an_ordinary_volume_still_mounts_writable() {
10149        let dev = formatted();
10150        assert_eq!(
10151            Superblock::read(dev.as_ref()).unwrap().last_orphan,
10152            0,
10153            "new formatter must not overlap hash seed with orphan head"
10154        );
10155        let fs = Filesystem::mount(dev.clone()).expect("mount");
10156        fs.apply_create("/after.txt", 0o644).expect("create");
10157    }
10158
10159    // ---------------------------------------------------------------
10160    // The refusal has to come BEFORE journal replay
10161    // ---------------------------------------------------------------
10162    //
10163    // Replay is not a read. It takes the blocks a previous writer
10164    // committed to the log and writes them into the filesystem proper.
10165    // Doing that to a volume carrying a feature this driver does not
10166    // maintain is the exact damage `write_breaking_incompat` exists to
10167    // prevent, so the refusal has to happen first.
10168    //
10169    // On `main` it does, by five lines. Nothing holds it there: move the
10170    // refusal below `replay_if_dirty` and every other test still passes,
10171    // because the only volumes that carry one of these bits today are
10172    // `formatted()` ext4 images and this crate's mkfs writes no journal
10173    // for them. The resulting driver would decline the mount *after*
10174    // having already written to the disk.
10175    //
10176    // These two tests fix the order by giving it a witness: a volume
10177    // that has both an unsupported bit and a genuinely replayable
10178    // journal, and a destination block whose contents say whether the
10179    // replay ran.
10180
10181    /// The block a planted transaction writes to. Chosen well past the
10182    /// metadata and the 1024-block journal mkfs lays down for ext3, and
10183    /// well inside a 32 MiB device, so replay's own bounds checks have
10184    /// no reason to refuse it.
10185    const REPLAY_TARGET_BLOCK: u64 = 4000;
10186
10187    /// Format an ext3 volume — the one flavour whose mkfs writes a real
10188    /// journal — and leave a single committed transaction in it that
10189    /// replay has not yet applied.
10190    ///
10191    /// Returns the device and the payload the transaction will write to
10192    /// `REPLAY_TARGET_BLOCK`, which is what the caller checks for.
10193    fn ext3_with_a_dirty_journal() -> (std::sync::Arc<MemDev>, Vec<u8>) {
10194        let dev = MemDev::new(VOL);
10195        crate::mkfs::format_filesystem_with_flavor(
10196            dev.as_ref(),
10197            Some("replay"),
10198            None,
10199            VOL,
10200            BS,
10201            crate::features::FsFlavor::Ext3,
10202        )
10203        .expect("format ext3");
10204
10205        let payload = {
10206            let fs = Filesystem::mount(dev.clone()).expect("mount to plant the journal");
10207            let block_size = fs.sb.block_size() as u64;
10208
10209            let raw = fs
10210                .read_inode_raw(fs.sb.journal_inode)
10211                .expect("read the journal inode");
10212            let jinode = crate::inode::Inode::parse(&raw).expect("parse the journal inode");
10213            let jsb = crate::jbd2::read_superblock(&fs)
10214                .expect("read the journal superblock")
10215                .expect("ext3 has a journal");
10216
10217            // One transaction, one write tag: descriptor, data, commit.
10218            let mut tx = crate::transaction::Transaction::begin(
10219                jsb.sequence,
10220                block_size as u32,
10221                jsb.uses_64bit(),
10222                jsb.feature_incompat & crate::jbd2::JbdIncompat::CSUM_V3.bits() != 0,
10223            );
10224            let payload: Vec<u8> = (0..block_size as usize)
10225                .map(|i| 0xA5u8.wrapping_add((i & 0xFF) as u8))
10226                .collect();
10227            tx.add_write(REPLAY_TARGET_BLOCK, payload.clone())
10228                .expect("add_write");
10229            let blocks = tx.commit().expect("commit");
10230            assert_eq!(blocks.len(), 3, "descriptor + data + commit");
10231
10232            // Journal logical block 0 is the journal superblock, so the
10233            // log itself starts at 1.
10234            for (i, blk) in blocks.iter().enumerate() {
10235                let phys = crate::jbd2::journal_block_to_physical(&fs, &jinode, (i as u64) + 1)
10236                    .expect("map the journal block")
10237                    .expect("the journal is contiguous, so it is mapped");
10238                fs.dev
10239                    .write_at(phys * block_size, blk)
10240                    .expect("write the journal slot");
10241            }
10242
10243            // s_start is at offset 0x1C and JBD2 is big-endian. Setting
10244            // it to 1 is what makes the journal dirty.
10245            let jsb_phys = crate::jbd2::journal_block_to_physical(&fs, &jinode, 0)
10246                .expect("map the journal superblock")
10247                .expect("mapped");
10248            let mut jsb_bytes = vec![0u8; block_size as usize];
10249            fs.dev
10250                .read_at(jsb_phys * block_size, &mut jsb_bytes)
10251                .expect("read the journal superblock");
10252            assert_eq!(
10253                u32::from_be_bytes(jsb_bytes[0..4].try_into().unwrap()),
10254                crate::jbd2::JBD2_MAGIC_NUMBER,
10255                "the block the journal inode maps is not a JBD2 superblock"
10256            );
10257            jsb_bytes[0x1C..0x20].copy_from_slice(&1u32.to_be_bytes());
10258            fs.dev
10259                .write_at(jsb_phys * block_size, &jsb_bytes)
10260                .expect("write the journal superblock");
10261            fs.dev.flush().expect("flush");
10262            payload
10263        };
10264
10265        assert!(
10266            !destination_holds_the_payload(&dev, &payload),
10267            "the destination already holds the payload, so replaying could not be observed"
10268        );
10269        (dev, payload)
10270    }
10271
10272    /// Whether the transaction's payload has reached its destination —
10273    /// which is to say, whether replay ran.
10274    ///
10275    /// Returns a bool rather than the block so a failure prints one line
10276    /// instead of two 4096-byte vectors.
10277    fn destination_holds_the_payload(dev: &std::sync::Arc<MemDev>, payload: &[u8]) -> bool {
10278        let mut buf = vec![0u8; BS as usize];
10279        crate::block_io::BlockDevice::read_at(
10280            dev.as_ref(),
10281            REPLAY_TARGET_BLOCK * BS as u64,
10282            &mut buf,
10283        )
10284        .expect("read the destination block");
10285        buf == payload
10286    }
10287
10288    /// The control. Without an unsupported bit, this volume's journal
10289    /// really does replay at mount, so the assertion below it — that
10290    /// the destination is untouched — is a statement about the refusal
10291    /// and not about a journal that was never going to replay anyway.
10292    #[test]
10293    fn a_dirty_journal_is_replayed_at_mount() {
10294        let (dev, payload) = ext3_with_a_dirty_journal();
10295        let fs = Filesystem::mount(dev.clone()).expect("mount");
10296        drop(fs);
10297        assert!(
10298            destination_holds_the_payload(&dev, &payload),
10299            "mount did not replay a dirty journal"
10300        );
10301    }
10302
10303    /// And with one, the mount is refused and the journal is left alone.
10304    ///
10305    /// The refusal on its own proves nothing: a driver that replayed
10306    /// first and refused afterwards would still return this error. What
10307    /// separates the two orders is whether the log was consumed, which
10308    /// is what the second assertion reads.
10309    #[test]
10310    fn an_unsupported_bit_is_refused_before_the_journal_is_replayed() {
10311        let (dev, payload) = ext3_with_a_dirty_journal();
10312        set_incompat_bit(&dev, crate::features::Incompat::CASEFOLD.bits());
10313
10314        match Filesystem::mount(dev.clone()) {
10315            Ok(_) => panic!("a writable mount of a CASEFOLD volume must be refused"),
10316            Err(Error::UnsupportedIncompat(bits)) => assert_eq!(
10317                bits,
10318                crate::features::Incompat::CASEFOLD.bits(),
10319                "the refusal must name the bit responsible"
10320            ),
10321            Err(other) => panic!("expected UnsupportedIncompat, got {other:?}"),
10322        }
10323
10324        assert!(
10325            !destination_holds_the_payload(&dev, &payload),
10326            "the journal was replayed into a volume the driver had already declined to mount"
10327        );
10328    }
10329
10330    /// A lazy mount that turns writable does not replay a dirty journal
10331    /// into a write-breaking INCOMPAT volume either (#117): replay is a
10332    /// write, and it did not go through `refuse_write`. The control
10333    /// replays the same journal once the bit is gone.
10334    #[test]
10335    fn a_lazy_mount_that_becomes_writable_does_not_replay_into_a_write_breaking_volume() {
10336        for bit in [0, crate::features::Incompat::MMP.bits()] {
10337            let (dev, payload) = ext3_with_a_dirty_journal();
10338            if bit != 0 {
10339                set_incompat_bit(&dev, bit);
10340            }
10341            let later = std::sync::Arc::new(LaterWritable {
10342                inner: dev.clone(),
10343                writable: std::sync::atomic::AtomicBool::new(false),
10344            });
10345            let mut fs = Filesystem::mount_lazy(later.clone()).expect("a read-only lazy mount");
10346            later
10347                .writable
10348                .store(true, std::sync::atomic::Ordering::SeqCst);
10349            let result = fs.replay_journal_if_dirty();
10350            if bit == 0 {
10351                assert_eq!(result.expect("the control replays"), 1);
10352                assert!(destination_holds_the_payload(&dev, &payload));
10353            } else {
10354                match result {
10355                    Err(Error::UnsupportedIncompat(b)) => assert_eq!(b, bit),
10356                    other => panic!("replay was not refused: {other:?}"),
10357                }
10358                assert!(
10359                    !destination_holds_the_payload(&dev, &payload),
10360                    "the journal was replayed into an MMP volume"
10361                );
10362            }
10363        }
10364    }
10365
10366    /// A writable lazy mount must not commit over a journal it has not
10367    /// replayed (#375). The first commit writes its descriptor at journal
10368    /// block 1, over the unreplayed log, and then marks the journal clean:
10369    /// every transaction the last writer committed but did not checkpoint
10370    /// is gone. The next mount therefore finds nothing to replay, and the
10371    /// payload the journal carried never reaches its destination.
10372    #[test]
10373    fn a_lazy_mount_refuses_writes_until_its_journal_is_replayed() {
10374        let (dev, payload) = ext3_with_a_dirty_journal();
10375        let fs = Filesystem::mount_lazy(dev.clone()).expect("lazy mount");
10376        let r = fs.apply_mkdir("/x", 0o755);
10377        drop(fs);
10378        let _ = Filesystem::mount(dev.clone());
10379        assert!(
10380            destination_holds_the_payload(&dev, &payload),
10381            "the unreplayed transaction was lost (write result: {r:?})"
10382        );
10383    }
10384
10385    /// The refusal names its reason, and it lifts once the journal is
10386    /// replayed: the same handle then writes, and the replayed transaction
10387    /// is still on disk afterwards (#375).
10388    #[test]
10389    fn a_lazy_mount_writes_once_its_journal_is_replayed() {
10390        let (dev, payload) = ext3_with_a_dirty_journal();
10391        let mut fs = Filesystem::mount_lazy(dev.clone()).expect("lazy mount");
10392        match fs.apply_mkdir("/x", 0o755) {
10393            Err(Error::JournalNotReplayed) => {}
10394            other => panic!("a write over an unreplayed journal: {other:?}"),
10395        }
10396        assert_eq!(fs.replay_journal_if_dirty().expect("replay"), 1);
10397        fs.apply_mkdir("/x", 0o755)
10398            .expect("the write is accepted once the journal is replayed");
10399        drop(fs);
10400        assert!(destination_holds_the_payload(&dev, &payload));
10401    }
10402
10403    /// A lazy mount of a clean journal has nothing to replay, so it writes
10404    /// straight away: the refusal is about the journal, not the lazy mount.
10405    #[test]
10406    fn a_lazy_mount_of_a_clean_journal_writes_without_a_replay() {
10407        let (dev, _payload) = ext3_with_a_dirty_journal();
10408        drop(Filesystem::mount(dev.clone()).expect("eager mount replays"));
10409        let fs = Filesystem::mount_lazy(dev.clone()).expect("lazy mount");
10410        fs.apply_mkdir("/x", 0o755)
10411            .expect("a clean journal needs no replay before a write");
10412    }
10413
10414    /// The read-only-then-writable shape (#375): the mount replayed the
10415    /// journal into the cache only, so the log on disk is still dirty when
10416    /// the device turns writable. A write then must wait for the replay
10417    /// too, or it lands on a volume whose next mount replays the older
10418    /// log over it.
10419    #[test]
10420    fn a_lazy_mount_that_becomes_writable_refuses_writes_until_its_journal_is_replayed() {
10421        let (dev, payload) = ext3_with_a_dirty_journal();
10422        let later = std::sync::Arc::new(LaterWritable {
10423            inner: dev.clone(),
10424            writable: std::sync::atomic::AtomicBool::new(false),
10425        });
10426        let mut fs = Filesystem::mount_lazy(later.clone()).expect("a read-only lazy mount");
10427        later
10428            .writable
10429            .store(true, std::sync::atomic::Ordering::SeqCst);
10430        match fs.apply_mkdir("/x", 0o755) {
10431            Err(Error::JournalNotReplayed) => {}
10432            other => panic!("a write over an unreplayed journal: {other:?}"),
10433        }
10434        assert_eq!(fs.replay_journal_if_dirty().expect("replay"), 1);
10435        assert!(destination_holds_the_payload(&dev, &payload));
10436        fs.apply_mkdir("/x", 0o755)
10437            .expect("the write is accepted once the journal is replayed");
10438    }
10439
10440    /// After a lazy replay the next commit must carry a sequence past the
10441    /// replayed transaction, as an eager mount's does (#376). The writer
10442    /// opened over the dirty journal kept its pre-replay sequence and wrote
10443    /// it back on the next commit, below the replayed tail: a crash then lets
10444    /// a later replay walk on into the older transaction behind ours.
10445    #[test]
10446    fn a_lazy_replay_leaves_the_writer_past_the_replayed_sequence() {
10447        let seq_after = |lazy: bool| {
10448            let (dev, _payload) = ext3_with_a_dirty_journal();
10449            let fs = if lazy {
10450                let mut fs = Filesystem::mount_lazy(dev.clone()).expect("lazy");
10451                assert_eq!(fs.replay_journal_if_dirty().expect("replay"), 1);
10452                fs
10453            } else {
10454                Filesystem::mount(dev.clone()).expect("eager")
10455            };
10456            fs.apply_chmod("/", 0o700).expect("chmod");
10457            crate::jbd2::read_superblock(&fs).unwrap().unwrap().sequence
10458        };
10459        assert_eq!(
10460            seq_after(true),
10461            seq_after(false),
10462            "lazy and eager must agree on the log's sequence"
10463        );
10464    }
10465
10466    /// A lazy mount that was read-only when it mounted has no journal writer,
10467    /// and nothing opened one once the device turned writable, so every write
10468    /// after the replay call went to the device unjournaled (#376). The
10469    /// replay call is where the writable handle begins, so it opens one.
10470    #[test]
10471    fn a_lazy_mount_that_becomes_writable_journals_after_the_replay_call() {
10472        let (dev, _payload) = ext3_with_a_dirty_journal();
10473        let later = std::sync::Arc::new(LaterWritable {
10474            inner: dev.clone(),
10475            writable: std::sync::atomic::AtomicBool::new(false),
10476        });
10477        let mut fs = Filesystem::mount_lazy(later.clone()).expect("a read-only lazy mount");
10478        assert!(fs.journal.is_none(), "a read-only mount opens no writer");
10479        later
10480            .writable
10481            .store(true, std::sync::atomic::Ordering::SeqCst);
10482        assert_eq!(fs.replay_journal_if_dirty().expect("replay"), 1);
10483        assert!(
10484            fs.journal.is_some(),
10485            "the handle is writable now, and its writes must be journaled"
10486        );
10487        fs.apply_chmod("/", 0o700).expect("chmod");
10488        let jsb = crate::jbd2::read_superblock(&fs).unwrap().unwrap();
10489        let eager = {
10490            let (dev, _payload) = ext3_with_a_dirty_journal();
10491            let fs = Filesystem::mount(dev.clone()).expect("eager");
10492            fs.apply_chmod("/", 0o700).expect("chmod");
10493            crate::jbd2::read_superblock(&fs).unwrap().unwrap().sequence
10494        };
10495        assert_eq!(
10496            jsb.sequence, eager,
10497            "the chmod went through the journal, past the replayed sequence"
10498        );
10499    }
10500
10501    // ---------------------------------------------------------------
10502    // EA_INODE: an attribute whose value lives in another inode
10503    // ---------------------------------------------------------------
10504
10505    /// Give `ino` an in-inode xattr named `name` whose `e_value_inum`
10506    /// points at `value_inum`, while the bytes at `e_value_offs` are
10507    /// `decoy` — which is the shape that makes the defect quiet. The
10508    /// decoy is a real, in-range value, so a parser that ignores
10509    /// `e_value_inum` returns plausible bytes rather than failing.
10510    fn plant_ea_inode_xattr(fs: &Filesystem, ino: u32, name: &str, value_inum: u32, decoy: &[u8]) {
10511        let (inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
10512        let inode_size = fs.sb.inode_size as usize;
10513        let extra_isize = u16::from_le_bytes(raw[128..130].try_into().unwrap()) as usize;
10514        let start = 128 + extra_isize;
10515        {
10516            let region = &mut raw[start..inode_size];
10517            crate::xattr::plan_set_in_inode_region(region, name, decoy).expect("set xattr");
10518            // The entry table begins after the 4-byte magic; this is the
10519            // only entry, so it is the first one. Point it at the EA
10520            // inode and leave `e_value_offs` and `e_value_size` alone, so
10521            // the decoy stays exactly where a careless read would find it.
10522            let e = 4;
10523            region[e + 4..e + 8].copy_from_slice(&value_inum.to_le_bytes());
10524        }
10525        let mut buf = BlockBuffer::new(fs.sb.block_size());
10526        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
10527            .expect("finalize");
10528        fs.buffer_write_inode(&mut buf, ino, &raw).expect("write");
10529        fs.commit_block_buffer(buf).expect("commit");
10530    }
10531
10532    /// Turn an ordinary file into an EA inode: set the flag its own
10533    /// reader checks for, and leave its body as the attribute's value.
10534    fn mark_as_ea_inode(fs: &Filesystem, ino: u32) {
10535        let (inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
10536        let flags = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
10537        let flags = flags | crate::inode::InodeFlags::EA_INODE.bits();
10538        raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
10539        let mut buf = BlockBuffer::new(fs.sb.block_size());
10540        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
10541            .expect("finalize");
10542        fs.buffer_write_inode(&mut buf, ino, &raw).expect("write");
10543        fs.commit_block_buffer(buf).expect("commit");
10544    }
10545
10546    const DECOY: [u8; 64] = [0xEE; 64];
10547
10548    fn ea_inode_volume() -> (std::sync::Arc<MemDev>, Vec<u8>) {
10549        let dev = formatted();
10550        let real: Vec<u8> = (0..64u8)
10551            .map(|i| i.wrapping_mul(7).wrapping_add(3))
10552            .collect();
10553        {
10554            let fs = mount(&dev);
10555            fs.apply_create("/subject.txt", 0o644).expect("create");
10556            let ea = fs.apply_create("/value.bin", 0o644).expect("create ea");
10557            fs.apply_pwrite("/value.bin", 0, &real)
10558                .expect("write value");
10559            mark_as_ea_inode(&fs, ea);
10560            let subject = resolve(&fs, "/subject.txt").expect("resolve");
10561            plant_ea_inode_xattr(&fs, subject, "user.big", ea, &DECOY);
10562        }
10563        (dev, real)
10564    }
10565
10566    /// THE BYTES THE ATTRIBUTE ACTUALLY HOLDS. Both parsers read
10567    /// `e_value_inum` and dropped it, then sliced the value out of the
10568    /// region at `e_value_offs` — which for an EA-inode entry points at
10569    /// nothing in particular, and here points at a decoy.
10570    #[test]
10571    fn an_ea_inode_backed_value_is_read_from_the_inode_it_names() {
10572        let (dev, real) = ea_inode_volume();
10573        let fs = mount(&dev);
10574        let ino = resolve(&fs, "/subject.txt").expect("resolve");
10575        let (inode, raw) = fs.read_inode_verified(ino).expect("read inode");
10576
10577        let got = crate::xattr::get_resolved(&fs, &inode, &raw, "user.big")
10578            .expect("get")
10579            .expect("the attribute is present");
10580
10581        assert_ne!(
10582            got, DECOY,
10583            "the value was read from e_value_offs instead of from the EA inode"
10584        );
10585        assert_eq!(got, real, "the value must be the EA inode's file body");
10586    }
10587
10588    /// THE OTHER DOOR. `get_resolved` is the single-attribute entry
10589    /// point and `read_all_resolved` is the list-all one, and they are
10590    /// separate functions with separate resolution. `capi.rs` reaches the
10591    /// first from `fs_ext4_getxattr`; `fs_ext4_listxattr` returns names
10592    /// only and uses `list_names`, which resolves nothing (#122).
10593    ///
10594    /// A Rust consumer enumerating attributes WITH their values rather
10595    /// than asking for one by name goes through the list-all path, so
10596    /// leaving it unresolved hands back an empty value with a success
10597    /// return: the same failure this issue is about, through a different
10598    /// door.
10599    #[test]
10600    fn the_list_all_path_resolves_an_ea_inode_value_too() {
10601        let (dev, real) = ea_inode_volume();
10602        let fs = mount(&dev);
10603        let ino = resolve(&fs, "/subject.txt").expect("resolve");
10604        let (inode, raw) = fs.read_inode_verified(ino).expect("read inode");
10605
10606        let entries =
10607            crate::xattr::read_all_resolved(&fs, &inode, &raw).expect("read_all_resolved");
10608        let e = entries
10609            .iter()
10610            .find(|e| e.name == "user.big")
10611            .expect("the attribute is present");
10612
10613        assert!(
10614            !e.value.is_empty(),
10615            "the list-all path returned an empty value with a success return"
10616        );
10617        assert_ne!(
10618            e.value, DECOY,
10619            "the value was read from e_value_offs instead of from the EA inode"
10620        );
10621        assert_eq!(e.value, real, "the value must be the EA inode's file body");
10622    }
10623
10624    /// A names-only listing does not read values, so a value that cannot be
10625    /// read does not fail it (#122).
10626    ///
10627    /// `fs_ext4_listxattr` resolved every EA-inode value and discarded it,
10628    /// and one unreadable value turned the whole listing into -1. Here the
10629    /// attribute points at an inode WITHOUT the EA_INODE flag, which
10630    /// `read_value_inode` refuses: resolving values fails (the control),
10631    /// and listing names still names it.
10632    #[test]
10633    fn listing_names_does_not_read_an_unreadable_ea_inode_value() {
10634        let dev = formatted();
10635        {
10636            let fs = mount(&dev);
10637            fs.apply_create("/subject.txt", 0o644).expect("create");
10638            let not_ea = fs.apply_create("/plain.bin", 0o644).expect("create");
10639            let subject = resolve(&fs, "/subject.txt").expect("resolve");
10640            plant_ea_inode_xattr(&fs, subject, "user.big", not_ea, &DECOY);
10641        }
10642        let fs = mount(&dev);
10643        let ino = resolve(&fs, "/subject.txt").expect("resolve");
10644        let (inode, raw) = fs.read_inode_verified(ino).expect("read inode");
10645
10646        assert!(
10647            crate::xattr::read_all_resolved(&fs, &inode, &raw).is_err(),
10648            "control: the value behind this attribute cannot be read"
10649        );
10650        let names =
10651            crate::xattr::list_names(&fs, &inode, &raw).expect("listing names needs no value");
10652        assert!(names.iter().any(|n| n == "user.big"), "{names:?}");
10653        drop(fs);
10654
10655        // And through the door the bug was reported at: the C entry point
10656        // must report the size, then write the name, instead of -1.
10657        let image =
10658            fs_ext4_test_support::temp_path!("fs_ext4_listxattr_{}.img", std::process::id());
10659        std::fs::write(&image, &*dev.bytes.lock().unwrap()).expect("write image");
10660        let c_image = std::ffi::CString::new(image.clone()).unwrap();
10661        let c_path = std::ffi::CString::new("/subject.txt").unwrap();
10662        unsafe {
10663            let handle = crate::capi::fs_ext4_mount(c_image.as_ptr());
10664            assert!(!handle.is_null(), "C mount");
10665            let needed =
10666                crate::capi::fs_ext4_listxattr(handle, c_path.as_ptr(), std::ptr::null_mut(), 0);
10667            assert!(needed > 0, "the probe returned {needed}");
10668            let mut buf = vec![0u8; needed as usize];
10669            let wrote = crate::capi::fs_ext4_listxattr(
10670                handle,
10671                c_path.as_ptr(),
10672                buf.as_mut_ptr().cast(),
10673                buf.len(),
10674            );
10675            assert_eq!(wrote, needed);
10676            assert!(
10677                buf.split(|&b| b == 0).any(|n| n == b"user.big"),
10678                "{:?}",
10679                String::from_utf8_lossy(&buf)
10680            );
10681            crate::capi::fs_ext4_umount(handle);
10682        }
10683        let _ = std::fs::remove_file(&image);
10684    }
10685
10686    /// The buffer-level parser cannot follow the pointer — it has no
10687    /// filesystem — so it must report the pointer and an EMPTY value
10688    /// rather than the bytes at `e_value_offs`. An empty value is a
10689    /// visible failure; a decoy is a plausible one.
10690    #[test]
10691    fn the_buffer_level_parser_reports_the_pointer_and_no_value() {
10692        let (dev, _real) = ea_inode_volume();
10693        let fs = mount(&dev);
10694        let ino = resolve(&fs, "/subject.txt").expect("resolve");
10695        let (inode, raw) = fs.read_inode_verified(ino).expect("read inode");
10696
10697        let entries = crate::xattr::read_all(
10698            fs.dev.as_ref(),
10699            &inode,
10700            &raw,
10701            fs.sb.inode_size,
10702            fs.sb.block_size(),
10703        )
10704        .expect("read_all");
10705        let e = entries
10706            .iter()
10707            .find(|e| e.name == "user.big")
10708            .expect("the attribute is present");
10709
10710        assert_ne!(e.value_inum, 0, "the pointer must be reported");
10711        assert!(
10712            e.value.is_empty(),
10713            "the value must not be taken from e_value_offs; got {:?}",
10714            e.value
10715        );
10716    }
10717
10718    /// THE WRITE THAT WOULD ORPHAN IT. The in-inode region is rewritten
10719    /// wholesale, so touching any attribute re-emits every other one —
10720    /// and an EA-inode-backed entry cannot be re-emitted, because its
10721    /// value is not there to repack. Refusing is the honest answer while
10722    /// following and refcounting EA inodes is unimplemented.
10723    #[test]
10724    fn rewriting_an_attribute_area_holding_an_ea_inode_entry_is_refused() {
10725        let (dev, _real) = ea_inode_volume();
10726        let fs = mount(&dev);
10727        let ino = resolve(&fs, "/subject.txt").expect("resolve");
10728        let (_inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
10729        let inode_size = fs.sb.inode_size as usize;
10730        let extra_isize = u16::from_le_bytes(raw[128..130].try_into().unwrap()) as usize;
10731        let region = &mut raw[128 + extra_isize..inode_size];
10732
10733        let removed = crate::xattr::plan_remove_in_inode_region(region, "user.big");
10734        assert!(
10735            matches!(removed, Err(Error::Unsupported(_))),
10736            "removing it must be refused, not silently orphan the EA inode; got {removed:?}"
10737        );
10738
10739        let set = crate::xattr::plan_set_in_inode_region(region, "user.other", b"x");
10740        assert!(
10741            matches!(set, Err(Error::Unsupported(_))),
10742            "setting a DIFFERENT attribute must also be refused, because the rewrite \
10743             re-emits the EA-inode entry too; got {set:?}"
10744        );
10745    }
10746
10747    /// The control: an ordinary inline attribute still reads and still
10748    /// rewrites, so none of the above can be satisfied by refusing
10749    /// everything.
10750    #[test]
10751    fn an_ordinary_inline_attribute_is_unaffected() {
10752        let dev = formatted();
10753        let fs = mount(&dev);
10754        let ino = fs.apply_create("/plain.txt", 0o644).expect("create");
10755        let (inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
10756        let inode_size = fs.sb.inode_size as usize;
10757        let extra_isize = u16::from_le_bytes(raw[128..130].try_into().unwrap()) as usize;
10758        {
10759            let region = &mut raw[128 + extra_isize..inode_size];
10760            crate::xattr::plan_set_in_inode_region(region, "user.small", b"hello")
10761                .expect("an ordinary set must still work");
10762        }
10763        let mut buf = BlockBuffer::new(fs.sb.block_size());
10764        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
10765            .expect("finalize");
10766        fs.buffer_write_inode(&mut buf, ino, &raw).expect("write");
10767        fs.commit_block_buffer(buf).expect("commit");
10768
10769        let (inode, raw) = fs.read_inode_verified(ino).expect("re-read");
10770        let got = crate::xattr::get_resolved(&fs, &inode, &raw, "user.small")
10771            .expect("get")
10772            .expect("present");
10773        assert_eq!(got, b"hello");
10774    }
10775
10776    /// THE CASE THAT DESTROYED DATA. An inode on the orphan chain with a
10777    /// non-zero link count is a `truncate()` a crash interrupted. It is
10778    /// still named by its directory entries. Recovery must finish the
10779    /// truncate, not delete the file.
10780    #[test]
10781    fn an_orphan_that_still_has_links_is_truncated_not_deleted() {
10782        let dev = formatted();
10783        let payload: Vec<u8> = (0..4 * BS as usize).map(|i| (i % 251) as u8).collect();
10784
10785        let ino = {
10786            let fs = mount(&dev);
10787            let ino = fs.apply_create("/keep.txt", 0o644).expect("create");
10788            fs.apply_pwrite("/keep.txt", 0, &payload).expect("write");
10789            fs.apply_link("/keep.txt", "/also-keep.txt").expect("link");
10790            ino
10791        };
10792
10793        let before_free = {
10794            let fs = mount(&dev);
10795            // i_size drops to one block; the four blocks of extents stay.
10796            plant_orphan(&fs, ino, 2, Some(BS as u64));
10797            fs.sb.free_blocks_count
10798        };
10799
10800        // Recovery runs on this mount; the next one observes the result.
10801        drop(mount(&dev));
10802
10803        let fs = mount(&dev);
10804        assert!(
10805            fs.orphan_list().expect("orphan_list").is_empty(),
10806            "recovery must empty the chain"
10807        );
10808
10809        let (inode, _) = fs.read_inode_verified(ino).expect("read inode");
10810        assert_eq!(
10811            inode.links_count, 2,
10812            "the file is named twice and was never unlinked; recovery deleted it"
10813        );
10814        assert_eq!(inode.dtime, 0, "a live file must not be stamped as deleted");
10815        assert_eq!(
10816            inode.size, BS as u64,
10817            "the interrupted truncate should be finished, not undone"
10818        );
10819
10820        assert_eq!(resolve(&fs, "/keep.txt").expect("keep.txt"), ino);
10821        assert_eq!(resolve(&fs, "/also-keep.txt").expect("also-keep.txt"), ino);
10822
10823        let data = crate::file_io::read_all(&fs, &inode).expect("read back");
10824        assert_eq!(
10825            data.len(),
10826            BS as usize,
10827            "the surviving file is its first i_size bytes"
10828        );
10829        assert_eq!(
10830            &data[..],
10831            &payload[..BS as usize],
10832            "and those bytes are the ones that were written"
10833        );
10834
10835        assert_eq!(
10836            fs.sb.free_blocks_count,
10837            before_free + 3,
10838            "only the three blocks past the new EOF should have been freed"
10839        );
10840    }
10841
10842    /// AN INTERRUPTED TRUNCATE THE PLANNER REFUSES STAYS ON THE CHAIN. A
10843    /// deep extent tree whose leaf fails its checksum cannot be planned.
10844    /// Recovery must leave the member at the head, untouched, and report
10845    /// no error -- the same answer it gives every member it cannot reclaim
10846    /// whole, so the members behind it and the previous member's pending
10847    /// `i_dtime` are handled the one way.
10848    #[test]
10849    fn an_interrupted_truncate_over_a_corrupt_deep_tree_stays_on_the_chain() {
10850        let dev = formatted();
10851        let ino = {
10852            let fs = mount(&dev);
10853            let ino = fs.apply_create("/deep.bin", 0o644).expect("create");
10854            // One block every other block: twelve extents, more than the
10855            // inline root's four, so the tree grows an external leaf.
10856            for i in 0..12u64 {
10857                fs.apply_pwrite("/deep.bin", i * 2 * BS as u64, &[0x5a; BS as usize])
10858                    .expect("write");
10859            }
10860            ino
10861        };
10862
10863        let (leaf, size_before) = {
10864            let fs = mount(&dev);
10865            let (inode, _) = fs.read_inode_verified(ino).expect("read inode");
10866            assert!(
10867                crate::extent::ExtentHeader::parse(&inode.block)
10868                    .expect("root")
10869                    .depth
10870                    >= 1,
10871                "fixture: the tree is deeper than the inline root"
10872            );
10873            let leaf = crate::extent::ExtentIdx::parse(&inode.block[12..24])
10874                .expect("index")
10875                .leaf_block;
10876            plant_orphan(&fs, ino, 1, Some(BS as u64));
10877            (leaf, inode.size)
10878        };
10879        // Break the leaf's checksum: the tail's last byte.
10880        dev.bytes.lock().unwrap()[(leaf * BS as u64 + BS as u64 - 1) as usize] ^= 1;
10881
10882        let fs = mount(&dev);
10883        assert_eq!(
10884            fs.recover_orphans()
10885                .expect("an unplannable member is not an error"),
10886            0
10887        );
10888        assert_eq!(
10889            fs.orphan_list().expect("orphan_list"),
10890            vec![ino],
10891            "the member it could not reclaim stays at the head"
10892        );
10893        let (inode, _) = fs.read_inode_verified(ino).expect("read inode");
10894        assert_eq!(inode.links_count, 1, "the file is still named");
10895        assert_eq!(inode.size, BS as u64, "its lowered size is kept");
10896        assert_ne!(size_before, inode.size, "fixture: the size was lowered");
10897    }
10898
10899    /// A FILE WHOSE EXTENT LEAF FAILS ITS CHECKSUM IS NOT FREED AROUND.
10900    /// Freeing walks the tree to learn which blocks to release; a node that
10901    /// does not verify may name blocks that belong to something else. The
10902    /// kernel refuses the removal (EBADMSG); so does this, before any
10903    /// bitmap is touched.
10904    #[test]
10905    fn unlinking_a_file_whose_extent_leaf_fails_its_checksum_is_refused() {
10906        let dev = formatted();
10907        {
10908            let fs = mount(&dev);
10909            fs.apply_create("/deep.bin", 0o644).expect("create");
10910            for i in 0..12u64 {
10911                fs.apply_pwrite("/deep.bin", i * 2 * BS as u64, &[0x5a; BS as usize])
10912                    .expect("write");
10913            }
10914        }
10915        let leaf = {
10916            let fs = mount(&dev);
10917            let ino = resolve(&fs, "/deep.bin").expect("resolve");
10918            let (inode, _) = fs.read_inode_verified(ino).expect("read inode");
10919            crate::extent::ExtentIdx::parse(&inode.block[12..24])
10920                .expect("index")
10921                .leaf_block
10922        };
10923        dev.bytes.lock().unwrap()[(leaf * BS as u64 + BS as u64 - 1) as usize] ^= 1;
10924
10925        let fs = mount(&dev);
10926        let free_before = fs.sb.free_blocks_count;
10927        let got = fs.apply_unlink("/deep.bin");
10928        assert!(
10929            matches!(got, Err(Error::BadChecksum { .. })),
10930            "unlink over an unverified extent leaf: {got:?}"
10931        );
10932        drop(fs);
10933        let fs = mount(&dev);
10934        assert!(resolve(&fs, "/deep.bin").is_ok(), "the file is still named");
10935        assert_eq!(fs.sb.free_blocks_count, free_before, "nothing was freed");
10936    }
10937
10938    // --- i_file_acl: written where it is read -------------------------
10939    //
10940    // THE OBVIOUS TEST IS NOT ENOUGH, and this is the whole reason the
10941    // defect survived. A round trip on a small image passes today: below
10942    // 2^32 blocks both halves of `i_file_acl` are zero, the wrong offset
10943    // is clobbered with zero over zero, and nothing disagrees. The block
10944    // number has to have bits above 32 set.
10945    //
10946    // Driven against a synthetic 256-byte inode rather than a 16 TiB
10947    // filesystem, because what is being tested is which BYTES the two
10948    // writers touch, and that is answerable without the volume.
10949
10950    /// A 256-byte inode with a plausible extra_isize, so `Inode::parse`
10951    /// reads it the way it reads a real one.
10952    fn synthetic_inode() -> Vec<u8> {
10953        let mut raw = vec![0u8; 256];
10954        raw[0x00..0x02].copy_from_slice(&0x81A4u16.to_le_bytes()); // S_IFREG | 0644
10955        raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2]
10956            .copy_from_slice(&EXTRA_ISIZE_DEFAULT.to_le_bytes());
10957        raw
10958    }
10959
10960    /// THE DEFECT. A block number above 2^32 must survive the write.
10961    ///
10962    /// `patch_inode_size_and_blocks` runs after the splice at both real
10963    /// call sites and owns `0x74..0x76`, so it is run here too: with the
10964    /// old offset the high half was written and then immediately
10965    /// overwritten, and a test that skipped this call would have passed
10966    /// against the broken code.
10967    #[test]
10968    fn a_file_acl_block_above_2_32_survives_patch_inode_size_and_blocks() {
10969        let block: u64 = 0x0003_1234_5678; // bits above 32 set
10970        let mut raw = synthetic_inode();
10971
10972        Filesystem::write_file_acl(&mut raw, block).expect("write_file_acl");
10973        Filesystem::patch_inode_size_and_blocks(&mut raw, 4096, 0x0000_0002_0000_0008)
10974            .expect("patch_inode_size_and_blocks");
10975
10976        let inode = Inode::parse(&raw).expect("parse");
10977        assert_eq!(
10978            inode.file_acl, block,
10979            "i_file_acl read back as {:#x}, written as {:#x} — the high half is at \
10980             0x76..0x78 and the low at 0x68..0x6C",
10981            inode.file_acl, block
10982        );
10983    }
10984
10985    /// THE FREE CONTROL the issue named: a fix that over-corrects by
10986    /// moving the wrong field fails here.
10987    ///
10988    /// `i_blocks_hi` still belongs to `patch_inode_size_and_blocks`, and
10989    /// the splice must not touch it. Without this, writing `i_file_acl_hi`
10990    /// to `0x74` and `i_blocks_hi` to `0x76` would satisfy the test above
10991    /// by symmetry.
10992    #[test]
10993    fn the_splice_leaves_i_blocks_hi_to_the_function_that_owns_it() {
10994        let mut raw = synthetic_inode();
10995        Filesystem::patch_inode_size_and_blocks(&mut raw, 4096, 0x0000_0002_0000_0008)
10996            .expect("patch");
10997        let blocks_hi_before = read_le16(&raw, 0x74);
10998        assert_eq!(
10999            blocks_hi_before, 2,
11000            "the fixture must set a nonzero blocks_hi"
11001        );
11002
11003        Filesystem::write_file_acl(&mut raw, 0x0003_1234_5678).expect("write_file_acl");
11004        assert_eq!(
11005            read_le16(&raw, 0x74),
11006            blocks_hi_before,
11007            "the file_acl splice wrote i_blocks_hi (0x74..0x76), which belongs to \
11008             patch_inode_size_and_blocks"
11009        );
11010        assert_eq!(
11011            Inode::parse(&raw).expect("parse").blocks,
11012            0x0000_0002_0000_0008,
11013            "and i_blocks reads back unchanged"
11014        );
11015    }
11016
11017    /// Clearing it clears BOTH halves. The free path zeroed only the low
11018    /// one, leaving `file_acl == old_hi << 32` — a nonzero pointer at a
11019    /// block just handed back to the allocator.
11020    #[test]
11021    fn clearing_file_acl_clears_the_high_half_too() {
11022        // THE STARTING STATE IS WRITTEN BY HAND, at the offsets the
11023        // on-disk format uses, because that is the inode this driver is
11024        // handed: one Linux or mkfs wrote with a real high half. Using
11025        // `write_file_acl` to set it up would make the test agree with
11026        // whatever offset that function happens to use, and a truncating
11027        // writer followed by a truncating clear reads back 0 either way.
11028        let mut raw = synthetic_inode();
11029        raw[0x68..0x6C].copy_from_slice(&0x1234_5678u32.to_le_bytes());
11030        raw[0x76..0x78].copy_from_slice(&3u16.to_le_bytes());
11031        assert_eq!(
11032            Inode::parse(&raw).expect("parse").file_acl,
11033            0x0003_1234_5678,
11034            "the fixture must present an inode whose file_acl needs both halves"
11035        );
11036
11037        Filesystem::write_file_acl(&mut raw, 0).expect("clear");
11038        Filesystem::patch_inode_size_and_blocks(&mut raw, 4096, 0x0000_0002_0000_0008)
11039            .expect("patch");
11040        assert_eq!(
11041            Inode::parse(&raw).expect("parse").file_acl,
11042            0,
11043            "a freed external xattr block must leave no pointer behind — clearing only \
11044             i_file_acl_lo leaves file_acl == old_hi << 32, at a block already handed \
11045             back to the allocator"
11046        );
11047    }
11048
11049    /// The low half on its own still round-trips, so a volume under
11050    /// 2^32 blocks — every volume this has ever run on — is unaffected.
11051    #[test]
11052    fn a_small_file_acl_block_round_trips_as_it_always_did() {
11053        let mut raw = synthetic_inode();
11054        Filesystem::write_file_acl(&mut raw, 0x1234_5678).expect("write_file_acl");
11055        Filesystem::patch_inode_size_and_blocks(&mut raw, 4096, 8).expect("patch");
11056        assert_eq!(Inode::parse(&raw).expect("parse").file_acl, 0x1234_5678);
11057    }
11058
11059    /// The length guard. `>= 0x76` admitted a buffer ending exactly where
11060    /// the field starts; and an inode that cannot hold the high half must
11061    /// refuse a block number that needs one rather than store a pointer
11062    /// to a different block.
11063    #[test]
11064    fn an_inode_too_short_for_the_high_half_refuses_a_block_that_needs_one() {
11065        let mut short = vec![0u8; 0x76];
11066        assert!(
11067            Filesystem::write_file_acl(&mut short, 0x0003_1234_5678).is_err(),
11068            "a 0x76-byte inode has no room for 0x76..0x78 and must not truncate"
11069        );
11070        assert_eq!(
11071            read_le32(&short, 0x68),
11072            0,
11073            "a refused write must not leave the truncated low half behind"
11074        );
11075
11076        // ...but a block number that fits in 32 bits is fine there, which
11077        // is what makes the refusal a bound rather than a blanket no.
11078        let mut short = vec![0u8; 0x76];
11079        Filesystem::write_file_acl(&mut short, 0x1234_5678).expect("the low half fits");
11080        assert_eq!(read_le32(&short, 0x68), 0x1234_5678);
11081
11082        let mut tiny = vec![0u8; 0x6B];
11083        assert!(
11084            Filesystem::write_file_acl(&mut tiny, 0).is_err(),
11085            "a buffer too short for even the low half must be refused"
11086        );
11087    }
11088
11089    /// ONE RECIPE, AND THE TESTS ABOVE CANNOT SEE A SECOND ONE.
11090    ///
11091    /// Everything above drives `write_file_acl` directly. Re-inlining the
11092    /// splice at either call site — which is the state this file was in —
11093    /// leaves all of them green, because they never execute a call site.
11094    /// Reaching one needs a filesystem above 2^32 blocks, i.e. a 16 TiB
11095    /// image, which is not a test anyone will run.
11096    ///
11097    /// So this reads the source instead and requires each offset to be
11098    /// written in exactly one place: `0x74..0x76` only by
11099    /// `patch_inode_size_and_blocks`, which owns `i_blocks_hi`, and
11100    /// `0x76..0x78` only by `write_file_acl`. A hand-written second copy
11101    /// of either is what let the two disagree with `Inode::parse` for as
11102    /// long as they did.
11103    /// Every statement in `src` that writes `field` with `copy_from_slice`,
11104    /// whitespace collapsed.
11105    ///
11106    /// BY STATEMENT, NOT BY LINE (#162). rustfmt wraps a long write as
11107    /// `raw[0x74..0x76]` on one line and `.copy_from_slice(..)` on the next,
11108    /// so a line scan saw neither half as a write, and #157's defect could be
11109    /// re-inlined with fmt, clippy and this test all green. Line comments are
11110    /// removed first so a `;` or an offset in prose does not join or form a
11111    /// statement.
11112    fn half_word_writes(src: &str, field: &str) -> Vec<String> {
11113        let code: String = src
11114            .lines()
11115            .map(|line| line.find("//").map_or(line, |at| &line[..at]))
11116            .collect::<Vec<_>>()
11117            .join("\n");
11118        code.split(';')
11119            .map(|statement| statement.split_whitespace().collect::<Vec<_>>().join(" "))
11120            .filter(|statement| statement.contains(field) && statement.contains("copy_from_slice"))
11121            .collect()
11122    }
11123
11124    /// The guard's reader sees a write however rustfmt lays it out.
11125    #[test]
11126    fn the_half_word_scan_reads_a_write_rustfmt_wrapped() {
11127        let wrapped = "
11128                    let acl_lo_value_for_the_external_xattr_block: u32 = 0;
11129                    raw[0x74..0x76]
11130                        .copy_from_slice(&acl_hi_value_for_the_external_xattr_block.to_le_bytes());
11131                    // raw[0x74..0x76].copy_from_slice(&in_a_comment);
11132        ";
11133        assert_eq!(half_word_writes(wrapped, "0x74..0x76").len(), 1);
11134        let one_line = "raw[0x74..0x76].copy_from_slice(&x.to_le_bytes());";
11135        assert_eq!(half_word_writes(one_line, "0x74..0x76").len(), 1);
11136        assert!(half_word_writes("let a = raw[0x74..0x76][0];", "0x74..0x76").is_empty());
11137    }
11138
11139    #[test]
11140    fn each_inode_half_word_is_written_in_exactly_one_place() {
11141        let whole = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/fs.rs"))
11142            .expect("src/fs.rs is readable");
11143
11144        // ONLY THE SHIPPING HALF. This module writes those offsets by hand
11145        // to build fixtures, and counting its own fixtures as writers made
11146        // the check fail the moment a test was added — which is a guard
11147        // that reports a defect it has manufactured. The split is on the
11148        // `#[cfg(test)]` that begins this module.
11149        let cut = whole
11150            .find("\n#[cfg(test)]\n")
11151            .expect("src/fs.rs has a #[cfg(test)] module, which is where this test lives");
11152        let src = &whole[..cut];
11153        assert!(
11154            src.contains("fn patch_inode_size_and_blocks"),
11155            "the non-test half must still contain the writers, or a count of 1 means \
11156             the split ate the file"
11157        );
11158
11159        let writes = |field: &str| half_word_writes(src, field);
11160
11161        let blocks_hi = writes("0x74..0x76");
11162        assert_eq!(
11163            blocks_hi.len(),
11164            1,
11165            "i_blocks_hi (0x74..0x76) is written {} times; it belongs to \
11166             patch_inode_size_and_blocks alone. Found: {blocks_hi:?}",
11167            blocks_hi.len()
11168        );
11169        assert!(
11170            blocks_hi[0].contains("blocks_hi"),
11171            "the one 0x74..0x76 write must be the i_blocks_hi one: {:?}",
11172            blocks_hi[0]
11173        );
11174
11175        let file_acl_hi = writes("0x76..0x78");
11176        assert_eq!(
11177            file_acl_hi.len(),
11178            1,
11179            "i_file_acl_hi (0x76..0x78) is written {} times; write_file_acl is the one \
11180             place. Found: {file_acl_hi:?}",
11181            file_acl_hi.len()
11182        );
11183
11184        // The control: this reader can see a write at all, so a count of
11185        // 1 means one and not a pattern that matches nothing.
11186        let file_acl_lo = writes("0x68..0x6C");
11187        assert_eq!(
11188            file_acl_lo.len(),
11189            1,
11190            "i_file_acl_lo (0x68..0x6C) should also be written exactly once, by the same \
11191             function. Found: {file_acl_lo:?}"
11192        );
11193    }
11194
11195    /// Make the regular file `ino` an inline-data file whose 4 content
11196    /// bytes, held in `i_block`, are `content` — the way the kernel keeps a
11197    /// file that fits in the inode.
11198    fn make_inline_file(fs: &Filesystem, ino: u32, content: u32) {
11199        let (inode, mut raw) = fs.read_inode_verified(ino).unwrap();
11200        let mut flags = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
11201        flags &= !crate::inode::InodeFlags::EXTENTS.bits();
11202        flags |= crate::inode::InodeFlags::INLINE_DATA.bits();
11203        raw[0x20..0x24].copy_from_slice(&flags.to_le_bytes());
11204        raw[0x28..0x64].fill(0);
11205        raw[0x28..0x2C].copy_from_slice(&content.to_le_bytes());
11206        raw[0x04..0x08].copy_from_slice(&4u32.to_le_bytes());
11207        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
11208            .unwrap();
11209        fs.write_inode_raw(ino, &raw).unwrap();
11210    }
11211
11212    /// Whether `block` is marked in use in its group's bitmap.
11213    fn block_in_use(fs: &Filesystem, block: u64) -> bool {
11214        let rel = block - fs.sb.first_data_block as u64;
11215        let gi = (rel / fs.sb.blocks_per_group as u64) as usize;
11216        let bit = (rel % fs.sb.blocks_per_group as u64) as usize;
11217        let bm = fs.read_block(fs.groups[gi].block_bitmap).unwrap();
11218        bm[bit / 8] & (1 << (bit % 8)) != 0
11219    }
11220
11221    /// #383: replacing the content of an inline-data file must not read its
11222    /// data bytes as block pointers and free them. Since #428 the replace
11223    /// goes through, in the inode.
11224    #[test]
11225    fn replacing_an_inline_file_does_not_free_its_bytes_as_blocks() {
11226        let dev = formatted();
11227        set_incompat_bit(&dev, crate::features::Incompat::INLINE_DATA.bits());
11228        let fs = mount(&dev);
11229        let ino = fs.apply_create("/f", 0o644).unwrap();
11230        let (root, _) = fs.read_inode_verified(2).unwrap();
11231        let victim = fs.map_inode_logical(&root, 0).unwrap().unwrap(); // root dir's block
11232        make_inline_file(&fs, ino, victim as u32);
11233        drop(fs);
11234        let fs = mount(&dev);
11235        let r = fs.apply_replace_file_content("/f", b"new content");
11236        drop(fs);
11237        let fs = mount(&dev);
11238        assert!(
11239            block_in_use(&fs, victim),
11240            "root directory block {victim} was freed: {r:?}"
11241        );
11242        assert_eq!(r.unwrap(), 11);
11243        let (inode, raw) = fs.read_inode_verified(ino).unwrap();
11244        assert!(inode.has_inline_data(), "11 bytes fit in the inode");
11245        assert_eq!(
11246            crate::file_io::read_inline(&fs, &inode, &raw).unwrap(),
11247            b"new content"
11248        );
11249    }
11250
11251    /// The inline file's bytes and whether it is still inline.
11252    fn file_bytes(fs: &Filesystem, ino: u32) -> (Vec<u8>, bool) {
11253        let (inode, raw) = fs.read_inode_verified(ino).unwrap();
11254        let mut out = vec![0u8; inode.size as usize];
11255        let n = crate::file_io::read_with_raw(fs, &inode, &raw, 0, inode.size, &mut out).unwrap();
11256        assert_eq!(n, inode.size);
11257        (out, inode.has_inline_data())
11258    }
11259
11260    /// #428: truncate and pwrite of an inline-data file are made in the
11261    /// inode while the result fits (a grow used to patch only `i_size`,
11262    /// past what the inline area holds, #383), and convert the file to an
11263    /// extent-mapped one when it does not; the other files are untouched
11264    /// and the volume audits clean after each step.
11265    #[test]
11266    fn truncating_or_writing_an_inline_file_keeps_it_inline_while_it_fits() {
11267        let dev = formatted();
11268        set_incompat_bit(&dev, crate::features::Incompat::INLINE_DATA.bits());
11269        let fs = mount(&dev);
11270        let ino = fs.apply_create("/f", 0o644).unwrap();
11271        fs.apply_create("/g", 0o644).unwrap();
11272        fs.apply_replace_file_content("/g", b"neighbour").unwrap();
11273        make_inline_file(&fs, ino, u32::from_le_bytes(*b"abc\n"));
11274        drop(fs);
11275
11276        let mut model = b"abc\n".to_vec();
11277        type Step = fn(&Filesystem, u32) -> Result<()>;
11278        type Model = fn(&mut Vec<u8>);
11279        let steps: [(&str, Step, Model, bool); 6] = [
11280            (
11281                "pwrite",
11282                |fs, _| fs.apply_pwrite("/f", 1, b"x").map(drop),
11283                |m| m[1] = b'x',
11284                true,
11285            ),
11286            (
11287                "grow into system.data",
11288                |fs, ino| fs.apply_truncate_grow(ino, 100),
11289                |m| m.resize(100, 0),
11290                true,
11291            ),
11292            (
11293                "pwrite in system.data",
11294                |fs, ino| fs.apply_pwrite_ino(ino, 90, b"tail").map(drop),
11295                |m| m[90..94].copy_from_slice(b"tail"),
11296                true,
11297            ),
11298            (
11299                "shrink",
11300                |fs, ino| fs.apply_truncate_shrink(ino, 2),
11301                |m| m.truncate(2),
11302                true,
11303            ),
11304            (
11305                "grow past the inode",
11306                |fs, ino| fs.apply_truncate_grow(ino, 4096),
11307                |m| m.resize(4096, 0),
11308                false,
11309            ),
11310            (
11311                "pwrite the converted file",
11312                |fs, ino| fs.apply_pwrite_ino(ino, 4000, b"end").map(drop),
11313                |m| m[4000..4003].copy_from_slice(b"end"),
11314                false,
11315            ),
11316        ];
11317        for (name, step, apply, inline) in steps {
11318            let fs = mount(&dev);
11319            step(&fs, ino).unwrap_or_else(|e| panic!("{name}: {e:?}"));
11320            drop(fs);
11321            apply(&mut model);
11322            let fs = mount(&dev);
11323            assert_eq!(file_bytes(&fs, ino), (model.clone(), inline), "{name}");
11324            let g = resolve(&fs, "/g").unwrap();
11325            assert_eq!(file_bytes(&fs, g).0, b"neighbour", "{name}: /g");
11326            let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).unwrap();
11327            assert!(
11328                report.anomalies.is_empty(),
11329                "{name}: {:?}",
11330                report.anomalies
11331            );
11332        }
11333    }
11334
11335    /// Tests that run an oracle tool (they run in the harness VM): mkfs.ext4, e2fsck.
11336    mod needs_host {
11337        use super::*;
11338
11339        /// #384, judged by e2fsck: recovering an orphaned fast symlink that
11340        /// holds an xattr block frees the inode and the xattr block and
11341        /// nothing its target text names -- a freed in-use block is what
11342        /// e2fsck's pass 5 reports.
11343        #[test]
11344        fn recovering_an_orphaned_fast_symlink_with_an_xattr_block_leaves_e2fsck_clean() {
11345            let dev = formatted();
11346            {
11347                let fs = mount(&dev);
11348                let (root, _) = fs.read_inode_verified(2).unwrap();
11349                let victim = fs.map_inode_logical(&root, 0).unwrap().unwrap();
11350                let mut tb: Vec<u8> = (victim as u32).to_le_bytes().to_vec();
11351                while tb.last() == Some(&0) {
11352                    tb.pop();
11353                }
11354                let target = String::from_utf8(tb).expect("an ASCII block number");
11355                let ino = fs.apply_symlink(&target, "/l").unwrap();
11356                give_xattr_block(&fs, ino);
11357                let (root, _) = fs.read_inode_verified(2).unwrap();
11358                let mut buf = BlockBuffer::new(fs.sb.block_size());
11359                fs.buffer_remove_dir_entry(&mut buf, 2, &root, b"l")
11360                    .expect("remove the name");
11361                fs.commit_block_buffer(buf).expect("commit");
11362                plant_orphan(&fs, ino, 0, None);
11363            }
11364            // Recovery runs on this mount.
11365            drop(mount(&dev));
11366
11367            let image = fs_ext4_test_support::temp_dir()
11368                .join(format!("fs_ext4_symlink_orphan_{}.img", std::process::id()));
11369            std::fs::write(&image, &*dev.bytes.lock().unwrap()).unwrap();
11370            let judged = fs_ext4_test_support::oracle("e2fsck")
11371                .arg("-fn")
11372                .arg(&image)
11373                .judged();
11374            let _ = std::fs::remove_file(&image);
11375            judged.clean("an orphaned fast symlink with an xattr block, recovered");
11376        }
11377
11378        /// A run reaching past `blocks_count` into the short final group's
11379        /// padding is refused; one ending at the last block is not.
11380        #[test]
11381        fn a_run_into_the_final_groups_padding_is_refused() {
11382            let dir = fs_ext4_test_support::temp_dir()
11383                .join(format!("ext4-short-group-{}", std::process::id()));
11384            std::fs::create_dir_all(&dir).unwrap();
11385            let img = dir.join("s.img");
11386            // 16284 blocks at 4096 per group: the last group has 4 blocks and
11387            // 4092 bits of padding.
11388            std::fs::File::create(&img)
11389                .unwrap()
11390                .set_len(16284 * 4096)
11391                .unwrap();
11392            let made = fs_ext4_test_support::oracle("mkfs.ext4")
11393                .args(["-q", "-F", "-b", "4096", "-g", "4096", "-O", "^has_journal"])
11394                .arg(&img)
11395                .output();
11396            assert!(
11397                made.status.success(),
11398                "{}",
11399                String::from_utf8_lossy(&made.stderr)
11400            );
11401            let fs = Filesystem::mount(std::sync::Arc::new(
11402                crate::block_io::FileDevice::open(img.to_str().unwrap()).unwrap(),
11403            ))
11404            .unwrap();
11405            let last = fs.sb.blocks_count;
11406            assert_ne!(
11407                (last - u64::from(fs.sb.first_data_block)) % u64::from(fs.sb.blocks_per_group),
11408                0,
11409                "fixture: the final group is short"
11410            );
11411            assert!(
11412                fs.group_chunks(last - 2, 2).is_ok(),
11413                "a run ending at the last block"
11414            );
11415            assert!(
11416                matches!(fs.group_chunks(last - 2, 3), Err(Error::InvalidBlock(_))),
11417                "a run one block into the padding"
11418            );
11419            let _ = std::fs::remove_dir_all(&dir);
11420        }
11421
11422        /// A freed run that crosses a group boundary credits each group its
11423        /// own blocks and clears the second group's bits (#118).
11424        ///
11425        /// Witnessed by the descriptors and the second group's bitmap, not the
11426        /// return value or the superblock delta: those are `len` before and
11427        /// after the fix alike. Measured before it: group 1 credited 8 and
11428        /// group 2 credited 0, its four bits left set. Fails without mkfs.ext4
11429        /// (they run in the harness VM).
11430        #[test]
11431        fn a_freed_run_across_a_group_boundary_credits_both_groups() {
11432            let dir = fs_ext4_test_support::temp_dir()
11433                .join(format!("ext4-cross-group-{}", std::process::id()));
11434            std::fs::create_dir_all(&dir).unwrap();
11435            let img = dir.join("g.img");
11436            std::fs::File::create(&img)
11437                .unwrap()
11438                .set_len(64 * 1024 * 1024)
11439                .unwrap();
11440            let made = fs_ext4_test_support::oracle("mkfs.ext4")
11441                .args([
11442                    "-q",
11443                    "-F",
11444                    "-b",
11445                    "4096",
11446                    "-g",
11447                    "4096",
11448                    "-O",
11449                    "^metadata_csum,^has_journal",
11450                ])
11451                .arg(&img)
11452                .output();
11453            assert!(
11454                made.status.success(),
11455                "{}",
11456                String::from_utf8_lossy(&made.stderr)
11457            );
11458            let path = img.to_str().unwrap().to_owned();
11459            let mount = || {
11460                Filesystem::mount(std::sync::Arc::new(
11461                    crate::block_io::FileDevice::open_rw(&path).unwrap(),
11462                ))
11463                .unwrap()
11464            };
11465            let boundary = 2 * 4096u64; // group 2 starts here
11466            let bit_set = |fs: &Filesystem, gi: usize, bit: u64| {
11467                let bm = fs.read_block(fs.groups[gi].block_bitmap).unwrap();
11468                bm[(bit / 8) as usize] & (1 << (bit % 8)) != 0
11469            };
11470
11471            // Four blocks either side of the boundary, allocated per group.
11472            {
11473                let fs = mount();
11474                let mut buf = BlockBuffer::new(fs.sb.block_size());
11475                fs.buffer_mark_block_run_used(&mut buf, boundary - 4, 4)
11476                    .unwrap();
11477                fs.buffer_mark_block_run_used(&mut buf, boundary, 4)
11478                    .unwrap();
11479                fs.commit_block_buffer(buf).unwrap();
11480            }
11481            let (free1, free2) = {
11482                let fs = mount();
11483                assert!(
11484                    (0..4).all(|b| bit_set(&fs, 2, b)),
11485                    "fixture: group 2's run is allocated"
11486                );
11487                (
11488                    fs.groups[1].free_blocks_count,
11489                    fs.groups[2].free_blocks_count,
11490                )
11491            };
11492
11493            // One run across the boundary, as a merged extent frees it.
11494            {
11495                let fs = mount();
11496                let mut buf = BlockBuffer::new(fs.sb.block_size());
11497                assert_eq!(
11498                    fs.buffer_free_block_run_and_bgd(&mut buf, boundary - 4, 8)
11499                        .unwrap(),
11500                    8
11501                );
11502                fs.commit_block_buffer(buf).unwrap();
11503            }
11504            let fs = mount();
11505            assert_eq!(
11506                (
11507                    fs.groups[1].free_blocks_count - free1,
11508                    fs.groups[2].free_blocks_count - free2
11509                ),
11510                (4, 4),
11511                "(group 1 credited, group 2 credited)"
11512            );
11513            assert!(
11514                (0..4).all(|b| !bit_set(&fs, 2, b)),
11515                "group 2's blocks are still marked allocated"
11516            );
11517            drop(fs);
11518
11519            let _ = std::fs::remove_dir_all(&dir);
11520        }
11521
11522        /// An ext2 orphan's data blocks and indirect blocks go back to the
11523        /// free pool with it (#79).
11524        ///
11525        /// The file is 20 blocks at 4 KiB, so its tree has a single indirect
11526        /// block beside the twelve direct pointers. Recovery used to free the
11527        /// inode and none of its blocks, and since #201 declined such an
11528        /// orphan altogether rather than strand them; either way the blocks
11529        /// stayed allocated. After the fix the free count returns to what it
11530        /// was before the file was written, and fsck finds nothing.
11531        #[test]
11532        fn an_indirect_mapped_orphan_frees_its_data_and_indirect_blocks() {
11533            const EXT2_VOL: u64 = 32 * 1024 * 1024;
11534            let dev = MemDev::new(EXT2_VOL);
11535            crate::mkfs::format_filesystem_with_flavor(
11536                dev.as_ref(),
11537                Some("ext2orph"),
11538                None,
11539                EXT2_VOL,
11540                BS,
11541                crate::features::FsFlavor::Ext2,
11542            )
11543            .expect("format");
11544            let (ino, free_before) = {
11545                let fs = mount(&dev);
11546                let free_before = fs.sb.free_blocks_count;
11547                let ino = fs.apply_create("/gone.bin", 0o644).expect("create");
11548                fs.apply_replace_file_content("/gone.bin", &vec![0x5A; 20 * BS as usize])
11549                    .expect("write");
11550                let (inode, _) = fs.read_inode_verified(ino).expect("read");
11551                assert!(!inode.has_extents(), "fixture: an indirect-mapped file");
11552                assert_ne!(
11553                    u32::from_le_bytes(inode.block[48..52].try_into().unwrap()),
11554                    0,
11555                    "fixture: the file uses its single indirect block"
11556                );
11557                (ino, free_before)
11558            };
11559            {
11560                let fs = mount(&dev);
11561                let (root, _) = fs.read_inode_verified(2).expect("root");
11562                let mut buf = BlockBuffer::new(fs.sb.block_size());
11563                fs.buffer_remove_dir_entry(&mut buf, 2, &root, b"gone.bin")
11564                    .expect("remove the name");
11565                fs.commit_block_buffer(buf).expect("commit");
11566                plant_orphan(&fs, ino, 0, None);
11567            }
11568            // Recovery runs on this mount; the next one observes the result.
11569            drop(mount(&dev));
11570
11571            let fs = mount(&dev);
11572            assert!(
11573                fs.orphan_list().expect("orphan_list").is_empty(),
11574                "recovery must take the orphan off the chain"
11575            );
11576            assert!(!fs.inode_bit_is_set(ino).expect("inode bitmap"));
11577            assert_eq!(
11578                fs.sb.free_blocks_count, free_before,
11579                "every block the file used -- 20 data and 1 indirect -- must be free again"
11580            );
11581            let report = crate::fsck::audit(&fs, u32::MAX, u32::MAX).expect("audit");
11582            assert!(report.is_clean(), "fsck: {:?}", report.anomalies);
11583            drop(fs);
11584
11585            // And e2fsck: a block left allocated with nothing mapping it is
11586            // exactly what its pass 5 reports.
11587            let image = fs_ext4_test_support::temp_dir().join(format!(
11588                "fs_ext4_indirect_orphan_{}.img",
11589                std::process::id()
11590            ));
11591            std::fs::write(&image, &*dev.bytes.lock().unwrap()).unwrap();
11592            let judged = fs_ext4_test_support::oracle("e2fsck")
11593                .arg("-fn")
11594                .arg(&image)
11595                .judged();
11596            let _ = std::fs::remove_file(&image);
11597            judged.clean("an indirect-mapped orphan after its data is freed");
11598        }
11599    }
11600
11601    /// #391: fsck counts an uninit group the way the format defines it —
11602    /// a BLOCK_UNINIT group's blocks from its own metadata, an INODE_UNINIT
11603    /// group's inodes as all free — not from bitmap blocks the format leaves
11604    /// unspecified. Group 2 of a 2 KiB-block volume (the smallest block that
11605    /// formats more than one group) holds nothing but its own bitmaps and
11606    /// inode table, so flagging it uninit is honest.
11607    #[test]
11608    fn fsck_does_not_count_an_uninit_groups_bitmaps() {
11609        const SMALL_BS: u32 = 2048;
11610        // Two 32 MiB groups and a partial third.
11611        const SMALL_VOL: u64 = 80 * 1024 * 1024;
11612        let dev = MemDev::new(SMALL_VOL);
11613        crate::mkfs::format_filesystem(dev.as_ref(), Some("uninit"), None, SMALL_VOL, SMALL_BS)
11614            .expect("format");
11615        let gi = 2usize;
11616        {
11617            let fs = mount(&dev);
11618            assert!(fs.groups.len() > gi, "the volume needs a third group");
11619            let g = fs.groups[gi];
11620            assert_eq!(g.free_inodes_count, fs.sb.inodes_per_group);
11621            assert_eq!(g.used_dirs_count, 0);
11622            let (blk, off) = fs.sb.descriptor_location(gi as u64);
11623            let mut raw = fs.read_block(blk).unwrap();
11624            let flags = u16::from_le_bytes(raw[off + 0x12..off + 0x14].try_into().unwrap())
11625                | crate::bgd::BgdFlags::BLOCK_UNINIT.bits()
11626                | crate::bgd::BgdFlags::INODE_UNINIT.bits();
11627            raw[off + 0x12..off + 0x14].copy_from_slice(&flags.to_le_bytes());
11628            fs.restamp_group_desc_csum(&mut raw[..], off, gi);
11629            dev.write_at(blk * u64::from(SMALL_BS), &raw).unwrap();
11630        }
11631        {
11632            let fs = mount(&dev);
11633            let before = fs.audit(1000, 10000).unwrap();
11634            assert!(
11635                before.is_clean(),
11636                "precondition: clean with the real bitmaps still in place: {before:?}"
11637            );
11638            let g = fs.groups[gi];
11639            let bs = SMALL_BS as usize;
11640            dev.write_at(g.block_bitmap * u64::from(SMALL_BS), &vec![0u8; bs])
11641                .unwrap();
11642            dev.write_at(g.inode_bitmap * u64::from(SMALL_BS), &vec![0xFFu8; bs])
11643                .unwrap();
11644        }
11645        let fs = mount(&dev);
11646        let r = fs.audit(1000, 10000).unwrap();
11647        assert!(
11648            r.is_clean(),
11649            "an uninit group's bitmap bytes were counted: {r:?}"
11650        );
11651    }
11652
11653    /// #390: the buffered BGD counter patch writes each counter's high half
11654    /// where `BlockGroupDescriptor::parse` reads it (free_blocks_hi 0x2C,
11655    /// free_inodes_hi 0x2E, used_dirs_hi 0x30), so a carry out of the low
11656    /// half lands in the counter and not in `bg_inode_table_hi`.
11657    #[test]
11658    fn buffered_bgd_counter_high_halves_are_written_where_they_are_read() {
11659        let dev = formatted();
11660        let fs = mount(&dev);
11661        assert!(fs.sb.desc_size >= 64, "the fixture must carry high halves");
11662        let (blk, off) = fs.sb.descriptor_location(0);
11663        let mut buf = BlockBuffer::new(BS);
11664        {
11665            let b = buf.get_mut(&fs, blk).unwrap();
11666            b[off + 0x0C..off + 0x0E].copy_from_slice(&0xFFFFu16.to_le_bytes());
11667            b[off + 0x0E..off + 0x10].copy_from_slice(&0xFFFFu16.to_le_bytes());
11668            b[off + 0x10..off + 0x12].copy_from_slice(&0xFFFFu16.to_le_bytes());
11669        }
11670        let parse = |buf: &BlockBuffer| {
11671            crate::bgd::BlockGroupDescriptor::parse(&buf.dirty[&blk][off..off + 64], 64).unwrap()
11672        };
11673        let before = parse(&buf);
11674        fs.buffer_patch_bgd_counters(&mut buf, 0, 1, 1, 1).unwrap();
11675        let after = parse(&buf);
11676        assert_eq!(
11677            after.inode_table, before.inode_table,
11678            "inode table pointer changed"
11679        );
11680        assert_eq!(after.block_bitmap, before.block_bitmap);
11681        assert_eq!(after.inode_bitmap, before.inode_bitmap);
11682        assert_eq!(after.free_blocks_count, 0x1_0000);
11683        assert_eq!(after.free_inodes_count, 0x1_0000);
11684        assert_eq!(after.used_dirs_count, 0x1_0000);
11685        assert_eq!(after.itable_unused, before.itable_unused);
11686
11687        // And a borrow back across the boundary returns every field.
11688        fs.buffer_patch_bgd_counters(&mut buf, 0, -1, -1, -1)
11689            .unwrap();
11690        let back = parse(&buf);
11691        assert_eq!(back.inode_table, before.inode_table);
11692        assert_eq!(back.free_blocks_count, 0xFFFF);
11693        assert_eq!(back.free_inodes_count, 0xFFFF);
11694        assert_eq!(back.used_dirs_count, 0xFFFF);
11695    }
11696
11697    /// #390: the unbuffered BGD counter patch (the one fsck repair uses)
11698    /// agrees with `parse` on where the high halves live.
11699    #[test]
11700    fn direct_bgd_counter_high_halves_are_written_where_they_are_read() {
11701        let dev = formatted();
11702        let fs = mount(&dev);
11703        assert!(fs.sb.desc_size >= 64, "the fixture must carry high halves");
11704        let (blk, off) = fs.sb.descriptor_location(0);
11705        let mut raw = fs.read_block(blk).unwrap();
11706        raw[off + 0x0C..off + 0x0E].copy_from_slice(&0xFFFFu16.to_le_bytes());
11707        raw[off + 0x0E..off + 0x10].copy_from_slice(&0xFFFFu16.to_le_bytes());
11708        raw[off + 0x10..off + 0x12].copy_from_slice(&0xFFFFu16.to_le_bytes());
11709        fs.restamp_group_desc_csum(&mut raw[..], off, 0);
11710        dev.write_at(blk * u64::from(BS), &raw).unwrap();
11711        drop(fs);
11712        let fs = mount(&dev);
11713        let before = crate::bgd::BlockGroupDescriptor::parse(&raw[off..off + 64], 64).unwrap();
11714        fs.patch_bgd_counters(0, 1, 1, 1).unwrap();
11715        let raw = fs.read_block(blk).unwrap();
11716        let after = crate::bgd::BlockGroupDescriptor::parse(&raw[off..off + 64], 64).unwrap();
11717        assert_eq!(
11718            after.inode_table, before.inode_table,
11719            "inode table pointer changed"
11720        );
11721        assert_eq!(after.free_blocks_count, 0x1_0000);
11722        assert_eq!(after.free_inodes_count, 0x1_0000);
11723        assert_eq!(after.used_dirs_count, 0x1_0000);
11724    }
11725
11726    // --- one copy of an attribute, wherever it lives (#377) -----------
11727
11728    /// The value length of every copy of `name` on `ino`, in the order the
11729    /// reader finds them: in-inode first, then the external block.
11730    fn xattr_copies(fs: &Filesystem, ino: u32, name: &str) -> Vec<usize> {
11731        let (inode, raw) = fs.read_inode_verified(ino).expect("read inode");
11732        crate::xattr::read_all_resolved(fs, &inode, &raw)
11733            .expect("read xattrs")
11734            .into_iter()
11735            .filter(|e| e.name == name)
11736            .map(|e| e.value.len())
11737            .collect()
11738    }
11739
11740    /// Replacing an in-inode attribute with a value too big for the inode
11741    /// leaves exactly one copy, holding the new value. The in-inode copy
11742    /// used to stay, and the reader, which returns the first match, went
11743    /// on answering with the old value.
11744    #[test]
11745    fn growing_an_in_inode_attribute_past_the_inode_leaves_one_copy() {
11746        let dev = formatted();
11747        let fs = mount(&dev);
11748        let ino = fs.apply_create("/f", 0o644).expect("create");
11749        fs.apply_setxattr("/f", "user.a", &[1u8; 40])
11750            .expect("set small");
11751        assert_eq!(xattr_copies(&fs, ino, "user.a"), vec![40]);
11752        fs.apply_setxattr("/f", "user.a", &[2u8; 200])
11753            .expect("set big");
11754        assert_eq!(
11755            xattr_copies(&fs, ino, "user.a"),
11756            vec![200],
11757            "one copy, the new value"
11758        );
11759        let (inode, raw) = fs.read_inode_verified(ino).unwrap();
11760        assert_eq!(
11761            crate::xattr::get_resolved(&fs, &inode, &raw, "user.a").unwrap(),
11762            Some(vec![2u8; 200]),
11763            "the value read back is the one written last"
11764        );
11765        // Removing it removes it.
11766        fs.apply_removexattr("/f", "user.a").expect("remove");
11767        assert_eq!(xattr_copies(&fs, ino, "user.a"), Vec::<usize>::new());
11768    }
11769
11770    /// Replacing an attribute that lives in the external block with a value
11771    /// that now fits in the inode does not leave the block's copy behind,
11772    /// and a remove then leaves no copy anywhere. The block's copy used to
11773    /// stay, and came back as the value once the in-inode one was removed.
11774    #[test]
11775    fn shrinking_an_external_attribute_into_the_inode_leaves_one_copy() {
11776        let dev = formatted();
11777        let fs = mount(&dev);
11778        let ino = fs.apply_create("/f", 0o644).expect("create");
11779        fs.apply_setxattr("/f", "user.big", &[1u8; 200])
11780            .expect("set big");
11781        assert_eq!(xattr_copies(&fs, ino, "user.big"), vec![200]);
11782        fs.apply_setxattr("/f", "user.big", &[2u8; 8])
11783            .expect("set small");
11784        assert_eq!(
11785            xattr_copies(&fs, ino, "user.big"),
11786            vec![8],
11787            "one copy, the new value"
11788        );
11789        fs.apply_removexattr("/f", "user.big").expect("remove");
11790        assert_eq!(
11791            xattr_copies(&fs, ino, "user.big"),
11792            Vec::<usize>::new(),
11793            "removed means gone"
11794        );
11795    }
11796
11797    // --- the external xattr block is checked before it is edited (#378) ---
11798
11799    /// Point `f`'s `i_file_acl` at `block_nr`, re-checksummed, as a stale
11800    /// or bit-rotted pointer would leave it.
11801    fn point_file_acl_at(fs: &Filesystem, ino: u32, block_nr: u64) {
11802        let (inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
11803        Filesystem::write_file_acl(&mut raw, block_nr).expect("write_file_acl");
11804        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
11805            .expect("finalize");
11806        fs.write_inode_raw(ino, &raw).expect("write inode");
11807    }
11808
11809    /// `/g`, one block of 0x5A, and the number of that block.
11810    fn a_data_block(fs: &Filesystem) -> u64 {
11811        let g = fs.apply_create("/g", 0o644).expect("create g");
11812        fs.apply_replace_file_content("/g", &[0x5Au8; 4096])
11813            .expect("write g");
11814        let (gi, _) = fs.read_inode_verified(g).expect("read g");
11815        fs.extent_tree_runs(g, &gi).expect("g's extents")[0].0
11816    }
11817
11818    fn block_bytes(dev: &std::sync::Arc<MemDev>, block_nr: u64) -> Vec<u8> {
11819        use crate::block_io::BlockDevice;
11820        let mut b = vec![0u8; BS as usize];
11821        dev.read_at(block_nr * BS as u64, &mut b)
11822            .expect("read block");
11823        b
11824    }
11825
11826    /// An `i_file_acl` naming a block without the xattr magic does not name
11827    /// an xattr block: setxattr refuses it rather than formatting another
11828    /// file's data as one and reporting success.
11829    #[test]
11830    fn setxattr_refuses_an_external_block_without_the_xattr_magic() {
11831        let dev = formatted();
11832        let fs = mount(&dev);
11833        let f = fs.apply_create("/f", 0o644).expect("create f");
11834        let gblk = a_data_block(&fs);
11835        point_file_acl_at(&fs, f, gblk);
11836        let r = fs.apply_setxattr("/f", "user.big", &[1u8; 200]);
11837        assert!(
11838            matches!(r, Err(Error::Corrupt(_))),
11839            "setxattr on a non-xattr block must be refused as corrupt, got {r:?}"
11840        );
11841        assert!(
11842            block_bytes(&dev, gblk).iter().all(|&x| x == 0x5A),
11843            "g's data must be untouched"
11844        );
11845    }
11846
11847    /// The same for removexattr and for the release an unlink does: neither
11848    /// edits nor frees a block that is not an xattr block.
11849    #[test]
11850    fn removexattr_and_unlink_refuse_an_external_block_without_the_xattr_magic() {
11851        let dev = formatted();
11852        let fs = mount(&dev);
11853        let f = fs.apply_create("/f", 0o644).expect("create f");
11854        let gblk = a_data_block(&fs);
11855        point_file_acl_at(&fs, f, gblk);
11856        let r = fs.apply_removexattr("/f", "user.anything");
11857        assert!(matches!(r, Err(Error::Corrupt(_))), "removexattr: {r:?}");
11858        let r = fs.apply_unlink("/f");
11859        assert!(matches!(r, Err(Error::Corrupt(_))), "unlink: {r:?}");
11860        assert!(
11861            block_bytes(&dev, gblk).iter().all(|&x| x == 0x5A),
11862            "g's data must be untouched"
11863        );
11864    }
11865
11866    /// A metadata_csum xattr block whose checksum does not verify is not
11867    /// edited, and so not restamped as if it did.
11868    #[test]
11869    fn setxattr_refuses_an_external_block_that_fails_its_checksum() {
11870        use crate::block_io::BlockDevice;
11871        let dev = formatted();
11872        let fs = mount(&dev);
11873        assert!(fs.csum.enabled, "fixture: a metadata_csum volume");
11874        let f = fs.apply_create("/f", 0o644).expect("create f");
11875        fs.apply_setxattr("/f", "user.big", &[1u8; 200])
11876            .expect("set big");
11877        let (fi, _) = fs.read_inode_verified(f).unwrap();
11878        assert_ne!(fi.file_acl, 0, "fixture: the value is in the block");
11879        let mut b = block_bytes(&dev, fi.file_acl);
11880        assert!(
11881            fs.csum.verify_xattr_block(fi.file_acl, &b),
11882            "fixture: valid"
11883        );
11884        b[BS as usize - 1] ^= 0xFF; // a byte of the value area
11885        dev.write_at(fi.file_acl * BS as u64, &b).unwrap();
11886        let fs = mount(&dev);
11887        let r = fs.apply_setxattr("/f", "user.other", &[2u8; 200]);
11888        assert!(
11889            matches!(r, Err(Error::BadChecksum { .. })),
11890            "an edit of a block that fails its checksum must be refused, got {r:?}"
11891        );
11892        assert_eq!(block_bytes(&dev, fi.file_acl), b, "the block is untouched");
11893        let r = fs.apply_removexattr("/f", "user.big");
11894        assert!(
11895            matches!(r, Err(Error::BadChecksum { .. })),
11896            "removexattr: {r:?}"
11897        );
11898        assert_eq!(block_bytes(&dev, fi.file_acl), b, "the block is untouched");
11899        // The read path says so too, rather than returning what may be
11900        // another attribute's bytes.
11901        let (fi, raw) = fs.read_inode_verified(f).unwrap();
11902        let r = crate::xattr::get_resolved(&fs, &fi, &raw, "user.big");
11903        assert!(
11904            matches!(r, Err(Error::BadChecksum { .. })),
11905            "getxattr: {r:?}"
11906        );
11907    }
11908
11909    /// `h_blocks` is 1 for every xattr block ext4 has ever written; a block
11910    /// claiming more is not one this crate or the kernel can edit.
11911    #[test]
11912    fn setxattr_refuses_an_external_block_with_h_blocks_other_than_one() {
11913        use crate::block_io::BlockDevice;
11914        let dev = formatted();
11915        let fs = mount(&dev);
11916        let f = fs.apply_create("/f", 0o644).expect("create f");
11917        fs.apply_setxattr("/f", "user.big", &[1u8; 200])
11918            .expect("set big");
11919        let (fi, _) = fs.read_inode_verified(f).unwrap();
11920        let mut b = block_bytes(&dev, fi.file_acl);
11921        b[8..12].copy_from_slice(&2u32.to_le_bytes());
11922        fs.csum.patch_xattr_block(fi.file_acl, &mut b);
11923        dev.write_at(fi.file_acl * BS as u64, &b).unwrap();
11924        let fs = mount(&dev);
11925        let r = fs.apply_setxattr("/f", "user.other", &[2u8; 200]);
11926        assert!(matches!(r, Err(Error::Corrupt(_))), "setxattr: {r:?}");
11927        assert_eq!(block_bytes(&dev, fi.file_acl), b, "the block is untouched");
11928    }
11929
11930    // --- an inode with i_extra_isize = 0 (#380) ------------------------
11931
11932    /// Rewrite `ino`'s raw image through `f`, re-checksummed.
11933    fn patch_inode(fs: &Filesystem, ino: u32, f: impl FnOnce(&mut Vec<u8>)) {
11934        let (inode, mut raw) = fs.read_inode_verified(ino).expect("read inode");
11935        f(&mut raw);
11936        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
11937            .expect("finalize");
11938        fs.write_inode_raw(ino, &raw).expect("write inode");
11939    }
11940
11941    /// With `i_extra_isize = 0` the kernel parses no in-inode xattr area
11942    /// and reads 0x80.. as the `i_*_extra` fields. setxattr must not put an
11943    /// area at 0x80: it gives the inode the extra fields first, as the
11944    /// kernel does, and the attribute goes after them.
11945    #[test]
11946    fn setxattr_on_an_inode_with_no_extra_isize_does_not_write_at_0x80() {
11947        let dev = formatted();
11948        let fs = mount(&dev);
11949        let ino = fs.apply_create("/f", 0o644).expect("create");
11950        patch_inode(&fs, ino, |raw| raw[0x80..].fill(0));
11951        fs.apply_setxattr("/f", "user.a", b"v").expect("setxattr");
11952        let (inode, raw) = fs
11953            .read_inode_verified(ino)
11954            .expect("the inode still verifies");
11955        let extra = u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap());
11956        assert_eq!(
11957            extra,
11958            32,
11959            "i_extra_isize: the xattr went where the kernel reads i_*_extra: {:02x?}",
11960            &raw[0x80..0xA4]
11961        );
11962        assert_eq!(
11963            raw[0xA0..0xA4],
11964            crate::xattr::EXT4_XATTR_MAGIC.to_le_bytes(),
11965            "the in-inode area starts after the extra fields"
11966        );
11967        assert_eq!(
11968            crate::xattr::get_resolved(&fs, &inode, &raw, "user.a").unwrap(),
11969            Some(b"v".to_vec())
11970        );
11971    }
11972
11973    /// An area a writer put at 0x80 is not an area the kernel reads, and
11974    /// neither is it one this crate reads or removes.
11975    #[test]
11976    fn an_xattr_area_at_0x80_is_not_read() {
11977        let dev = formatted();
11978        let fs = mount(&dev);
11979        let ino = fs.apply_create("/f", 0o644).expect("create");
11980        patch_inode(&fs, ino, |raw| {
11981            raw[0x80..].fill(0);
11982            let mut region = raw[0x80..].to_vec();
11983            crate::xattr::plan_set_in_inode_region(&mut region, "user.a", b"v").unwrap();
11984            raw[0x80..].copy_from_slice(&region);
11985            // What makes it look like one: i_extra_isize reads 0.
11986            assert_eq!(raw[0x80..0x82], [0, 0]);
11987        });
11988        let (inode, raw) = fs.read_inode_verified(ino).unwrap();
11989        assert_eq!(
11990            crate::xattr::get_resolved(&fs, &inode, &raw, "user.a").unwrap(),
11991            None
11992        );
11993    }
11994
11995    /// With `i_extra_isize < 4` there is no `i_checksum_hi`: 0x82..0x84 is
11996    /// ordinary inode bytes, covered by the checksum and compared by
11997    /// nobody. Writing the checksum must not overwrite them, and verifying
11998    /// must compare only the low 16 bits, as the kernel and libext2fs do.
11999    #[test]
12000    fn an_inode_without_room_for_checksum_hi_keeps_its_bytes_at_0x82() {
12001        let dev = formatted();
12002        let fs = mount(&dev);
12003        assert!(fs.csum.enabled, "fixture: a metadata_csum volume");
12004        let ino = fs.apply_create("/f", 0o644).expect("create");
12005        let (inode, mut raw) = fs.read_inode_verified(ino).unwrap();
12006        raw[0x80..].fill(0);
12007        raw[0x82..0x84].copy_from_slice(&[0x02, 0xEA]);
12008        // The kernel's checksum of this inode: 16 bits, over every byte
12009        // but i_checksum_lo, 0x82..0x84 included.
12010        let (lo, _) = fs
12011            .csum
12012            .compute_inode_checksum(ino, inode.generation, &raw)
12013            .unwrap();
12014        raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
12015        assert!(
12016            fs.csum.verify_inode(ino, inode.generation, &raw),
12017            "a kernel-checksummed inode with i_extra_isize = 0 verifies"
12018        );
12019        let mut rewritten = raw.clone();
12020        fs.finalize_inode_raw(ino, inode.generation, &mut rewritten)
12021            .unwrap();
12022        assert_eq!(rewritten, raw, "re-checksumming changes nothing");
12023    }
12024
12025    /// Overwrite an inode's raw record in place, re-stamping its checksum.
12026    fn audit70_patch_inode(fs: &Filesystem, ino: u32, f: impl FnOnce(&mut Vec<u8>)) {
12027        let (inode, mut raw) = fs.read_inode_verified(ino).unwrap();
12028        f(&mut raw);
12029        fs.finalize_inode_raw(ino, inode.generation, &mut raw)
12030            .unwrap();
12031        fs.write_inode_raw(ino, &raw).unwrap();
12032    }
12033
12034    fn audit70_set_links(fs: &Filesystem, ino: u32, count: u16) {
12035        audit70_patch_inode(fs, ino, |raw| {
12036            raw[0x1A..0x1C].copy_from_slice(&count.to_le_bytes())
12037        });
12038    }
12039
12040    fn audit70_links(fs: &Filesystem, ino: u32) -> u16 {
12041        fs.read_inode_verified(ino).unwrap().0.links_count
12042    }
12043
12044    /// Set a RO_COMPAT bit on a formatted volume, fixing the superblock
12045    /// checksum so the result still mounts.
12046    fn set_ro_compat_bit(dev: &std::sync::Arc<MemDev>, bit: u32) {
12047        let mut sb = vec![0u8; 1024];
12048        dev.read_at(crate::superblock::SUPERBLOCK_OFFSET, &mut sb)
12049            .expect("read sb");
12050        let cur = u32::from_le_bytes(sb[0x64..0x68].try_into().unwrap());
12051        sb[0x64..0x68].copy_from_slice(&(cur | bit).to_le_bytes());
12052        let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
12053        sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
12054        dev.write_at(crate::superblock::SUPERBLOCK_OFFSET, &sb)
12055            .expect("write sb");
12056    }
12057
12058    /// #385: a directory link count of 1 means "too many to count"; rmdir
12059    /// of a child must leave it at 1, not write the 0 that makes Linux
12060    /// refuse the directory.
12061    #[test]
12062    fn audit70_rmdir_under_a_dir_nlink_parent_keeps_its_count() {
12063        let dev = formatted();
12064        set_ro_compat_bit(&dev, crate::features::RoCompat::DIR_NLINK.bits());
12065        let fs = mount(&dev);
12066        fs.apply_mkdir("/p", 0o755).unwrap();
12067        fs.apply_mkdir("/p/c", 0o755).unwrap();
12068        let p = resolve(&fs, "/p").unwrap();
12069        audit70_set_links(&fs, p, 1);
12070        fs.apply_rmdir("/p/c").unwrap();
12071        assert_eq!(
12072            audit70_links(&fs, p),
12073            1,
12074            "a directory's count of 1 is not decremented"
12075        );
12076    }
12077
12078    /// #385: mkdir under a parent whose count is already "too many to
12079    /// count" leaves it there rather than writing a literal 2.
12080    #[test]
12081    fn audit70_mkdir_under_a_dir_nlink_parent_keeps_its_count() {
12082        let dev = formatted();
12083        set_ro_compat_bit(&dev, crate::features::RoCompat::DIR_NLINK.bits());
12084        let fs = mount(&dev);
12085        fs.apply_mkdir("/p", 0o755).unwrap();
12086        let p = resolve(&fs, "/p").unwrap();
12087        audit70_set_links(&fs, p, 1);
12088        fs.apply_mkdir("/p/c", 0o755).unwrap();
12089        assert_eq!(audit70_links(&fs, p), 1);
12090    }
12091
12092    /// #385: the subdirectory that takes a DIR_NLINK parent past
12093    /// MAX_LINKS pins the count at 1, as the kernel does.
12094    #[test]
12095    fn audit70_mkdir_past_the_maximum_pins_a_dir_nlink_parent_at_one() {
12096        let dev = formatted();
12097        set_ro_compat_bit(&dev, crate::features::RoCompat::DIR_NLINK.bits());
12098        let fs = mount(&dev);
12099        fs.apply_mkdir("/p", 0o755).unwrap();
12100        let p = resolve(&fs, "/p").unwrap();
12101        audit70_set_links(&fs, p, 65000);
12102        fs.apply_mkdir("/p/c", 0o755).unwrap();
12103        assert_eq!(audit70_links(&fs, p), 1);
12104    }
12105
12106    /// #385: without DIR_NLINK a directory at MAX_LINKS takes no more
12107    /// subdirectories: EMLINK, and nothing is written.
12108    #[test]
12109    fn audit70_mkdir_past_the_maximum_without_dir_nlink_is_refused() {
12110        let dev = formatted();
12111        let fs = mount(&dev);
12112        fs.apply_mkdir("/p", 0o755).unwrap();
12113        let p = resolve(&fs, "/p").unwrap();
12114        audit70_set_links(&fs, p, 65000);
12115        let r = fs.apply_mkdir("/p/c", 0o755);
12116        assert!(
12117            matches!(r, Err(Error::TooManyLinks)),
12118            "EMLINK expected, got {r:?}; count now {}",
12119            audit70_links(&fs, p)
12120        );
12121        assert_eq!(audit70_links(&fs, p), 65000);
12122        assert!(
12123            resolve(&fs, "/p/c").is_err(),
12124            "the refused directory was not created"
12125        );
12126    }
12127
12128    /// #385: a hard link past the link maximum is refused, not wrapped
12129    /// to zero.
12130    #[test]
12131    fn audit70_a_link_past_the_maximum_is_refused_not_wrapped() {
12132        let dev = formatted();
12133        let fs = mount(&dev);
12134        let f = fs.apply_create("/f", 0o644).unwrap();
12135        audit70_set_links(&fs, f, 65535);
12136        let r = fs.apply_link("/f", "/g");
12137        assert!(
12138            matches!(r, Err(Error::TooManyLinks)),
12139            "EMLINK expected; link count now {}",
12140            audit70_links(&fs, f)
12141        );
12142        assert_eq!(audit70_links(&fs, f), 65535);
12143    }
12144
12145    /// #385: MAX_LINKS (65000) is the ceiling the format documentation
12146    /// gives for `i_links_count`, not the width of the field.
12147    #[test]
12148    fn audit70_a_link_at_max_links_is_refused() {
12149        let dev = formatted();
12150        let fs = mount(&dev);
12151        let f = fs.apply_create("/f", 0o644).unwrap();
12152        audit70_set_links(&fs, f, 65000);
12153        assert!(matches!(
12154            fs.apply_link("/f", "/g"),
12155            Err(Error::TooManyLinks)
12156        ));
12157        assert!(resolve(&fs, "/g").is_err());
12158        audit70_set_links(&fs, f, 64999);
12159        fs.apply_link("/f", "/g").unwrap();
12160        assert_eq!(audit70_links(&fs, f), 65000);
12161    }
12162
12163    /// #385: renaming a directory into a parent at MAX_LINKS on a
12164    /// volume without DIR_NLINK is refused, like mkdir there.
12165    #[test]
12166    fn audit70_rename_of_a_dir_into_a_full_parent_is_refused() {
12167        let dev = formatted();
12168        let fs = mount(&dev);
12169        fs.apply_mkdir("/p", 0o755).unwrap();
12170        fs.apply_mkdir("/d", 0o755).unwrap();
12171        let p = resolve(&fs, "/p").unwrap();
12172        audit70_set_links(&fs, p, 65000);
12173        assert!(matches!(
12174            fs.apply_rename("/d", "/p/d", false),
12175            Err(Error::TooManyLinks)
12176        ));
12177        assert_eq!(audit70_links(&fs, p), 65000);
12178        assert!(
12179            resolve(&fs, "/d").is_ok(),
12180            "the refused rename left the source alone"
12181        );
12182    }
12183
12184    /// #385: the documented link-count rules, step by step.
12185    #[test]
12186    fn next_links_count_follows_the_kernel() {
12187        // Files: up to MAX_LINKS and no further; down to 0 and no further.
12188        assert_eq!(next_links_count(false, 64999, 1, true).unwrap(), 65000);
12189        assert!(matches!(
12190            next_links_count(false, 65000, 1, true),
12191            Err(Error::TooManyLinks)
12192        ));
12193        assert_eq!(next_links_count(false, 1, -1, true).unwrap(), 0);
12194        assert_eq!(next_links_count(false, 0, -1, true).unwrap(), 0);
12195        // Directories: pinned at 1 past the maximum with DIR_NLINK, refused without.
12196        assert_eq!(next_links_count(true, 65000, 1, true).unwrap(), 1);
12197        assert!(matches!(
12198            next_links_count(true, 65000, 1, false),
12199            Err(Error::TooManyLinks)
12200        ));
12201        assert_eq!(next_links_count(true, 1, 1, false).unwrap(), 1);
12202        // A directory count never drops through 2 or out of the pinned 1.
12203        assert_eq!(next_links_count(true, 3, -1, true).unwrap(), 2);
12204        assert_eq!(next_links_count(true, 2, -1, true).unwrap(), 2);
12205        assert_eq!(next_links_count(true, 1, -1, true).unwrap(), 1);
12206        // A summed delta is applied one link at a time.
12207        assert_eq!(next_links_count(true, 64999, 2, true).unwrap(), 1);
12208        assert_eq!(next_links_count(true, 5, -2, true).unwrap(), 3);
12209    }
12210
12211    /// #385: fsck accepts a directory count of 1 on a DIR_NLINK volume --
12212    /// it is the "too many to count" value, not a count that is too low.
12213    #[test]
12214    fn audit70_fsck_accepts_a_dir_nlink_count_of_one() {
12215        let dev = formatted();
12216        set_ro_compat_bit(&dev, crate::features::RoCompat::DIR_NLINK.bits());
12217        let fs = mount(&dev);
12218        fs.apply_mkdir("/p", 0o755).unwrap();
12219        fs.apply_mkdir("/p/c", 0o755).unwrap();
12220        let p = resolve(&fs, "/p").unwrap();
12221        audit70_set_links(&fs, p, 1);
12222        let report = fs.audit(u32::MAX, u32::MAX).unwrap();
12223        assert!(report.is_clean(), "{:?}", report.anomalies);
12224    }
12225
12226    /// The type byte of the root-directory entry `name`.
12227    fn root_entry_type(fs: &Filesystem, name: &[u8]) -> crate::dir::DirEntryType {
12228        let (root, _) = fs.read_inode_verified(2).unwrap();
12229        let phys = fs.map_inode_logical(&root, 0).unwrap().unwrap();
12230        let blk = fs.read_block(phys).unwrap();
12231        crate::dir::DirBlockIter::new(&blk, true)
12232            .map(|e| e.unwrap())
12233            .find(|e| e.name == name)
12234            .unwrap_or_else(|| panic!("{} not found", String::from_utf8_lossy(name)))
12235            .file_type
12236    }
12237
12238    /// #386: rename keeps the entry's file type for every special file --
12239    /// a FIFO, a socket and both device kinds -- in both of its paths.
12240    #[test]
12241    fn audit70_rename_keeps_a_special_files_type() {
12242        use crate::dir::DirEntryType;
12243        let dev = formatted();
12244        let fs = mount(&dev);
12245        for (name, mode, want) in [
12246            ("fifo", 0o010644u16, DirEntryType::Fifo),
12247            ("sock", 0o140644, DirEntryType::Socket),
12248            ("chr", 0o020644, DirEntryType::CharDev),
12249            ("blk", 0o060644, DirEntryType::BlockDev),
12250        ] {
12251            let src = format!("/{name}");
12252            fs.apply_mknod(&src, mode, 1, 3).unwrap();
12253            // The no-overwrite path.
12254            let moved = format!("/{name}_moved");
12255            fs.apply_rename(&src, &moved, false).unwrap();
12256            assert_eq!(
12257                root_entry_type(&fs, &moved.as_bytes()[1..]),
12258                want,
12259                "{name}: rename"
12260            );
12261            // The replace path.
12262            let victim = format!("/{name}_victim");
12263            fs.apply_create(&victim, 0o644).unwrap();
12264            fs.apply_rename(&moved, &victim, true).unwrap();
12265            assert_eq!(
12266                root_entry_type(&fs, &victim.as_bytes()[1..]),
12267                want,
12268                "{name}: rename over a file"
12269            );
12270        }
12271    }
12272
12273    /// #427: an inline-data directory laid out the way the kernel lays one
12274    /// out -- `i_block` holds the parent's inode number, then entries from
12275    /// byte 4; entries that do not fit continue in the `system.data` xattr
12276    /// -- is read by both the lookup and the readdir path.
12277    mod inline_dir_reads {
12278        use super::*;
12279        use crate::inode::{InodeFlags, OFF_BLOCK, OFF_FLAGS, OFF_SIZE_LO};
12280
12281        /// One directory entry, `rec_len` bytes long.
12282        fn dirent(ino: u32, name: &[u8], file_type: u8, rec_len: u16) -> Vec<u8> {
12283            let mut e = vec![0u8; rec_len as usize];
12284            e[0..4].copy_from_slice(&ino.to_le_bytes());
12285            e[4..6].copy_from_slice(&rec_len.to_le_bytes());
12286            e[6] = name.len() as u8;
12287            e[7] = file_type;
12288            e[8..8 + name.len()].copy_from_slice(name);
12289            e
12290        }
12291
12292        /// Rewrite the directory `dir` in the kernel's inline layout:
12293        /// `i_block` = `parent` + `in_block` (56 bytes), and `continuation`
12294        /// as the `system.data` xattr value; `i_size` = 60 + its length.
12295        fn make_inline_dir(
12296            fs: &Filesystem,
12297            dir: u32,
12298            parent: u32,
12299            in_block: &[u8],
12300            continuation: &[u8],
12301        ) {
12302            assert_eq!(in_block.len(), 56, "i_block holds 56 bytes of entries");
12303            let (inode, mut raw) = fs.read_inode_verified(dir).unwrap();
12304            let flags =
12305                (inode.flags & !InodeFlags::EXTENTS.bits()) | InodeFlags::INLINE_DATA.bits();
12306            raw[OFF_FLAGS..OFF_FLAGS + 4].copy_from_slice(&flags.to_le_bytes());
12307            raw[OFF_BLOCK..OFF_BLOCK + 4].copy_from_slice(&parent.to_le_bytes());
12308            raw[OFF_BLOCK + 4..OFF_BLOCK + 60].copy_from_slice(in_block);
12309            let size = 60 + continuation.len() as u32;
12310            raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&size.to_le_bytes());
12311            let extra = u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap()) as usize;
12312            let end = (fs.sb.inode_size as usize).min(raw.len());
12313            crate::xattr::plan_set_in_inode_region(
12314                &mut raw[128 + extra..end],
12315                "system.data",
12316                continuation,
12317            )
12318            .expect("system.data fits in the inode");
12319            fs.finalize_inode_raw(dir, inode.generation, &mut raw)
12320                .unwrap();
12321            fs.write_inode_raw(dir, &raw).unwrap();
12322        }
12323
12324        /// A volume with `/d` inline: `x` and `sub` in `i_block`, `zz` in
12325        /// the continuation. Returns the device and the inode numbers.
12326        fn volume() -> (std::sync::Arc<MemDev>, u32, u32, u32, u32) {
12327            let dev = formatted();
12328            set_incompat_bit(&dev, crate::features::Incompat::INLINE_DATA.bits());
12329            let fs = mount(&dev);
12330            let d = fs.apply_mkdir("/d", 0o755).unwrap();
12331            let x = fs.apply_create("/x", 0o644).unwrap();
12332            let sub = fs.apply_mkdir("/sub", 0o755).unwrap();
12333            let zz = fs.apply_create("/zz", 0o644).unwrap();
12334            let mut in_block = dirent(x, b"x", 1, 12);
12335            in_block.extend(dirent(sub, b"sub", 2, 44));
12336            make_inline_dir(&fs, d, 2, &in_block, &dirent(zz, b"zz", 1, 20));
12337            drop(fs);
12338            (dev, d, x, sub, zz)
12339        }
12340
12341        #[test]
12342        fn a_lookup_reads_entries_from_byte_4_and_from_the_continuation() {
12343            let (dev, d, x, sub, zz) = volume();
12344            let fs = mount(&dev);
12345            assert!(fs.read_inode_verified(d).unwrap().0.has_inline_data());
12346            for (path, want) in [
12347                ("/d/x", x),
12348                ("/d/sub", sub),
12349                ("/d/zz", zz),
12350                ("/d/.", d),
12351                ("/d/..", 2),
12352            ] {
12353                assert_eq!(
12354                    resolve(&fs, path).map_err(|e| format!("{e:?}")),
12355                    Ok(want),
12356                    "{path}"
12357                );
12358            }
12359            assert!(matches!(resolve(&fs, "/d/nope"), Err(Error::NotFound)));
12360        }
12361
12362        #[test]
12363        fn readdir_lists_the_synthesised_dots_the_block_entries_and_the_continuation() {
12364            use std::ffi::{CStr, CString};
12365            let (dev, d, x, sub, zz) = volume();
12366            let image = fs_ext4_test_support::temp_dir()
12367                .join(format!("fs_ext4_inline_readdir_{}.img", std::process::id()));
12368            std::fs::write(&image, &*dev.bytes.lock().unwrap()).unwrap();
12369            let path = CString::new(image.to_str().unwrap()).unwrap();
12370            let mut listed = Vec::new();
12371            let err = unsafe {
12372                let fs = crate::capi::fs_ext4_mount(path.as_ptr());
12373                assert!(!fs.is_null(), "mount");
12374                let dir = CString::new("/d").unwrap();
12375                let it = crate::capi::fs_ext4_dir_open(fs, dir.as_ptr());
12376                let err = if it.is_null() {
12377                    Some(
12378                        CStr::from_ptr(crate::capi::fs_ext4_last_error())
12379                            .to_string_lossy()
12380                            .into_owned(),
12381                    )
12382                } else {
12383                    loop {
12384                        let e = crate::capi::fs_ext4_dir_next(it);
12385                        if e.is_null() {
12386                            break;
12387                        }
12388                        let name = CStr::from_ptr((*e).name.as_ptr())
12389                            .to_string_lossy()
12390                            .into_owned();
12391                        listed.push((name, (*e).inode));
12392                    }
12393                    crate::capi::fs_ext4_dir_close(it);
12394                    None
12395                };
12396                crate::capi::fs_ext4_umount(fs);
12397                err
12398            };
12399            let _ = std::fs::remove_file(&image);
12400            assert_eq!(err, None, "dir_open /d");
12401            listed.sort();
12402            let mut want: Vec<(String, u32)> =
12403                [(".", d), ("..", 2), ("sub", sub), ("x", x), ("zz", zz)]
12404                    .iter()
12405                    .map(|(n, i)| (n.to_string(), *i))
12406                    .collect();
12407            want.sort();
12408            assert_eq!(listed, want);
12409        }
12410
12411        #[test]
12412        fn the_inode_api_reads_an_inline_directory() {
12413            let (dev, d, x, sub, zz) = volume();
12414            let fs = mount(&dev);
12415            for (name, want) in [
12416                (&b"x"[..], x),
12417                (b"sub", sub),
12418                (b"zz", zz),
12419                (b".", d),
12420                (b"..", 2),
12421            ] {
12422                assert_eq!(
12423                    fs.lookup_at(d, name).map_err(|e| format!("{e:?}")),
12424                    Ok(want),
12425                    "lookup_at {}",
12426                    String::from_utf8_lossy(name)
12427                );
12428            }
12429            assert!(matches!(fs.lookup_at(d, b"nope"), Err(Error::NotFound)));
12430            let mut listed: Vec<(Vec<u8>, u32)> = fs
12431                .read_dir_ino(d)
12432                .map_err(|e| format!("{e:?}"))
12433                .expect("read_dir_ino")
12434                .into_iter()
12435                .map(|e| (e.name, e.inode))
12436                .collect();
12437            listed.sort();
12438            let mut want: Vec<(Vec<u8>, u32)> =
12439                [(".", d), ("..", 2), ("sub", sub), ("x", x), ("zz", zz)]
12440                    .iter()
12441                    .map(|(n, i)| (n.as_bytes().to_vec(), *i))
12442                    .collect();
12443            want.sort();
12444            assert_eq!(listed, want);
12445        }
12446    }
12447}
12448
12449/// [`Filesystem::dir_block_role`] against indexed directories the kernel
12450/// and debugfs built.
12451#[cfg(test)]
12452mod dir_block_role_tests {
12453    use super::{DirBlockRole, Filesystem};
12454    use crate::inode::{Inode, InodeFlags};
12455
12456    fn dir(indexed: bool) -> Inode {
12457        let mut raw = vec![0u8; 256];
12458        raw[0..2].copy_from_slice(&0x41EDu16.to_le_bytes());
12459        if indexed {
12460            raw[0x20..0x24].copy_from_slice(&InodeFlags::INDEX.bits().to_le_bytes());
12461        }
12462        Inode::parse(&raw).unwrap()
12463    }
12464
12465    fn block_starting_with(inode: u32, span: u16) -> Vec<u8> {
12466        let mut block = vec![0u8; 1024];
12467        block[0..4].copy_from_slice(&inode.to_le_bytes());
12468        block[4..6].copy_from_slice(&span.to_le_bytes());
12469        block
12470    }
12471
12472    #[test]
12473    fn roles_follow_the_documented_shapes() {
12474        let node = block_starting_with(0, 1024);
12475        let leaf = block_starting_with(12, 16);
12476        let empty_leaf_start = block_starting_with(0, 16);
12477        let role = Filesystem::dir_block_role;
12478        assert_eq!(role(&dir(true), 0, &leaf), DirBlockRole::IndexRoot);
12479        assert_eq!(role(&dir(true), 3, &node), DirBlockRole::IndexNode);
12480        assert_eq!(role(&dir(true), 3, &leaf), DirBlockRole::Entries);
12481        assert_eq!(
12482            role(&dir(true), 3, &empty_leaf_start),
12483            DirBlockRole::Entries
12484        );
12485        // Without the INDEX flag nothing is index, whatever it looks like.
12486        assert_eq!(role(&dir(false), 0, &node), DirBlockRole::Entries);
12487        assert_eq!(role(&dir(false), 3, &node), DirBlockRole::Entries);
12488    }
12489
12490    /// Tests that ask the kernel and debugfs in the harness VM.
12491    mod needs_host {
12492        use super::super::{DirBlockRole, Filesystem};
12493        use crate::block_io::FileDevice;
12494        use std::collections::BTreeSet;
12495        use std::sync::Arc;
12496
12497        /// `(interior node blocks, leaf blocks)` of `path`, from `debugfs
12498        /// htree`: the root's map names the nodes when the tree has an
12499        /// indirect level, and every leaf is announced as it is read.
12500        fn debugfs_tree(image: &str, path: &str) -> (BTreeSet<u64>, BTreeSet<u64>) {
12501            let out = fs_ext4_test_support::oracle("debugfs")
12502                .args(["-R", &format!("htree {path}"), image])
12503                .output();
12504            let text = String::from_utf8_lossy(&out.stdout).into_owned();
12505            let levels: u32 = text
12506                .lines()
12507                .find_map(|l| l.trim().strip_prefix("Indirect levels: "))
12508                .unwrap_or_else(|| panic!("{path} is not indexed:\n{text}"))
12509                .trim()
12510                .parse()
12511                .unwrap();
12512            assert_eq!(
12513                levels, 1,
12514                "{path}: the fixture should have one interior level"
12515            );
12516            let block_of = |l: &str| -> Option<u64> {
12517                l.split("block ")
12518                    .nth(1)?
12519                    .split(|c: char| !c.is_ascii_digit())
12520                    .next()?
12521                    .parse()
12522                    .ok()
12523            };
12524            // The root's own map comes first, up to the first blank line.
12525            let root_map: BTreeSet<u64> = text
12526                .lines()
12527                .skip_while(|l| !l.starts_with("Entry #"))
12528                .take_while(|l| !l.trim().is_empty())
12529                .filter_map(block_of)
12530                .collect();
12531            let leaves: BTreeSet<u64> = text
12532                .lines()
12533                .filter(|l| l.starts_with("Reading directory block "))
12534                .filter_map(|l| l.trim_start_matches("Reading directory ").split(',').next())
12535                .filter_map(block_of)
12536                .collect();
12537            (root_map, leaves)
12538        }
12539
12540        fn classify(built_by: &str, image: &str) {
12541            let (nodes, leaves) = debugfs_tree(image, "/d");
12542            assert!(
12543                nodes.len() >= 2 && leaves.len() > 100,
12544                "[{built_by}] tree too small"
12545            );
12546            let fs = Filesystem::mount(Arc::new(FileDevice::open(image).unwrap())).unwrap();
12547            let ino = fs.lookup_path_bytes(b"/d").unwrap();
12548            let (inode, _) = fs.read_inode_verified(ino).unwrap();
12549            let bytes = crate::file_io::read_all(&fs, &inode).unwrap();
12550            let bs = fs.sb.block_size() as usize;
12551            let mut seen = (0, 0, 0);
12552            for (logical, block) in bytes.chunks(bs).enumerate() {
12553                let logical = logical as u64;
12554                let want = if logical == 0 {
12555                    seen.0 += 1;
12556                    DirBlockRole::IndexRoot
12557                } else if nodes.contains(&logical) {
12558                    seen.1 += 1;
12559                    DirBlockRole::IndexNode
12560                } else {
12561                    assert!(leaves.contains(&logical), "[{built_by}] block {logical}");
12562                    seen.2 += 1;
12563                    DirBlockRole::Entries
12564                };
12565                assert_eq!(
12566                    Filesystem::dir_block_role(&inode, logical, block),
12567                    want,
12568                    "[{built_by}] logical block {logical}"
12569                );
12570                // And the pre-edit check passes every block of a clean tree.
12571                fs.refuse_unverified_dir_block(ino, &inode, logical, block)
12572                    .unwrap_or_else(|e| panic!("[{built_by}] block {logical}: {e:?}"));
12573            }
12574            assert_eq!(seen, (1, nodes.len(), leaves.len()), "[{built_by}]");
12575            drop(fs);
12576
12577            // Edits that walk the blocks go through, and e2fsck agrees.
12578            {
12579                let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(image).unwrap())).unwrap();
12580                fs.apply_create("/d/added", 0o644).unwrap();
12581                fs.apply_unlink(&format!("/d/{}", name(7))).unwrap();
12582            }
12583            fs_ext4_test_support::assert_e2fsck_clean(image, built_by);
12584        }
12585
12586        fn name(i: usize) -> String {
12587            format!("entry_{i:04}_{}", "n".repeat(180))
12588        }
12589
12590        fn volume(tag: &str, source: Option<&str>) -> String {
12591            let image = fs_ext4_test_support::temp_path!(
12592                "fs_ext4_dx_role_{tag}_{}.img",
12593                std::process::id()
12594            );
12595            std::fs::File::create(&image)
12596                .and_then(|f| f.set_len(32 << 20))
12597                .unwrap();
12598            let mut mkfs =
12599                fs_ext4_test_support::oracle("mkfs.ext4").args(["-q", "-F", "-b", "1024"]);
12600            if let Some(dir) = source {
12601                mkfs = mkfs.args(["-d", dir]);
12602            }
12603            let out = mkfs.arg(&image).output();
12604            assert!(
12605                out.status.success(),
12606                "{}",
12607                String::from_utf8_lossy(&out.stderr)
12608            );
12609            image
12610        }
12611
12612        /// The kernel grows `/d` from a linear directory into a two-level
12613        /// tree as the entries arrive.
12614        #[test]
12615        fn a_kernel_grown_tree_is_classified_as_debugfs_reads_it() {
12616            let image = volume("kernel", None);
12617            let script = format!(
12618                "mkdir \"$MNT/d\"\ncd \"$MNT/d\"\nfor i in $(seq 0 699); do : > \"$(printf 'entry_%04d_' $i){}\"; done\nsync\n",
12619                "n".repeat(180)
12620            );
12621            let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
12622            assert!(
12623                out.status.success(),
12624                "{}",
12625                String::from_utf8_lossy(&out.stderr)
12626            );
12627            classify("kernel", &image);
12628            let _ = std::fs::remove_file(&image);
12629        }
12630
12631        /// e2fsck rebuilds `/d`, written linear by mkfs.ext4 -d, as an index.
12632        #[test]
12633        fn an_e2fsck_built_tree_is_classified_as_debugfs_reads_it() {
12634            let root =
12635                fs_ext4_test_support::temp_path!("fs_ext4_dx_role_src_{}", std::process::id());
12636            std::fs::create_dir_all(format!("{root}/d")).unwrap();
12637            for i in 0..700 {
12638                std::fs::write(format!("{root}/d/{}", name(i)), b"").unwrap();
12639            }
12640            let image = volume("e2fsck", Some(&root));
12641            let _ = fs_ext4_test_support::oracle("e2fsck")
12642                .args(["-fyD", &image])
12643                .judged();
12644            classify("e2fsck -D", &image);
12645            let _ = std::fs::remove_dir_all(&root);
12646            let _ = std::fs::remove_file(&image);
12647        }
12648    }
12649}