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::Inode;
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/// Pack the low bits of an ext4 nanosecond timestamp field.
89///
90/// ext4 stores extra precision in a 32-bit extra field: bits [31:2] hold the
91/// low 30 bits of the nanosecond value; bits [1:0] are the 2-bit epoch
92/// extension that extends the 32-bit seconds counter beyond 2038.
93#[inline]
94fn pack_nsec_lo(nsec: u32) -> u32 {
95    (nsec & 0x3FFF_FFFF) << 2
96}
97
98/// Passed to [`Filesystem::apply_utimens`] in place of a seconds value
99/// to leave that timestamp unchanged — the equivalent of POSIX's
100/// `UTIME_OMIT`, which `utimensat(2)` spells in the nanoseconds field.
101///
102/// `i64::MIN` and not `u32::MAX`: seconds are signed and 64-bit, so
103/// `u32::MAX` is an ordinary date in 2106 and can no longer double as a
104/// sentinel. `i64::MIN` is far outside anything ext4 can store.
105pub const TIME_OMIT: i64 = i64::MIN;
106
107/// Split a `/a/b/c` path into (`/a/b`, `c`). Returns an error for empty or
108/// `"/"` paths (no basename to act on).
109fn split_parent_and_base(path: &str) -> Result<(String, String)> {
110    let trimmed = path.trim_end_matches('/');
111    if trimmed.is_empty() {
112        return Err(Error::InvalidArgument("empty path"));
113    }
114    let last_slash = trimmed
115        .rfind('/')
116        .ok_or(Error::InvalidArgument("relative path"))?;
117    let base = &trimmed[last_slash + 1..];
118    let parent = if last_slash == 0 {
119        "/"
120    } else {
121        &trimmed[..last_slash]
122    };
123    if base.is_empty() {
124        // Trailing slash on a non-dir path is POSIX ENOTDIR, not a generic arg error.
125        return Err(Error::NotADirectory);
126    }
127    Ok((parent.to_string(), base.to_string()))
128}
129
130/// `DeepReader` adapter that pulls extent-tree internal/leaf node blocks
131/// straight from a `Filesystem`'s underlying device (which at mount time
132/// is wrapped in a `CachedDevice`, so reads benefit from the buffer cache
133/// holding post-commit pre-checkpoint journaled writes).
134///
135/// Used by `apply_pwrite` to satisfy `plan_insert_extent_deep`'s
136/// `&dyn DeepReader` argument when the inline extent root overflows and
137/// the tree needs to be promoted to depth ≥ 1.
138pub(crate) struct FsBlockReader<'a> {
139    pub(crate) fs: &'a Filesystem,
140}
141
142impl<'a> crate::extent_mut::DeepReader for FsBlockReader<'a> {
143    fn read_block(&self, block: u64, out: &mut [u8]) -> Result<()> {
144        let bytes = self.fs.read_block(block)?;
145        if bytes.len() != out.len() {
146            return Err(Error::Corrupt(
147                "FsBlockReader: block length mismatch (callers must pass a buffer sized to fs block_size)",
148            ));
149        }
150        out.copy_from_slice(&bytes);
151        Ok(())
152    }
153}
154
155/// Current wall time as a u32 — matches ext4's `i_dtime` field. Uses
156/// `SystemTime::now()`; we don't care about monotonicity here, just that
157/// `dtime > ctime` so `ext4 audit tool` recognises the slot as recently deleted.
158fn now_unix_seconds() -> u32 {
159    use std::time::{SystemTime, UNIX_EPOCH};
160    SystemTime::now()
161        .duration_since(UNIX_EPOCH)
162        .map(|d| d.as_secs() as u32)
163        .unwrap_or(0)
164}
165
166// -----------------------------------------------------------------------
167// Inode builder helpers (H2)
168// -----------------------------------------------------------------------
169// Shared across all build_*_inode functions. Extracted to avoid five
170// identical copies of timestamps, generation, extra_isize, and checksum.
171
172use std::sync::atomic::{AtomicU32, Ordering};
173/// Process-lifetime counter shared by all inode builders so successive
174/// creates within the same session produce distinct i_generation values.
175static INODE_GEN_COUNTER: AtomicU32 = AtomicU32::new(1);
176
177/// Write atime, ctime, mtime (and crtime when the inode buffer is large
178/// enough) from `now` into the raw inode bytes.
179fn write_inode_timestamps(raw: &mut [u8], now: u32) {
180    use crate::inode::{INODE_SIZE_WITH_CRTIME, OFF_ATIME, OFF_CRTIME, OFF_CTIME, OFF_MTIME};
181    raw[OFF_ATIME..OFF_ATIME + 4].copy_from_slice(&now.to_le_bytes());
182    raw[OFF_CTIME..OFF_CTIME + 4].copy_from_slice(&now.to_le_bytes());
183    raw[OFF_MTIME..OFF_MTIME + 4].copy_from_slice(&now.to_le_bytes());
184    // i_crtime (birth time) only exists in the extra section. Without it,
185    // Darwin's st_birthtime / Finder "Created" date shows 1970-01-01.
186    if raw.len() >= INODE_SIZE_WITH_CRTIME {
187        raw[OFF_CRTIME..OFF_CRTIME + 4].copy_from_slice(&now.to_le_bytes());
188    }
189}
190
191/// Allocate a unique i_generation value for a new inode: PID combined with
192/// a per-process counter. Ensures distinct values across rapid successive
193/// creates (NFS stale-handle detection depends on generation uniqueness).
194fn alloc_inode_generation() -> u32 {
195    std::process::id().wrapping_add(INODE_GEN_COUNTER.fetch_add(1, Ordering::Relaxed))
196}
197
198/// Write a pre-allocated generation value into the raw inode bytes.
199fn write_inode_generation(raw: &mut [u8], generation: u32) {
200    use crate::inode::OFF_GENERATION;
201    raw[OFF_GENERATION..OFF_GENERATION + 4].copy_from_slice(&generation.to_le_bytes());
202}
203
204/// Write i_extra_isize = 32 when the inode buffer is large enough.
205/// 32 covers checksum_hi, nsec timestamps, and i_crtime beyond the 128-byte base.
206fn write_inode_extra_isize(raw: &mut [u8]) {
207    use crate::inode::{EXTRA_ISIZE_DEFAULT, INODE_SIZE_WITH_EXTRA, OFF_EXTRA_ISIZE};
208    if raw.len() >= INODE_SIZE_WITH_EXTRA {
209        raw[OFF_EXTRA_ISIZE..OFF_EXTRA_ISIZE + 2]
210            .copy_from_slice(&EXTRA_ISIZE_DEFAULT.to_le_bytes());
211    }
212}
213
214pub struct Filesystem {
215    pub dev: Arc<dyn BlockDevice>,
216    pub sb: Superblock,
217    pub groups: Vec<BlockGroupDescriptor>,
218    /// Uninit flags this mount has already taken down on disk, by group.
219    ///
220    /// `groups` is a snapshot read once at mount and every write path holds
221    /// `&self`, so the snapshot cannot be corrected in place when a group's
222    /// INODE_UNINIT / BLOCK_UNINIT is cleared. That matters because the
223    /// allocators *plan* against those flags: a group still flagged uninit is
224    /// treated as entirely free without the bitmap being read at all. Left
225    /// stale, the second allocation into a freshly-woken group hands back the
226    /// very inode or block the first one just took — in the same mount, not
227    /// merely the next one.
228    ///
229    /// Read through [`Filesystem::allocation_groups`], which is what the
230    /// planners must be given.
231    uninit_cleared: Mutex<HashMap<usize, u16>>,
232    pub csum: Checksummer,
233    /// Dialect detected at mount time from the superblock's feature flags.
234    /// Drives runtime dispatch where ext2 / ext3 / ext4 differ — most
235    /// notably the inode block-mapping scheme (extent vs indirect) used
236    /// when allocating new inodes.
237    pub flavor: features::FsFlavor,
238    /// Live-write journal writer, present iff the FS has a journal AND
239    /// the device is writable. `None` for read-only mounts and for ext2-
240    /// style images. Locked per-op so mutating capi calls serialize on
241    /// the JBD2 sequence cursor.
242    pub journal: Option<std::sync::Mutex<crate::journal_writer::JournalWriter>>,
243}
244
245/// Encapsulates the common setup for creating a new inode in a directory:
246/// resolved parent, pre-allocated inode number, and a `BlockBuffer` with the
247/// inode-bitmap + BGD + SB counter updates already staged. Produced by
248/// `Filesystem::plan_new_inode_in_dir`.
249struct NewInodePlan {
250    /// Newly allocated inode number (1-based).
251    new_ino: u32,
252    /// Inode number of the parent directory.
253    parent_ino: u32,
254    /// Parsed parent inode (for reading the directory block).
255    parent_inode: crate::inode::Inode,
256    /// Staged write buffer (bitmap + counter deltas already applied).
257    buf: BlockBuffer,
258    /// Final component of `path` — the name to add as a dir entry.
259    base_name: String,
260}
261
262/// Which BGD "uninit" flag a bitmap-marking call is about — see
263/// `Filesystem::clear_bgd_uninit_flag_if_set`.
264#[derive(Clone, Copy, PartialEq, Eq)]
265pub(crate) enum BgdUninitFlag {
266    Inode,
267    Block,
268}
269
270impl Filesystem {
271    /// Mount the ext4 filesystem on `dev`. Read-only unless the device reports
272    /// `is_writable()`, in which case a dirty journal is replayed before
273    /// returning so callers see a consistent on-disk state.
274    ///
275    /// When `RO_COMPAT_METADATA_CSUM` is set, the superblock checksum is
276    /// verified — failure aborts the mount with `Error::BadChecksum`.
277    pub fn mount(dev: Arc<dyn BlockDevice>) -> Result<Self> {
278        Self::mount_inner(dev, false)
279    }
280
281    /// Like `mount`, but skips the mount-time journal replay even when the
282    /// device is writable. The caller is responsible for invoking
283    /// [`Filesystem::replay_journal_if_dirty`] once the underlying write
284    /// path is actually ready to service writes (e.g. in the FSKit case the
285    /// kernel-level write FD on `FSBlockDeviceResource` only becomes
286    /// writable AFTER `loadResource` returns successfully — replaying mid-
287    /// `loadResource` produces EIO).
288    ///
289    /// Until replay runs, reads observe the on-disk pre-replay state and
290    /// any write through this handle will fail (the journal still says
291    /// dirty). This is the lazy/deferred-replay sibling of `mount`; for
292    /// most callers `mount` is correct.
293    pub fn mount_lazy(dev: Arc<dyn BlockDevice>) -> Result<Self> {
294        Self::mount_inner(dev, true)
295    }
296
297    fn mount_inner(dev: Arc<dyn BlockDevice>, defer_replay: bool) -> Result<Self> {
298        let sb = Superblock::read(dev.as_ref())?;
299        features::check_mountable(sb.feature_incompat, sb.feature_ro_compat)?;
300        let flavor = features::FsFlavor::detect(sb.feature_compat, sb.feature_incompat);
301        let csum = Checksummer::from_superblock(&sb);
302        if csum.enabled && !csum.verify_superblock(&sb.raw) {
303            return Err(Error::BadChecksum { what: "superblock" });
304        }
305        let groups = bgd::read_all(dev.as_ref(), &sb, &csum)?;
306        // Wrap the raw device in a write-through buffer cache. All
307        // reads and writes for the rest of this mount session route
308        // through the cache; `commit_block_buffer` populates pinned
309        // entries with journaled-but-not-yet-checkpointed bytes so
310        // allocator scans don't re-read stale on-disk bitmaps. This is
311        // the role Linux's buffer cache plays for journaled
312        // filesystems. Capacity 256 ≈ 1 MiB at 4 KiB blocks — enough
313        // to cover hot metadata (BGD, bitmaps, recently-touched inode
314        // blocks) for typical sessions; pinned entries are unbounded
315        // until journal replay calls `unpin_all`.
316        let dev: Arc<dyn BlockDevice> = Arc::new(crate::block_cache::CachedDevice::new(
317            dev,
318            sb.block_size(),
319            256,
320        ));
321        let mut fs = Self {
322            dev,
323            sb,
324            groups,
325            uninit_cleared: Mutex::new(HashMap::new()),
326            csum,
327            flavor,
328            journal: None,
329        };
330
331        // Replay a dirty journal if the device is writable. Silently skips
332        // for read-only mounts — the read path tolerates a non-clean journal
333        // (pending transactions are invisible, which is correct for a
334        // read-only view).
335        //
336        // Both the walker (`journal_block_to_physical`) and the writer
337        // (`JournalWriter::open`) now dispatch on `indirect::map_logical_any`,
338        // so ext3 (whose journal inode uses legacy indirect block pointers)
339        // works the same as ext4 (extent tree). The Phase A blanket refusal
340        // of ext3 RW is therefore lifted.
341        // MMP — Multi-Mount Protection — exists to stop two hosts
342        // mounting one filesystem read-write at the same time and
343        // destroying it. Honouring it means reading the MMP block,
344        // checking its sequence, writing our own node name, waiting,
345        // and re-checking; none of that is implemented.
346        //
347        // Ignoring the bit is defensible for a read-only mount: a
348        // reader cannot corrupt anything, and the other host's
349        // protection is unaffected. It is NOT defensible the moment we
350        // are the one writing -- which this crate does, through
351        // twenty-one apply_* entry points and a live journal writer,
352        // both reached below on exactly this condition.
353        //
354        // So the refusal is scoped to the writable case. A read-only
355        // mount of an MMP filesystem still works, which is what a user
356        // recovering data from a disk another machine has open
357        // actually wants.
358        if fs.dev.is_writable()
359            && fs.sb.feature_incompat & crate::features::Incompat::MMP.bits() != 0
360        {
361            return Err(crate::error::Error::UnsupportedIncompat(
362                crate::features::Incompat::MMP.bits(),
363            ));
364        }
365
366        if !defer_replay && fs.dev.is_writable() {
367            // Best-effort: a replay failure here is logged via the returned
368            // error but does NOT abort the mount, because many images have
369            // cosmetic journal issues that shouldn't prevent read access.
370            // The error surfaces up so the caller can decide whether to
371            // retry or proceed; we fail loud rather than silent.
372            crate::journal_apply::replay_if_dirty(&fs)?;
373        }
374
375        // Open the live-write journal writer once replay is done. Any
376        // pending transactions are now applied; the writer can take over
377        // the JBD2 cursor from a clean state. Returns None when there is
378        // no journal at all (ext2), so the if-let handles every flavor
379        // uniformly.
380        if fs.dev.is_writable() {
381            if let Some(jw) = crate::journal_writer::JournalWriter::open(&fs)? {
382                fs.journal = Some(std::sync::Mutex::new(jw));
383            }
384        }
385
386        // Phase 6.2 — orphan recovery. Runs after journal replay so any
387        // pending kernel-level transactions have already played back;
388        // any inode still on the orphan chain at this point is genuinely
389        // dead and we can reclaim it. Best-effort: a recovery failure
390        // surfaces as an error but doesn't abort the mount.
391        if fs.dev.is_writable() && !defer_replay {
392            let _ = fs.recover_orphans();
393        }
394
395        Ok(fs)
396    }
397
398    /// Run journal replay now if the journal is dirty. Idempotent — calling
399    /// this on a clean (or read-only) volume is a no-op that returns 0.
400    /// Designed to pair with [`Filesystem::mount_lazy`], but safe to call
401    /// on any handle.
402    pub fn replay_journal_if_dirty(&self) -> Result<usize> {
403        let n = crate::journal_apply::replay_if_dirty(self)?;
404        // Replay applied every pending journaled write to the data area,
405        // so the device-layer cache's "pinned" entries (post-commit but
406        // pre-checkpoint) are now consistent with disk. Tell the cache
407        // it can stop pinning them — future evictions are safe.
408        // Skip when nothing replayed: a clean journal returns 0, and
409        // unpinning here would demote pinned-but-still-needed entries
410        // from a live handle's prior journaled writes, letting later
411        // cache misses serve stale data-area bytes.
412        if n > 0 {
413            self.dev.unpin_all();
414        }
415        Ok(n)
416    }
417
418    /// Phase 6.1 — walk the orphan inode chain rooted at `s_last_orphan`
419    /// and return its members in chain order.
420    ///
421    /// Each orphan inode is a unlink-while-open candidate: its data
422    /// blocks should be reclaimed by recovery. The chain is encoded by
423    /// overloading `i_dtime` as "next orphan inode number"; the chain
424    /// terminates when `dtime == 0`. We cap at `inodes_count` to avoid
425    /// runaway loops on cycle-corrupted images.
426    ///
427    /// Read-only (no recovery yet — that's Phase 6.2). Returns `Ok([])`
428    /// when there are no orphans.
429    pub fn orphan_list(&self) -> Result<Vec<u32>> {
430        let mut out = Vec::new();
431        let mut cur = self.sb.last_orphan;
432        let cap = self.sb.inodes_count;
433        let mut steps = 0u32;
434        while cur != 0 {
435            if steps > cap {
436                return Err(Error::Corrupt(
437                    "orphan_list: chain longer than inodes_count (cycle?)",
438                ));
439            }
440            out.push(cur);
441            // Read the inode's i_dtime (offset 0x14..0x18) to find the
442            // next link. Don't go through read_inode_verified because an
443            // orphan inode's checksum may be stale by design.
444            let raw = self.read_inode_raw(cur)?;
445            if raw.len() < 0x18 {
446                return Err(Error::Corrupt("orphan_list: inode too short"));
447            }
448            cur = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
449            steps += 1;
450        }
451        Ok(out)
452    }
453
454    /// Phase 6.2 — orphan replay. For each inode on the
455    /// `s_last_orphan` chain, free its data blocks + inode-bitmap slot,
456    /// zero its inode body (with `i_dtime = now`), and clear
457    /// `s_last_orphan`. Runs as ONE multi-block journaled transaction
458    /// so a crash mid-recovery either commits all the frees or none of
459    /// them.
460    ///
461    /// Returns the number of orphan inodes reclaimed. No-op (returns 0)
462    /// when the chain is empty or the device is read-only.
463    ///
464    /// Designed to be called from the mount path AFTER journal replay,
465    /// so the orphans we're about to reclaim are guaranteed not still in
466    /// use by an in-flight kernel-level transaction.
467    pub fn recover_orphans(&self) -> Result<usize> {
468        if !self.dev.is_writable() {
469            return Ok(0);
470        }
471        let chain = self.orphan_list()?;
472        if chain.is_empty() {
473            return Ok(0);
474        }
475
476        let bs = self.sb.block_size();
477        let sectors_per_block = bs as u64 / 512;
478        let mut buf = BlockBuffer::new(bs);
479        let mut total_freed_blocks: u64 = 0;
480        let mut reclaimed = 0usize;
481
482        for &orphan_ino in &chain {
483            // Read the orphan's raw bytes (skip csum verify — orphan
484            // inodes routinely carry stale csums by design).
485            let mut raw = self.read_inode_raw(orphan_ino)?;
486            let parsed = match Inode::parse(&raw) {
487                Ok(i) => i,
488                Err(_) => continue, // unparseable orphan — skip + leak rather than panic
489            };
490            // Free data blocks (extents path only — orphan recovery for
491            // legacy indirect inodes is a follow-up).
492            if parsed.has_extents() && parsed.size > 0 {
493                let (_sc, muts) = match crate::file_mut::plan_truncate_shrink(
494                    parsed.size,
495                    0,
496                    &parsed.block,
497                    bs,
498                ) {
499                    Ok(p) => p,
500                    Err(_) => continue,
501                };
502                for m in &muts {
503                    if let crate::extent_mut::ExtentMutation::FreePhysicalRun { start, len } = m {
504                        total_freed_blocks +=
505                            self.buffer_free_block_run_and_bgd(&mut buf, *start, *len as u64)?;
506                    }
507                }
508            }
509            // Free the inode bitmap slot + BGD free_inodes++.
510            self.buffer_free_inode_slot(&mut buf, orphan_ino)?;
511
512            // Zero the inode body (preserve generation), set dtime.
513            let inode_size = self.sb.inode_size as usize;
514            let old_gen = parsed.generation;
515            for b in &mut raw[..inode_size] {
516                *b = 0;
517            }
518            let dtime = now_unix_seconds();
519            raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
520            raw[0x64..0x68].copy_from_slice(&old_gen.to_le_bytes());
521            self.finalize_inode_raw(orphan_ino, old_gen, &mut raw)?;
522            self.buffer_write_inode(&mut buf, orphan_ino, &raw)?;
523
524            reclaimed += 1;
525        }
526
527        // SB: free_blocks_count += total_freed, free_inodes_count +=
528        // reclaimed, s_last_orphan = 0.
529        self.buffer_patch_sb_counters(&mut buf, total_freed_blocks as i64, reclaimed as i32)?;
530        self.buffer_patch_sb_last_orphan(&mut buf, 0)?;
531
532        // i_blocks tracking on the freed inodes is moot (they're zero
533        // now); their per-extent sectors are accounted for in the
534        // BGD/SB counter updates above.
535        let _ = sectors_per_block;
536
537        self.commit_block_buffer(buf)?;
538        Ok(reclaimed)
539    }
540
541    /// Read a whole block by its logical block number. Routes through
542    /// `self.dev`, which at mount time is wrapped in a `CachedDevice` —
543    /// so this single call benefits from the buffer cache that holds
544    /// post-commit, pre-checkpoint journaled writes.
545    pub fn read_block(&self, block_num: u64) -> Result<Vec<u8>> {
546        let block_size = self.sb.block_size() as usize;
547        let byte_offset = block_num
548            .checked_mul(block_size as u64)
549            .ok_or(Error::Corrupt("block byte offset overflow"))?;
550        let mut buf = vec![0u8; block_size];
551        self.dev.read_at(byte_offset, &mut buf)?;
552        Ok(buf)
553    }
554
555    /// Read raw inode bytes for a given inode number (does not parse).
556    pub fn read_inode_raw(&self, ino: u32) -> Result<Vec<u8>> {
557        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
558        let block_data = self.read_block(block)?;
559        let inode_size = self.sb.inode_size as usize;
560        let off = offset as usize;
561        let end = off
562            .checked_add(inode_size)
563            .ok_or(Error::Corrupt("inode slice end overflows usize"))?;
564        if end > block_data.len() {
565            return Err(Error::Corrupt("inode slice exceeds block data"));
566        }
567        Ok(block_data[off..end].to_vec())
568    }
569
570    /// Read + parse + checksum-verify an inode in one shot.
571    ///
572    /// When `RO_COMPAT_METADATA_CSUM` is enabled the inode CRC32C is checked
573    /// (salted by inode number + generation per ext4 spec). A mismatch
574    /// returns `Error::BadChecksum { what: "inode" }`.
575    pub fn read_inode_verified(&self, ino: u32) -> Result<(Inode, Vec<u8>)> {
576        let raw = self.read_inode_raw(ino)?;
577        let inode = Inode::parse(&raw)?;
578        if self.csum.enabled && !self.csum.verify_inode(ino, inode.generation, &raw) {
579            return Err(Error::BadChecksum { what: "inode" });
580        }
581        // A DIRECTORY IS NOT SPARSE.
582        //
583        // Every directory scan in this crate walks
584        // `0..size.div_ceil(block_size)` and steps over a logical block
585        // that is not mapped -- which is what the kernel does too, so
586        // the loop is never ended by an error and never bounded by real
587        // content. `i_size` is `join32(i_size_high, i_size_lo)` off the
588        // disk: setting `i_size_high` on the root of a small image gave
589        // a directory of 2^44 bytes and a lookup that was still
590        // spinning after twenty seconds, with `MAX_DIR_ENTRIES` never
591        // reached because no entry is ever found.
592        //
593        // A regular file may legitimately declare more bytes than the
594        // filesystem holds -- that is what a sparse file is -- but a
595        // directory's blocks are all really there.
596        if inode.is_dir() {
597            let filesystem_bytes = self
598                .sb
599                .blocks_count
600                .saturating_mul(self.sb.block_size() as u64);
601            if inode.size > filesystem_bytes {
602                return Err(Error::Corrupt(
603                    "directory inode declares more bytes than the filesystem holds",
604                ));
605            }
606        }
607        Ok((inode, raw))
608    }
609
610    /// Map a logical block within `inode` to its physical block, choosing
611    /// between the extent tree and the legacy direct/indirect scheme based
612    /// on `EXT4_EXTENTS_FL`. Returns `None` for sparse holes and (for the
613    /// extent path) uninitialised extents — callers wanting zeros there
614    /// must handle the `None` case explicitly.
615    ///
616    /// This is the per-inode dispatcher every directory traversal /
617    /// extent-walking call site should use instead of touching
618    /// `extent::map_logical` directly — without it, an ext2/3 inode with
619    /// raw block pointers in `i_block` gets misparsed as an extent header
620    /// (yielding `CorruptExtentTree("bad extent header magic")`).
621    ///
622    /// The indirect path internally maintains its own block cache for the
623    /// duration of the call; sequential lookups via repeated calls don't
624    /// share that cache (file_io's read paths build a longer-lived cache
625    /// to amortize across blocks).
626    pub fn map_inode_logical(&self, inode: &Inode, logical_block: u64) -> Result<Option<u64>> {
627        let bs = self.sb.block_size();
628        if (inode.flags & crate::inode::InodeFlags::EXTENTS.bits()) != 0 {
629            crate::extent::map_logical(&inode.block, self.dev.as_ref(), bs, logical_block)
630        } else {
631            let mut cache = crate::indirect::IndirectCache::new();
632            crate::indirect::lookup(
633                &inode.block,
634                self.dev.as_ref(),
635                bs,
636                logical_block,
637                &mut cache,
638            )
639        }
640    }
641
642    /// Write the given raw inode bytes back to disk. Read-only devices return
643    /// the default `Error::Corrupt` from `BlockDevice::write_at`.
644    ///
645    /// **Not checksum-aware**: callers that update fields affecting the inode
646    /// CRC32C (anything except `checksum_lo` / `checksum_hi`) must recompute
647    /// + patch the checksum into `raw` before calling this. Not wrapped in a
648    /// journal transaction — see E11 / `journal_apply` for the journaled
649    /// version. Use only when the caller has the full write-ordering story
650    /// under control.
651    pub fn write_inode_raw(&self, ino: u32, raw: &[u8]) -> Result<()> {
652        if raw.len() != self.sb.inode_size as usize {
653            return Err(Error::Corrupt("write_inode_raw: length != inode_size"));
654        }
655        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
656        let block_size = self.sb.block_size() as u64;
657        let byte_offset = block * block_size + offset as u64;
658        self.dev.write_at(byte_offset, raw)?;
659        Ok(())
660    }
661
662    /// Patch fields in a raw inode image: size, blocks_count. Leaves all
663    /// other bytes (including the extent tree header + entries in `i_block`)
664    /// intact. `new_block_count` is in 512-byte sectors per spec (same
665    /// convention as `Inode::blocks`).
666    pub fn patch_inode_size_and_blocks(
667        raw: &mut [u8],
668        new_size: u64,
669        new_block_count: u64,
670    ) -> Result<()> {
671        if raw.len() < 128 {
672            return Err(Error::Corrupt("patch_inode: buffer too small"));
673        }
674        // size = size_lo (0x04..0x08) + size_hi (0x6C..0x70)
675        let size_lo = (new_size & 0xFFFF_FFFF) as u32;
676        let size_hi = (new_size >> 32) as u32;
677        raw[0x04..0x08].copy_from_slice(&size_lo.to_le_bytes());
678        raw[0x6C..0x70].copy_from_slice(&size_hi.to_le_bytes());
679        // blocks = blocks_lo (0x1C..0x20, u32) + blocks_hi (0x74..0x76, u16)
680        let blocks_lo = (new_block_count & 0xFFFF_FFFF) as u32;
681        let blocks_hi = ((new_block_count >> 32) & 0xFFFF) as u16;
682        raw[0x1C..0x20].copy_from_slice(&blocks_lo.to_le_bytes());
683        raw[0x74..0x76].copy_from_slice(&blocks_hi.to_le_bytes());
684        Ok(())
685    }
686
687    /// Overwrite the 60-byte `i_block` area of an inode image with `new_root`.
688    /// Used when an extent-tree mutation changes the inline root.
689    pub fn patch_inode_block_area(raw: &mut [u8], new_root: &[u8]) -> Result<()> {
690        if raw.len() < 128 {
691            return Err(Error::Corrupt("patch_inode_block_area: buffer too small"));
692        }
693        if new_root.len() != 60 {
694            return Err(Error::Corrupt(
695                "patch_inode_block_area: new_root != 60 bytes",
696            ));
697        }
698        raw[0x28..0x64].copy_from_slice(new_root);
699        Ok(())
700    }
701
702    /// Shrink a file to `new_size`. Composes `file_mut::plan_truncate_shrink`
703    /// (extent-tree updates + freed-block ranges) with actual disk writes —
704    /// rewrites the inode and zeros the freed bitmap bits.
705    ///
706    /// Journaled. The inode write, the bitmap writes, the BGD and the
707    /// superblock accumulate into one `BlockBuffer` and commit as a
708    /// single transaction, so they are atomic with respect to a crash.
709    ///
710    /// This said "Not journaled … safe only in a test scratch image", and
711    /// promised the transaction as future work. The future work landed;
712    /// the warning outlived it and was steering callers away from an API
713    /// that is safe.
714    pub fn apply_truncate_shrink(&self, ino: u32, new_size: u64) -> Result<()> {
715        if !self.dev.is_writable() {
716            return Err(Error::ReadOnly);
717        }
718        let (inode, mut raw) = self.read_inode_verified(ino)?;
719        if new_size > inode.size {
720            return Err(Error::InvalidArgument(
721                "truncate: new_size > old_size (grow not supported)",
722            ));
723        }
724
725        let (_size_change, muts) = crate::file_mut::plan_truncate_shrink(
726            inode.size,
727            new_size,
728            &inode.block,
729            self.sb.block_size(),
730        )?;
731
732        let bs = self.sb.block_size() as u64;
733        let mut freed_sectors: u64 = 0;
734        let mut freed_blocks: u64 = 0;
735
736        // Multi-block transaction: accumulate inode + bitmap + BGD + SB
737        // mutations into one buffer, commit through the journal atomically.
738        let mut buf = BlockBuffer::new(self.sb.block_size());
739
740        for m in &muts {
741            match m {
742                crate::extent_mut::ExtentMutation::WriteRoot { bytes } => {
743                    Self::patch_inode_block_area(&mut raw, bytes)?;
744                }
745                crate::extent_mut::ExtentMutation::FreePhysicalRun { start, len } => {
746                    freed_blocks +=
747                        self.buffer_free_block_run_and_bgd(&mut buf, *start, *len as u64)?;
748                    freed_sectors += (*len as u64) * (bs / 512);
749                }
750                _ => {
751                    return Err(Error::Corrupt(
752                        "apply_truncate_shrink: unexpected mutation type",
753                    ));
754                }
755            }
756        }
757
758        // Patch size + blocks_count in the inode image, finalize csum.
759        let new_blocks = inode.blocks.saturating_sub(freed_sectors);
760        Self::patch_inode_size_and_blocks(&mut raw, new_size, new_blocks)?;
761        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
762        self.buffer_write_inode(&mut buf, ino, &raw)?;
763
764        if freed_blocks > 0 {
765            self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 0)?;
766        }
767
768        self.commit_block_buffer(buf)
769    }
770
771    /// Extend a file to `new_size`. The new range is a sparse hole — ext4's
772    /// extent tree treats unmapped logical blocks as zeros, so no extent
773    /// mutation and no block allocation are required. Only `i_size`,
774    /// `i_mtime`, `i_ctime`, and the inode checksum change.
775    ///
776    /// Caller (capi dispatch) guarantees `new_size >= inode.size`. If
777    /// `new_size == inode.size` this is a no-op that still bumps the
778    /// timestamps — matches `truncate(2)` semantics.
779    pub fn apply_truncate_grow(&self, ino: u32, new_size: u64) -> Result<()> {
780        if !self.dev.is_writable() {
781            return Err(Error::ReadOnly);
782        }
783        let (inode, mut raw) = self.read_inode_verified(ino)?;
784        if new_size < inode.size {
785            return Err(Error::InvalidArgument(
786                "apply_truncate_grow: new_size < old_size (use apply_truncate_shrink)",
787            ));
788        }
789        Self::patch_inode_size_and_blocks(&mut raw, new_size, inode.blocks)?;
790
791        let now = now_unix_seconds();
792        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes()); // ctime
793        raw[0x10..0x14].copy_from_slice(&now.to_le_bytes()); // mtime
794
795        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
796        self.commit_inode_write(ino, &raw)
797    }
798
799    /// Phase 2.2: `fallocate(FALLOC_FL_KEEP_SIZE)` — preallocate blocks
800    /// in the byte range `[offset, offset+len)` as uninitialized
801    /// extents. The blocks are reserved (count against `i_blocks`) but
802    /// reads return zeros until they're written. `i_size` is left
803    /// unchanged per KEEP_SIZE semantics.
804    ///
805    /// v1 limitations:
806    /// - Range must be entirely unmapped — partially-overlapping ranges
807    ///   return `Error::InvalidArgument`. (Splitting around existing
808    ///   extents is a follow-up.)
809    /// - Single contiguous physical allocation. If the bitmap can't
810    ///   serve `ceil(len / block_size)` contiguous blocks, returns
811    ///   `Error::Corrupt("no group has a contiguous free run...")`.
812    /// - Extent insertion must succeed against the inline-root depth-0
813    ///   tree (or trigger the existing depth-1 promotion). Multi-level
814    ///   trees aren't yet supported.
815    pub fn apply_fallocate_keep_size(&self, ino: u32, offset: u64, len: u64) -> Result<()> {
816        if !self.dev.is_writable() {
817            return Err(Error::ReadOnly);
818        }
819        if len == 0 {
820            return Ok(());
821        }
822        let bs = self.sb.block_size() as u64;
823        let bs_u32 = self.sb.block_size();
824        let first_block = offset / bs;
825        let last_block_excl = offset
826            .checked_add(len)
827            .ok_or(Error::InvalidArgument("fallocate: offset+len overflow"))?
828            .div_ceil(bs);
829        let need_blocks_u64 = last_block_excl - first_block;
830        if need_blocks_u64 > u32::MAX as u64 {
831            return Err(Error::InvalidArgument(
832                "fallocate: range exceeds u32 block count",
833            ));
834        }
835        let need_blocks = need_blocks_u64 as u32;
836
837        let (inode, mut raw) = self.read_inode_verified(ino)?;
838        if !inode.is_file() {
839            return Err(Error::InvalidArgument(
840                "fallocate: target is not a regular file",
841            ));
842        }
843        if !inode.has_extents() {
844            return Err(Error::InvalidArgument(
845                "fallocate: legacy (non-extents) inodes not supported",
846            ));
847        }
848
849        // V1: refuse if any block in range is already mapped — handling
850        // the partial-overlap case requires splitting existing extents
851        // mid-range, deferred to a follow-up.
852        for log in first_block..last_block_excl {
853            if crate::extent::map_logical(&inode.block, self.dev.as_ref(), bs_u32, log)?.is_some() {
854                return Err(Error::InvalidArgument(
855                    "fallocate: range partially mapped (v1 limitation)",
856                ));
857            }
858        }
859
860        // Allocate one contiguous physical run.
861        let inode_group = (ino - 1) / self.sb.inodes_per_group;
862        let mut bitmap_reader = |block: u64| self.read_block(block);
863        let plan = crate::alloc::plan_block_allocation(
864            &self.sb,
865            &self.allocation_groups(),
866            need_blocks,
867            inode_group,
868            &mut bitmap_reader,
869        )?;
870
871        // Insert as an uninitialized extent so reads see zeros without
872        // hitting disk. Clamp to u16 — the range check above already
873        // bounded need_blocks, but the on-disk extent length is u16.
874        if need_blocks > 0x7FFF {
875            return Err(Error::InvalidArgument(
876                "fallocate: single-extent length > 32K blocks (split needed)",
877            ));
878        }
879        let new_extent = crate::extent::Extent {
880            logical_block: first_block as u32,
881            length: need_blocks as u16,
882            physical_block: plan.first_block,
883            uninitialized: true,
884        };
885        let muts = crate::extent_mut::plan_insert_extent(&inode.block, new_extent)?;
886
887        // Apply via BlockBuffer — atomic across bitmap, BGD, SB, inode.
888        let mut buf = BlockBuffer::new(self.sb.block_size());
889        self.buffer_mark_block_run_used(&mut buf, plan.first_block, need_blocks as u64)?;
890        self.buffer_patch_bgd_counters(
891            &mut buf,
892            plan.bgd.group_idx as usize,
893            plan.bgd.free_blocks_delta,
894            plan.bgd.free_inodes_delta,
895            plan.bgd.used_dirs_delta,
896        )?;
897        self.buffer_patch_sb_counters(
898            &mut buf,
899            plan.sb.free_blocks_delta,
900            plan.sb.free_inodes_delta,
901        )?;
902
903        // Splice the new extent root into the inode image.
904        for m in &muts {
905            if let crate::extent_mut::ExtentMutation::WriteRoot { bytes } = m {
906                Self::patch_inode_block_area(&mut raw, bytes)?;
907            }
908        }
909
910        // Bump i_blocks (sectors). KEEP_SIZE: i_size unchanged.
911        let sectors_per_block = bs / 512;
912        let new_i_blocks = inode
913            .blocks
914            .saturating_add(need_blocks as u64 * sectors_per_block);
915        Self::patch_inode_size_and_blocks(&mut raw, inode.size, new_i_blocks)?;
916
917        // POSIX: fallocate bumps mtime + ctime.
918        let now = now_unix_seconds();
919        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
920        raw[0x10..0x14].copy_from_slice(&now.to_le_bytes());
921
922        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
923        self.buffer_write_inode(&mut buf, ino, &raw)?;
924
925        self.commit_block_buffer(buf)
926    }
927
928    /// Phase 2.3 — `fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)`.
929    /// Frees the data blocks underlying `[offset, offset+len)`, splitting
930    /// straddling extents as needed. Reads of the punched range return
931    /// zeros (sparse hole) thereafter; `i_size` is unchanged.
932    ///
933    /// v1 limits:
934    /// - Depth-0 inline-root extent trees only. Surviving entries must
935    ///   fit in 4 slots (the inline-root capacity); anything larger
936    ///   returns `Corrupt(...)`. A real punch on a heavily-fragmented
937    ///   file may need depth ≥ 1, which is a Phase 4 follow-up.
938    /// - Indirect-block (ext2/3) inodes return EINVAL — punch is an
939    ///   ext4-specific kernel API.
940    pub fn apply_fallocate_punch_hole(&self, ino: u32, offset: u64, len: u64) -> Result<()> {
941        if !self.dev.is_writable() {
942            return Err(Error::ReadOnly);
943        }
944        if len == 0 {
945            return Ok(());
946        }
947        let bs = self.sb.block_size() as u64;
948        let bs_u32 = self.sb.block_size();
949        let punch_first = offset / bs;
950        let punch_last_excl = offset
951            .checked_add(len)
952            .ok_or(Error::InvalidArgument("punch_hole: offset+len overflow"))?
953            .div_ceil(bs);
954
955        let (inode, mut raw) = self.read_inode_verified(ino)?;
956        if !inode.is_file() {
957            return Err(Error::InvalidArgument("punch_hole: not a regular file"));
958        }
959        if !inode.has_extents() {
960            return Err(Error::InvalidArgument(
961                "punch_hole: legacy (non-extents) inodes not supported",
962            ));
963        }
964
965        let extents = crate::extent::collect_all(&inode.block, self.dev.as_ref(), bs_u32)?;
966        let mut new_entries: Vec<crate::extent::Extent> = Vec::new();
967        let mut freed_blocks: u64 = 0;
968        let mut buf = BlockBuffer::new(bs_u32);
969
970        for e in &extents {
971            let el = e.logical_block as u64;
972            let er = el + e.length as u64;
973
974            if er <= punch_first || el >= punch_last_excl {
975                // Fully outside the punch range — keep verbatim.
976                new_entries.push(*e);
977                continue;
978            }
979            if el >= punch_first && er <= punch_last_excl {
980                // Fully inside punch — free entirely.
981                freed_blocks += self.buffer_free_block_run_and_bgd(
982                    &mut buf,
983                    e.physical_block,
984                    e.length as u64,
985                )?;
986                continue;
987            }
988            // Partial overlap. Compute the freed sub-range; emit head /
989            // tail retains around it.
990            let free_lo = el.max(punch_first);
991            let free_hi = er.min(punch_last_excl);
992            let free_offset_in_e = free_lo - el;
993            let free_len = (free_hi - free_lo) as u32;
994            let free_phys = e.physical_block + free_offset_in_e;
995            freed_blocks +=
996                self.buffer_free_block_run_and_bgd(&mut buf, free_phys, free_len as u64)?;
997
998            if el < punch_first {
999                new_entries.push(crate::extent::Extent {
1000                    logical_block: el as u32,
1001                    length: (punch_first - el) as u16,
1002                    physical_block: e.physical_block,
1003                    uninitialized: e.uninitialized,
1004                });
1005            }
1006            if er > punch_last_excl {
1007                new_entries.push(crate::extent::Extent {
1008                    logical_block: punch_last_excl as u32,
1009                    length: (er - punch_last_excl) as u16,
1010                    physical_block: e.physical_block + (punch_last_excl - el),
1011                    uninitialized: e.uninitialized,
1012                });
1013            }
1014        }
1015
1016        if new_entries.len() > 4 {
1017            return Err(Error::Corrupt(
1018                "punch_hole: surviving entries exceed inline-root capacity (4); needs depth>=1",
1019            ));
1020        }
1021
1022        // Rebuild the inline root with the surviving entries.
1023        let gen = u32::from_le_bytes(inode.block[8..12].try_into().unwrap());
1024        let mut root = vec![0u8; 60];
1025        root[0..2].copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
1026        root[2..4].copy_from_slice(&(new_entries.len() as u16).to_le_bytes());
1027        root[4..6].copy_from_slice(&4u16.to_le_bytes());
1028        // depth = 0 (zero already)
1029        root[8..12].copy_from_slice(&gen.to_le_bytes());
1030        for (i, e) in new_entries.iter().enumerate() {
1031            let off = 12 + i * 12;
1032            root[off..off + 4].copy_from_slice(&e.logical_block.to_le_bytes());
1033            let ee_len = if e.uninitialized {
1034                e.length + crate::extent::EXT_INIT_MAX_LEN
1035            } else {
1036                e.length
1037            };
1038            root[off + 4..off + 6].copy_from_slice(&ee_len.to_le_bytes());
1039            let (phys_hi, phys_lo) = crate::extent_mut::split_phys_block(e.physical_block);
1040            root[off + 6..off + 8].copy_from_slice(&phys_hi.to_le_bytes());
1041            root[off + 8..off + 12].copy_from_slice(&phys_lo.to_le_bytes());
1042        }
1043        Self::patch_inode_block_area(&mut raw, &root)?;
1044
1045        // i_blocks decreases; i_size unchanged (KEEP_SIZE semantics
1046        // built in — punch always preserves size).
1047        let sectors_per_block = bs / 512;
1048        let new_i_blocks = inode
1049            .blocks
1050            .saturating_sub(freed_blocks * sectors_per_block);
1051        Self::patch_inode_size_and_blocks(&mut raw, inode.size, new_i_blocks)?;
1052        let now = now_unix_seconds();
1053        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
1054        raw[0x10..0x14].copy_from_slice(&now.to_le_bytes());
1055        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
1056        self.buffer_write_inode(&mut buf, ino, &raw)?;
1057
1058        if freed_blocks > 0 {
1059            self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 0)?;
1060        }
1061
1062        self.commit_block_buffer(buf)
1063    }
1064
1065    /// Phase 2.4 — `fallocate(FALLOC_FL_ZERO_RANGE)`. Logically zero the
1066    /// byte range `[offset, offset+len)` without writing actual data.
1067    /// Implemented as punch-hole + KEEP_SIZE preallocate of the same
1068    /// range, so reads return zeros (uninitialized-extent semantics) and
1069    /// future writes don't need an allocation.
1070    ///
1071    /// Two separate transactions today (punch then alloc); a future
1072    /// optimization could fold them into one.
1073    pub fn apply_fallocate_zero_range(&self, ino: u32, offset: u64, len: u64) -> Result<()> {
1074        if len == 0 {
1075            return Ok(());
1076        }
1077        self.apply_fallocate_punch_hole(ino, offset, len)?;
1078        self.apply_fallocate_keep_size(ino, offset, len)
1079    }
1080
1081    /// Change the permission bits on `path`. Only the low 12 bits of `mode`
1082    /// (`S_ISUID|S_ISGID|S_ISVTX` plus rwx/rwx/rwx) are applied; the file-type
1083    /// bits (`S_IFMT`) are preserved from the existing inode.
1084    ///
1085    /// Updates `i_ctime = now` and recomputes the inode checksum on csum-
1086    /// enabled mounts. Returns `Error::NotFound` if the path doesn't resolve,
1087    /// `Error::ReadOnly` on a RO mount.
1088    pub fn apply_chmod(&self, path: &str, mode: u16) -> Result<()> {
1089        if !self.dev.is_writable() {
1090            return Err(Error::ReadOnly);
1091        }
1092        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
1093        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
1094        let (inode, mut raw) = self.read_inode_verified(ino)?;
1095
1096        // Preserve file-type bits (high 4 bits of i_mode); only the low 12
1097        // permission/suid/sgid/sticky bits are user-settable.
1098        let file_type_bits = inode.mode & crate::inode::S_IFMT;
1099        let new_mode = file_type_bits | (mode & 0x0FFF);
1100        raw[0x00..0x02].copy_from_slice(&new_mode.to_le_bytes());
1101
1102        // POSIX: chmod bumps ctime (not mtime).
1103        let now = now_unix_seconds();
1104        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
1105
1106        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
1107        self.commit_inode_write(ino, &raw)
1108    }
1109
1110    /// Write a single mutated inode back, routing through the journal
1111    /// writer when one is available so the change is crash-safe. Falls
1112    /// back to a direct write + flush on unjournaled mounts.
1113    ///
1114    /// Used by every operation whose only mutation is one inode block:
1115    /// chmod, chown, utimens, and the in-place xattr ops once they're
1116    /// migrated to the journaled path.
1117    fn commit_inode_write(&self, ino: u32, new_inode_raw: &[u8]) -> Result<()> {
1118        let mut buf = BlockBuffer::new(self.sb.block_size());
1119        self.buffer_write_inode(&mut buf, ino, new_inode_raw)?;
1120        self.commit_block_buffer(buf)
1121    }
1122
1123    // ----------------------------------------------------------------------
1124    // BlockBuffer helpers (Phase 5.2 multi-block transactions)
1125    // ----------------------------------------------------------------------
1126    //
1127    // These mirror the disk-touching helpers (free_block_run_and_bgd,
1128    // patch_bgd_counters, patch_sb_counters, write_inode_raw) but operate
1129    // on an in-memory BlockBuffer instead. A multi-block op accumulates
1130    // its mutations into one buffer and commits the whole thing atomically
1131    // — either through the journal writer (when present) or via a flush-
1132    // gated direct-write fallback.
1133
1134    /// Splice a freshly-built inode into the inode-table block buffer.
1135    pub(crate) fn buffer_write_inode(
1136        &self,
1137        buf: &mut BlockBuffer,
1138        ino: u32,
1139        inode_raw: &[u8],
1140    ) -> Result<()> {
1141        let (block, offset) = bgd::locate_inode(&self.sb, &self.groups, ino)?;
1142        let it_buf = buf.get_mut(self, block)?;
1143        let off = offset as usize;
1144        it_buf[off..off + inode_raw.len()].copy_from_slice(inode_raw);
1145        Ok(())
1146    }
1147
1148    /// Buffer-side equivalent of `free_block_run_and_bgd`: clears the
1149    /// bitmap bits AND patches the BGD counters in the buffer. Returns
1150    /// `len` so callers can accumulate a running freed-block total to
1151    /// feed to `buffer_patch_sb_counters`.
1152    pub(crate) fn buffer_free_block_run_and_bgd(
1153        &self,
1154        buf: &mut BlockBuffer,
1155        start: u64,
1156        len: u64,
1157    ) -> Result<u64> {
1158        let bpg = self.sb.blocks_per_group as u64;
1159        let first_data = self.sb.first_data_block as u64;
1160        let gi = ((start - first_data) / bpg) as usize;
1161        if gi >= self.groups.len() {
1162            return Err(Error::InvalidBlock(start));
1163        }
1164        let group_start = first_data + gi as u64 * bpg;
1165        let bit_start = (start - group_start) as u32;
1166        let bitmap_block = self.groups[gi].block_bitmap;
1167        {
1168            let bm = buf.get_mut(self, bitmap_block)?;
1169            for i in 0..len {
1170                let bit = bit_start as u64 + i;
1171                let byte = (bit / 8) as usize;
1172                let mask = 1u8 << (bit % 8);
1173                if byte < bm.len() {
1174                    bm[byte] &= !mask;
1175                }
1176            }
1177        }
1178        self.buffer_refresh_bitmap_csum(buf, gi, false)?;
1179        self.buffer_patch_bgd_counters(buf, gi, len as i32, 0, 0)?;
1180        Ok(len)
1181    }
1182
1183    /// Buffer-side equivalent of `mark_block_run_used`: sets the bitmap
1184    /// bits for `[start, start+len)` in the buffer's bitmap block.
1185    /// If group `gi`'s BGD has the given uninit flag set, clear it in `buf`
1186    /// and return `true` (the caller must then zero the bitmap block
1187    /// itself — the flag being set is precisely the license callers had to
1188    /// leave that block's on-disk content unspecified). Returns `false`,
1189    /// no-op, if the flag was already clear.
1190    fn clear_bgd_uninit_flag_if_set(
1191        &self,
1192        buf: &mut BlockBuffer,
1193        gi: usize,
1194        which: BgdUninitFlag,
1195    ) -> Result<bool> {
1196        const INODE_UNINIT: u16 = 0x0001;
1197        const BLOCK_UNINIT: u16 = 0x0002;
1198        let flag = match which {
1199            BgdUninitFlag::Inode => INODE_UNINIT,
1200            BgdUninitFlag::Block => BLOCK_UNINIT,
1201        };
1202
1203        let bs = self.sb.block_size() as u64;
1204        let desc_size = self.sb.desc_size as u64;
1205        let bgt_first_block = self.sb.first_data_block as u64 + 1;
1206        let byte_in_bgt = gi as u64 * desc_size;
1207        let bgt_block = bgt_first_block + byte_in_bgt / bs;
1208        let off = (byte_in_bgt % bs) as usize;
1209
1210        let block = buf.get_mut(self, bgt_block)?;
1211        let flags_off = off + 0x12;
1212        let flags = u16::from_le_bytes(block[flags_off..flags_off + 2].try_into().unwrap());
1213        if flags & flag == 0 {
1214            return Ok(false);
1215        }
1216        let new_flags = flags & !flag;
1217        block[flags_off..flags_off + 2].copy_from_slice(&new_flags.to_le_bytes());
1218        // Record it against the mount-time snapshot too, or the very next
1219        // allocation plans as though the group were still untouched — but
1220        // record it on the *buffer*, so it becomes visible only when the
1221        // buffer commits.
1222        //
1223        // Publishing it here instead would survive a failed commit: the
1224        // operation returns an error, the mount carries on, and the next
1225        // allocation is told the group's bitmap is initialised while the
1226        // bytes on disk are still whatever the uninit flag licensed
1227        // leaving there.
1228        buf.uninit_cleared
1229            .entry(gi)
1230            .and_modify(|f| *f &= !flag)
1231            .or_insert(new_flags);
1232        Ok(true)
1233    }
1234
1235    /// The group descriptors the allocators must plan against: the mount-time
1236    /// snapshot, with any uninit flag this mount has since cleared taken back
1237    /// out. Borrows the snapshot untouched in the overwhelmingly common case
1238    /// where nothing has been cleared yet.
1239    fn allocation_groups(&self) -> Cow<'_, [BlockGroupDescriptor]> {
1240        let cleared = self.uninit_cleared.lock().unwrap();
1241        if cleared.is_empty() {
1242            return Cow::Borrowed(&self.groups);
1243        }
1244        let mut groups = self.groups.clone();
1245        for (&gi, &flags) in cleared.iter() {
1246            groups[gi].flags = flags;
1247        }
1248        Cow::Owned(groups)
1249    }
1250
1251    /// The blocks group `gi` owns that physically live inside it, as
1252    /// `(first_bit, count)` runs relative to the group's first block.
1253    ///
1254    /// Used when a BLOCK_UNINIT group's bitmap is zeroed for the first time:
1255    /// everything here has to go straight back in, or the group's own
1256    /// metadata becomes allocatable free space. Reading it off the descriptor
1257    /// rather than deriving it from the feature flags means an unusual layout
1258    /// is handled by inspection instead of by assumption.
1259    fn group_owned_metadata_blocks(
1260        &self,
1261        gi: usize,
1262        group_start: u64,
1263        bpg: u64,
1264    ) -> Vec<(u64, u64)> {
1265        let bs = self.sb.block_size() as u64;
1266        let mut runs = Vec::new();
1267
1268        // Superblock, group-descriptor-table backup and the blocks held
1269        // back for growing the table, at the head of every group that
1270        // carries a backup.
1271        //
1272        // Which groups those are is the filesystem's decision, not a
1273        // constant: `SPARSE_SUPER2` puts backups in two named groups and
1274        // no others, and a filesystem without `SPARSE_SUPER` puts one in
1275        // every group. Assuming the classic rule reports "no backup
1276        // here" for groups that have one, and a rebuilt bitmap then
1277        // offers a live backup superblock as free space.
1278        //
1279        // `s_reserved_gdt_blocks` belongs in the same run. It sits
1280        // between the descriptor table and the block bitmap, and it is
1281        // the room the filesystem keeps to grow into — free-looking, and
1282        // not free.
1283        if self.sb.group_has_super(gi as u64) {
1284            let gdt_blocks = (self.groups.len() as u64 * self.sb.desc_size as u64).div_ceil(bs);
1285            let reserved = u64::from(self.sb.reserved_gdt_blocks);
1286            runs.push((0, 1 + gdt_blocks + reserved));
1287        }
1288
1289        // The group's own bitmaps and inode table, wherever the descriptor
1290        // says they are — included only when that is inside this group.
1291        let itable_blocks =
1292            (self.sb.inodes_per_group as u64 * self.sb.inode_size as u64).div_ceil(bs);
1293        let g = &self.groups[gi];
1294        for (block, count) in [
1295            (g.block_bitmap, 1),
1296            (g.inode_bitmap, 1),
1297            (g.inode_table, itable_blocks),
1298        ] {
1299            if block >= group_start && block < group_start + bpg {
1300                runs.push((block - group_start, count));
1301            }
1302        }
1303        runs
1304    }
1305
1306    pub(crate) fn buffer_mark_block_run_used(
1307        &self,
1308        buf: &mut BlockBuffer,
1309        start: u64,
1310        len: u64,
1311    ) -> Result<()> {
1312        let bpg = self.sb.blocks_per_group as u64;
1313        let first_data = self.sb.first_data_block as u64;
1314        let gi = ((start - first_data) / bpg) as usize;
1315        if gi >= self.groups.len() {
1316            return Err(Error::InvalidBlock(start));
1317        }
1318        let group_start = first_data + gi as u64 * bpg;
1319        let bit_start = (start - group_start) as u32;
1320
1321        // Same staleness problem as `buffer_mark_inode_used`, for the block
1322        // bitmap this time: BLOCK_UNINIT is every reader's license to skip
1323        // the on-disk bitmap and treat the group as empty, so the *next*
1324        // mount kept proposing the same "first free" block for every new
1325        // allocation into this group — including a file's own data block
1326        // landing on top of a directory's just-created data block in the
1327        // same group. Reproduced by hand: the second file written into a
1328        // freshly-created directory corrupted the directory's own data
1329        // block ("corrupt directory entry: bad rec_len during add") because
1330        // its content block silently reused the directory's block number.
1331        //
1332        // Unlike an uninit inode bitmap, "all blocks free" isn't quite
1333        // right here: a group still owns whatever fixed overhead physically
1334        // lives inside it, and zeroing the bitmap without putting that back
1335        // hands the group's own metadata out as free space. Two kinds of
1336        // overhead can be there — the RO_COMPAT_SPARSE_SUPER superblock +
1337        // GDT backup (groups 0, 1, and powers of 3/5/7), and the group's own
1338        // block bitmap, inode bitmap and inode table.
1339        //
1340        // With flex_bg those last three usually sit in the cohort's head
1341        // group, and a group is only left BLOCK_UNINIT when mkfs had no real
1342        // bitmap/table data to write for it — so on a flex_bg volume they are
1343        // reliably elsewhere. That is an assumption about the formatter,
1344        // though, not something the on-disk format guarantees: without
1345        // flex_bg every group holds its own. So rather than assume, ask where
1346        // the descriptor actually points and reserve whatever lands inside
1347        // this group.
1348        let was_uninit = self.clear_bgd_uninit_flag_if_set(buf, gi, BgdUninitFlag::Block)?;
1349        let reserved_runs = if was_uninit {
1350            self.group_owned_metadata_blocks(gi, group_start, bpg)
1351        } else {
1352            Vec::new()
1353        };
1354        let bitmap_block = self.groups[gi].block_bitmap;
1355        let bm = buf.get_mut(self, bitmap_block)?;
1356        if was_uninit {
1357            bm.iter_mut().for_each(|byte| *byte = 0);
1358            for (first_bit, count) in reserved_runs {
1359                for bit in first_bit..(first_bit + count).min(bpg) {
1360                    let byte = (bit / 8) as usize;
1361                    let mask = 1u8 << (bit % 8);
1362                    if byte < bm.len() {
1363                        bm[byte] |= mask;
1364                    }
1365                }
1366            }
1367        }
1368        for i in 0..len {
1369            let bit = bit_start as u64 + i;
1370            let byte = (bit / 8) as usize;
1371            let mask = 1u8 << (bit % 8);
1372            if byte < bm.len() {
1373                bm[byte] |= mask;
1374            }
1375        }
1376        self.buffer_refresh_bitmap_csum(buf, gi, false)?;
1377        Ok(())
1378    }
1379
1380    /// Recompute a group's bitmap checksum (inode or block) after its bitmap
1381    /// block changed, then refresh the BGD checksum. metadata_csum stores the
1382    /// bitmap crc split lo + hi in the descriptor (inode: 0x1A/0x3A, block:
1383    /// 0x18/0x38); a stale value makes e2fsck and the kernel report "bitmap
1384    /// does not match checksum". No-op when checksums are disabled.
1385    pub(crate) fn buffer_refresh_bitmap_csum(
1386        &self,
1387        buf: &mut BlockBuffer,
1388        gi: usize,
1389        inode_bitmap: bool,
1390    ) -> Result<()> {
1391        if !self.csum.enabled {
1392            return Ok(());
1393        }
1394        let (bitmap_block, coverage, lo_off, hi_off) = if inode_bitmap {
1395            (
1396                self.groups[gi].inode_bitmap,
1397                (self.sb.inodes_per_group as usize).div_ceil(8),
1398                0x1A,
1399                0x3A,
1400            )
1401        } else {
1402            (
1403                self.groups[gi].block_bitmap,
1404                (self.sb.blocks_per_group as usize).div_ceil(8),
1405                0x18,
1406                0x38,
1407            )
1408        };
1409        let csum = {
1410            let bm = buf.get_mut(self, bitmap_block)?;
1411            let end = coverage.min(bm.len());
1412            crate::checksum::linux_crc32c(self.csum.seed, &bm[..end])
1413        };
1414
1415        let bs = self.sb.block_size() as u64;
1416        let desc_size = self.sb.desc_size as u64;
1417        let bgt_first_block = self.sb.first_data_block as u64 + 1;
1418        let byte_in_bgt = gi as u64 * desc_size;
1419        let bgt_block = bgt_first_block + byte_in_bgt / bs;
1420        let off = (byte_in_bgt % bs) as usize;
1421        let has_hi = desc_size >= 0x40;
1422        let block = buf.get_mut(self, bgt_block)?;
1423        block[off + lo_off..off + lo_off + 2]
1424            .copy_from_slice(&((csum & 0xFFFF) as u16).to_le_bytes());
1425        if has_hi {
1426            block[off + hi_off..off + hi_off + 2]
1427                .copy_from_slice(&(((csum >> 16) & 0xFFFF) as u16).to_le_bytes());
1428        }
1429        // Refresh the BGD checksum (0x1E) so the descriptor stays consistent.
1430        let stored_at = off + 0x1E;
1431        let end_desc = off + desc_size as usize;
1432        block[stored_at..stored_at + 2].copy_from_slice(&[0, 0]);
1433        let mut c = crate::checksum::linux_crc32c(self.csum.seed, &(gi as u32).to_le_bytes());
1434        c = crate::checksum::linux_crc32c(c, &block[off..end_desc]);
1435        block[stored_at..stored_at + 2].copy_from_slice(&(c as u16).to_le_bytes());
1436        Ok(())
1437    }
1438
1439    /// Buffer-side equivalent of `free_inode_slot`: clears the inode
1440    /// bitmap bit AND patches the BGD's `bg_free_inodes_count` (+1) in
1441    /// the buffer. Matches the kernel's pairing — the SB
1442    /// `s_free_inodes_count` is the caller's responsibility (one bump
1443    /// per high-level op, via `buffer_patch_sb_counters`).
1444    pub(crate) fn buffer_free_inode_slot(&self, buf: &mut BlockBuffer, ino: u32) -> Result<()> {
1445        let ipg = self.sb.inodes_per_group;
1446        let gi = ((ino - 1) / ipg) as usize;
1447        if gi >= self.groups.len() {
1448            return Err(Error::InvalidInode(ino));
1449        }
1450        let bit = ((ino - 1) % ipg) as u64;
1451        let bitmap_block = self.groups[gi].inode_bitmap;
1452        {
1453            let bm = buf.get_mut(self, bitmap_block)?;
1454            let byte = (bit / 8) as usize;
1455            let mask = 1u8 << (bit % 8);
1456            if byte < bm.len() {
1457                bm[byte] &= !mask;
1458            }
1459        }
1460        self.buffer_refresh_bitmap_csum(buf, gi, true)?;
1461        self.buffer_patch_bgd_counters(buf, gi, 0, 1, 0)
1462    }
1463
1464    /// Buffer-side equivalent of `mark_inode_used`: sets the inode
1465    /// bitmap bit. BGD/SB counter patches are the caller's
1466    /// responsibility (different ops want different deltas — e.g.
1467    /// mkdir bumps `used_dirs_count`).
1468    pub(crate) fn buffer_mark_inode_used(&self, buf: &mut BlockBuffer, ino: u32) -> Result<()> {
1469        let ipg = self.sb.inodes_per_group;
1470        let gi = ((ino - 1) / ipg) as usize;
1471        if gi >= self.groups.len() {
1472            return Err(Error::InvalidInode(ino));
1473        }
1474        let bit = ((ino - 1) % ipg) as u64;
1475        let bitmap_block = self.groups[gi].inode_bitmap;
1476
1477        // If this group's inode bitmap is still INODE_UNINIT, every reader
1478        // (including a future mount of this same filesystem) is required to
1479        // ignore whatever bytes are actually on disk there and assume the
1480        // whole group is free — that's the entire point of the flag, and
1481        // it's why uninit groups' bitmap blocks are allowed to contain
1482        // stale/unspecified garbage from mkfs. The moment we allocate a
1483        // real inode out of such a group, that assumption becomes false, so
1484        // we must (a) zero the block ourselves before setting our bit —
1485        // group index > 0 has zero pre-reserved inodes, so "everything but
1486        // our bit is free" is exactly correct here — and (b) clear the
1487        // flag. Skipping either step means the *next* mount still treats
1488        // the group as empty and hands out the same inode number again,
1489        // silently overwriting whatever was just written here. Found by
1490        // hand: creating a file/directory whose parent lands in a
1491        // previously-untouched group corrupted the parent on the very next
1492        // allocation, every time, until this was fixed.
1493        let was_uninit = self.clear_bgd_uninit_flag_if_set(buf, gi, BgdUninitFlag::Inode)?;
1494        let bm = buf.get_mut(self, bitmap_block)?;
1495        if was_uninit {
1496            bm.iter_mut().for_each(|byte| *byte = 0);
1497            // e2fsck convention: bits beyond `inodes_per_group`, up to the
1498            // end of the bitmap block, represent no real inode and must
1499            // read as 1 ("in use"), not 0 ("free") — that's what "padding
1500            // at end of inode bitmap is not set" flags otherwise. Harmless
1501            // on its own (no inode ever maps there), but worth getting
1502            // right since we're already the one deciding this block's
1503            // entire content for the first time.
1504            let bits_per_block = (bm.len() as u64) * 8;
1505            for pad_bit in (ipg as u64)..bits_per_block {
1506                let byte = (pad_bit / 8) as usize;
1507                let mask = 1u8 << (pad_bit % 8);
1508                bm[byte] |= mask;
1509            }
1510        }
1511        let byte = (bit / 8) as usize;
1512        let mask = 1u8 << (bit % 8);
1513        if byte < bm.len() {
1514            bm[byte] |= mask;
1515        }
1516        self.buffer_refresh_bitmap_csum(buf, gi, true)?;
1517
1518        // Maintain bg_itable_unused: this inode is now in use, so the count of
1519        // never-used inodes at the END of the group's table can be no larger
1520        // than the inodes after this one. A stale value makes e2fsck and the
1521        // kernel treat freshly-allocated inodes as unused ("references inode
1522        // found in unused inodes area" / "invalid unused inodes count"). lo at
1523        // 0x1C, hi at 0x32 (desc_size >= 64). The BGD checksum is recomputed so
1524        // the change stands alone; the following counter patch recomputes it
1525        // again harmlessly.
1526        let floor = ipg.saturating_sub(bit as u32 + 1);
1527        let bs = self.sb.block_size() as u64;
1528        let desc_size = self.sb.desc_size as u64;
1529        let bgt_first_block = self.sb.first_data_block as u64 + 1;
1530        let byte_in_bgt = gi as u64 * desc_size;
1531        let bgt_block = bgt_first_block + byte_in_bgt / bs;
1532        let off = (byte_in_bgt % bs) as usize;
1533        let has_hi = desc_size >= 0x40;
1534        let block = buf.get_mut(self, bgt_block)?;
1535        let cur_lo = u16::from_le_bytes(block[off + 0x1C..off + 0x1E].try_into().unwrap()) as u32;
1536        let cur_hi = if has_hi {
1537            u16::from_le_bytes(block[off + 0x32..off + 0x34].try_into().unwrap()) as u32
1538        } else {
1539            0
1540        };
1541        let cur = (cur_hi << 16) | cur_lo;
1542        if floor < cur {
1543            block[off + 0x1C..off + 0x1E].copy_from_slice(&((floor & 0xFFFF) as u16).to_le_bytes());
1544            if has_hi {
1545                block[off + 0x32..off + 0x34]
1546                    .copy_from_slice(&(((floor >> 16) & 0xFFFF) as u16).to_le_bytes());
1547            }
1548            if self.csum.enabled {
1549                let stored_at = off + 0x1E;
1550                let end_desc = off + desc_size as usize;
1551                block[stored_at..stored_at + 2].copy_from_slice(&[0, 0]);
1552                let seed = self.csum.seed;
1553                let mut c = crate::checksum::linux_crc32c(seed, &(gi as u32).to_le_bytes());
1554                c = crate::checksum::linux_crc32c(c, &block[off..end_desc]);
1555                block[stored_at..stored_at + 2].copy_from_slice(&(c as u16).to_le_bytes());
1556            }
1557        }
1558        Ok(())
1559    }
1560
1561    /// Buffer-side BGD counter patch. Mirrors `patch_bgd_counters` byte
1562    /// for byte; only the I/O target differs (the BGD block is read from
1563    /// the buffer if already touched, else from disk).
1564    pub(crate) fn buffer_patch_bgd_counters(
1565        &self,
1566        buf: &mut BlockBuffer,
1567        gi: usize,
1568        free_blocks_delta: i32,
1569        free_inodes_delta: i32,
1570        used_dirs_delta: i32,
1571    ) -> Result<()> {
1572        let bs = self.sb.block_size() as u64;
1573        let desc_size = self.sb.desc_size as u64;
1574        let bgt_first_block = self.sb.first_data_block as u64 + 1;
1575        let byte_in_bgt = gi as u64 * desc_size;
1576        let bgt_block = bgt_first_block + byte_in_bgt / bs;
1577        let off_in_block = (byte_in_bgt % bs) as usize;
1578
1579        let block = buf.get_mut(self, bgt_block)?;
1580        patch_counter_u32(
1581            block,
1582            off_in_block + 0x0C,
1583            if desc_size >= 0x40 {
1584                Some(off_in_block + 0x2A)
1585            } else {
1586                None
1587            },
1588            free_blocks_delta,
1589        );
1590        patch_counter_u32(
1591            block,
1592            off_in_block + 0x0E,
1593            if desc_size >= 0x40 {
1594                Some(off_in_block + 0x2C)
1595            } else {
1596                None
1597            },
1598            free_inodes_delta,
1599        );
1600        patch_counter_u32(
1601            block,
1602            off_in_block + 0x10,
1603            if desc_size >= 0x40 {
1604                Some(off_in_block + 0x2E)
1605            } else {
1606                None
1607            },
1608            used_dirs_delta,
1609        );
1610
1611        if self.csum.enabled {
1612            let stored_at = off_in_block + 0x1E;
1613            let end_desc = off_in_block + desc_size as usize;
1614            block[stored_at..stored_at + 2].copy_from_slice(&[0, 0]);
1615            let seed = self.csum.seed;
1616            let mut c = crate::checksum::linux_crc32c(seed, &(gi as u32).to_le_bytes());
1617            c = crate::checksum::linux_crc32c(c, &block[off_in_block..end_desc]);
1618            let new_csum = c as u16;
1619            block[stored_at..stored_at + 2].copy_from_slice(&new_csum.to_le_bytes());
1620        }
1621        Ok(())
1622    }
1623
1624    /// Buffer-side SB counter patch. The SB lives at byte offset 1024
1625    /// inside the device; for 4 KiB blocks that's offset 1024 within fs
1626    /// block 0, for 1 KiB blocks the SB IS fs block 1. We patch the
1627    /// 1024-byte SB region in-place inside the relevant whole block, so
1628    /// the journal can transport it as a normal full-block write.
1629    pub(crate) fn buffer_patch_sb_counters(
1630        &self,
1631        buf: &mut BlockBuffer,
1632        free_blocks_delta: i64,
1633        free_inodes_delta: i32,
1634    ) -> Result<()> {
1635        let bs = self.sb.block_size() as u64;
1636        let sb_offset = crate::superblock::SUPERBLOCK_OFFSET; // 1024
1637        let sb_block = sb_offset / bs;
1638        let off_in_block = (sb_offset % bs) as usize;
1639
1640        let block = buf.get_mut(self, sb_block)?;
1641        let sb = &mut block[off_in_block..off_in_block + 1024];
1642
1643        // s_free_inodes_count at 0x10..0x14 (u32 le)
1644        let fi = u32::from_le_bytes(sb[0x10..0x14].try_into().unwrap()) as i64;
1645        let fi_new = (fi + free_inodes_delta as i64).max(0) as u32;
1646        sb[0x10..0x14].copy_from_slice(&fi_new.to_le_bytes());
1647
1648        // s_free_blocks_count split lo (0x0C..0x10, u32) + hi (0x158..0x15C, u32)
1649        let lo = u32::from_le_bytes(sb[0x0C..0x10].try_into().unwrap()) as u64;
1650        let hi = u32::from_le_bytes(sb[0x158..0x15C].try_into().unwrap()) as u64;
1651        let cur = ((hi << 32) | lo) as i64;
1652        let new = (cur + free_blocks_delta).max(0) as u64;
1653        sb[0x0C..0x10].copy_from_slice(&(new as u32).to_le_bytes());
1654        sb[0x158..0x15C].copy_from_slice(&((new >> 32) as u32).to_le_bytes());
1655
1656        if self.csum.enabled {
1657            let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
1658            sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
1659        }
1660        Ok(())
1661    }
1662
1663    /// Buffer-side patch of the SB's `s_last_orphan` field at byte
1664    /// 0xE8. Used by orphan recovery (Phase 6.2) to clear / advance the
1665    /// chain head atomically with the inode/block frees.
1666    pub(crate) fn buffer_patch_sb_last_orphan(
1667        &self,
1668        buf: &mut BlockBuffer,
1669        value: u32,
1670    ) -> Result<()> {
1671        let bs = self.sb.block_size() as u64;
1672        let sb_offset = crate::superblock::SUPERBLOCK_OFFSET;
1673        let sb_block = sb_offset / bs;
1674        let off_in_block = (sb_offset % bs) as usize;
1675        let block = buf.get_mut(self, sb_block)?;
1676        let sb = &mut block[off_in_block..off_in_block + 1024];
1677        sb[0xE8..0xEC].copy_from_slice(&value.to_le_bytes());
1678        if self.csum.enabled {
1679            let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
1680            sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
1681        }
1682        Ok(())
1683    }
1684
1685    /// Buffer-side equivalent of `remove_dir_entry`: scans `parent`'s
1686    /// dir blocks, removes the named entry, recomputes the tail csum,
1687    /// stages the modified block in `buf`. Returns `Error::NotFound`
1688    /// when the name isn't present.
1689    pub(crate) fn buffer_remove_dir_entry(
1690        &self,
1691        buf: &mut BlockBuffer,
1692        parent_ino: u32,
1693        parent_inode: &Inode,
1694        name: &[u8],
1695    ) -> Result<()> {
1696        let bs = self.sb.block_size();
1697        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
1698        let n_blocks = parent_inode.size.div_ceil(bs as u64);
1699        for logical in 0..n_blocks {
1700            let Some(phys) = self.map_inode_logical(parent_inode, logical)? else {
1701                continue;
1702            };
1703            let block = buf.get_mut(self, phys)?;
1704            let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
1705                12
1706            } else {
1707                0
1708            };
1709            if crate::dir::remove_entry_from_block(block, name, has_ft, reserved_tail)? {
1710                if self.csum.enabled && reserved_tail == 12 {
1711                    self.csum
1712                        .patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
1713                }
1714                return Ok(());
1715            }
1716        }
1717        Err(Error::NotFound)
1718    }
1719
1720    /// Buffer-side equivalent of `update_dotdot`: rewrites the `..`
1721    /// entry in `dir_inode`'s first data block (in-buffer) to point at
1722    /// `new_parent_ino`, recomputes the tail csum.
1723    pub(crate) fn buffer_update_dotdot(
1724        &self,
1725        buf: &mut BlockBuffer,
1726        dir_ino: u32,
1727        dir_inode: &Inode,
1728        new_parent_ino: u32,
1729    ) -> Result<()> {
1730        let phys = self
1731            .map_inode_logical(dir_inode, 0)?
1732            .ok_or(Error::Corrupt("buffer_update_dotdot: dir block 0 missing"))?;
1733        let block = buf.get_mut(self, phys)?;
1734        if block.len() < 24 {
1735            return Err(Error::Corrupt("buffer_update_dotdot: dir block too small"));
1736        }
1737        block[12..16].copy_from_slice(&new_parent_ino.to_le_bytes());
1738        if self.csum.enabled && crate::dir::has_csum_tail(block) {
1739            self.csum
1740                .patch_dir_entry_tail(dir_ino, dir_inode.generation, block);
1741        }
1742        Ok(())
1743    }
1744
1745    /// Buffer-side equivalent of `add_dir_entry` for the IN-PLACE case
1746    /// only (an existing parent block has room for the new entry). The
1747    /// dir block is read into the buffer (or reused if already touched),
1748    /// `add_entry_to_block` rewrites it, csum patched, returns Ok(()).
1749    ///
1750    /// Returns `Error::OutOfBounds` when no existing parent block has
1751    /// room — caller should then fall through to
1752    /// `buffer_extend_dir_and_add_entry` to grow the directory by one
1753    /// block (which has its own scope limits).
1754    pub(crate) fn buffer_add_dir_entry_inplace(
1755        &self,
1756        buf: &mut BlockBuffer,
1757        parent_ino: u32,
1758        parent_inode: &Inode,
1759        name: &[u8],
1760        target_ino: u32,
1761        file_type: crate::dir::DirEntryType,
1762    ) -> Result<()> {
1763        let bs = self.sb.block_size();
1764        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
1765        let n_blocks = parent_inode.size.div_ceil(bs as u64);
1766        for logical in 0..n_blocks {
1767            let Some(phys) = self.map_inode_logical(parent_inode, logical)? else {
1768                continue;
1769            };
1770            let block = buf.get_mut(self, phys)?;
1771            let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
1772                12
1773            } else {
1774                0
1775            };
1776            match crate::dir::add_entry_to_block(
1777                block,
1778                target_ino,
1779                name,
1780                file_type,
1781                has_ft,
1782                reserved_tail,
1783            ) {
1784                Ok(()) => {
1785                    if self.csum.enabled && reserved_tail == 12 {
1786                        self.csum
1787                            .patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
1788                    }
1789                    return Ok(());
1790                }
1791                Err(Error::OutOfBounds) => continue,
1792                Err(e) => return Err(e),
1793            }
1794        }
1795        // No existing block has room — caller must extend the directory
1796        // (or fall back to the un-journaled extend path).
1797        Err(Error::OutOfBounds)
1798    }
1799
1800    /// Commit a `BlockBuffer` atomically. Routes through the journal
1801    /// writer when one is available (crash-safe four-fence protocol);
1802    /// falls back to direct device writes + flush otherwise.
1803    ///
1804    /// In journaled mode, writes go to the **journal log** on disk —
1805    /// the *data area* on disk doesn't see them until journal replay
1806    /// (checkpointing). To make those bytes visible to subsequent reads
1807    /// **before** checkpoint (the read-after-write coherence Linux's
1808    /// buffer cache guarantees), every committed block is `populate`'d
1809    /// into the device-layer cache after the journal commit succeeds.
1810    /// Without this hook, allocators (inode/block bitmap) would re-read
1811    /// pre-commit on-disk bytes and produce duplicate allocations.
1812    pub(crate) fn commit_block_buffer(&self, buf: BlockBuffer) -> Result<()> {
1813        if buf.dirty.is_empty() {
1814            return Ok(());
1815        }
1816        let cleared = buf.uninit_cleared.clone();
1817        let publish = |fs: &Self| {
1818            let mut map = fs.uninit_cleared.lock().unwrap();
1819            for (gi, flags) in cleared {
1820                map.entry(gi).and_modify(|f| *f &= flags).or_insert(flags);
1821            }
1822        };
1823        if let Some(jw_mu) = &self.journal {
1824            let mut jw = jw_mu.lock().map_err(|_| {
1825                Error::Corrupt("journal writer mutex poisoned (prior write panicked)")
1826            })?;
1827            let mut tx = jw.begin();
1828            for (block, bytes) in &buf.dirty {
1829                tx.add_write(*block, bytes.clone())?;
1830            }
1831            jw.commit(self.dev.as_ref(), &tx)?;
1832            // Populate the buffer cache with the post-commit bytes so
1833            // any read (this thread or another) sees them before the
1834            // journal is checkpointed back to the data area.
1835            for (block, bytes) in buf.dirty {
1836                self.dev.populate_cache(block, bytes);
1837            }
1838            publish(self);
1839            Ok(())
1840        } else {
1841            let bs = self.sb.block_size() as u64;
1842            for (block, bytes) in buf.dirty {
1843                self.dev.write_at(block * bs, &bytes)?;
1844            }
1845            self.dev.flush()?;
1846            publish(self);
1847            Ok(())
1848        }
1849    }
1850
1851    /// Change the owner of `path` to (`uid`, `gid`). Both values are full
1852    /// 32-bit — the inode stores them as hi+lo u16 halves at different
1853    /// offsets per the ext4 on-disk format. Passing `u32::MAX` for either
1854    /// field leaves that value untouched (Linux lchown(2) convention).
1855    ///
1856    /// Updates `i_ctime = now` and recomputes the inode checksum on
1857    /// csum-enabled mounts.
1858    pub fn apply_chown(&self, path: &str, uid: u32, gid: u32) -> Result<()> {
1859        if !self.dev.is_writable() {
1860            return Err(Error::ReadOnly);
1861        }
1862        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
1863        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
1864        let (inode, mut raw) = self.read_inode_verified(ino)?;
1865
1866        if uid != u32::MAX {
1867            let lo = (uid & 0xFFFF) as u16;
1868            let hi = ((uid >> 16) & 0xFFFF) as u16;
1869            raw[0x02..0x04].copy_from_slice(&lo.to_le_bytes());
1870            raw[0x78..0x7A].copy_from_slice(&hi.to_le_bytes());
1871        }
1872        if gid != u32::MAX {
1873            let lo = (gid & 0xFFFF) as u16;
1874            let hi = ((gid >> 16) & 0xFFFF) as u16;
1875            raw[0x18..0x1A].copy_from_slice(&lo.to_le_bytes());
1876            raw[0x7A..0x7C].copy_from_slice(&hi.to_le_bytes());
1877        }
1878
1879        let now = now_unix_seconds();
1880        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
1881
1882        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
1883        self.commit_inode_write(ino, &raw)
1884    }
1885
1886    /// Set the `i_flags` field (FS_IOC_SETFLAGS) for the inode at `path`.
1887    ///
1888    /// Bumps ctime. Fails with `Error::ReadOnly` on read-only mounts, or
1889    /// `Error::InvalidArgument` if the caller attempts to flip any of the
1890    /// layout-critical flags managed internally (EXTENTS_FL, INLINE_DATA_FL,
1891    /// EA_INODE_FL) — changing those without rewriting the inode payload would
1892    /// corrupt the filesystem.
1893    pub fn apply_set_flags(&self, path: &str, flags: u32) -> Result<()> {
1894        use crate::inode::{InodeFlags, OFF_CTIME, OFF_FLAGS};
1895        if !self.dev.is_writable() {
1896            return Err(Error::ReadOnly);
1897        }
1898        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
1899        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
1900        let (inode, mut raw) = self.read_inode_verified(ino)?;
1901
1902        let managed = InodeFlags::EXTENTS.bits()
1903            | InodeFlags::INLINE_DATA.bits()
1904            | InodeFlags::EA_INODE.bits();
1905        if (flags ^ inode.flags) & managed != 0 {
1906            return Err(Error::InvalidArgument(
1907                "set_flags: cannot modify internally-managed inode flags (EXTENTS, INLINE_DATA, EA_INODE)",
1908            ));
1909        }
1910
1911        raw[OFF_FLAGS..OFF_FLAGS + 4].copy_from_slice(&flags.to_le_bytes());
1912
1913        let now = now_unix_seconds();
1914        raw[OFF_CTIME..OFF_CTIME + 4].copy_from_slice(&now.to_le_bytes());
1915
1916        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
1917        self.commit_inode_write(ino, &raw)
1918    }
1919
1920    /// Remove the extended attribute named `name` from the inode at `path`.
1921    /// `name` must carry a known namespace prefix (e.g. `"user.color"`).
1922    ///
1923    /// v1 scope: **in-inode xattrs only.** The in-inode region (bytes
1924    /// between `128 + i_extra_isize` and the end of the on-disk inode)
1925    /// is decoded, the matching entry is dropped, and the region is
1926    /// re-encoded in place. External xattr blocks (pointed at by
1927    /// Search the in-inode region first, then the external xattr block. If
1928    /// the external block becomes empty after removal, free it and zero
1929    /// `i_file_acl` (matches kernel behavior — empty xattr blocks are
1930    /// reaped on the spot rather than left dangling).
1931    ///
1932    /// Returns:
1933    /// - `Ok(())` on success.
1934    /// - `Error::NotFound` if the entry isn't present in either region.
1935    /// - `Error::InvalidArgument` on namespace-prefix issues.
1936    pub fn apply_removexattr(&self, path: &str, name: &str) -> Result<()> {
1937        if !self.dev.is_writable() {
1938            return Err(Error::ReadOnly);
1939        }
1940        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
1941        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
1942        let (inode, mut raw) = self.read_inode_verified(ino)?;
1943
1944        // Locate the in-inode xattr region (starts at 128 + i_extra_isize).
1945        let inode_size = self.sb.inode_size as usize;
1946        let i_extra_isize = if raw.len() >= 0x82 {
1947            u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap()) as usize
1948        } else {
1949            0
1950        };
1951        let region_start = 128 + i_extra_isize;
1952        let region_end = inode_size.min(raw.len());
1953        if region_start + 4 <= region_end {
1954            let region = &mut raw[region_start..region_end];
1955            match crate::xattr::plan_remove_in_inode_region(region, name)? {
1956                crate::xattr::RemoveOutcome::Removed => {
1957                    self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
1958                    return self.commit_inode_write(ino, &raw);
1959                }
1960                crate::xattr::RemoveOutcome::NotFound => { /* check external */ }
1961            }
1962        }
1963
1964        // External block path: read, plan-remove, write back (or free it
1965        // when it becomes empty).
1966        if inode.file_acl != 0 {
1967            let bs = self.sb.block_size();
1968            let bs_u64 = bs as u64;
1969            let block_nr = inode.file_acl;
1970            let mut block = vec![0u8; bs as usize];
1971            self.dev.read_at(block_nr * bs_u64, &mut block)?;
1972            match crate::xattr::plan_remove_from_external_block(&mut block, name, 1)? {
1973                crate::xattr::BlockRemoveOutcome::Removed => {
1974                    if self.csum.enabled {
1975                        self.csum.patch_xattr_block(block_nr, &mut block);
1976                    }
1977                    self.dev.write_at(block_nr * bs_u64, &block)?;
1978                    self.bump_inode_ctime(ino, inode.generation, &mut raw)?;
1979                    self.dev.flush()?;
1980                    return Ok(());
1981                }
1982                crate::xattr::BlockRemoveOutcome::RemovedNowEmpty => {
1983                    // Free the now-empty external block + clear i_file_acl + drop
1984                    // i_blocks, all in one journaled transaction. The previous
1985                    // direct path used free_block_run_and_bgd, which skipped the
1986                    // block-bitmap checksum recompute and wrote a stale BGD —
1987                    // corrupting the bitmap csum and the free counters. The
1988                    // buffer helpers do it correctly and atomically.
1989                    let mut buf = BlockBuffer::new(bs);
1990                    self.buffer_free_block_run_and_bgd(&mut buf, block_nr, 1)?;
1991                    self.buffer_patch_sb_counters(&mut buf, 1, 0)?;
1992                    raw[0x68..0x6C].copy_from_slice(&0u32.to_le_bytes());
1993                    if raw.len() >= 0x76 {
1994                        raw[0x74..0x76].copy_from_slice(&0u16.to_le_bytes());
1995                    }
1996                    let sectors_per_block = bs_u64 / 512;
1997                    let new_blocks = inode.blocks.saturating_sub(sectors_per_block);
1998                    Self::patch_inode_size_and_blocks(&mut raw, inode.size, new_blocks)?;
1999                    raw[0x0C..0x10].copy_from_slice(&now_unix_seconds().to_le_bytes());
2000                    self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2001                    self.buffer_write_inode(&mut buf, ino, &raw)?;
2002                    return self.commit_block_buffer(buf);
2003                }
2004                crate::xattr::BlockRemoveOutcome::NotFound => { /* fall through */ }
2005            }
2006        }
2007        Err(Error::NotFound)
2008    }
2009
2010    /// Set (create or replace) the extended attribute `name` with `value`
2011    /// on the inode at `path`. `name` must carry a known namespace prefix
2012    /// (e.g. `"user.com.apple.FinderInfo"`).
2013    ///
2014    /// Try-order, matching the kernel:
2015    /// 1. **In-inode region** — between `128 + i_extra_isize` and the end
2016    ///    of the on-disk inode. Cheapest; no extra block.
2017    /// 2. **External xattr block** — when in-inode is full, fall back to a
2018    ///    dedicated block referenced by `i_file_acl`. Allocates a fresh
2019    ///    block when none exists, otherwise rewrites the existing one.
2020    ///    Returns `Error::NoSpaceLeftOnDevice` if even a full block can't
2021    ///    hold the new layout.
2022    pub fn apply_setxattr(&self, path: &str, name: &str, value: &[u8]) -> Result<()> {
2023        if !self.dev.is_writable() {
2024            return Err(Error::ReadOnly);
2025        }
2026        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
2027        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
2028        let (inode, mut raw) = self.read_inode_verified(ino)?;
2029
2030        let inode_size = self.sb.inode_size as usize;
2031        let i_extra_isize = if raw.len() >= 0x82 {
2032            u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap()) as usize
2033        } else {
2034            0
2035        };
2036        let region_start = 128 + i_extra_isize;
2037        let region_end = inode_size.min(raw.len());
2038        let inline_capable = region_start + 8 <= region_end;
2039
2040        // Try in-inode first; on overflow fall through to the external block.
2041        let inline_result = if inline_capable {
2042            let region = &mut raw[region_start..region_end];
2043            crate::xattr::plan_set_in_inode_region(region, name, value)
2044        } else {
2045            Err(Error::NoSpaceLeftOnDevice)
2046        };
2047
2048        match inline_result {
2049            Ok(_) => {
2050                // In-inode rewrite already in `raw`. Refresh inode csum + commit.
2051                self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2052                self.commit_inode_write(ino, &raw)
2053            }
2054            Err(Error::NoSpaceLeftOnDevice) => {
2055                self.apply_setxattr_external_block(ino, &inode, &mut raw, name, value)
2056            }
2057            Err(e) => Err(e),
2058        }
2059    }
2060
2061    /// Recompute the inode checksum (when enabled) and splice both halves
2062    /// back into the inode image. No-op when csum disabled.
2063    fn finalize_inode_raw(&self, ino: u32, generation: u32, raw: &mut [u8]) -> Result<()> {
2064        if self.csum.enabled {
2065            if let Some((lo, hi)) = self.csum.compute_inode_checksum(ino, generation, raw) {
2066                raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
2067                if raw.len() >= 0x84 {
2068                    raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
2069                }
2070            }
2071        }
2072        Ok(())
2073    }
2074
2075    /// Helper: route a setxattr that overflowed the in-inode region to the
2076    /// external xattr block. Either rewrites the existing block (when
2077    /// `i_file_acl != 0`) or allocates a fresh one.
2078    fn apply_setxattr_external_block(
2079        &self,
2080        ino: u32,
2081        inode: &crate::inode::Inode,
2082        raw: &mut [u8],
2083        name: &str,
2084        value: &[u8],
2085    ) -> Result<()> {
2086        let bs = self.sb.block_size();
2087        let bs_u64 = bs as u64;
2088
2089        // Multi-block transaction: xattr block bytes + (alloc-side bitmap +
2090        // BGD + SB when fresh-block) + inode body. Atomic across the op.
2091        let mut buf = BlockBuffer::new(bs);
2092
2093        // Path A: existing external block — rewrite in-buffer, re-checksum.
2094        if inode.file_acl != 0 {
2095            let block_nr = inode.file_acl;
2096            let mut block = vec![0u8; bs as usize];
2097            self.dev.read_at(block_nr * bs_u64, &mut block)?;
2098            crate::xattr::plan_set_in_external_block(&mut block, name, value, 1)?;
2099            if self.csum.enabled {
2100                self.csum.patch_xattr_block(block_nr, &mut block);
2101            }
2102            buf.put(block_nr, block);
2103            // i_file_acl unchanged — only need to bump ctime.
2104            let now = now_unix_seconds();
2105            raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
2106            self.finalize_inode_raw(ino, inode.generation, raw)?;
2107            self.buffer_write_inode(&mut buf, ino, raw)?;
2108            return self.commit_block_buffer(buf);
2109        }
2110
2111        // Path B: no external block yet — allocate, build, stage, then
2112        // point i_file_acl + i_blocks at it.
2113        let mut bitmap_reader = |block: u64| self.read_block(block);
2114        let inode_group = (ino - 1) / self.sb.inodes_per_group;
2115        let plan = crate::alloc::plan_block_allocation(
2116            &self.sb,
2117            &self.allocation_groups(),
2118            1,
2119            inode_group,
2120            &mut bitmap_reader,
2121        )?;
2122        let block_nr = plan.first_block;
2123
2124        let mut block = vec![0u8; bs as usize];
2125        crate::xattr::plan_set_in_external_block(&mut block, name, value, 1)?;
2126        if self.csum.enabled {
2127            self.csum.patch_xattr_block(block_nr, &mut block);
2128        }
2129        buf.put(block_nr, block);
2130
2131        // Stage allocator side-effects in the buffer.
2132        self.buffer_mark_block_run_used(&mut buf, block_nr, 1)?;
2133        self.buffer_patch_bgd_counters(
2134            &mut buf,
2135            plan.bgd.group_idx as usize,
2136            plan.bgd.free_blocks_delta,
2137            plan.bgd.free_inodes_delta,
2138            plan.bgd.used_dirs_delta,
2139        )?;
2140        self.buffer_patch_sb_counters(
2141            &mut buf,
2142            plan.sb.free_blocks_delta,
2143            plan.sb.free_inodes_delta,
2144        )?;
2145
2146        // Splice block_nr into the inode: i_file_acl_lo at 0x68..0x6C, hi
2147        // at 0x74..0x76.
2148        let (acl_hi, acl_lo) = crate::extent_mut::split_phys_block(block_nr);
2149        raw[0x68..0x6C].copy_from_slice(&acl_lo.to_le_bytes());
2150        if raw.len() >= 0x76 {
2151            raw[0x74..0x76].copy_from_slice(&acl_hi.to_le_bytes());
2152        }
2153        // Bump i_blocks by sectors_per_block (the xattr block now belongs
2154        // to this inode for du purposes).
2155        let sectors_per_block = bs_u64 / 512;
2156        let new_blocks = inode.blocks.saturating_add(sectors_per_block);
2157        Self::patch_inode_size_and_blocks(raw, inode.size, new_blocks)?;
2158        let now = now_unix_seconds();
2159        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
2160        self.finalize_inode_raw(ino, inode.generation, raw)?;
2161        self.buffer_write_inode(&mut buf, ino, raw)?;
2162
2163        self.commit_block_buffer(buf)
2164    }
2165
2166    /// Bump `i_ctime` to now and re-checksum + write the inode. Used on
2167    /// attribute writes that touch external storage but don't otherwise
2168    /// modify the inode body.
2169    fn bump_inode_ctime(&self, ino: u32, generation: u32, raw: &mut [u8]) -> Result<()> {
2170        let now = now_unix_seconds();
2171        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
2172        self.finalize_inode_raw(ino, generation, raw)?;
2173        self.commit_inode_write(ino, raw)
2174    }
2175
2176    /// Set the access + modification times on `path`. Mirrors POSIX
2177    /// `utimensat(2)`: `atime_sec/nsec` and `mtime_sec/nsec` each replace
2178    /// the inode's atime/mtime. `ctime` is bumped to now (POSIX requires
2179    /// the change-time stamp on any attribute write). The [`TIME_OMIT`]
2180    /// sentinel on either `_sec` leaves that pair unchanged (lets callers
2181    /// touch just atime or just mtime).
2182    ///
2183    /// Seconds are signed and 64-bit because that is what the format
2184    /// means: the on-disk base is a signed 32-bit count, extended by the
2185    /// low two bits of the matching `*_extra` field. A `u32` here could
2186    /// not express a pre-1970 date at all, and stored every date past
2187    /// 2038 as one in the 1900s — the base was written and the epoch
2188    /// bits left zero, so the value read back 136 years early.
2189    ///
2190    /// `nsec` values are the sub-second timestamp in nanoseconds and are
2191    /// only written when the inode's `i_extra_isize` region is large
2192    /// enough to hold them (requires ≥ 160-byte inodes — the ext4 tooling
2193    /// default). That same region holds the epoch bits, so on an inode
2194    /// too small to carry it, a time needing them is refused rather than
2195    /// silently stored as the wrong century.
2196    pub fn apply_utimens(
2197        &self,
2198        path: &str,
2199        atime_sec: i64,
2200        atime_nsec: u32,
2201        mtime_sec: i64,
2202        mtime_nsec: u32,
2203    ) -> Result<()> {
2204        if !self.dev.is_writable() {
2205            return Err(Error::ReadOnly);
2206        }
2207        for secs in [atime_sec, mtime_sec] {
2208            if secs != TIME_OMIT
2209                && !(crate::inode::MIN_ENCODABLE_TIME..=crate::inode::MAX_ENCODABLE_TIME)
2210                    .contains(&secs)
2211            {
2212                return Err(Error::InvalidArgument(
2213                    "timestamp outside the range ext4 can store (1901..2446)",
2214                ));
2215            }
2216        }
2217        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
2218        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
2219        let (inode, mut raw) = self.read_inode_verified(ino)?;
2220
2221        let (atime_base, atime_epoch) = crate::inode::encode_extra_time(atime_sec);
2222        let (mtime_base, mtime_epoch) = crate::inode::encode_extra_time(mtime_sec);
2223
2224        // Extra-isize region carries the nsec fields AND the epoch bits.
2225        // Offsets (relative to inode start):
2226        //   0x84 i_ctime_extra  (needs i_extra_isize ≥  8)
2227        //   0x88 i_mtime_extra  (needs i_extra_isize ≥ 12)
2228        //   0x8C i_atime_extra  (needs i_extra_isize ≥ 16)
2229        // Linux packs each as `(nsec << 2) | epoch_bits`.
2230        let i_extra_isize = if raw.len() >= 0x82 {
2231            u16::from_le_bytes(raw[0x80..0x82].try_into().unwrap())
2232        } else {
2233            0
2234        };
2235        let has_mtime_extra = i_extra_isize >= 12 && raw.len() >= 0x8C;
2236        let has_atime_extra = i_extra_isize >= 16 && raw.len() >= 0x90;
2237
2238        // Refuse before writing anything, so a rejected call leaves the
2239        // inode exactly as it was rather than half-updated.
2240        if (mtime_sec != TIME_OMIT && mtime_epoch != 0 && !has_mtime_extra)
2241            || (atime_sec != TIME_OMIT && atime_epoch != 0 && !has_atime_extra)
2242        {
2243            return Err(Error::InvalidArgument(
2244                "timestamp past 2038 needs an *_extra field this inode is too small to hold",
2245            ));
2246        }
2247
2248        if atime_sec != TIME_OMIT {
2249            raw[0x08..0x0C].copy_from_slice(&atime_base.to_le_bytes());
2250        }
2251        if mtime_sec != TIME_OMIT {
2252            raw[0x10..0x14].copy_from_slice(&mtime_base.to_le_bytes());
2253        }
2254        // POSIX: any attribute write bumps ctime.
2255        let now = now_unix_seconds();
2256        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes());
2257
2258        if i_extra_isize >= 8 && raw.len() >= 0x88 {
2259            // Bump ctime_nsec to 0 alongside the ctime bump above. `now`
2260            // is a u32 second count, so its epoch bits are zero until
2261            // 2038 — see G6 in docs/format-conformance-gaps.md.
2262            raw[0x84..0x88].copy_from_slice(&0u32.to_le_bytes());
2263        }
2264        if mtime_sec != TIME_OMIT && has_mtime_extra {
2265            let packed = pack_nsec_lo(mtime_nsec) | mtime_epoch;
2266            raw[0x88..0x8C].copy_from_slice(&packed.to_le_bytes());
2267        }
2268        if atime_sec != TIME_OMIT && has_atime_extra {
2269            let packed = pack_nsec_lo(atime_nsec) | atime_epoch;
2270            raw[0x8C..0x90].copy_from_slice(&packed.to_le_bytes());
2271        }
2272
2273        self.finalize_inode_raw(ino, inode.generation, &mut raw)?;
2274        self.commit_inode_write(ino, &raw)
2275    }
2276
2277    /// Unlink a regular file / symlink / special file at `path`.
2278    ///
2279    /// Semantics:
2280    /// - Refuses to unlink a directory (use a future `apply_rmdir`).
2281    /// - Decrements the target inode's `i_links_count`. When that reaches
2282    ///   zero, frees every data block via `plan_truncate_shrink(size → 0)`,
2283    ///   clears the inode bitmap bit, zeroes the inode body, and sets
2284    ///   `i_dtime = now`. When `links_count > 1` we only drop the dir entry
2285    ///   and decrement — matches POSIX unlink semantics for hard-linked files.
2286    /// - Mutates: parent-dir block (entry removal), target inode, block +
2287    ///   inode bitmaps, BGD counters, SB counters. No journaling yet —
2288    ///   safe only on scratch images (same caveat as `apply_truncate_shrink`).
2289    ///
2290    /// Returns `Error::NotFound` if the path doesn't exist,
2291    /// `Error::NotADirectory` if the parent isn't a directory, and
2292    /// `Error::IsADirectory` (POSIX EISDIR) if the target is a directory.
2293    pub fn apply_unlink(&self, path: &str) -> Result<()> {
2294        if !self.dev.is_writable() {
2295            return Err(Error::ReadOnly);
2296        }
2297        // POSIX: a trailing slash asserts the path refers to a directory,
2298        // which is incompatible with `unlink(2)` no matter what kind of file
2299        // the path resolves to. `split_parent_and_base` swallows the slash,
2300        // so snapshot the flag first and fail-fast on non-dirs below.
2301        let trailing_slash = path.len() > 1 && path.ends_with('/');
2302        let (parent_ino, base_name) = split_parent_and_base(path)?;
2303
2304        // Resolve parent + target inodes.
2305        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
2306        let parent_ino_num =
2307            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &parent_ino)?;
2308        let (parent_inode, _parent_raw) = self.read_inode_verified(parent_ino_num)?;
2309        if !parent_inode.is_dir() {
2310            return Err(Error::NotADirectory);
2311        }
2312
2313        let target_ino = self.find_entry_in_dir(&parent_inode, base_name.as_bytes())?;
2314        let (target_inode, mut target_raw) = self.read_inode_verified(target_ino)?;
2315        if target_inode.is_dir() {
2316            // POSIX: unlink(2) on a directory must fail with EISDIR; the
2317            // caller should use rmdir(2) instead.
2318            return Err(Error::IsADirectory);
2319        }
2320        if trailing_slash {
2321            // `unlink("/foo/")` where /foo is a regular file → ENOTDIR per
2322            // POSIX: the trailing slash tells us the caller expected a dir.
2323            return Err(Error::NotADirectory);
2324        }
2325
2326        // All mutations land in this buffer and commit as one transaction.
2327        let mut buf = BlockBuffer::new(self.sb.block_size());
2328
2329        // Remove the dir entry from the parent. Scans each block until
2330        // `remove_entry_from_block` reports success.
2331        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
2332        let bs = self.sb.block_size();
2333        let parent_blocks = parent_inode.size.div_ceil(bs as u64);
2334        let mut removed = false;
2335        for logical in 0..parent_blocks {
2336            let Some(phys) = self.map_inode_logical(&parent_inode, logical)? else {
2337                continue;
2338            };
2339            let block = buf.get_mut(self, phys)?;
2340            // `dir_entry_tail` occupies the last 12 bytes when metadata_csum
2341            // is on; don't scribble over it.
2342            let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
2343                12
2344            } else {
2345                0
2346            };
2347            if crate::dir::remove_entry_from_block(
2348                block,
2349                base_name.as_bytes(),
2350                has_ft,
2351                reserved_tail,
2352            )? {
2353                // Recompute the tail csum if present — entry-list shape changed.
2354                if self.csum.enabled && reserved_tail == 12 {
2355                    self.csum
2356                        .patch_dir_entry_tail(parent_ino_num, parent_inode.generation, block);
2357                }
2358                removed = true;
2359                break;
2360            }
2361        }
2362        if !removed {
2363            return Err(Error::NotFound);
2364        }
2365
2366        // Decrement link count. Non-zero after → just persist the new count.
2367        let new_links = target_inode.links_count.saturating_sub(1);
2368        target_raw[0x1A..0x1C].copy_from_slice(&new_links.to_le_bytes());
2369
2370        if new_links > 0 {
2371            self.finalize_inode_raw(target_ino, target_inode.generation, &mut target_raw)?;
2372            self.buffer_write_inode(&mut buf, target_ino, &target_raw)?;
2373            return self.commit_block_buffer(buf);
2374        }
2375
2376        // Last link gone — free data blocks + inode slot, all into the same
2377        // transaction so a crash either keeps everything or undoes everything.
2378        let mut freed_sectors: u64 = 0;
2379        let sectors_per_block = bs as u64 / 512;
2380        if target_inode.has_extents() && target_inode.size > 0 {
2381            let (_sc, muts) = crate::file_mut::plan_truncate_shrink(
2382                target_inode.size,
2383                0,
2384                &target_inode.block,
2385                bs,
2386            )?;
2387            for m in &muts {
2388                if let crate::extent_mut::ExtentMutation::FreePhysicalRun { start, len } = m {
2389                    self.buffer_free_block_run_and_bgd(&mut buf, *start, *len as u64)?;
2390                    freed_sectors += *len as u64 * sectors_per_block;
2391                }
2392            }
2393        }
2394
2395        // Inode bitmap + BGD free_inodes_count; SB counter for both
2396        // freed_blocks AND +1 inode goes via one buffer_patch_sb_counters
2397        // call below.
2398        self.buffer_free_inode_slot(&mut buf, target_ino)?;
2399
2400        let freed_blocks = freed_sectors.checked_div(sectors_per_block).unwrap_or(0);
2401        self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 1)?;
2402
2403        // Zero the inode body. Kernel sets dtime = now, mode = 0, and
2404        // leaves the generation intact (helps tooling detect the dead slot).
2405        let inode_size = self.sb.inode_size as usize;
2406        let old_gen = target_inode.generation;
2407        for b in &mut target_raw[..inode_size] {
2408            *b = 0;
2409        }
2410        let dtime = now_unix_seconds();
2411        target_raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes()); // dtime
2412        target_raw[0x64..0x68].copy_from_slice(&old_gen.to_le_bytes()); // generation
2413        self.finalize_inode_raw(target_ino, old_gen, &mut target_raw)?;
2414        self.buffer_write_inode(&mut buf, target_ino, &target_raw)?;
2415
2416        self.commit_block_buffer(buf)
2417    }
2418
2419    /// Common setup for creating a new inode inside a directory: resolves
2420    /// the parent, checks preconditions, allocates an inode, and stages the
2421    /// bitmap + counter updates into a fresh `BlockBuffer`. The caller then
2422    /// builds the inode bytes and adds the dir entry.
2423    fn plan_new_inode_in_dir(&self, path: &str) -> Result<NewInodePlan> {
2424        let (parent_path, base_name) = split_parent_and_base(path)?;
2425        if base_name.len() > 255 {
2426            return Err(Error::NameTooLong);
2427        }
2428
2429        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
2430        let parent_ino =
2431            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &parent_path)?;
2432        let (parent_inode, _) = self.read_inode_verified(parent_ino)?;
2433        if !parent_inode.is_dir() {
2434            return Err(Error::NotADirectory);
2435        }
2436        if self
2437            .find_entry_in_dir(&parent_inode, base_name.as_bytes())
2438            .is_ok()
2439        {
2440            return Err(Error::AlreadyExists);
2441        }
2442
2443        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
2444        let bs = self.sb.block_size();
2445        let mut bitmap_reader = |block: u64| self.read_block(block);
2446        let plan = crate::alloc::plan_inode_allocation(
2447            &self.sb,
2448            &self.allocation_groups(),
2449            false,
2450            parent_group,
2451            &mut bitmap_reader,
2452        )?;
2453        let new_ino = plan.inode;
2454
2455        let mut buf = BlockBuffer::new(bs);
2456        self.buffer_mark_inode_used(&mut buf, new_ino)?;
2457        self.buffer_patch_bgd_counters(
2458            &mut buf,
2459            plan.bgd.group_idx as usize,
2460            plan.bgd.free_blocks_delta,
2461            plan.bgd.free_inodes_delta,
2462            plan.bgd.used_dirs_delta,
2463        )?;
2464        self.buffer_patch_sb_counters(
2465            &mut buf,
2466            plan.sb.free_blocks_delta,
2467            plan.sb.free_inodes_delta,
2468        )?;
2469
2470        Ok(NewInodePlan {
2471            new_ino,
2472            parent_ino,
2473            parent_inode,
2474            buf,
2475            base_name,
2476        })
2477    }
2478
2479    /// Create a new regular file at `path` with permission bits `mode`
2480    /// (e.g. `0o644`). Returns the allocated inode number on success.
2481    ///
2482    /// Semantics:
2483    /// - Parent must exist and be a directory.
2484    /// - Refuses if `path` already exists.
2485    /// - Allocates an inode via `plan_inode_allocation` (hints to the
2486    ///   parent's group), marks the bitmap, bumps BGD + SB counters.
2487    /// - Initialises the inode as a regular file with EXTENTS flag and an
2488    ///   empty extent tree (size=0, blocks=0). Timestamps set to `now`.
2489    /// - Adds the directory entry into the first parent block with room
2490    ///   (linear; htree-extending dirs are a follow-up).
2491    /// - Not journaled — scratch-image safe, same caveat as other Phase-4
2492    ///   applies.
2493    pub fn apply_create(&self, path: &str, mode: u16) -> Result<u32> {
2494        if !self.dev.is_writable() {
2495            return Err(Error::ReadOnly);
2496        }
2497        let NewInodePlan {
2498            new_ino,
2499            parent_ino,
2500            parent_inode,
2501            mut buf,
2502            base_name,
2503        } = self.plan_new_inode_in_dir(path)?;
2504
2505        let raw = self.build_regular_file_inode(new_ino, mode)?;
2506        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
2507
2508        // Multi-block transaction: inode bitmap + BGD + SB + new inode +
2509        // parent dir entry, all atomic. The fall-through to extend-dir
2510        // (when the parent has no room) must commit the buffer first
2511        // and then run extend un-journaled — see end of fn.
2512        match self.buffer_add_dir_entry_inplace(
2513            &mut buf,
2514            parent_ino,
2515            &parent_inode,
2516            base_name.as_bytes(),
2517            new_ino,
2518            crate::dir::DirEntryType::RegFile,
2519        ) {
2520            Ok(()) => {
2521                self.commit_block_buffer(buf)?;
2522                Ok(new_ino)
2523            }
2524            Err(Error::OutOfBounds) => {
2525                // Parent dir is full → commit what we have so the inode
2526                // allocation is durable, then run the un-journaled extend
2527                // path. If the extend crashes mid-way we leak the
2528                // already-allocated inode (orphan candidate); this is a
2529                // documented limitation until extend has a buffer-twin.
2530                self.commit_block_buffer(buf)?;
2531                self.extend_dir_and_add_entry(
2532                    parent_ino,
2533                    base_name.as_bytes(),
2534                    new_ino,
2535                    crate::dir::DirEntryType::RegFile,
2536                )?;
2537                Ok(new_ino)
2538            }
2539            Err(e) => Err(e),
2540        }
2541    }
2542
2543    /// Create a special file (FIFO, socket, char device, block device).
2544    /// `mode` must include the type bits (`S_IFIFO`, `S_IFSOCK`, `S_IFCHR`,
2545    /// or `S_IFBLK`) plus the permission bits. `major` and `minor` are the
2546    /// device numbers (both 0 for FIFOs and sockets). Mirrors POSIX `mknod`.
2547    pub fn apply_mknod(&self, path: &str, mode: u16, major: u32, minor: u32) -> Result<u32> {
2548        if !self.dev.is_writable() {
2549            return Err(Error::ReadOnly);
2550        }
2551        let file_type = mode & crate::inode::S_IFMT;
2552        let dir_entry_type = match file_type {
2553            crate::inode::S_IFCHR => crate::dir::DirEntryType::CharDev,
2554            crate::inode::S_IFBLK => crate::dir::DirEntryType::BlockDev,
2555            crate::inode::S_IFIFO => crate::dir::DirEntryType::Fifo,
2556            crate::inode::S_IFSOCK => crate::dir::DirEntryType::Socket,
2557            _ => {
2558                return Err(Error::InvalidArgument(
2559                    "mknod: unsupported type; use create/mkdir for reg/dir",
2560                ))
2561            }
2562        };
2563        let NewInodePlan {
2564            new_ino,
2565            parent_ino,
2566            parent_inode,
2567            mut buf,
2568            base_name,
2569        } = self.plan_new_inode_in_dir(path)?;
2570
2571        let raw = self.build_special_file_inode(new_ino, mode, major, minor)?;
2572        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
2573
2574        match self.buffer_add_dir_entry_inplace(
2575            &mut buf,
2576            parent_ino,
2577            &parent_inode,
2578            base_name.as_bytes(),
2579            new_ino,
2580            dir_entry_type,
2581        ) {
2582            Ok(()) => {
2583                self.commit_block_buffer(buf)?;
2584                Ok(new_ino)
2585            }
2586            Err(Error::OutOfBounds) => {
2587                self.commit_block_buffer(buf)?;
2588                self.extend_dir_and_add_entry(
2589                    parent_ino,
2590                    base_name.as_bytes(),
2591                    new_ino,
2592                    dir_entry_type,
2593                )?;
2594                Ok(new_ino)
2595            }
2596            Err(e) => Err(e),
2597        }
2598    }
2599
2600    /// Write inode checksum fields (lo at OFF_CHECKSUM_LO, hi at OFF_CHECKSUM_HI)
2601    /// when metadata checksums are enabled for this filesystem.
2602    fn stamp_inode_checksum(&self, raw: &mut [u8], ino: u32, generation: u32) {
2603        use crate::inode::{INODE_SIZE_WITH_EXTRA, OFF_CHECKSUM_HI, OFF_CHECKSUM_LO};
2604        if self.csum.enabled {
2605            if let Some((lo, hi)) = self.csum.compute_inode_checksum(ino, generation, raw) {
2606                raw[OFF_CHECKSUM_LO..OFF_CHECKSUM_LO + 2].copy_from_slice(&lo.to_le_bytes());
2607                if raw.len() >= INODE_SIZE_WITH_EXTRA {
2608                    raw[OFF_CHECKSUM_HI..OFF_CHECKSUM_HI + 2].copy_from_slice(&hi.to_le_bytes());
2609                }
2610            }
2611        }
2612    }
2613
2614    fn build_special_file_inode(
2615        &self,
2616        ino: u32,
2617        mode: u16,
2618        major: u32,
2619        minor: u32,
2620    ) -> Result<Vec<u8>> {
2621        use crate::inode::{OFF_BLOCK, OFF_LINKS_COUNT, OFF_MODE};
2622        let inode_size = self.sb.inode_size as usize;
2623        let mut raw = vec![0u8; inode_size];
2624
2625        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode.to_le_bytes());
2626        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
2627
2628        // Device files: store encoded device number in i_block (no EXTENTS).
2629        // Linux stores old (i_block[0]) and new (i_block[1]) formats.
2630        let file_type = mode & crate::inode::S_IFMT;
2631        if file_type == crate::inode::S_IFBLK || file_type == crate::inode::S_IFCHR {
2632            let old_dev = (major << 8) | (minor & 0xff);
2633            raw[OFF_BLOCK..OFF_BLOCK + 4].copy_from_slice(&old_dev.to_le_bytes());
2634            let new_dev = (minor & 0xff) | (major << 8) | ((minor & !0xff) << 12);
2635            raw[OFF_BLOCK + 4..OFF_BLOCK + 8].copy_from_slice(&new_dev.to_le_bytes());
2636        }
2637
2638        let now = now_unix_seconds();
2639        write_inode_timestamps(&mut raw, now);
2640        let generation = alloc_inode_generation();
2641        write_inode_generation(&mut raw, generation);
2642        write_inode_extra_isize(&mut raw);
2643        self.stamp_inode_checksum(&mut raw, ino, generation);
2644        Ok(raw)
2645    }
2646
2647    /// Create a symbolic link at `linkpath` whose target is `target`.
2648    /// Mirrors POSIX `symlink(target, linkpath)`: allocates a fresh inode
2649    /// with mode S_IFLNK, installs the target bytes, and adds a dir entry
2650    /// at the link path.
2651    ///
2652    /// Two storage paths:
2653    /// - **Fast symlink** (`target.len() <= 60`): target stored inline in
2654    ///   the 60-byte `i_block` area; no data-block allocation.
2655    /// - **Slow symlink** (`61..=255` bytes): one filesystem block is
2656    ///   allocated and the target is written there, with an EXTENTS
2657    ///   i_block pointing at it.
2658    ///
2659    /// POSIX caps symlink targets at SYMLINK_MAX (255 bytes on Linux +
2660    /// macOS). Longer returns `Error::NameTooLong` → ENAMETOOLONG.
2661    pub fn apply_symlink(&self, target: &str, linkpath: &str) -> Result<u32> {
2662        if !self.dev.is_writable() {
2663            return Err(Error::ReadOnly);
2664        }
2665        if target.is_empty() {
2666            return Err(Error::InvalidArgument("symlink target is empty"));
2667        }
2668        // PATH_MAX cap (matches Linux). Slow path allocates exactly one fs
2669        // block, so we additionally require target.len() <= block_size — the
2670        // 4096 ceiling matches the typical ext4 block size and Linux PATH_MAX.
2671        let max_target = 4096usize.min(self.sb.block_size() as usize);
2672        if target.len() > max_target {
2673            return Err(Error::NameTooLong);
2674        }
2675
2676        let NewInodePlan {
2677            new_ino,
2678            parent_ino,
2679            parent_inode,
2680            mut buf,
2681            base_name,
2682        } = self.plan_new_inode_in_dir(linkpath)?;
2683
2684        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
2685        let bs = self.sb.block_size();
2686
2687        // Fast-symlink if target strictly fits inline (i_block is 60 bytes);
2688        // otherwise allocate a block and stage its bytes into the buffer.
2689        // Linux's `ext4_symlink` switches to the slow path when
2690        // `target.len() >= sizeof(i_block)` (i.e. >= 60), and our readlink
2691        // path mirrors that boundary, so we match here.
2692        let raw = if target.len() < 60 {
2693            self.build_fast_symlink_inode(new_ino, target.as_bytes())?
2694        } else {
2695            let mut bitmap_reader = |block: u64| self.read_block(block);
2696            let bplan = crate::alloc::plan_block_allocation(
2697                &self.sb,
2698                &self.allocation_groups(),
2699                1,
2700                parent_group,
2701                &mut bitmap_reader,
2702            )?;
2703            let data_phys = bplan.first_block;
2704
2705            self.buffer_mark_block_run_used(&mut buf, data_phys, 1)?;
2706            self.buffer_patch_bgd_counters(
2707                &mut buf,
2708                bplan.bgd.group_idx as usize,
2709                bplan.bgd.free_blocks_delta,
2710                bplan.bgd.free_inodes_delta,
2711                bplan.bgd.used_dirs_delta,
2712            )?;
2713            self.buffer_patch_sb_counters(
2714                &mut buf,
2715                bplan.sb.free_blocks_delta,
2716                bplan.sb.free_inodes_delta,
2717            )?;
2718
2719            let mut block = vec![0u8; bs as usize];
2720            block[..target.len()].copy_from_slice(target.as_bytes());
2721            buf.put(data_phys, block);
2722
2723            self.build_slow_symlink_inode(new_ino, target.as_bytes(), data_phys)?
2724        };
2725        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
2726
2727        match self.buffer_add_dir_entry_inplace(
2728            &mut buf,
2729            parent_ino,
2730            &parent_inode,
2731            base_name.as_bytes(),
2732            new_ino,
2733            crate::dir::DirEntryType::Symlink,
2734        ) {
2735            Ok(()) => {
2736                self.commit_block_buffer(buf)?;
2737                Ok(new_ino)
2738            }
2739            Err(Error::OutOfBounds) => {
2740                self.commit_block_buffer(buf)?;
2741                self.extend_dir_and_add_entry(
2742                    parent_ino,
2743                    base_name.as_bytes(),
2744                    new_ino,
2745                    crate::dir::DirEntryType::Symlink,
2746                )?;
2747                Ok(new_ino)
2748            }
2749            Err(e) => Err(e),
2750        }
2751    }
2752
2753    /// Compose a fresh fast-symlink inode image: `S_IFLNK | 0o777`, 1 link,
2754    /// `i_size = target.len()`, 0 blocks, NO EXTENTS flag (fast symlinks
2755    /// store their target directly in the 60-byte `i_block` area — no
2756    /// extent tree).
2757    fn build_fast_symlink_inode(&self, ino: u32, target: &[u8]) -> Result<Vec<u8>> {
2758        use crate::inode::{OFF_BLOCK, OFF_FLAGS, OFF_LINKS_COUNT, OFF_MODE, OFF_SIZE_LO};
2759        debug_assert!(target.len() < 60);
2760        let mut raw = vec![0u8; self.sb.inode_size as usize];
2761
2762        // Symlinks are traditionally rwxrwxrwx — the OS enforces access on
2763        // the *target*, not the symlink itself.
2764        let mode_bits = crate::inode::S_IFLNK | 0o0777;
2765        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
2766        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&(target.len() as u32).to_le_bytes());
2767        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
2768        // Fast symlinks store the target inline in the i_block area — no extent tree.
2769        raw[OFF_FLAGS..OFF_FLAGS + 4].copy_from_slice(&0u32.to_le_bytes());
2770        let inline_target_off = OFF_BLOCK;
2771        raw[inline_target_off..inline_target_off + target.len()].copy_from_slice(target);
2772
2773        let now = now_unix_seconds();
2774        write_inode_timestamps(&mut raw, now);
2775        let generation = alloc_inode_generation();
2776        write_inode_generation(&mut raw, generation);
2777        write_inode_extra_isize(&mut raw);
2778        self.stamp_inode_checksum(&mut raw, ino, generation);
2779        Ok(raw)
2780    }
2781
2782    /// Compose a slow-symlink inode image: `S_IFLNK | 0o777`, 1 link,
2783    /// `i_size = target.len()`, EXTENTS flag set with a single-entry leaf
2784    /// root pointing at `data_phys` (logical block 0, length 1). One fs
2785    /// block worth of 512-byte sectors charged to `i_blocks`.
2786    ///
2787    /// Caller must have already written the target bytes (zero-padded) to
2788    /// `data_phys * block_size`.
2789    fn build_slow_symlink_inode(&self, ino: u32, target: &[u8], data_phys: u64) -> Result<Vec<u8>> {
2790        use crate::inode::{
2791            OFF_BLOCK, OFF_BLOCKS_LO, OFF_FLAGS, OFF_LINKS_COUNT, OFF_MODE, OFF_SIZE_LO,
2792        };
2793        debug_assert!(target.len() >= 60 && target.len() <= 4096);
2794        let mut raw = vec![0u8; self.sb.inode_size as usize];
2795
2796        let mode_bits = crate::inode::S_IFLNK | 0o0777;
2797        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
2798        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&(target.len() as u32).to_le_bytes());
2799        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
2800        let bs = self.sb.block_size() as u64;
2801        let sectors = bs / 512;
2802        raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].copy_from_slice(&(sectors as u32).to_le_bytes());
2803        raw[OFF_FLAGS..OFF_FLAGS + 4]
2804            .copy_from_slice(&crate::inode::InodeFlags::EXTENTS.bits().to_le_bytes());
2805
2806        // i_block: extent leaf header + one entry covering the single data block.
2807        let extent_header_off = OFF_BLOCK;
2808        raw[extent_header_off..extent_header_off + 2]
2809            .copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
2810        raw[extent_header_off + 2..extent_header_off + 4].copy_from_slice(&1u16.to_le_bytes());
2811        raw[extent_header_off + 4..extent_header_off + 6].copy_from_slice(&4u16.to_le_bytes());
2812        raw[extent_header_off + 6..extent_header_off + 8].copy_from_slice(&0u16.to_le_bytes());
2813
2814        // Single leaf extent: logical block 0, length 1, physical = data_phys.
2815        let extent_entry_off = extent_header_off + 12;
2816        raw[extent_entry_off..extent_entry_off + 4].copy_from_slice(&0u32.to_le_bytes());
2817        raw[extent_entry_off + 4..extent_entry_off + 6].copy_from_slice(&1u16.to_le_bytes());
2818        let (extent_phys_hi, extent_phys_lo) = crate::extent_mut::split_phys_block(data_phys);
2819        raw[extent_entry_off + 6..extent_entry_off + 8]
2820            .copy_from_slice(&extent_phys_hi.to_le_bytes());
2821        raw[extent_entry_off + 8..extent_entry_off + 12]
2822            .copy_from_slice(&extent_phys_lo.to_le_bytes());
2823
2824        let now = now_unix_seconds();
2825        write_inode_timestamps(&mut raw, now);
2826        let generation = alloc_inode_generation();
2827        write_inode_generation(&mut raw, generation);
2828        write_inode_extra_isize(&mut raw);
2829        self.stamp_inode_checksum(&mut raw, ino, generation);
2830        Ok(raw)
2831    }
2832
2833    /// Compose a fresh regular-file inode image: `S_IFREG | mode`, 1 link,
2834    /// 0 size, 0 blocks, EXTENTS flag set with an empty 4-entry leaf root,
2835    /// timestamps = now, generation = process-id-derived counter, extra_isize
2836    /// = 32 so the inode has room for nsec timestamps + checksum_hi.
2837    fn build_regular_file_inode(&self, ino: u32, mode: u16) -> Result<Vec<u8>> {
2838        use crate::inode::{OFF_BLOCK, OFF_FLAGS, OFF_LINKS_COUNT, OFF_MODE};
2839        let mut raw = vec![0u8; self.sb.inode_size as usize];
2840
2841        let mode_bits = crate::inode::S_IFREG | (mode & 0x0FFF);
2842        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
2843        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&1u16.to_le_bytes());
2844
2845        // i_flags + i_block layout depend on the FS dialect:
2846        // - ext4 (FsFlavor::Ext4): EXTENTS_FL set, i_block holds an empty
2847        //   extent leaf header (magic + entries=0 + max=4 + depth=0).
2848        // - ext2 / ext3: no flag, i_block stays all-zero (no direct or
2849        //   indirect pointers — file is empty so there's nothing to map).
2850        if self.flavor.uses_extents() {
2851            raw[OFF_FLAGS..OFF_FLAGS + 4]
2852                .copy_from_slice(&crate::inode::InodeFlags::EXTENTS.bits().to_le_bytes());
2853
2854            let extent_header_off = OFF_BLOCK;
2855            raw[extent_header_off..extent_header_off + 2]
2856                .copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
2857            raw[extent_header_off + 2..extent_header_off + 4].copy_from_slice(&0u16.to_le_bytes());
2858            raw[extent_header_off + 4..extent_header_off + 6].copy_from_slice(&4u16.to_le_bytes());
2859            raw[extent_header_off + 6..extent_header_off + 8].copy_from_slice(&0u16.to_le_bytes());
2860        }
2861
2862        let now = now_unix_seconds();
2863        write_inode_timestamps(&mut raw, now);
2864        let generation = alloc_inode_generation();
2865        write_inode_generation(&mut raw, generation);
2866        write_inode_extra_isize(&mut raw);
2867        self.stamp_inode_checksum(&mut raw, ino, generation);
2868        Ok(raw)
2869    }
2870
2871    /// Replace the content of `path` with `data`. The file must already
2872    /// exist. Frees every existing extent, allocates a single contiguous run
2873    /// of blocks large enough for `data`, writes the bytes (zero-padding the
2874    /// tail of the last block), then inserts one extent into the inode.
2875    ///
2876    /// This is the "Finder just saved a document" path — complete rewrite of
2877    /// a file. Piecewise writes / appends / sparse writes come later.
2878    ///
2879    /// Journaled, and atomic across the whole replace: freeing the old
2880    /// data, allocating the new run, the bitmap, BGD and superblock
2881    /// updates, the new block contents and the inode all commit as one
2882    /// transaction — as the comment twenty-eight lines into the body
2883    /// already said.
2884    ///
2885    /// Returns the new file size on success.
2886    pub fn apply_replace_file_content(&self, path: &str, data: &[u8]) -> Result<u64> {
2887        if !self.dev.is_writable() {
2888            return Err(Error::ReadOnly);
2889        }
2890        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
2891        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
2892        let (inode, mut raw) = self.read_inode_verified(ino)?;
2893        if !inode.is_file() {
2894            return Err(Error::InvalidArgument(
2895                "write_file target is not a regular file",
2896            ));
2897        }
2898        if !inode.has_extents() {
2899            // ext2 / ext3 (or ext4 inode without EXTENTS_FL): legacy
2900            // direct/indirect block-pointer scheme. Same overall shape as
2901            // the extent path below — free old → allocate → write data →
2902            // patch inode — but the i_block tree comes from `indirect_mut`
2903            // and any indirect-tree blocks are co-allocated with the data
2904            // run (one bitmap call covers both).
2905            return self.apply_replace_file_content_indirect(ino, inode, raw, data);
2906        }
2907
2908        let bs = self.sb.block_size();
2909        let sectors_per_block = bs as u64 / 512;
2910        let group_idx_of_inode = ((ino - 1) / self.sb.inodes_per_group) as usize;
2911
2912        // Multi-block transaction: free existing data + alloc new run +
2913        // bitmap + BGD + SB + new data block contents + inode update.
2914        // Atomic across the whole replace.
2915        let mut buf = BlockBuffer::new(bs);
2916
2917        // Phase 1: free existing data blocks. Each freed run credits its
2918        // own group's BGD via `buffer_free_block_run_and_bgd`.
2919        let mut freed_fs_blocks: u64 = 0;
2920        if inode.size > 0 {
2921            let (_sc, muts) =
2922                crate::file_mut::plan_truncate_shrink(inode.size, 0, &inode.block, bs)?;
2923            for m in &muts {
2924                if let crate::extent_mut::ExtentMutation::FreePhysicalRun { start, len } = m {
2925                    freed_fs_blocks +=
2926                        self.buffer_free_block_run_and_bgd(&mut buf, *start, *len as u64)?;
2927                }
2928            }
2929        }
2930
2931        // Reset the inode's extent root to an empty leaf.
2932        let mut root = vec![0u8; 60];
2933        root[0..2].copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
2934        root[4..6].copy_from_slice(&4u16.to_le_bytes()); // max entries
2935        Self::patch_inode_block_area(&mut raw, &root)?;
2936
2937        // Empty write: BGDs already credited per-run above; only SB needs
2938        // a single update + inode rewrite.
2939        if data.is_empty() {
2940            self.finalize_inode_raw_after_write(ino, &mut raw, &inode, 0, 0)?;
2941            if freed_fs_blocks > 0 {
2942                self.buffer_patch_sb_counters(&mut buf, freed_fs_blocks as i64, 0)?;
2943            }
2944            self.buffer_write_inode(&mut buf, ino, &raw)?;
2945            self.commit_block_buffer(buf)?;
2946            return Ok(0);
2947        }
2948
2949        // Phase 2: allocate one contiguous run for the whole payload.
2950        let needed_blocks: u32 = data.len().div_ceil(bs as usize) as u32;
2951        let mut bitmap_reader = |block: u64| self.read_block(block);
2952        let plan = crate::alloc::plan_block_allocation(
2953            &self.sb,
2954            &self.allocation_groups(),
2955            needed_blocks,
2956            group_idx_of_inode as u32,
2957            &mut bitmap_reader,
2958        )?;
2959
2960        // Phase 3: mark allocated bitmap + patch destination BGD; SB nets
2961        // the alloc delta against the freed total computed above.
2962        self.buffer_mark_block_run_used(&mut buf, plan.first_block, needed_blocks as u64)?;
2963        self.buffer_patch_bgd_counters(
2964            &mut buf,
2965            plan.bgd.group_idx as usize,
2966            plan.bgd.free_blocks_delta,
2967            plan.bgd.free_inodes_delta,
2968            plan.bgd.used_dirs_delta,
2969        )?;
2970        let net_block_delta = freed_fs_blocks as i64 - needed_blocks as i64;
2971        self.buffer_patch_sb_counters(&mut buf, net_block_delta, 0)?;
2972
2973        // Phase 4: stage the payload into the allocated physical run.
2974        for i in 0..needed_blocks as u64 {
2975            let off_in_data = (i as usize) * bs as usize;
2976            let chunk_end = ((i as usize + 1) * bs as usize).min(data.len());
2977            let mut block = vec![0u8; bs as usize];
2978            block[..chunk_end - off_in_data].copy_from_slice(&data[off_in_data..chunk_end]);
2979            buf.put(plan.first_block + i, block);
2980        }
2981
2982        // Phase 5: insert the single extent into the (now-empty) inline
2983        // root and stage the inode.
2984        let new_extent = crate::extent::Extent {
2985            logical_block: 0,
2986            length: needed_blocks as u16,
2987            physical_block: plan.first_block,
2988            uninitialized: false,
2989        };
2990        let muts = crate::extent_mut::plan_insert_extent(&root, new_extent)?;
2991        for m in &muts {
2992            if let crate::extent_mut::ExtentMutation::WriteRoot { bytes } = m {
2993                Self::patch_inode_block_area(&mut raw, bytes)?;
2994            }
2995        }
2996        let new_size = data.len() as u64;
2997        let new_sectors = needed_blocks as u64 * sectors_per_block;
2998        self.finalize_inode_raw_after_write(ino, &mut raw, &inode, new_size, new_sectors)?;
2999        self.buffer_write_inode(&mut buf, ino, &raw)?;
3000
3001        self.commit_block_buffer(buf)?;
3002        Ok(new_size)
3003    }
3004
3005    /// ext2/ext3 sibling of `apply_replace_file_content`'s extent path.
3006    /// Frees the inode's existing direct/indirect tree, allocates one
3007    /// contiguous run sized for both the data payload AND the indirect-tree
3008    /// metadata blocks, builds the new tree via `indirect_mut::plan_contiguous`,
3009    /// then persists everything (data → indirect blocks → inode).
3010    ///
3011    /// No journal interaction: ext2 has no journal at all, and the user's
3012    /// `JournalWriter` returns `None` for those mounts so `self.journal` is
3013    /// already None at this point. ext3 mounts (Phase B) will plumb writes
3014    /// through the journal once the writer can address indirect-block
3015    /// journal inodes.
3016    fn apply_replace_file_content_indirect(
3017        &self,
3018        ino: u32,
3019        inode: Inode,
3020        mut raw: Vec<u8>,
3021        data: &[u8],
3022    ) -> Result<u64> {
3023        let bs = self.sb.block_size();
3024        let sectors_per_block = bs as u64 / 512;
3025        let group_idx_of_inode = ((ino - 1) / self.sb.inodes_per_group) as usize;
3026
3027        // Phase 1: free existing data + indirect-tree blocks. `collect_for_free`
3028        // walks the tree and returns coalesced data runs + individual indirect
3029        // blocks, so cross-group fragmented files are accounted for correctly.
3030        let mut freed_fs_blocks: u64 = 0;
3031        if inode.size > 0 {
3032            let block_count = inode.size.div_ceil(bs as u64) as u32;
3033            let freed = crate::indirect_mut::collect_for_free(
3034                &inode.block,
3035                bs,
3036                block_count,
3037                self.dev.as_ref(),
3038            )?;
3039            for run in &freed.data_runs {
3040                freed_fs_blocks += self.free_block_run_and_bgd(run.start, run.len as u64)?;
3041            }
3042            for &iblk in &freed.indirect_blocks {
3043                freed_fs_blocks += self.free_block_run_and_bgd(iblk, 1)?;
3044            }
3045        }
3046        // Reset i_block to all zeros — no extent magic for legacy inodes.
3047        let zero_iblock = [0u8; 60];
3048        Self::patch_inode_block_area(&mut raw, &zero_iblock)?;
3049
3050        if data.is_empty() {
3051            self.finalize_inode_after_write(ino, &mut raw, &inode, 0, 0)?;
3052            if freed_fs_blocks > 0 {
3053                self.patch_sb_counters(freed_fs_blocks as i64, 0)?;
3054            }
3055            self.dev.flush()?;
3056            return Ok(0);
3057        }
3058
3059        // Phase 2: allocate one contiguous run sized for data + indirect tree.
3060        // Indirect blocks live at the head of the run, data at the tail.
3061        // `count_indirect_blocks` is exactly the number of allocator pulls
3062        // `plan_contiguous` will make, so the budget is tight (verified by
3063        // the `count_indirect_blocks_matches_plan_contiguous` unit test).
3064        let needed_data_blocks: u32 = data.len().div_ceil(bs as usize) as u32;
3065        let n_indirect: u32 = crate::indirect_mut::count_indirect_blocks(needed_data_blocks, bs)
3066            .try_into()
3067            .map_err(|_| Error::Corrupt("indirect_mut: indirect block count overflow"))?;
3068        let total_run = needed_data_blocks
3069            .checked_add(n_indirect)
3070            .ok_or(Error::Corrupt("indirect_mut: total run count overflow"))?;
3071
3072        let mut bitmap_reader = |block: u64| self.read_block(block);
3073        let plan = crate::alloc::plan_block_allocation(
3074            &self.sb,
3075            &self.allocation_groups(),
3076            total_run,
3077            group_idx_of_inode as u32,
3078            &mut bitmap_reader,
3079        )?;
3080        let first_indirect = plan.first_block;
3081        let first_data = plan.first_block + n_indirect as u64;
3082
3083        // Phase 3: build the indirect tree. The closure hands out blocks
3084        // sequentially from `first_indirect` — `plan_contiguous` doesn't
3085        // care about address ordering, so any allocation order is fine.
3086        let mut next_indirect = first_indirect;
3087        let i_plan =
3088            crate::indirect_mut::plan_contiguous(needed_data_blocks, first_data, bs, || {
3089                let v = next_indirect;
3090                next_indirect += 1;
3091                Ok(v)
3092            })?;
3093
3094        // Phase 4: bitmap + BGD + SB counters cover the whole run in one
3095        // mark-used + one BGD-credit + one SB-update.
3096        self.set_block_run_used(plan.first_block, total_run as u64)?;
3097        self.patch_bgd_counters(
3098            plan.bgd.group_idx as usize,
3099            plan.bgd.free_blocks_delta,
3100            plan.bgd.free_inodes_delta,
3101            plan.bgd.used_dirs_delta,
3102        )?;
3103        let net_block_delta = freed_fs_blocks as i64 - total_run as i64;
3104        self.patch_sb_counters(net_block_delta, 0)?;
3105
3106        // Phase 5: write the data payload into the data-portion of the run.
3107        for i in 0..needed_data_blocks as u64 {
3108            let off_in_data = (i as usize) * bs as usize;
3109            let chunk_end = ((i as usize + 1) * bs as usize).min(data.len());
3110            let mut block = vec![0u8; bs as usize];
3111            block[..chunk_end - off_in_data].copy_from_slice(&data[off_in_data..chunk_end]);
3112            self.dev.write_at((first_data + i) * bs as u64, &block)?;
3113        }
3114
3115        // Phase 6: write the indirect-tree blocks.
3116        for (blk, buf) in &i_plan.block_writes {
3117            self.dev.write_at(blk * bs as u64, buf)?;
3118        }
3119
3120        // Phase 7: patch i_block region with the new tree root.
3121        Self::patch_inode_block_area(&mut raw, &i_plan.i_block)?;
3122
3123        // Phase 8: finalize. ext2/3 i_blocks counts BOTH data AND indirect
3124        // blocks (in 512-byte sectors) — extent metadata blocks count the
3125        // same way for ext4 so the rule is consistent across flavors.
3126        let new_size = data.len() as u64;
3127        let new_sectors = (needed_data_blocks as u64 + n_indirect as u64) * sectors_per_block;
3128        self.finalize_inode_after_write(ino, &mut raw, &inode, new_size, new_sectors)?;
3129        self.dev.flush()?;
3130        Ok(new_size)
3131    }
3132
3133    /// Positional write: splice `data` into the file at byte `offset`,
3134    /// allocating new physical blocks for any logical blocks that aren't
3135    /// yet mapped (sparse holes, or blocks past EOF). Existing mapped
3136    /// blocks are read-modify-written for partial overlap; full-block
3137    /// writes go in fresh.
3138    ///
3139    /// This is the primitive needed by streaming write paths
3140    /// (FUSE/WinFsp/FSKit cache-manager dispatches) — `apply_replace_file_content`
3141    /// is "save-as", `apply_pwrite` is `pwrite(2)`.
3142    ///
3143    /// Returns the new file size on success.
3144    ///
3145    /// Allocation behaviour:
3146    /// - Each unmapped logical run is satisfied by one or more physical
3147    ///   runs. If `plan_block_allocation` can't find a single contiguous
3148    ///   group-local run sized for the whole logical run, the request is
3149    ///   halved and retried — each successful sub-run becomes its own
3150    ///   extent. True ENOSPC (single-block allocation also fails)
3151    ///   surfaces as `Error::NoSpaceLeftOnDevice`.
3152    /// - Extent inserts try the inline-root path first; on
3153    ///   `LEAF_FULL_NEEDS_PROMOTION` they fall back to
3154    ///   `plan_insert_extent_deep`, which promotes the tree to depth ≥ 1
3155    ///   and allocates the additional internal/leaf node blocks via the
3156    ///   same buffer-aware allocator. Tail checksums on tree blocks are
3157    ///   patched when `metadata_csum` is on.
3158    ///
3159    /// v1 limitations:
3160    /// - Extent-tree inodes only. Legacy ext2/3 (direct/indirect blocks)
3161    ///   returns `Error::InvalidArgument`. The streaming-copy use case
3162    ///   for this path is on freshly-mkfs'd ext4 volumes that always
3163    ///   have `EXTENTS_FL`.
3164    /// - Pre-existing uninitialised extents (from `fallocate`) in the
3165    ///   write range: not handled — the unmapped-run walk treats them
3166    ///   the same as holes and tries to insert a fresh extent that
3167    ///   would overlap, hitting `CorruptExtentTree("extent overlaps
3168    ///   existing")`. Skipping fallocate-then-write, the streaming
3169    ///   copy path doesn't trigger this.
3170    pub fn apply_pwrite(&self, path: &str, offset: u64, data: &[u8]) -> Result<u64> {
3171        if !self.dev.is_writable() {
3172            return Err(Error::ReadOnly);
3173        }
3174        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
3175        let ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, path)?;
3176        let (inode, mut raw) = self.read_inode_verified(ino)?;
3177        if !inode.is_file() {
3178            return Err(Error::InvalidArgument(
3179                "pwrite target is not a regular file",
3180            ));
3181        }
3182        if !inode.has_extents() {
3183            return Err(Error::InvalidArgument(
3184                "pwrite: legacy (non-extents) inodes not supported in v1",
3185            ));
3186        }
3187
3188        if data.is_empty() {
3189            // No-op (no size change either — a zero-length pwrite at any
3190            // offset is a no-op per POSIX `pwrite(2)`).
3191            return Ok(inode.size);
3192        }
3193
3194        let bs = self.sb.block_size() as u64;
3195        let bs_usize = bs as usize;
3196        let sectors_per_block = bs / 512;
3197        let len = data.len() as u64;
3198        let end = offset
3199            .checked_add(len)
3200            .ok_or(Error::InvalidArgument("pwrite: offset+len overflow"))?;
3201        let first_lb = offset / bs;
3202        let last_lb_excl = end.div_ceil(bs);
3203
3204        // A single pwrite journals all its data blocks plus the inode/bitmap/
3205        // BGD/SB metadata in ONE transaction, whose descriptor block holds only
3206        // ~(block_size - 12)/16 tags. A write spanning more than that overflows
3207        // it ("descriptor block overflow"). Split large writes into block-
3208        // aligned chunks that each fit one transaction; every chunk commits
3209        // atomically (POSIX pwrite is not atomic across a large range anyway).
3210        let tags_per_desc = (bs_usize.saturating_sub(12)) / 16;
3211        // Reserve 8 tag slots for this transaction's own metadata: inode, block
3212        // bitmap, BGD, superblock, plus up to ~4 extent-tree node blocks when a
3213        // chunk's extents grow the tree. A chunk of (tags_per_desc - 8) data
3214        // blocks always lands in a single block group (247 blocks at 4 KiB, well
3215        // inside a 128 MiB group), so the real overhead is <= 4 — 8 is a
3216        // conservative ~2x bound.
3217        let max_data_blocks = tags_per_desc.saturating_sub(8).max(1) as u64;
3218        let max_chunk = max_data_blocks * bs;
3219        if len > max_chunk {
3220            let mut chunk_off = 0u64;
3221            while chunk_off < len {
3222                let take = max_chunk.min(len - chunk_off);
3223                let s = chunk_off as usize;
3224                let e = (chunk_off + take) as usize;
3225                self.apply_pwrite(path, offset + chunk_off, &data[s..e])?;
3226                chunk_off += take;
3227            }
3228            let (after, _) = self.read_inode_verified(ino)?;
3229            return Ok(after.size);
3230        }
3231
3232        // Working copy of the 60-byte inline extent root. Updated in place
3233        // as we insert extents for each unmapped run; patched into `raw`
3234        // once at the end.
3235        let mut root_bytes: Vec<u8> = inode.block.to_vec();
3236
3237        let mut buf = BlockBuffer::new(self.sb.block_size());
3238        let group_idx_of_inode = ((ino - 1) / self.sb.inodes_per_group) as u32;
3239
3240        // Track which logical blocks were freshly allocated by this call.
3241        // Phase-2 writes for these MUST NOT read from disk (the prior
3242        // contents of those physical blocks are stale junk from whoever
3243        // freed them last); they get a zero-init buffer instead.
3244        let mut newly_alloc: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
3245        let mut alloc_total_blocks: u64 = 0;
3246
3247        // Phase 1: walk affected logical blocks; allocate each contiguous
3248        // unmapped run as one physical extent and stage the bitmap/BGD
3249        // updates. Repeated `map_logical` calls re-parse `root_bytes` each
3250        // time, so the in-progress inserts are visible to subsequent
3251        // lookups in the same loop.
3252        let mut lb = first_lb;
3253        while lb < last_lb_excl {
3254            let mapped = crate::extent::map_logical(
3255                &root_bytes,
3256                self.dev.as_ref(),
3257                self.sb.block_size(),
3258                lb,
3259            )?;
3260            if mapped.is_some() {
3261                lb += 1;
3262                continue;
3263            }
3264            // Find the end of this unmapped run.
3265            let mut run_end = lb + 1;
3266            while run_end < last_lb_excl {
3267                let p = crate::extent::map_logical(
3268                    &root_bytes,
3269                    self.dev.as_ref(),
3270                    self.sb.block_size(),
3271                    run_end,
3272                )?;
3273                if p.is_some() {
3274                    break;
3275                }
3276                run_end += 1;
3277            }
3278            let run_len_u64 = run_end - lb;
3279            if run_len_u64 > u32::MAX as u64 {
3280                return Err(Error::InvalidArgument(
3281                    "pwrite: unmapped run exceeds u32 block count",
3282                ));
3283            }
3284
3285            // Allocate physical blocks for this logical run, splitting
3286            // across smaller contiguous physical runs when no single
3287            // group has a free run that size. Each sub-allocation is
3288            // staged into the buffer (bitmap + BGD) and inserted as its
3289            // own extent. plan_insert_extent auto-merges adjacent extents
3290            // so the *common* sequential-write case still produces one
3291            // extent overall.
3292            let mut remaining_in_run = run_len_u64 as u32;
3293            let mut sub_lb = lb;
3294            while remaining_in_run > 0 {
3295                let mut want = remaining_in_run;
3296                let plan = loop {
3297                    let plan_result = {
3298                        let mut bitmap_reader = |b: u64| -> Result<Vec<u8>> {
3299                            if let Some(bytes) = buf.dirty.get(&b) {
3300                                return Ok(bytes.clone());
3301                            }
3302                            self.read_block(b)
3303                        };
3304                        crate::alloc::plan_block_allocation(
3305                            &self.sb,
3306                            &self.allocation_groups(),
3307                            want,
3308                            group_idx_of_inode,
3309                            &mut bitmap_reader,
3310                        )
3311                    };
3312                    match plan_result {
3313                        Ok(p) => break p,
3314                        Err(Error::Corrupt(msg)) if msg.contains("contiguous free run") => {
3315                            if want == 1 {
3316                                // Even a single block isn't available
3317                                // anywhere — true ENOSPC.
3318                                return Err(Error::NoSpaceLeftOnDevice);
3319                            }
3320                            // Fragmented: halve the request and retry.
3321                            // Each successful sub-run becomes its own
3322                            // extent; the outer while loop keeps drawing
3323                            // until the whole logical run is covered.
3324                            want /= 2;
3325                        }
3326                        Err(e) => return Err(e),
3327                    }
3328                };
3329
3330                let got = want;
3331                let got_u64 = got as u64;
3332
3333                self.buffer_mark_block_run_used(&mut buf, plan.first_block, got_u64)?;
3334                self.buffer_patch_bgd_counters(
3335                    &mut buf,
3336                    plan.bgd.group_idx as usize,
3337                    plan.bgd.free_blocks_delta,
3338                    plan.bgd.free_inodes_delta,
3339                    plan.bgd.used_dirs_delta,
3340                )?;
3341                alloc_total_blocks += got_u64;
3342
3343                let new_extent = crate::extent::Extent {
3344                    logical_block: sub_lb as u32,
3345                    length: got as u16,
3346                    physical_block: plan.first_block,
3347                    uninitialized: false,
3348                };
3349
3350                // Try the inline-root insert first; on overflow fall back
3351                // to the depth-promoting deep insert. Both paths produce a
3352                // new 60-byte root that we splice into `raw` at the end.
3353                match crate::extent_mut::plan_insert_extent(&root_bytes, new_extent) {
3354                    Ok(muts) => {
3355                        for m in &muts {
3356                            if let crate::extent_mut::ExtentMutation::WriteRoot { bytes } = m {
3357                                root_bytes = bytes.clone();
3358                            }
3359                        }
3360                    }
3361                    Err(Error::CorruptExtentTree(msg))
3362                        if msg.contains("LEAF_FULL_NEEDS_PROMOTION")
3363                            || msg.contains("multi-level tree mutation") =>
3364                    {
3365                        // Two distinct failures both route to the deep path:
3366                        // 1. Inline leaf root has 4 entries already
3367                        //    (LEAF_FULL_NEEDS_PROMOTION) → promote to depth 1.
3368                        // 2. Root has *already* been promoted on a prior
3369                        //    insert in this same call → root is an index
3370                        //    node, so the inline-leaf-only `plan_insert_extent`
3371                        //    bails with "multi-level tree mutation". The
3372                        //    deep planner descends correctly.
3373                        // Allocate tree-meta blocks one at a time via the
3374                        // same buffer-aware allocator. Each call stages a
3375                        // bitmap + BGD update so subsequent allocations
3376                        // see the just-claimed bits.
3377                        let reader = FsBlockReader { fs: self };
3378                        let mut meta_blocks_alloc: u64 = 0;
3379                        let inode_generation = inode.generation;
3380                        let deep_plan = {
3381                            let mut alloc_closure = || -> Result<u64> {
3382                                let p = {
3383                                    let mut bitmap_reader = |b: u64| -> Result<Vec<u8>> {
3384                                        if let Some(bytes) = buf.dirty.get(&b) {
3385                                            return Ok(bytes.clone());
3386                                        }
3387                                        self.read_block(b)
3388                                    };
3389                                    crate::alloc::plan_block_allocation(
3390                                        &self.sb,
3391                                        &self.allocation_groups(),
3392                                        1,
3393                                        group_idx_of_inode,
3394                                        &mut bitmap_reader,
3395                                    )?
3396                                };
3397                                self.buffer_mark_block_run_used(&mut buf, p.first_block, 1)?;
3398                                self.buffer_patch_bgd_counters(
3399                                    &mut buf,
3400                                    p.bgd.group_idx as usize,
3401                                    p.bgd.free_blocks_delta,
3402                                    0,
3403                                    0,
3404                                )?;
3405                                meta_blocks_alloc += 1;
3406                                Ok(p.first_block)
3407                            };
3408                            crate::extent_mut::plan_insert_extent_deep(
3409                                &root_bytes,
3410                                new_extent,
3411                                self.sb.block_size(),
3412                                &reader,
3413                                &mut alloc_closure,
3414                            )?
3415                        };
3416                        root_bytes = deep_plan.new_root;
3417                        let bs_u64 = self.sb.block_size() as u64;
3418                        for (block, bytes) in deep_plan.block_writes {
3419                            let mut bytes = bytes;
3420                            if self.csum.enabled {
3421                                self.csum
3422                                    .patch_extent_tail(ino, inode_generation, &mut bytes);
3423                            }
3424                            // Eager-write tree-meta blocks to disk so a
3425                            // *subsequent* plan_insert_extent_deep within
3426                            // this same apply_pwrite (when more sub-runs
3427                            // follow and need to descend the just-promoted
3428                            // tree) can fetch them via FsBlockReader. Also
3429                            // stage in buf so the final commit_block_buffer
3430                            // covers them inside the same transaction tail.
3431                            // On a pre-commit crash these become orphaned
3432                            // bytes that fsck reclaims (the block bitmap
3433                            // mark is in `buf` and only lands on commit).
3434                            self.dev.write_at(block * bs_u64, &bytes)?;
3435                            buf.put(block, bytes);
3436                        }
3437                        alloc_total_blocks += meta_blocks_alloc;
3438                    }
3439                    Err(e) => return Err(e),
3440                }
3441
3442                // Mark these logical blocks as freshly-allocated so Phase 2
3443                // writes use put() (zero-init) instead of get_mut()
3444                // (read-from-disk-and-modify).
3445                for x in sub_lb..(sub_lb + got_u64) {
3446                    newly_alloc.insert(x);
3447                }
3448
3449                sub_lb += got_u64;
3450                remaining_in_run -= got;
3451            }
3452
3453            lb = run_end;
3454        }
3455
3456        // Phase 2: splice the chunk into each affected block.
3457        let mut data_off: usize = 0;
3458        for cur_lb in first_lb..last_lb_excl {
3459            let block_byte_start = cur_lb * bs;
3460            let block_byte_end = block_byte_start + bs;
3461            let chunk_start = offset.max(block_byte_start);
3462            let chunk_end = end.min(block_byte_end);
3463            let in_block_off = (chunk_start - block_byte_start) as usize;
3464            let chunk_len = (chunk_end - chunk_start) as usize;
3465
3466            let phys = crate::extent::map_logical(
3467                &root_bytes,
3468                self.dev.as_ref(),
3469                self.sb.block_size(),
3470                cur_lb,
3471            )?
3472            .ok_or(Error::Corrupt(
3473                "pwrite Phase 2: logical block unmapped after Phase 1 (allocator/extent insert mismatch)",
3474            ))?;
3475
3476            if newly_alloc.contains(&cur_lb) {
3477                // Fresh block: zero-init then splice. Avoids reading stale
3478                // bytes from a previously-freed extent.
3479                let mut block = vec![0u8; bs_usize];
3480                block[in_block_off..in_block_off + chunk_len]
3481                    .copy_from_slice(&data[data_off..data_off + chunk_len]);
3482                buf.put(phys, block);
3483            } else {
3484                // Existing block: read-modify-write to preserve untouched
3485                // bytes (head before `chunk_start`, tail after `chunk_end`).
3486                let block = buf.get_mut(self, phys)?;
3487                if block.len() != bs_usize {
3488                    return Err(Error::Corrupt(
3489                        "pwrite Phase 2: existing block has wrong size",
3490                    ));
3491                }
3492                block[in_block_off..in_block_off + chunk_len]
3493                    .copy_from_slice(&data[data_off..data_off + chunk_len]);
3494            }
3495
3496            data_off += chunk_len;
3497        }
3498        debug_assert_eq!(data_off, data.len());
3499
3500        // Phase 3: patch the extent root onto `raw`, update size + sectors,
3501        // recompute the inode checksum, stage the inode write.
3502        Self::patch_inode_block_area(&mut raw, &root_bytes)?;
3503        let new_size = inode.size.max(end);
3504        let new_sectors = inode
3505            .blocks
3506            .checked_add(alloc_total_blocks * sectors_per_block)
3507            .ok_or(Error::Corrupt("pwrite: i_blocks overflow"))?;
3508        self.finalize_inode_raw_after_write(ino, &mut raw, &inode, new_size, new_sectors)?;
3509        self.buffer_write_inode(&mut buf, ino, &raw)?;
3510
3511        // Phase 4: SB delta for the newly-allocated blocks.
3512        if alloc_total_blocks > 0 {
3513            self.buffer_patch_sb_counters(&mut buf, -(alloc_total_blocks as i64), 0)?;
3514        }
3515
3516        // Phase 5: commit everything atomically (journaled if available).
3517        self.commit_block_buffer(buf)?;
3518        Ok(new_size)
3519    }
3520
3521    /// Patch size + blocks counter on the inode image, recompute the csum
3522    /// if enabled, and write it back. Shared tail for apply_replace_file_content and
3523    /// any future writer that produces a new `raw` image.
3524    fn finalize_inode_after_write(
3525        &self,
3526        ino: u32,
3527        raw: &mut [u8],
3528        orig: &Inode,
3529        new_size: u64,
3530        new_sectors: u64,
3531    ) -> Result<()> {
3532        self.finalize_inode_raw_after_write(ino, raw, orig, new_size, new_sectors)?;
3533        self.write_inode_raw(ino, raw)
3534    }
3535
3536    /// Buffer-friendly variant of `finalize_inode_after_write`: patches
3537    /// size, blocks, ctime, mtime, and checksum on `raw` IN PLACE without
3538    /// writing to disk. Caller stages the result via `buffer_write_inode`
3539    /// so the inode update is atomic with the surrounding multi-block tx.
3540    fn finalize_inode_raw_after_write(
3541        &self,
3542        ino: u32,
3543        raw: &mut [u8],
3544        orig: &Inode,
3545        new_size: u64,
3546        new_sectors: u64,
3547    ) -> Result<()> {
3548        Self::patch_inode_size_and_blocks(raw, new_size, new_sectors)?;
3549        let now = now_unix_seconds();
3550        raw[0x0C..0x10].copy_from_slice(&now.to_le_bytes()); // ctime
3551        raw[0x10..0x14].copy_from_slice(&now.to_le_bytes()); // mtime
3552        if self.csum.enabled {
3553            if let Some((lo, hi)) = self.csum.compute_inode_checksum(ino, orig.generation, raw) {
3554                raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
3555                if raw.len() >= 0x84 {
3556                    raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
3557                }
3558            }
3559        }
3560        Ok(())
3561    }
3562
3563    fn set_block_run_used(&self, start: u64, len: u64) -> Result<()> {
3564        let bpg = self.sb.blocks_per_group as u64;
3565        let first_data = self.sb.first_data_block as u64;
3566        let gi = ((start - first_data) / bpg) as usize;
3567        if gi >= self.groups.len() {
3568            return Err(Error::InvalidBlock(start));
3569        }
3570        let group_start = first_data + gi as u64 * bpg;
3571        let bit_start = (start - group_start) as u32;
3572        let bitmap_block = self.groups[gi].block_bitmap;
3573        let bs = self.sb.block_size() as u64;
3574        let mut buf = vec![0u8; bs as usize];
3575        self.dev.read_at(bitmap_block * bs, &mut buf)?;
3576        for i in 0..len {
3577            let bit = bit_start as u64 + i;
3578            let byte = (bit / 8) as usize;
3579            let mask = 1u8 << (bit % 8);
3580            if byte < buf.len() {
3581                buf[byte] |= mask;
3582            }
3583        }
3584        self.dev.write_at(bitmap_block * bs, &buf)?;
3585        Ok(())
3586    }
3587
3588    /// Find `name` in directory `dir_inode` — scans each data block. Returns
3589    /// the inode number or `Error::NotFound`.
3590    fn find_entry_in_dir(&self, dir_inode: &Inode, name: &[u8]) -> Result<u32> {
3591        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
3592        let bs = self.sb.block_size();
3593        let n_blocks = dir_inode.size.div_ceil(bs as u64);
3594        for logical in 0..n_blocks {
3595            let Some(phys) = self.map_inode_logical(dir_inode, logical)? else {
3596                continue;
3597            };
3598            let block = self.read_block(phys)?;
3599            for entry in crate::dir::DirBlockIter::new(&block, has_ft) {
3600                let e = entry?;
3601                if e.name == name {
3602                    return Ok(e.inode);
3603                }
3604            }
3605        }
3606        Err(Error::NotFound)
3607    }
3608
3609    /// Apply per-group counter deltas on disk for group `gi`. Positive deltas
3610    /// increase the corresponding `bg_free_*` / `bg_used_dirs` counter,
3611    /// negative deltas decrease. Recomputes the BGD csum when `metadata_csum`
3612    /// is enabled. The in-memory `self.groups` copy is NOT updated — callers
3613    /// doing a sequence of allocations should `Filesystem::mount` fresh.
3614    pub(crate) fn patch_bgd_counters(
3615        &self,
3616        gi: usize,
3617        free_blocks_delta: i32,
3618        free_inodes_delta: i32,
3619        used_dirs_delta: i32,
3620    ) -> Result<()> {
3621        let bs = self.sb.block_size() as u64;
3622        let desc_size = self.sb.desc_size as u64;
3623        let bgt_first_block = self.sb.first_data_block as u64 + 1;
3624        let byte_in_bgt = gi as u64 * desc_size;
3625        let bgt_block = bgt_first_block + byte_in_bgt / bs;
3626        let off_in_block = (byte_in_bgt % bs) as usize;
3627
3628        let mut block = self.read_block(bgt_block)?;
3629
3630        // Free-blocks: 16-bit at 0x0C, hi at 0x2A when 64-bit
3631        patch_counter_u32(
3632            &mut block,
3633            off_in_block + 0x0C,
3634            if desc_size >= 0x40 {
3635                Some(off_in_block + 0x2A)
3636            } else {
3637                None
3638            },
3639            free_blocks_delta,
3640        );
3641        // Free-inodes: 16-bit at 0x0E, hi at 0x2C when 64-bit
3642        patch_counter_u32(
3643            &mut block,
3644            off_in_block + 0x0E,
3645            if desc_size >= 0x40 {
3646                Some(off_in_block + 0x2C)
3647            } else {
3648                None
3649            },
3650            free_inodes_delta,
3651        );
3652        // Used-dirs: 16-bit only (kernel defines u16+u16 hi at 0x2E too, but
3653        // dirs per group realistically fit in u16 — handle both anyway).
3654        patch_counter_u32(
3655            &mut block,
3656            off_in_block + 0x10,
3657            if desc_size >= 0x40 {
3658                Some(off_in_block + 0x2E)
3659            } else {
3660                None
3661            },
3662            used_dirs_delta,
3663        );
3664
3665        if self.csum.enabled {
3666            let stored_at = off_in_block + 0x1E;
3667            let end_desc = off_in_block + desc_size as usize;
3668            block[stored_at..stored_at + 2].copy_from_slice(&[0, 0]);
3669            let seed = self.csum.seed;
3670            let mut c = crate::checksum::linux_crc32c(seed, &(gi as u32).to_le_bytes());
3671            c = crate::checksum::linux_crc32c(c, &block[off_in_block..end_desc]);
3672            let new_csum = c as u16;
3673            block[stored_at..stored_at + 2].copy_from_slice(&new_csum.to_le_bytes());
3674        }
3675        self.dev.write_at(bgt_block * bs, &block)?;
3676        Ok(())
3677    }
3678
3679    /// Apply deltas to SB `s_free_blocks_count` and `s_free_inodes_count`.
3680    /// Recomputes the SB checksum when enabled. Does not mutate `self.sb`.
3681    pub(crate) fn patch_sb_counters(
3682        &self,
3683        free_blocks_delta: i64,
3684        free_inodes_delta: i32,
3685    ) -> Result<()> {
3686        // Route through the cache-coherent buffer path (which reads the SB via
3687        // read_block) so consecutive ops accumulate against the CURRENT
3688        // on-disk superblock. The old body re-read the immutable mount-time
3689        // snapshot `self.sb.raw` every call, so within a single mount each
3690        // call rewrote the SB from mount-time values — a sequence of
3691        // frees/allocs clobbered each other (e.g. directory growth froze
3692        // free_blocks at mount-1 and reset free_inodes, which e2fsck flags as
3693        // "Free blocks/inodes count wrong").
3694        let mut buf = BlockBuffer::new(self.sb.block_size());
3695        self.buffer_patch_sb_counters(&mut buf, free_blocks_delta, free_inodes_delta)?;
3696        self.commit_block_buffer(buf)?;
3697        Ok(())
3698    }
3699
3700    /// Zero the bitmap bits covering the physical block run
3701    /// `[start, start+len)`. Assumes the run lies entirely within one block
3702    /// group (true for allocator-produced runs; fragmentation across groups
3703    /// is a future concern).
3704    fn free_block_run(&self, start: u64, len: u64) -> Result<()> {
3705        let bpg = self.sb.blocks_per_group as u64;
3706        let first_data = self.sb.first_data_block as u64;
3707        // Block group index of the first block in the run.
3708        let gi = ((start - first_data) / bpg) as usize;
3709        if gi >= self.groups.len() {
3710            return Err(Error::InvalidBlock(start));
3711        }
3712        let group_start = first_data + gi as u64 * bpg;
3713        let bit_start = (start - group_start) as u32;
3714        let bg = &self.groups[gi];
3715        let bitmap_block = bg.block_bitmap;
3716
3717        let bs = self.sb.block_size() as u64;
3718        let mut buf = vec![0u8; bs as usize];
3719        self.dev.read_at(bitmap_block * bs, &mut buf)?;
3720        for i in 0..len {
3721            let bit = bit_start as u64 + i;
3722            let byte = (bit / 8) as usize;
3723            let mask = 1u8 << (bit % 8);
3724            if byte < buf.len() {
3725                buf[byte] &= !mask;
3726            }
3727        }
3728        self.dev.write_at(bitmap_block * bs, &buf)?;
3729        Ok(())
3730    }
3731
3732    /// Free a physical-block run AND patch the containing group's
3733    /// `bg_free_blocks_count`. Returns `len` so the caller can accumulate a
3734    /// running total to feed `patch_sb_counters` once per high-level op.
3735    ///
3736    /// Per-call BGD updates correctly handle runs that span groups (each
3737    /// call lands in exactly one group per [`free_block_run`]'s contract).
3738    /// SB updates are deliberately deferred so freeing a 1000-extent file
3739    /// produces 1 SB write instead of 1000.
3740    fn free_block_run_and_bgd(&self, start: u64, len: u64) -> Result<u64> {
3741        self.free_block_run(start, len)?;
3742        let bpg = self.sb.blocks_per_group as u64;
3743        let first_data = self.sb.first_data_block as u64;
3744        let gi = ((start - first_data) / bpg) as usize;
3745        if gi < self.groups.len() {
3746            self.patch_bgd_counters(gi, len as i32, 0, 0)?;
3747        }
3748        Ok(len)
3749    }
3750
3751    // -----------------------------------------------------------------------
3752    // mkdir / rmdir
3753    // -----------------------------------------------------------------------
3754
3755    /// Build an on-disk inode image for a freshly-created directory. Sets
3756    /// `S_IFDIR | mode`, `i_links_count = 2` (for `.` and the dir entry in
3757    /// the parent), `i_size = block_size` (one data block), EXTENTS flag
3758    /// with a single leaf extent mapping logical 0 → `data_phys_block`,
3759    /// timestamps = now.
3760    fn build_directory_inode(&self, ino: u32, mode: u16, data_phys_block: u64) -> Result<Vec<u8>> {
3761        use crate::inode::{
3762            OFF_BLOCK, OFF_BLOCKS_HI, OFF_BLOCKS_LO, OFF_FLAGS, OFF_LINKS_COUNT, OFF_MODE,
3763            OFF_SIZE_HI, OFF_SIZE_LO,
3764        };
3765        let mut raw = vec![0u8; self.sb.inode_size as usize];
3766
3767        let mode_bits = crate::inode::S_IFDIR | (mode & 0x0FFF);
3768        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&mode_bits.to_le_bytes());
3769        // 2 hard links: one for "." and one for the parent's entry naming this dir.
3770        raw[OFF_LINKS_COUNT..OFF_LINKS_COUNT + 2].copy_from_slice(&2u16.to_le_bytes());
3771        raw[OFF_FLAGS..OFF_FLAGS + 4]
3772            .copy_from_slice(&crate::inode::InodeFlags::EXTENTS.bits().to_le_bytes());
3773
3774        // i_block (60 B): extent header (leaf, 1 entry, max 4) + one Extent.
3775        let extent_header_off = OFF_BLOCK;
3776        raw[extent_header_off..extent_header_off + 2]
3777            .copy_from_slice(&crate::extent::EXT4_EXT_MAGIC.to_le_bytes());
3778        raw[extent_header_off + 2..extent_header_off + 4].copy_from_slice(&1u16.to_le_bytes());
3779        raw[extent_header_off + 4..extent_header_off + 6].copy_from_slice(&4u16.to_le_bytes());
3780        // depth=0 leaf, generation=0 — both stay zero from initial vec![0u8; ...]
3781
3782        // Entry at extent_header_off+12: logical 0, len 1, phys = data_phys_block.
3783        let extent_entry_off = extent_header_off + 12;
3784        raw[extent_entry_off..extent_entry_off + 4].copy_from_slice(&0u32.to_le_bytes());
3785        raw[extent_entry_off + 4..extent_entry_off + 6].copy_from_slice(&1u16.to_le_bytes());
3786        let (extent_phys_hi, extent_phys_lo) = crate::extent_mut::split_phys_block(data_phys_block);
3787        raw[extent_entry_off + 6..extent_entry_off + 8]
3788            .copy_from_slice(&extent_phys_hi.to_le_bytes());
3789        raw[extent_entry_off + 8..extent_entry_off + 12]
3790            .copy_from_slice(&extent_phys_lo.to_le_bytes());
3791
3792        // Size = block_size (the single data block fills the dir).
3793        let bs = self.sb.block_size() as u64;
3794        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4]
3795            .copy_from_slice(&((bs & 0xFFFF_FFFF) as u32).to_le_bytes());
3796        raw[OFF_SIZE_HI..OFF_SIZE_HI + 4].copy_from_slice(&((bs >> 32) as u32).to_le_bytes());
3797
3798        let sectors = bs / 512;
3799        raw[OFF_BLOCKS_LO..OFF_BLOCKS_LO + 4].copy_from_slice(&(sectors as u32).to_le_bytes());
3800        raw[OFF_BLOCKS_HI..OFF_BLOCKS_HI + 2]
3801            .copy_from_slice(&(((sectors >> 32) & 0xFFFF) as u16).to_le_bytes());
3802
3803        let now = now_unix_seconds();
3804        write_inode_timestamps(&mut raw, now);
3805        let generation = alloc_inode_generation();
3806        write_inode_generation(&mut raw, generation);
3807        write_inode_extra_isize(&mut raw);
3808        self.stamp_inode_checksum(&mut raw, ino, generation);
3809        Ok(raw)
3810    }
3811
3812    /// Seed a freshly-allocated dir block with the two canonical entries
3813    /// `.` (→ new_ino) and `..` (→ parent_ino). Handles the metadata-csum
3814    /// tail when required: the last 12 bytes are reserved, and the CRC is
3815    /// computed over everything before them.
3816    fn seed_directory_block(
3817        &self,
3818        new_ino: u32,
3819        parent_ino: u32,
3820        new_generation: u32,
3821    ) -> Result<Vec<u8>> {
3822        let bs = self.sb.block_size() as usize;
3823        let mut block = vec![0u8; bs];
3824        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
3825        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
3826        let usable = bs - reserved_tail;
3827
3828        // "." entry: rec_len = 12
3829        block[0..4].copy_from_slice(&new_ino.to_le_bytes());
3830        block[4..6].copy_from_slice(&12u16.to_le_bytes());
3831        block[6] = 1; // name_len
3832        block[7] = if has_ft {
3833            crate::dir::DirEntryType::Directory as u8
3834        } else {
3835            0
3836        };
3837        block[8] = b'.';
3838
3839        // ".." entry: rec_len absorbs the rest of the usable region.
3840        let off = 12;
3841        block[off..off + 4].copy_from_slice(&parent_ino.to_le_bytes());
3842        let rec_len = (usable - off) as u16;
3843        block[off + 4..off + 6].copy_from_slice(&rec_len.to_le_bytes());
3844        block[off + 6] = 2;
3845        block[off + 7] = if has_ft {
3846            crate::dir::DirEntryType::Directory as u8
3847        } else {
3848            0
3849        };
3850        block[off + 8] = b'.';
3851        block[off + 9] = b'.';
3852
3853        // Tail (when metadata_csum enabled): fake inode=0, rec_len=12,
3854        // name_len=0, file_type=0xDE, u32 checksum.
3855        if reserved_tail == 12 {
3856            self.csum
3857                .patch_dir_entry_tail(new_ino, new_generation, &mut block);
3858        }
3859
3860        Ok(block)
3861    }
3862
3863    /// Adjust `i_links_count` on a raw inode image. Recomputes CSUM.
3864    fn patch_inode_nlink(&self, ino: u32, raw: &mut [u8], inode: &Inode, delta: i32) -> Result<()> {
3865        let new_count = (inode.links_count as i32 + delta).max(0) as u16;
3866        raw[0x1A..0x1C].copy_from_slice(&new_count.to_le_bytes());
3867        if self.csum.enabled {
3868            if let Some((lo, hi)) = self.csum.compute_inode_checksum(ino, inode.generation, raw) {
3869                raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
3870                if raw.len() >= 0x84 {
3871                    raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
3872                }
3873            }
3874        }
3875        Ok(())
3876    }
3877
3878    /// Create a subdirectory at `path` with POSIX mode bits (low 12 bits of
3879    /// `mode`). Returns the new directory's inode number. Steps: allocate
3880    /// inode (Orlov-hinted) → allocate one data block → seed it with `.` / `..`
3881    /// → build dir inode → write inode + data block → add dir entry in parent
3882    /// → bump parent's `i_links_count` → commit BGD/SB counters.
3883    ///
3884    /// Not journaled — safe only in scratch-image contexts until transaction
3885    /// wrapping lands.
3886    pub fn apply_mkdir(&self, path: &str, mode: u16) -> Result<u32> {
3887        if !self.dev.is_writable() {
3888            return Err(Error::ReadOnly);
3889        }
3890        let (parent_path, base_name) = split_parent_and_base(path)?;
3891        if base_name.len() > 255 {
3892            return Err(Error::NameTooLong);
3893        }
3894
3895        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
3896        let parent_ino =
3897            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &parent_path)?;
3898        let (parent_inode, mut parent_raw) = self.read_inode_verified(parent_ino)?;
3899        if !parent_inode.is_dir() {
3900            return Err(Error::NotADirectory);
3901        }
3902        if self
3903            .find_entry_in_dir(&parent_inode, base_name.as_bytes())
3904            .is_ok()
3905        {
3906            return Err(Error::AlreadyExists);
3907        }
3908
3909        let bs = self.sb.block_size();
3910        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
3911        let mut bitmap_reader = |block: u64| self.read_block(block);
3912
3913        // 1. Allocate inode (is_dir = true so Orlov picks a dir-friendly group).
3914        let iplan = crate::alloc::plan_inode_allocation(
3915            &self.sb,
3916            &self.allocation_groups(),
3917            true,
3918            parent_group,
3919            &mut bitmap_reader,
3920        )?;
3921        let new_ino = iplan.inode;
3922
3923        // 2. Allocate one data block for the dir contents.
3924        let bplan = crate::alloc::plan_block_allocation(
3925            &self.sb,
3926            &self.allocation_groups(),
3927            1,
3928            iplan.bgd.group_idx,
3929            &mut bitmap_reader,
3930        )?;
3931        let data_block = bplan.first_block;
3932
3933        // Multi-block transaction: inode bitmap + block bitmap + counters
3934        // + new dir inode + seeded data block + parent dir entry +
3935        // parent nlink bump, all atomic.
3936        let mut buf = BlockBuffer::new(bs);
3937        self.buffer_mark_inode_used(&mut buf, new_ino)?;
3938        self.buffer_patch_bgd_counters(
3939            &mut buf,
3940            iplan.bgd.group_idx as usize,
3941            iplan.bgd.free_blocks_delta,
3942            iplan.bgd.free_inodes_delta,
3943            iplan.bgd.used_dirs_delta,
3944        )?;
3945        self.buffer_patch_sb_counters(
3946            &mut buf,
3947            iplan.sb.free_blocks_delta,
3948            iplan.sb.free_inodes_delta,
3949        )?;
3950
3951        self.buffer_mark_block_run_used(&mut buf, data_block, 1)?;
3952        self.buffer_patch_bgd_counters(
3953            &mut buf,
3954            bplan.bgd.group_idx as usize,
3955            bplan.bgd.free_blocks_delta,
3956            bplan.bgd.free_inodes_delta,
3957            bplan.bgd.used_dirs_delta,
3958        )?;
3959        self.buffer_patch_sb_counters(
3960            &mut buf,
3961            bplan.sb.free_blocks_delta,
3962            bplan.sb.free_inodes_delta,
3963        )?;
3964
3965        let raw = self.build_directory_inode(new_ino, mode, data_block)?;
3966        let gen = u32::from_le_bytes(raw[0x64..0x68].try_into().unwrap());
3967        self.buffer_write_inode(&mut buf, new_ino, &raw)?;
3968
3969        // Seed the data block (`.` and `..` entries) and stage it.
3970        let seed = self.seed_directory_block(new_ino, parent_ino, gen)?;
3971        buf.put(data_block, seed);
3972
3973        // Try to install the dir entry in the parent in-place first.
3974        let parent_extends = match self.buffer_add_dir_entry_inplace(
3975            &mut buf,
3976            parent_ino,
3977            &parent_inode,
3978            base_name.as_bytes(),
3979            new_ino,
3980            crate::dir::DirEntryType::Directory,
3981        ) {
3982            Ok(()) => false,
3983            Err(Error::OutOfBounds) => true,
3984            Err(e) => return Err(e),
3985        };
3986
3987        if !parent_extends {
3988            // In-place add succeeded — bump parent's nlink in the same buffer.
3989            self.patch_inode_nlink(parent_ino, &mut parent_raw, &parent_inode, 1)?;
3990            self.buffer_write_inode(&mut buf, parent_ino, &parent_raw)?;
3991            self.commit_block_buffer(buf)?;
3992        } else {
3993            // Parent dir is full → commit what we have, then run the
3994            // un-journaled extend, then commit the parent nlink bump as a
3995            // small follow-up.
3996            self.commit_block_buffer(buf)?;
3997            self.extend_dir_and_add_entry(
3998                parent_ino,
3999                base_name.as_bytes(),
4000                new_ino,
4001                crate::dir::DirEntryType::Directory,
4002            )?;
4003            // Re-read parent (extend rewrote it) before patching nlink.
4004            let (refreshed_parent, mut refreshed_raw) = self.read_inode_verified(parent_ino)?;
4005            self.patch_inode_nlink(parent_ino, &mut refreshed_raw, &refreshed_parent, 1)?;
4006            self.commit_inode_write(parent_ino, &refreshed_raw)?;
4007        }
4008
4009        Ok(new_ino)
4010    }
4011
4012    /// Create a hard link at `dst` pointing to the same inode as `src`.
4013    ///
4014    /// Semantics:
4015    /// - `src` must exist and must NOT be a directory (POSIX forbids
4016    ///   directory hardlinks to avoid reference cycles).
4017    /// - `dst`'s parent must exist and be a directory.
4018    /// - `dst` must not already exist.
4019    /// - On success the shared inode's `i_links_count` is incremented by 1.
4020    ///
4021    /// Not journaled — same caveat as other Phase-4 ops.
4022    pub fn apply_link(&self, src: &str, dst: &str) -> Result<()> {
4023        if !self.dev.is_writable() {
4024            return Err(Error::ReadOnly);
4025        }
4026        let (dst_parent_path, dst_name) = split_parent_and_base(dst)?;
4027        if dst_name.len() > 255 {
4028            return Err(Error::NameTooLong);
4029        }
4030
4031        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
4032        let src_ino = crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, src)?;
4033        let (src_inode, mut src_raw) = self.read_inode_verified(src_ino)?;
4034        if src_inode.is_dir() {
4035            // POSIX: hard-linking a directory is forbidden. Map to EISDIR
4036            // (rather than EPERM) — matches our IsADirectory convention.
4037            return Err(Error::IsADirectory);
4038        }
4039
4040        let dst_parent_ino =
4041            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &dst_parent_path)?;
4042        let (dst_parent_inode, _) = self.read_inode_verified(dst_parent_ino)?;
4043        if !dst_parent_inode.is_dir() {
4044            return Err(Error::NotADirectory);
4045        }
4046        if self
4047            .find_entry_in_dir(&dst_parent_inode, dst_name.as_bytes())
4048            .is_ok()
4049        {
4050            return Err(Error::AlreadyExists);
4051        }
4052
4053        let dir_type = match src_inode.file_type() {
4054            crate::inode::S_IFREG => crate::dir::DirEntryType::RegFile,
4055            crate::inode::S_IFLNK => crate::dir::DirEntryType::Symlink,
4056            crate::inode::S_IFCHR => crate::dir::DirEntryType::CharDev,
4057            crate::inode::S_IFBLK => crate::dir::DirEntryType::BlockDev,
4058            crate::inode::S_IFIFO => crate::dir::DirEntryType::Fifo,
4059            crate::inode::S_IFSOCK => crate::dir::DirEntryType::Socket,
4060            _ => crate::dir::DirEntryType::Unknown,
4061        };
4062
4063        // Build the multi-block transaction: bump nlink + add dir entry,
4064        // both staged into one buffer so a crash either applies both or
4065        // neither.
4066        let mut buf = BlockBuffer::new(self.sb.block_size());
4067        self.patch_inode_nlink(src_ino, &mut src_raw, &src_inode, 1)?;
4068        self.buffer_write_inode(&mut buf, src_ino, &src_raw)?;
4069
4070        match self.buffer_add_dir_entry_inplace(
4071            &mut buf,
4072            dst_parent_ino,
4073            &dst_parent_inode,
4074            dst_name.as_bytes(),
4075            src_ino,
4076            dir_type,
4077        ) {
4078            Ok(()) => self.commit_block_buffer(buf),
4079            Err(Error::OutOfBounds) => {
4080                // Parent dir is full → fall back to the un-journaled extend
4081                // path. Commit the inode-only buffer first so the nlink bump
4082                // is atomic w.r.t. itself, then run the legacy extend.
4083                self.commit_block_buffer(buf)?;
4084                self.extend_dir_and_add_entry(
4085                    dst_parent_ino,
4086                    dst_name.as_bytes(),
4087                    src_ino,
4088                    dir_type,
4089                )
4090            }
4091            Err(e) => Err(e),
4092        }
4093    }
4094
4095    /// Rename `src` → `dst` within the same filesystem.
4096    ///
4097    /// Semantics:
4098    /// - Both endpoints are within this mount.
4099    /// - Works for files and directories.
4100    /// - Cross-parent moves update the moved dir's `..` entry + bump /
4101    ///   decrement both parents' `i_links_count`.
4102    /// - Refuses to move a directory into its own subtree (cycle check).
4103    /// - Same source and dest: no-op success.
4104    /// - When dst already exists:
4105    ///     - `replace_if_exists = false` → returns `Error::AlreadyExists`.
4106    ///     - `replace_if_exists = true` → overwrites dst. See
4107    ///       "Atomicity" below for exactly how far that holds.
4108    ///       Type-compatibility rules (POSIX rename(2)):
4109    ///         * file→dir   → `Error::IsADirectory`
4110    ///         * dir→file   → `Error::NotADirectory`
4111    ///         * non-empty-dir overwrite → `Error::DirectoryNotEmpty`
4112    ///         * src and dst resolve to the same inode (hardlink) →
4113    ///           no-op success.
4114    ///       Otherwise the previous dst inode's link count is decremented
4115    ///       in the same buffer; if that drops it to zero the inode's
4116    ///       extents and slot are freed in the same atomic commit.
4117    ///
4118    /// # Atomicity, and the one place it does not hold
4119    ///
4120    /// Both paths stage their work into a single [`BlockBuffer`] and
4121    /// commit it through the journal, so a crash either applies the
4122    /// whole rename or none of it.
4123    ///
4124    /// **Except when the destination directory has no room for the new
4125    /// entry.** Then the buffer is committed early and
4126    /// `extend_dir_and_add_entry` — which is not journaled — runs
4127    /// afterwards. That splits the operation in two, and the window
4128    /// between them is a real one:
4129    ///
4130    /// - On the overwrite path, the early commit has already removed
4131    ///   dst's directory entry. A crash there leaves dst's name gone
4132    ///   and src still present: the file that was at dst is
4133    ///   unreachable, and src has not moved.
4134    /// - On the no-overwrite path, the early commit is empty, so a
4135    ///   crash in the extend leaves the filesystem as it was — but a
4136    ///   crash *after* it leaves both names pointing at src's inode
4137    ///   with a link count of one.
4138    ///
4139    /// Closing this needs `extend_dir_and_add_entry` to stage into the
4140    /// buffer rather than write on its own, which is a change to the
4141    /// directory-growth path rather than to this function. Until then
4142    /// the guarantee is: **atomic unless the destination directory has
4143    /// to grow.**
4144    pub fn apply_rename(&self, src: &str, dst: &str, replace_if_exists: bool) -> Result<()> {
4145        if !self.dev.is_writable() {
4146            return Err(Error::ReadOnly);
4147        }
4148        if src == dst {
4149            return Ok(());
4150        }
4151
4152        let (src_parent_path, src_name) = split_parent_and_base(src)?;
4153        let (dst_parent_path, dst_name) = split_parent_and_base(dst)?;
4154        if dst_name.len() > 255 {
4155            return Err(Error::NameTooLong);
4156        }
4157
4158        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
4159        let src_parent_ino =
4160            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &src_parent_path)?;
4161        let dst_parent_ino =
4162            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &dst_parent_path)?;
4163        let (src_parent_inode, _) = self.read_inode_verified(src_parent_ino)?;
4164        let (dst_parent_inode, _) = self.read_inode_verified(dst_parent_ino)?;
4165        if !src_parent_inode.is_dir() || !dst_parent_inode.is_dir() {
4166            return Err(Error::NotADirectory);
4167        }
4168
4169        let src_ino = self.find_entry_in_dir(&src_parent_inode, src_name.as_bytes())?;
4170        let existing_dst_ino = self
4171            .find_entry_in_dir(&dst_parent_inode, dst_name.as_bytes())
4172            .ok();
4173        if existing_dst_ino.is_some() && !replace_if_exists {
4174            return Err(Error::AlreadyExists);
4175        }
4176
4177        let (src_inode, _) = self.read_inode_verified(src_ino)?;
4178        let src_is_dir = src_inode.is_dir();
4179
4180        // Cycle check: moving a dir INTO itself is illegal. Simple prefix
4181        // check on normalised paths — rejects rename /a /a/b/c.
4182        if src_is_dir {
4183            let src_slash = format!("{}/", src.trim_end_matches('/'));
4184            if dst == src || dst.starts_with(&src_slash) {
4185                return Err(Error::InvalidArgument(
4186                    "rename: cannot move directory into its own subtree",
4187                ));
4188            }
4189        }
4190
4191        // Map POSIX mode bits to the directory-entry file-type byte.
4192        let dir_type = match src_inode.file_type() {
4193            crate::inode::S_IFREG => crate::dir::DirEntryType::RegFile,
4194            crate::inode::S_IFDIR => crate::dir::DirEntryType::Directory,
4195            crate::inode::S_IFLNK => crate::dir::DirEntryType::Symlink,
4196            _ => crate::dir::DirEntryType::Unknown,
4197        };
4198
4199        // ===================================================================
4200        // Replace-overwrite branch — dst already exists and caller opted in.
4201        // ===================================================================
4202        if let Some(dst_old_ino) = existing_dst_ino {
4203            // Hardlink case: src and dst already share an inode. POSIX
4204            // rename(2) requires this to be a no-op success — entry count
4205            // is unchanged, and removing src would unconditionally drop the
4206            // shared link count by one which is wrong.
4207            if dst_old_ino == src_ino {
4208                return Ok(());
4209            }
4210
4211            let (dst_old_inode, mut dst_old_raw) = self.read_inode_verified(dst_old_ino)?;
4212            let dst_is_dir = dst_old_inode.is_dir();
4213
4214            // Type compatibility — rename(2) forbids crossing the
4215            // file/directory boundary.
4216            if !src_is_dir && dst_is_dir {
4217                return Err(Error::IsADirectory);
4218            }
4219            if src_is_dir && !dst_is_dir {
4220                return Err(Error::NotADirectory);
4221            }
4222
4223            // Non-empty-dir overwrite is forbidden by POSIX. Walk every
4224            // block of dst and reject any entry that isn't `.` / `..`.
4225            if dst_is_dir {
4226                let bs = self.sb.block_size();
4227                let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
4228                let blocks = dst_old_inode.size.div_ceil(bs as u64);
4229                for logical in 0..blocks {
4230                    let Some(phys) = crate::extent::map_logical(
4231                        &dst_old_inode.block,
4232                        self.dev.as_ref(),
4233                        bs,
4234                        logical,
4235                    )?
4236                    else {
4237                        continue;
4238                    };
4239                    let block = self.read_block(phys)?;
4240                    for entry in crate::dir::DirBlockIter::new(&block, has_ft) {
4241                        let e = entry?;
4242                        if e.name != b"." && e.name != b".." {
4243                            return Err(Error::DirectoryNotEmpty);
4244                        }
4245                    }
4246                }
4247            }
4248
4249            // Stage the whole overwrite into a single buffer so a crash
4250            // either fully replaces dst or leaves the FS in its prior
4251            // state — UNLESS the destination directory has to grow, in
4252            // which case this buffer is committed early and the
4253            // un-journaled extend runs after it. See the "Atomicity"
4254            // section on this function for what that window costs.
4255            let mut buf = BlockBuffer::new(self.sb.block_size());
4256
4257            // Parent link-count changes are ACCUMULATED rather than
4258            // applied where they are discovered.
4259            //
4260            // Each site used to read its parent inode back from disk and
4261            // stage a write of it. Two such sites naming the same inode
4262            // in one buffer would have the second read stale bytes and
4263            // overwrite the first's change — and the only thing
4264            // preventing that was that their branch conditions happened
4265            // to be mutually exclusive, which nothing said and nothing
4266            // enforced.
4267            //
4268            // Summing deltas and applying them once removes the hazard
4269            // instead of relying on it not being reached: every parent
4270            // is read exactly once, after every delta is known, and
4271            // written exactly once. It also turns the dir-replaces-dir
4272            // "these two cancel out" reasoning into arithmetic that
4273            // cancels, rather than a suppressed branch that has to be
4274            // kept in step with the branch it suppresses.
4275            let mut parent_nlink: BTreeMap<u32, i32> = BTreeMap::new();
4276
4277            // 1. Pop the existing dst entry from dst_parent so the
4278            //    in-place add below has somewhere to land.
4279            self.buffer_remove_dir_entry(
4280                &mut buf,
4281                dst_parent_ino,
4282                &dst_parent_inode,
4283                dst_name.as_bytes(),
4284            )?;
4285
4286            // 2. Add the new dst entry pointing at src_ino. Try in-place
4287            //    first; if no block has room, mirror the dst_extends
4288            //    fall-back from the non-replace path.
4289            let dst_extends = match self.buffer_add_dir_entry_inplace(
4290                &mut buf,
4291                dst_parent_ino,
4292                &dst_parent_inode,
4293                dst_name.as_bytes(),
4294                src_ino,
4295                dir_type,
4296            ) {
4297                Ok(()) => false,
4298                Err(Error::OutOfBounds) => true,
4299                Err(e) => return Err(e),
4300            };
4301            if dst_extends {
4302                // Commit removal (and any prior in-buffer mutations) so
4303                // the un-journaled extend doesn't race with replays.
4304                self.commit_block_buffer(buf)?;
4305                self.extend_dir_and_add_entry(
4306                    dst_parent_ino,
4307                    dst_name.as_bytes(),
4308                    src_ino,
4309                    dir_type,
4310                )?;
4311                buf = BlockBuffer::new(self.sb.block_size());
4312            }
4313
4314            // 3. Remove src entry from its parent.
4315            self.buffer_remove_dir_entry(
4316                &mut buf,
4317                src_parent_ino,
4318                &src_parent_inode,
4319                src_name.as_bytes(),
4320            )?;
4321
4322            // 4. Cross-parent dir move: fix `..` + parent nlinks.
4323            //    For dir-replaces-dir the dst_parent gains the moved
4324            //    subdir and loses the dropped one; both deltas are
4325            //    recorded and cancel in the sum.
4326            if src_is_dir && src_parent_ino != dst_parent_ino {
4327                self.buffer_update_dotdot(&mut buf, src_ino, &src_inode, dst_parent_ino)?;
4328                *parent_nlink.entry(src_parent_ino).or_default() -= 1;
4329                *parent_nlink.entry(dst_parent_ino).or_default() += 1;
4330            }
4331
4332            // 5. Decrement dst_old_ino's link count. If it hits zero,
4333            //    free its data extents + inode slot in this same buffer.
4334            //    Directories always reap (they only ever have one external
4335            //    name in our v1 — directory hardlinks aren't supported).
4336            let new_links = dst_old_inode.links_count.saturating_sub(1);
4337            if new_links > 0 && !dst_is_dir {
4338                // Hardlinked file overwrite — just persist the new count.
4339                dst_old_raw[0x1A..0x1C].copy_from_slice(&new_links.to_le_bytes());
4340                self.finalize_inode_raw(dst_old_ino, dst_old_inode.generation, &mut dst_old_raw)?;
4341                self.buffer_write_inode(&mut buf, dst_old_ino, &dst_old_raw)?;
4342            } else {
4343                let bs = self.sb.block_size();
4344                let sectors_per_block = bs as u64 / 512;
4345                let mut freed_sectors: u64 = 0;
4346                if dst_old_inode.has_extents() && dst_old_inode.size > 0 {
4347                    if dst_is_dir {
4348                        // Directory data blocks aren't tracked through
4349                        // plan_truncate_shrink (that path expects regular
4350                        // files); use extent::collect_all + free per run.
4351                        let extents = crate::extent::collect_all(
4352                            &dst_old_inode.block,
4353                            self.dev.as_ref(),
4354                            bs,
4355                        )?;
4356                        for e in &extents {
4357                            self.buffer_free_block_run_and_bgd(
4358                                &mut buf,
4359                                e.physical_block,
4360                                e.length as u64,
4361                            )?;
4362                            freed_sectors += e.length as u64 * sectors_per_block;
4363                        }
4364                    } else {
4365                        let (_sc, muts) = crate::file_mut::plan_truncate_shrink(
4366                            dst_old_inode.size,
4367                            0,
4368                            &dst_old_inode.block,
4369                            bs,
4370                        )?;
4371                        for m in &muts {
4372                            if let crate::extent_mut::ExtentMutation::FreePhysicalRun {
4373                                start,
4374                                len,
4375                            } = m
4376                            {
4377                                self.buffer_free_block_run_and_bgd(&mut buf, *start, *len as u64)?;
4378                                freed_sectors += *len as u64 * sectors_per_block;
4379                            }
4380                        }
4381                    }
4382                }
4383
4384                self.buffer_free_inode_slot(&mut buf, dst_old_ino)?;
4385                if dst_is_dir {
4386                    // Reaped a directory → bg_used_dirs_count -= 1.
4387                    let dst_old_gi = ((dst_old_ino - 1) / self.sb.inodes_per_group) as usize;
4388                    self.buffer_patch_bgd_counters(&mut buf, dst_old_gi, 0, 0, -1)?;
4389                }
4390                let freed_blocks = freed_sectors.checked_div(sectors_per_block).unwrap_or(0);
4391                self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 1)?;
4392
4393                // Zero the inode body, set dtime = now, preserve generation.
4394                let inode_size = self.sb.inode_size as usize;
4395                let old_gen = dst_old_inode.generation;
4396                for b in &mut dst_old_raw[..inode_size] {
4397                    *b = 0;
4398                }
4399                let dtime = now_unix_seconds();
4400                dst_old_raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
4401                dst_old_raw[0x64..0x68].copy_from_slice(&old_gen.to_le_bytes());
4402                self.finalize_inode_raw(dst_old_ino, old_gen, &mut dst_old_raw)?;
4403                self.buffer_write_inode(&mut buf, dst_old_ino, &dst_old_raw)?;
4404
4405                // Dir-replaces-dir: dst_parent loses the removed subdir's
4406                // `..` reference → -1 nlink. Recorded unconditionally;
4407                // when a cross-parent dir move already recorded a +1 for
4408                // the same parent, the sum is what cancels them.
4409                if dst_is_dir {
4410                    *parent_nlink.entry(dst_parent_ino).or_default() -= 1;
4411                }
4412            }
4413
4414            self.apply_parent_nlink_deltas(&mut buf, &parent_nlink)?;
4415            return self.commit_block_buffer(buf);
4416        }
4417
4418        // ===================================================================
4419        // No-overwrite path — dst doesn't exist. Mirrors the v1 behaviour.
4420        // ===================================================================
4421        // Multi-block transaction: insert dst entry + remove src entry +
4422        // (cross-parent dir) update .. + adjust parent nlinks. Atomic so
4423        // a crash either fully renames or leaves the original — UNLESS
4424        // the destination directory has to grow, which commits this
4425        // buffer early and then runs the un-journaled extend. See the
4426        // "Atomicity" section on this function.
4427        let mut buf = BlockBuffer::new(self.sb.block_size());
4428        let mut parent_nlink: BTreeMap<u32, i32> = BTreeMap::new();
4429
4430        let dst_extends = match self.buffer_add_dir_entry_inplace(
4431            &mut buf,
4432            dst_parent_ino,
4433            &dst_parent_inode,
4434            dst_name.as_bytes(),
4435            src_ino,
4436            dir_type,
4437        ) {
4438            Ok(()) => false,
4439            Err(Error::OutOfBounds) => true,
4440            Err(e) => return Err(e),
4441        };
4442
4443        if dst_extends {
4444            // Dest parent full → fall back to the un-journaled extend.
4445            // Commit any partial state first to avoid mixing journaled
4446            // and un-journaled writes that race.
4447            self.commit_block_buffer(buf)?;
4448            self.extend_dir_and_add_entry(dst_parent_ino, dst_name.as_bytes(), src_ino, dir_type)?;
4449            // Now the source removal + .. + nlink adjustments in a
4450            // fresh buffer.
4451            buf = BlockBuffer::new(self.sb.block_size());
4452        }
4453
4454        self.buffer_remove_dir_entry(
4455            &mut buf,
4456            src_parent_ino,
4457            &src_parent_inode,
4458            src_name.as_bytes(),
4459        )?;
4460
4461        if src_is_dir && src_parent_ino != dst_parent_ino {
4462            self.buffer_update_dotdot(&mut buf, src_ino, &src_inode, dst_parent_ino)?;
4463            *parent_nlink.entry(src_parent_ino).or_default() -= 1;
4464            *parent_nlink.entry(dst_parent_ino).or_default() += 1;
4465        }
4466
4467        // Read after the extend above, if there was one, so the counts
4468        // come from what is actually on disk now.
4469        self.apply_parent_nlink_deltas(&mut buf, &parent_nlink)?;
4470        self.commit_block_buffer(buf)
4471    }
4472
4473    /// Apply accumulated `i_links_count` deltas, one read and one write
4474    /// per inode.
4475    ///
4476    /// The point is the "one read" half. Patching a link count means
4477    /// reading the inode, changing the field and staging the whole
4478    /// record — so two patches of the same inode staged into one buffer
4479    /// would have the second read the *pre-buffer* bytes from disk and
4480    /// write them back over the first. Summing first makes that
4481    /// impossible rather than merely unreached.
4482    ///
4483    /// A delta of zero writes nothing. That is what makes the
4484    /// dir-replaces-dir case (+1 for the arriving subdirectory, -1 for
4485    /// the departing one) come out as no write at all, without a branch
4486    /// anywhere having to know about the other.
4487    fn apply_parent_nlink_deltas(
4488        &self,
4489        buf: &mut BlockBuffer,
4490        deltas: &BTreeMap<u32, i32>,
4491    ) -> Result<()> {
4492        for (&ino, &delta) in deltas {
4493            if delta == 0 {
4494                continue;
4495            }
4496            let (inode, mut raw) = self.read_inode_verified(ino)?;
4497            self.patch_inode_nlink(ino, &mut raw, &inode, delta)?;
4498            self.buffer_write_inode(buf, ino, &raw)?;
4499        }
4500        Ok(())
4501    }
4502
4503    /// Grow `parent_ino`'s directory file by one fs block, seed that block
4504    /// with the entry `(name → target_ino)`, and update the parent inode
4505    /// image (size +block_size, +1 extent, recomputed CSUM). Assumes the
4506    /// parent's inline extent root still has a free slot (the common case
4507    /// until htree promotion lands).
4508    /// Mark a freshly-allocated single block used and apply its BGD + SB
4509    /// free-count deltas in one cache-coherent transaction. Routes through
4510    /// `buffer_mark_block_run_used`, which refreshes the block-bitmap
4511    /// checksum — the bare `mark_block_run_used` + `patch_*_counters` sequence
4512    /// the directory-grow path used to run left that csum stale, so e2fsck
4513    /// reported "block bitmap does not match checksum" once a directory grew a
4514    /// block (and on 1 KiB images, where dirs grow at far fewer entries).
4515    fn commit_dir_block_alloc(
4516        &self,
4517        phys: u64,
4518        plan: &crate::alloc::BlockAllocationPlan,
4519    ) -> Result<()> {
4520        let mut buf = BlockBuffer::new(self.sb.block_size());
4521        self.buffer_mark_block_run_used(&mut buf, phys, 1)?;
4522        self.buffer_patch_bgd_counters(
4523            &mut buf,
4524            plan.bgd.group_idx as usize,
4525            plan.bgd.free_blocks_delta,
4526            plan.bgd.free_inodes_delta,
4527            plan.bgd.used_dirs_delta,
4528        )?;
4529        self.buffer_patch_sb_counters(
4530            &mut buf,
4531            plan.sb.free_blocks_delta,
4532            plan.sb.free_inodes_delta,
4533        )?;
4534        self.commit_block_buffer(buf)
4535    }
4536
4537    fn extend_dir_and_add_entry(
4538        &self,
4539        parent_ino: u32,
4540        name: &[u8],
4541        target_ino: u32,
4542        file_type: crate::dir::DirEntryType,
4543    ) -> Result<()> {
4544        let bs = self.sb.block_size();
4545        let bs_u64 = bs as u64;
4546        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
4547
4548        // Re-read parent so we operate on the freshest on-disk bytes.
4549        let (parent_inode, mut parent_raw) = self.read_inode_verified(parent_ino)?;
4550        if !parent_inode.is_dir() {
4551            return Err(Error::NotADirectory);
4552        }
4553        let new_logical_block = parent_inode.size.div_ceil(bs_u64);
4554
4555        // 1. Allocate one fs block. Hint to parent's group.
4556        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
4557        let mut bitmap_reader = |block: u64| self.read_block(block);
4558        let plan = crate::alloc::plan_block_allocation(
4559            &self.sb,
4560            &self.allocation_groups(),
4561            1,
4562            parent_group,
4563            &mut bitmap_reader,
4564        )?;
4565        let new_phys = plan.first_block;
4566
4567        // 2. Insert extent into parent's inline extent root. If the root is
4568        //    saturated at depth 0, promote to depth 1 by allocating a fresh
4569        //    leaf block, moving all entries into it, and writing a single
4570        //    index entry into the inline root.
4571        let new_extent = crate::extent::Extent {
4572            logical_block: new_logical_block as u32,
4573            length: 1,
4574            physical_block: new_phys,
4575            uninitialized: false,
4576        };
4577        // If the parent root is already promoted (depth ≥ 1), operate on the
4578        // leaf block directly instead of the 60-byte inline root. This keeps
4579        // the inode.block area unchanged; only the leaf-node physical block
4580        // gets rewritten.
4581        let root_header = crate::extent::ExtentHeader::parse(&parent_inode.block)?;
4582        if root_header.depth == 1 {
4583            return self.extend_dir_and_add_entry_depth1(
4584                parent_ino,
4585                &parent_inode,
4586                &mut parent_raw,
4587                name,
4588                target_ino,
4589                file_type,
4590                has_ft,
4591                new_phys,
4592                new_extent,
4593                plan,
4594            );
4595        }
4596        if root_header.depth > 1 {
4597            return self.extend_dir_and_add_entry_deep(
4598                parent_ino,
4599                &parent_inode,
4600                &mut parent_raw,
4601                name,
4602                target_ino,
4603                file_type,
4604                has_ft,
4605                new_phys,
4606                new_extent,
4607                plan,
4608            );
4609        }
4610
4611        let (new_root, leaf_meta_alloc) =
4612            match crate::extent_mut::plan_insert_extent(&parent_inode.block, new_extent) {
4613                Ok(muts) => {
4614                    let root = muts
4615                        .into_iter()
4616                        .find_map(|m| match m {
4617                            crate::extent_mut::ExtentMutation::WriteRoot { bytes } => Some(bytes),
4618                            _ => None,
4619                        })
4620                        .ok_or(Error::Corrupt(
4621                            "extend_dir_and_add_entry: plan produced no WriteRoot",
4622                        ))?;
4623                    (root, None)
4624                }
4625                Err(Error::CorruptExtentTree(msg)) if msg.contains("LEAF_FULL_NEEDS_PROMOTION") => {
4626                    // Commit the data-block allocation NOW so the next plan picks
4627                    // a different run (plan_block_allocation reads the bitmap).
4628                    self.commit_dir_block_alloc(new_phys, &plan)?;
4629
4630                    // Second allocation: the leaf node block.
4631                    let mut reader2 = |block: u64| -> Result<Vec<u8>> {
4632                        let mut buf = vec![0u8; bs as usize];
4633                        self.dev.read_at(block * bs_u64, &mut buf)?;
4634                        Ok(buf)
4635                    };
4636                    let meta_plan = crate::alloc::plan_block_allocation(
4637                        &self.sb,
4638                        &self.allocation_groups(),
4639                        1,
4640                        parent_group,
4641                        &mut reader2,
4642                    )?;
4643                    let leaf_meta_phys = meta_plan.first_block;
4644
4645                    let promo = crate::extent_mut::plan_promote_leaf(
4646                        &parent_inode.block,
4647                        new_extent,
4648                        bs as usize,
4649                        leaf_meta_phys,
4650                        self.csum.enabled,
4651                    )?;
4652                    let mut leaf = promo.leaf_bytes;
4653                    if self.csum.enabled {
4654                        self.csum
4655                            .patch_extent_tail(parent_ino, parent_inode.generation, &mut leaf);
4656                    }
4657                    self.dev.write_at(leaf_meta_phys * bs_u64, &leaf)?;
4658                    (promo.new_root_bytes, Some(meta_plan))
4659                }
4660                Err(e) => return Err(e),
4661            };
4662        Self::patch_inode_block_area(&mut parent_raw, &new_root)?;
4663
4664        // 3. Patch size (+= block_size) and i_blocks. On the promotion path
4665        //    the inode claims both the data block AND the leaf-node block.
4666        let blocks_consumed: u64 = 1 + if leaf_meta_alloc.is_some() { 1 } else { 0 };
4667        let new_size = parent_inode.size + bs_u64;
4668        let new_blocks = parent_inode.blocks + (bs_u64 / 512) * blocks_consumed;
4669        Self::patch_inode_size_and_blocks(&mut parent_raw, new_size, new_blocks)?;
4670
4671        // 4. Recompute parent inode CSUM and write it back.
4672        if self.csum.enabled {
4673            if let Some((lo, hi)) =
4674                self.csum
4675                    .compute_inode_checksum(parent_ino, parent_inode.generation, &parent_raw)
4676            {
4677                parent_raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
4678                if parent_raw.len() >= 0x84 {
4679                    parent_raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
4680                }
4681            }
4682        }
4683        self.write_inode_raw(parent_ino, &parent_raw)?;
4684
4685        // 5. Seed the new data block with a "whole-block unused" placeholder
4686        //    that add_entry_to_block can split into (new entry + remainder).
4687        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
4688        let usable = (bs as usize) - reserved_tail;
4689        let mut block = vec![0u8; bs as usize];
4690        block[0..4].copy_from_slice(&0u32.to_le_bytes());
4691        block[4..6].copy_from_slice(&(usable as u16).to_le_bytes());
4692
4693        crate::dir::add_entry_to_block(
4694            &mut block,
4695            target_ino,
4696            name,
4697            file_type,
4698            has_ft,
4699            reserved_tail,
4700        )?;
4701
4702        if self.csum.enabled && reserved_tail == 12 {
4703            self.csum
4704                .patch_dir_entry_tail(parent_ino, parent_inode.generation, &mut block);
4705        }
4706        self.dev.write_at(new_phys * bs_u64, &block)?;
4707
4708        // 6. Commit block allocator side-effects. On the promotion path the
4709        //    data-block allocation was already committed above; here we only
4710        //    commit the leaf-node allocation. On the simple path we commit the
4711        //    data block as usual.
4712        if let Some(meta_plan) = leaf_meta_alloc {
4713            self.commit_dir_block_alloc(meta_plan.first_block, &meta_plan)?;
4714        } else {
4715            self.commit_dir_block_alloc(new_phys, &plan)?;
4716        }
4717
4718        Ok(())
4719    }
4720
4721    /// Grow a directory whose extent tree is already at depth ≥ 2.
4722    /// Uses `plan_insert_extent_deep` to navigate and split the tree,
4723    /// allocating index-node blocks on demand via `plan_block_allocation`.
4724    /// The pre-allocated data block `new_phys` is committed first so the
4725    /// alloc closure won't re-use it for tree-meta blocks.
4726    #[allow(clippy::too_many_arguments)]
4727    fn extend_dir_and_add_entry_deep(
4728        &self,
4729        parent_ino: u32,
4730        parent_inode: &Inode,
4731        parent_raw: &mut [u8],
4732        name: &[u8],
4733        target_ino: u32,
4734        file_type: crate::dir::DirEntryType,
4735        has_ft: bool,
4736        new_phys: u64,
4737        new_extent: crate::extent::Extent,
4738        data_plan: crate::alloc::BlockAllocationPlan,
4739    ) -> Result<()> {
4740        let bs = self.sb.block_size();
4741        let bs_u64 = bs as u64;
4742        let parent_group = (parent_ino - 1) / self.sb.inodes_per_group;
4743
4744        // Collect all allocation plans without committing them yet.  Committing
4745        // eagerly (old behaviour) leaked blocks permanently when
4746        // plan_insert_extent_deep or the subsequent writes failed — the bitmap
4747        // was marked used but no extent ever referenced those blocks.  Instead,
4748        // we gather all plans and commit them only after every write succeeds,
4749        // matching the late-commit ordering of extend_dir_and_add_entry_depth1.
4750        //
4751        // To prevent alloc_fn from picking data_plan.first_block for a meta
4752        // node (which plan_block_allocation could do since the bitmap is
4753        // unchanged), the closure skips that block and retries once.
4754        let data_block = data_plan.first_block;
4755        let mut pending_meta: Vec<crate::alloc::BlockAllocationPlan> = Vec::new();
4756
4757        let reader = FsBlockReader { fs: self };
4758        let mut meta_block_count: u64 = 0;
4759        let mut alloc_fn = || -> Result<u64> {
4760            let mut bm_reader = |block: u64| -> Result<Vec<u8>> {
4761                let mut buf = vec![0u8; bs as usize];
4762                self.dev.read_at(block * bs_u64, &mut buf)?;
4763                Ok(buf)
4764            };
4765            let meta_plan = crate::alloc::plan_block_allocation(
4766                &self.sb,
4767                &self.allocation_groups(),
4768                1,
4769                parent_group,
4770                &mut bm_reader,
4771            )?;
4772            if meta_plan.first_block == data_block {
4773                // The allocator returned the same block we reserved for the
4774                // data page.  There are no other free blocks in this group,
4775                // so the tree cannot grow further.
4776                return Err(Error::NoSpaceLeftOnDevice);
4777            }
4778            meta_block_count += 1;
4779            pending_meta.push(meta_plan);
4780            Ok(pending_meta.last().unwrap().first_block)
4781        };
4782
4783        let deep_plan = crate::extent_mut::plan_insert_extent_deep(
4784            &parent_inode.block,
4785            new_extent,
4786            bs,
4787            &reader,
4788            &mut alloc_fn,
4789        )?;
4790
4791        // Write tree-meta blocks (rewritten leaves + any new index nodes).
4792        for (block, mut bytes) in deep_plan.block_writes {
4793            if self.csum.enabled {
4794                self.csum
4795                    .patch_extent_tail(parent_ino, parent_inode.generation, &mut bytes);
4796            }
4797            self.dev.write_at(block * bs_u64, &bytes)?;
4798        }
4799
4800        // Patch inode: root bytes, size (+1 data block), i_blocks.
4801        Self::patch_inode_block_area(parent_raw, &deep_plan.new_root)?;
4802        let new_size = parent_inode.size + bs_u64;
4803        let new_blocks = parent_inode.blocks + (bs_u64 / 512) * (1 + meta_block_count);
4804        Self::patch_inode_size_and_blocks(parent_raw, new_size, new_blocks)?;
4805        if self.csum.enabled {
4806            if let Some((lo, hi)) =
4807                self.csum
4808                    .compute_inode_checksum(parent_ino, parent_inode.generation, parent_raw)
4809            {
4810                parent_raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
4811                if parent_raw.len() >= 0x84 {
4812                    parent_raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
4813                }
4814            }
4815        }
4816        self.write_inode_raw(parent_ino, parent_raw)?;
4817
4818        // Seed + write the new data block with the directory entry.
4819        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
4820        let usable = (bs as usize) - reserved_tail;
4821        let mut block = vec![0u8; bs as usize];
4822        block[0..4].copy_from_slice(&0u32.to_le_bytes());
4823        block[4..6].copy_from_slice(&(usable as u16).to_le_bytes());
4824        crate::dir::add_entry_to_block(
4825            &mut block,
4826            target_ino,
4827            name,
4828            file_type,
4829            has_ft,
4830            reserved_tail,
4831        )?;
4832        if self.csum.enabled && reserved_tail == 12 {
4833            self.csum
4834                .patch_dir_entry_tail(parent_ino, parent_inode.generation, &mut block);
4835        }
4836        self.dev.write_at(new_phys * bs_u64, &block)?;
4837
4838        // All writes succeeded — now commit the allocation accounting. Route
4839        // through commit_dir_block_alloc so the block-bitmap checksum is
4840        // refreshed together with the BGD + SB free-count deltas (the bare
4841        // mark_block_run_used + patch_*_counters sequence left the csum stale).
4842        self.commit_dir_block_alloc(data_plan.first_block, &data_plan)?;
4843        for plan in pending_meta {
4844            self.commit_dir_block_alloc(plan.first_block, &plan)?;
4845        }
4846
4847        Ok(())
4848    }
4849
4850    /// Grow a directory whose extent tree is already at depth 1 (i.e. has
4851    /// been promoted). The inline root holds a single index entry → one leaf
4852    /// block. The mutation happens entirely inside the leaf block; the inode
4853    /// root is unchanged.
4854    ///
4855    /// Leaf overflow (>340 entries in a 4 KiB block with csum) returns a
4856    /// clean error. Callers that hit this should retry via `extend_dir_and_add_entry_deep`.
4857    #[allow(clippy::too_many_arguments)]
4858    fn extend_dir_and_add_entry_depth1(
4859        &self,
4860        parent_ino: u32,
4861        parent_inode: &Inode,
4862        parent_raw: &mut [u8],
4863        name: &[u8],
4864        target_ino: u32,
4865        file_type: crate::dir::DirEntryType,
4866        has_ft: bool,
4867        new_phys: u64,
4868        new_extent: crate::extent::Extent,
4869        plan: crate::alloc::BlockAllocationPlan,
4870    ) -> Result<()> {
4871        let bs = self.sb.block_size();
4872        let bs_u64 = bs as u64;
4873
4874        // Resolve the single index entry in the 60-byte inline root.
4875        let idx = crate::extent::ExtentIdx::parse(
4876            &parent_inode.block
4877                [crate::extent::EXT4_EXT_NODE_SIZE..2 * crate::extent::EXT4_EXT_NODE_SIZE],
4878        )?;
4879        let leaf_phys = idx.leaf_block;
4880
4881        // Read the leaf block + run plan_insert_extent on its 4 KiB buffer.
4882        // `plan_insert_extent` operates on any depth-0 root — it uses
4883        // `header.max` for capacity, which was set to (bs-12-4)/12 = 340
4884        // when the leaf was built by `plan_promote_leaf`.
4885        let mut leaf = vec![0u8; bs as usize];
4886        self.dev.read_at(leaf_phys * bs_u64, &mut leaf)?;
4887        // CRC-verify before mutating — if the leaf's tail is corrupt we'd
4888        // write a false "fixed" version back.
4889        if self.csum.enabled
4890            && !self
4891                .csum
4892                .verify_extent_tail(parent_ino, parent_inode.generation, &leaf)
4893        {
4894            return Err(Error::BadChecksum {
4895                what: "extent block",
4896            });
4897        }
4898
4899        let muts = match crate::extent_mut::plan_insert_extent(&leaf, new_extent) {
4900            Ok(muts) => muts,
4901            Err(Error::CorruptExtentTree(msg)) if msg.contains("LEAF_FULL_NEEDS_PROMOTION") => {
4902                // The single depth-1 leaf is full (≥340 extents in a 4 KiB block
4903                // with csum). Fall back to the deep path, which handles adding a
4904                // sibling leaf or promoting to depth 2. The data block hasn't
4905                // been committed yet, so pass `plan` unchanged.
4906                return self.extend_dir_and_add_entry_deep(
4907                    parent_ino,
4908                    parent_inode,
4909                    parent_raw,
4910                    name,
4911                    target_ino,
4912                    file_type,
4913                    has_ft,
4914                    new_phys,
4915                    new_extent,
4916                    plan,
4917                );
4918            }
4919            Err(e) => return Err(e),
4920        };
4921        let new_leaf = muts
4922            .into_iter()
4923            .find_map(|m| match m {
4924                crate::extent_mut::ExtentMutation::WriteRoot { bytes } => Some(bytes),
4925                _ => None,
4926            })
4927            .ok_or(Error::Corrupt(
4928                "extend_dir_and_add_entry_depth1: plan produced no WriteRoot",
4929            ))?;
4930        let mut new_leaf = new_leaf;
4931        if self.csum.enabled {
4932            self.csum
4933                .patch_extent_tail(parent_ino, parent_inode.generation, &mut new_leaf);
4934        }
4935        self.dev.write_at(leaf_phys * bs_u64, &new_leaf)?;
4936
4937        // Inode root is unchanged — just grow size + blocks by one data block.
4938        let new_size = parent_inode.size + bs_u64;
4939        let new_blocks = parent_inode.blocks + (bs_u64 / 512);
4940        Self::patch_inode_size_and_blocks(parent_raw, new_size, new_blocks)?;
4941        if self.csum.enabled {
4942            if let Some((lo, hi)) =
4943                self.csum
4944                    .compute_inode_checksum(parent_ino, parent_inode.generation, parent_raw)
4945            {
4946                parent_raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
4947                if parent_raw.len() >= 0x84 {
4948                    parent_raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
4949                }
4950            }
4951        }
4952        self.write_inode_raw(parent_ino, parent_raw)?;
4953
4954        // Seed + write the new data block (same recipe as the depth-0 path).
4955        let reserved_tail = if self.csum.enabled { 12 } else { 0 };
4956        let usable = (bs as usize) - reserved_tail;
4957        let mut block = vec![0u8; bs as usize];
4958        block[0..4].copy_from_slice(&0u32.to_le_bytes());
4959        block[4..6].copy_from_slice(&(usable as u16).to_le_bytes());
4960
4961        crate::dir::add_entry_to_block(
4962            &mut block,
4963            target_ino,
4964            name,
4965            file_type,
4966            has_ft,
4967            reserved_tail,
4968        )?;
4969
4970        if self.csum.enabled && reserved_tail == 12 {
4971            self.csum
4972                .patch_dir_entry_tail(parent_ino, parent_inode.generation, &mut block);
4973        }
4974        self.dev.write_at(new_phys * bs_u64, &block)?;
4975
4976        // Commit data-block allocation.
4977        self.commit_dir_block_alloc(new_phys, &plan)?;
4978
4979        Ok(())
4980    }
4981
4982    /// Remove an empty directory at `path`. Requires the target to contain
4983    /// only `.` and `..`. Frees the data block(s) + inode, removes the
4984    /// entry from the parent, decrements parent's `i_links_count`.
4985    pub fn apply_rmdir(&self, path: &str) -> Result<()> {
4986        if !self.dev.is_writable() {
4987            return Err(Error::ReadOnly);
4988        }
4989        let (parent_path, base_name) = split_parent_and_base(path)?;
4990        let mut reader = |ino: u32| self.read_inode_verified(ino).map(|(i, _)| i);
4991        let parent_ino =
4992            crate::path::lookup(self.dev.as_ref(), &self.sb, &mut reader, &parent_path)?;
4993        let (parent_inode, mut parent_raw) = self.read_inode_verified(parent_ino)?;
4994        if !parent_inode.is_dir() {
4995            return Err(Error::NotADirectory);
4996        }
4997        let target_ino = self.find_entry_in_dir(&parent_inode, base_name.as_bytes())?;
4998        let (target_inode, _) = self.read_inode_verified(target_ino)?;
4999        if !target_inode.is_dir() {
5000            return Err(Error::NotADirectory);
5001        }
5002
5003        // Empty-check: walk every block, reject if any entry is not "." or "..".
5004        let bs = self.sb.block_size();
5005        let has_ft = self.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
5006        let blocks = target_inode.size.div_ceil(bs as u64);
5007        for logical in 0..blocks {
5008            let Some(phys) =
5009                crate::extent::map_logical(&target_inode.block, self.dev.as_ref(), bs, logical)?
5010            else {
5011                continue;
5012            };
5013            let block = self.read_block(phys)?;
5014            for entry in crate::dir::DirBlockIter::new(&block, has_ft) {
5015                let e = entry?;
5016                if e.name != b"." && e.name != b".." {
5017                    return Err(Error::DirectoryNotEmpty);
5018                }
5019            }
5020        }
5021
5022        // Multi-block transaction: free target data blocks + free inode +
5023        // remove parent's dir entry + decrement parent nlink, all atomic.
5024        let mut buf = BlockBuffer::new(bs);
5025
5026        // Free target's data blocks. Each freed run credits its own group's
5027        // BGD; SB credit accumulates and lands once below.
5028        let extents = crate::extent::collect_all(&target_inode.block, self.dev.as_ref(), bs)?;
5029        let mut freed_blocks: u64 = 0;
5030        for e in &extents {
5031            freed_blocks +=
5032                self.buffer_free_block_run_and_bgd(&mut buf, e.physical_block, e.length as u64)?;
5033        }
5034
5035        // Free the inode slot. A removed dir decrements `bg_used_dirs_count`
5036        // — buffer_free_inode_slot already credits free_inodes by +1, so we
5037        // separately patch used_dirs by -1 here.
5038        self.buffer_free_inode_slot(&mut buf, target_ino)?;
5039        let target_gi = ((target_ino - 1) / self.sb.inodes_per_group) as usize;
5040        self.buffer_patch_bgd_counters(&mut buf, target_gi, 0, 0, -1)?;
5041        // SB: free_blocks_count += freed, free_inodes_count += 1.
5042        self.buffer_patch_sb_counters(&mut buf, freed_blocks as i64, 1)?;
5043
5044        // Zero the freed directory inode body (mode/links -> 0, set dtime, keep
5045        // the generation) so the slot no longer reads as a live directory.
5046        // Without this the freed inode keeps S_IFDIR + its "." / ".." and
5047        // e2fsck reports "unconnected directory inode", a stale ".." and bad
5048        // refcounts — the same cleanup apply_unlink already does for files.
5049        let inode_size = self.sb.inode_size as usize;
5050        let mut target_raw = vec![0u8; inode_size];
5051        let dtime = now_unix_seconds();
5052        target_raw[0x14..0x18].copy_from_slice(&dtime.to_le_bytes());
5053        target_raw[0x64..0x68].copy_from_slice(&target_inode.generation.to_le_bytes());
5054        self.finalize_inode_raw(target_ino, target_inode.generation, &mut target_raw)?;
5055        self.buffer_write_inode(&mut buf, target_ino, &target_raw)?;
5056
5057        // Remove the entry from the parent directory.
5058        let parent_blocks = parent_inode.size.div_ceil(bs as u64);
5059        let mut removed = false;
5060        for logical in 0..parent_blocks {
5061            let Some(phys) = self.map_inode_logical(&parent_inode, logical)? else {
5062                continue;
5063            };
5064            let block = buf.get_mut(self, phys)?;
5065            let reserved_tail = if self.csum.enabled && crate::dir::has_csum_tail(block) {
5066                12
5067            } else {
5068                0
5069            };
5070            if crate::dir::remove_entry_from_block(
5071                block,
5072                base_name.as_bytes(),
5073                has_ft,
5074                reserved_tail,
5075            )? {
5076                if self.csum.enabled && reserved_tail == 12 {
5077                    self.csum
5078                        .patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
5079                }
5080                removed = true;
5081                break;
5082            }
5083        }
5084        if !removed {
5085            return Err(Error::Corrupt(
5086                "apply_rmdir: entry disappeared mid-operation",
5087            ));
5088        }
5089
5090        // Parent loses the ".." reference from the removed child → nlink -1.
5091        self.patch_inode_nlink(parent_ino, &mut parent_raw, &parent_inode, -1)?;
5092        self.buffer_write_inode(&mut buf, parent_ino, &parent_raw)?;
5093
5094        self.commit_block_buffer(buf)
5095    }
5096}
5097
5098#[cfg(test)]
5099mod tests {
5100    use super::*;
5101    use crate::inode::{
5102        EXTRA_ISIZE_DEFAULT, INODE_SIZE_WITH_CRTIME, INODE_SIZE_WITH_EXTRA, OFF_ATIME, OFF_CRTIME,
5103        OFF_CTIME, OFF_EXTRA_ISIZE, OFF_GENERATION, OFF_MTIME,
5104    };
5105
5106    fn read_le32(buf: &[u8], off: usize) -> u32 {
5107        u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
5108    }
5109    fn read_le16(buf: &[u8], off: usize) -> u16 {
5110        u16::from_le_bytes(buf[off..off + 2].try_into().unwrap())
5111    }
5112
5113    // --- write_inode_timestamps ---
5114
5115    #[test]
5116    fn write_inode_timestamps_sets_atime_ctime_mtime() {
5117        let mut raw = vec![0u8; 256];
5118        write_inode_timestamps(&mut raw, 0xDEAD_BEEF);
5119        assert_eq!(read_le32(&raw, OFF_ATIME), 0xDEAD_BEEF);
5120        assert_eq!(read_le32(&raw, OFF_CTIME), 0xDEAD_BEEF);
5121        assert_eq!(read_le32(&raw, OFF_MTIME), 0xDEAD_BEEF);
5122    }
5123
5124    #[test]
5125    fn write_inode_timestamps_sets_crtime_when_large_enough() {
5126        let mut raw = vec![0u8; INODE_SIZE_WITH_CRTIME + 4];
5127        write_inode_timestamps(&mut raw, 0x1234_5678);
5128        assert_eq!(read_le32(&raw, OFF_CRTIME), 0x1234_5678);
5129    }
5130
5131    #[test]
5132    fn write_inode_timestamps_skips_crtime_when_too_small() {
5133        let mut raw = vec![0xAAu8; INODE_SIZE_WITH_CRTIME - 1];
5134        write_inode_timestamps(&mut raw, 0x1234_5678);
5135        // Buffer too small for crtime — no write, no panic.
5136        // atime/ctime/mtime still set.
5137        assert_eq!(read_le32(&raw, OFF_ATIME), 0x1234_5678);
5138    }
5139
5140    #[test]
5141    fn write_inode_timestamps_zero_now() {
5142        let mut raw = vec![0xFFu8; 256];
5143        write_inode_timestamps(&mut raw, 0);
5144        assert_eq!(read_le32(&raw, OFF_ATIME), 0);
5145        assert_eq!(read_le32(&raw, OFF_CTIME), 0);
5146        assert_eq!(read_le32(&raw, OFF_MTIME), 0);
5147        assert_eq!(read_le32(&raw, OFF_CRTIME), 0);
5148    }
5149
5150    // --- write_inode_generation ---
5151
5152    #[test]
5153    fn write_inode_generation_writes_at_correct_offset() {
5154        let mut raw = vec![0u8; 256];
5155        write_inode_generation(&mut raw, 0xCAFE_BABE);
5156        assert_eq!(read_le32(&raw, OFF_GENERATION), 0xCAFE_BABE);
5157    }
5158
5159    #[test]
5160    fn write_inode_generation_overwrites_existing() {
5161        let mut raw = vec![0xFFu8; 256];
5162        write_inode_generation(&mut raw, 0);
5163        assert_eq!(read_le32(&raw, OFF_GENERATION), 0);
5164    }
5165
5166    // --- write_inode_extra_isize ---
5167
5168    #[test]
5169    fn write_inode_extra_isize_sets_default_when_large_enough() {
5170        let mut raw = vec![0u8; INODE_SIZE_WITH_EXTRA + 4];
5171        write_inode_extra_isize(&mut raw);
5172        assert_eq!(read_le16(&raw, OFF_EXTRA_ISIZE), EXTRA_ISIZE_DEFAULT);
5173    }
5174
5175    #[test]
5176    fn write_inode_extra_isize_skips_when_too_small() {
5177        let mut raw = vec![0u8; INODE_SIZE_WITH_EXTRA - 1];
5178        write_inode_extra_isize(&mut raw); // must not panic
5179                                           // No bytes should have been written — buffer too small.
5180    }
5181
5182    // --- alloc_inode_generation ---
5183
5184    #[test]
5185    fn alloc_inode_generation_produces_unique_values() {
5186        let g1 = alloc_inode_generation();
5187        let g2 = alloc_inode_generation();
5188        assert_ne!(g1, g2, "successive calls must produce distinct values");
5189    }
5190}