Skip to main content

fs_ext4/
alloc.rs

1//! Block + inode bitmap allocator — planning layer.
2//!
3//! Phase 4 write path scaffolding. This module produces typed
4//! [`AllocationPlan`] values describing what bits to flip in which bitmap
5//! block + the updated free-counter deltas. It does NOT write to disk;
6//! E11 (journaled writes) will apply the plans atomically under a
7//! JBD2 transaction.
8//!
9//! Rationale: separating allocation (pure function over bitmap bytes) from
10//! commit (journaled block write) makes tests trivial and keeps the
11//! read-only mount path untouched. Block device traits stay read-only in
12//! Phase 1; the write trait lives at the commit boundary.
13//!
14//! ### Block bitmap layout
15//! One bit per block in the group. Bit `i` = block `group_start + i`.
16//! A 4 KiB block bitmap covers `32768` blocks (one block group on a
17//! 4 KiB-block fs). Bits are packed LSB-first within each byte: bit 0 of
18//! byte 0 represents the first block in the group.
19//!
20//! ### Inode bitmap layout
21//! Same LSB-first packing. Bit `i` = inode `(group_idx * inodes_per_group) + i + 1`
22//! (inode numbers are 1-based).
23//!
24//! ### Orlov allocator (directories)
25//! Linux ext4 chooses a group for new directories using the Orlov heuristic:
26//! prefer groups whose `(free_blocks, free_inodes, used_dirs)` triple is
27//! "below average" — distributing directories evenly across groups so sibling
28//! files end up near their parent dir. We implement a simplified variant:
29//! iterate groups starting from `hint`, prefer one whose used_dirs is below
30//! the fleet average and has the most free_inodes.
31
32use crate::bgd::{BgdFlags, BlockGroupDescriptor};
33use crate::error::{Error, Result};
34use crate::superblock::Superblock;
35
36/// A change to one bitmap block: flip bits `bit_start .. bit_start + count`
37/// from 0 (free) to 1 (used). The new bitmap bytes are NOT materialised here —
38/// only the semantic description is.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct BitmapWrite {
41    /// Physical block number of the bitmap (from `bg_block_bitmap` or
42    /// `bg_inode_bitmap`).
43    pub bitmap_block: u64,
44    /// First bit index within this bitmap to flip.
45    pub bit_start: u32,
46    /// Number of consecutive bits to flip.
47    pub count: u32,
48    /// `true` if marking used, `false` if freeing.
49    pub set: bool,
50}
51
52/// A change to one block-group descriptor's free-counter and/or
53/// used_dirs_count. Applied together with the matching [`BitmapWrite`].
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct BgdCounterUpdate {
56    pub group_idx: u32,
57    /// Delta to apply to `bg_free_blocks_count` (+free, -allocated).
58    pub free_blocks_delta: i32,
59    /// Delta to apply to `bg_free_inodes_count`.
60    pub free_inodes_delta: i32,
61    /// Delta to apply to `bg_used_dirs_count`.
62    pub used_dirs_delta: i32,
63}
64
65/// A change to the superblock free-counter totals.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct SuperblockCounterUpdate {
68    pub free_blocks_delta: i64,
69    pub free_inodes_delta: i32,
70}
71
72/// Complete plan for one block-allocation request.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct BlockAllocationPlan {
75    /// First allocated fs block (absolute, not group-relative).
76    pub first_block: u64,
77    /// Number of contiguous blocks allocated.
78    pub count: u32,
79    pub bitmap: BitmapWrite,
80    pub bgd: BgdCounterUpdate,
81    pub sb: SuperblockCounterUpdate,
82}
83
84/// Complete plan for one inode-allocation request.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct InodeAllocationPlan {
87    /// Allocated inode number (1-based).
88    pub inode: u32,
89    /// True if this was a directory allocation (also bumps used_dirs).
90    pub is_dir: bool,
91    pub bitmap: BitmapWrite,
92    pub bgd: BgdCounterUpdate,
93    pub sb: SuperblockCounterUpdate,
94}
95
96// ---------------------------------------------------------------------------
97// Pure bit-manipulation helpers — unit-testable without a device
98// ---------------------------------------------------------------------------
99
100/// Test bit `idx` in a bitmap (LSB-first within each byte).
101#[inline]
102pub fn bit_is_set(bitmap: &[u8], idx: u32) -> bool {
103    let byte = (idx / 8) as usize;
104    let mask = 1u8 << (idx % 8);
105    byte < bitmap.len() && bitmap[byte] & mask != 0
106}
107
108/// Find the first free (0-valued) bit at or after `start`, searching up to
109/// `max_bits` total. Returns `None` if none found.
110///
111/// Fast path: once `start` is aligned to an 8-byte word, we scan the bitmap
112/// as `u64`s and skip any word of all-ones in a single branch. On sparse
113/// bitmaps (typical after mkfs) the scan is effectively memory-bandwidth
114/// bound and ~8–16× faster than per-bit `bit_is_set`.
115pub fn find_first_free(bitmap: &[u8], start: u32, max_bits: u32) -> Option<u32> {
116    // A BIT THAT IS NOT IN THE BITMAP IS NOT A FREE BIT.
117    //
118    // `bit_is_set` answers "not set" for an index past the end of the
119    // buffer, which reads as free, and `max_bits` comes from
120    // `s_blocks_per_group` / `s_inodes_per_group` -- superblock fields
121    // that nothing bounds to what a bitmap block can hold. So a group
122    // claiming 2^31 inodes per group returned bit indices that are not
123    // in its bitmap at all: the plan named a real inode or block, and
124    // the write that was supposed to mark it used silently did nothing
125    // while the counters were debited anyway. The next allocation then
126    // returned the same one.
127    let max_bits = max_bits.min(u32::try_from(bitmap.len().saturating_mul(8)).unwrap_or(u32::MAX));
128    if start >= max_bits {
129        return None;
130    }
131    let mut i = start;
132
133    // 1) Scan to the next 64-bit-aligned bit boundary with the per-bit path.
134    while i < max_bits && !i.is_multiple_of(64) {
135        if !bit_is_set(bitmap, i) {
136            return Some(i);
137        }
138        i += 1;
139    }
140
141    // 2) Word-at-a-time scan. Every word that is not `u64::MAX` has at least
142    //    one zero bit; `trailing_ones` pinpoints the first one in LSB order
143    //    (matching ext4's LSB-first within-byte convention).
144    while i + 64 <= max_bits {
145        let byte = (i as usize) / 8;
146        if byte + 8 > bitmap.len() {
147            break;
148        }
149        let word = u64::from_le_bytes(bitmap[byte..byte + 8].try_into().unwrap());
150        if word != u64::MAX {
151            let bit = word.trailing_ones();
152            let cand = i + bit;
153            // Guard against spurious max_bits boundary within the word.
154            if cand < max_bits {
155                return Some(cand);
156            }
157            return None;
158        }
159        i += 64;
160    }
161
162    // 3) Tail — any remaining bits below `max_bits` go through the per-bit path.
163    while i < max_bits {
164        if !bit_is_set(bitmap, i) {
165            return Some(i);
166        }
167        i += 1;
168    }
169    None
170}
171
172/// Find the first run of `count` consecutive free bits at or after `start`,
173/// within the first `max_bits` bits of the bitmap. Returns the starting bit
174/// index of the run, or `None` if no such run exists.
175///
176/// Phase 8.3 vectorization: the outer "find a starting candidate" step
177/// uses [`find_first_free`] (u64-stride skip over fully-used regions),
178/// then the run-length verification walks bit-at-a-time. On sparse
179/// bitmaps (typical post-mkfs) this is effectively memory-bandwidth
180/// bound; on densely-packed bitmaps it skips fully-used 64-bit words in
181/// one branch instead of 64.
182pub fn find_free_run(bitmap: &[u8], start: u32, max_bits: u32, count: u32) -> Option<u32> {
183    if count == 0 {
184        return None;
185    }
186    let mut i = start;
187    while i + count <= max_bits {
188        // Vectorized: jump straight to the next free bit at-or-after `i`,
189        // skipping all-ones words 64 bits at a time.
190        let run_start = find_first_free(bitmap, i, max_bits)?;
191        if run_start + count > max_bits {
192            return None;
193        }
194        // Verify `count` contiguous free bits — bit-at-a-time, since the
195        // blocker (if any) almost always sits within the first few bits.
196        let mut j = run_start + 1;
197        while j < run_start + count && !bit_is_set(bitmap, j) {
198            j += 1;
199        }
200        if j - run_start >= count {
201            return Some(run_start);
202        }
203        // Hit a used bit before reaching `count`; skip past it and retry.
204        i = j + 1;
205    }
206    None
207}
208
209// ---------------------------------------------------------------------------
210// Block allocator (E5)
211// ---------------------------------------------------------------------------
212
213/// Plan allocation of `count` contiguous blocks.
214///
215/// `bitmap_reader` is called with a BGD's `bg_block_bitmap` and must return
216/// the full bitmap block (block_size bytes). Groups are tried in order:
217/// hint_group first, then wrapping forward. For groups flagged
218/// `BLOCK_UNINIT`, the bitmap is treated as all-free without reading.
219pub fn plan_block_allocation<F>(
220    sb: &Superblock,
221    groups: &[BlockGroupDescriptor],
222    count: u32,
223    hint_group: u32,
224    mut bitmap_reader: F,
225) -> Result<BlockAllocationPlan>
226where
227    F: FnMut(u64) -> Result<Vec<u8>>,
228{
229    if count == 0 {
230        return Err(Error::Corrupt("plan_block_allocation: count == 0"));
231    }
232    let blocks_per_group = sb.blocks_per_group;
233    let ngroups = groups.len() as u32;
234    if ngroups == 0 {
235        return Err(Error::Corrupt("no block groups"));
236    }
237    let hint = hint_group.min(ngroups.saturating_sub(1));
238
239    for step in 0..ngroups {
240        let gi = (hint + step) % ngroups;
241        let bgd = &groups[gi as usize];
242        if bgd.free_blocks_count < count {
243            continue;
244        }
245        // Compute how many blocks are actually valid in this group (last group
246        // may be short).
247        let max_bits = blocks_in_group(sb, gi);
248
249        let bitmap_bytes: Vec<u8> = if bgd.flags().contains(BgdFlags::BLOCK_UNINIT) {
250            vec![0u8; sb.block_size() as usize]
251        } else {
252            bitmap_reader(bgd.block_bitmap)?
253        };
254
255        let Some(bit_start) = find_free_run(&bitmap_bytes, 0, max_bits, count) else {
256            continue;
257        };
258
259        let group_first_block =
260            (gi as u64) * (blocks_per_group as u64) + sb.first_data_block as u64;
261        let first_block = group_first_block + bit_start as u64;
262
263        return Ok(BlockAllocationPlan {
264            first_block,
265            count,
266            bitmap: BitmapWrite {
267                bitmap_block: bgd.block_bitmap,
268                bit_start,
269                count,
270                set: true,
271            },
272            bgd: BgdCounterUpdate {
273                group_idx: gi,
274                free_blocks_delta: -(count as i32),
275                free_inodes_delta: 0,
276                used_dirs_delta: 0,
277            },
278            sb: SuperblockCounterUpdate {
279                free_blocks_delta: -(count as i64),
280                free_inodes_delta: 0,
281            },
282        });
283    }
284
285    Err(Error::Corrupt(
286        "no group has a contiguous free run of this size",
287    ))
288}
289
290/// Returns the number of blocks that actually exist in group `gi` (the last
291/// group may be shorter than `blocks_per_group`).
292fn blocks_in_group(sb: &Superblock, gi: u32) -> u32 {
293    let ngroups = sb.block_group_count() as u32;
294    if gi + 1 < ngroups {
295        return sb.blocks_per_group;
296    }
297    // Saturating: `s_first_data_block` is not among the fields
298    // `Superblock::parse` bounds, and a value above `blocks_count`
299    // wrapped this subtraction -- which made the last group's bit
300    // ceiling far larger than the group, so the allocator handed out
301    // blocks outside the filesystem.
302    let remainder =
303        sb.blocks_count.saturating_sub(sb.first_data_block as u64) % sb.blocks_per_group as u64;
304    if remainder == 0 {
305        sb.blocks_per_group
306    } else {
307        remainder as u32
308    }
309}
310
311// ---------------------------------------------------------------------------
312// Inode allocator (E6)
313// ---------------------------------------------------------------------------
314
315/// Plan allocation of a single inode. For directories, uses a simplified
316/// Orlov heuristic to spread dirs across groups; for regular files, prefers
317/// the `hint_group` (typically the parent directory's group).
318pub fn plan_inode_allocation<F>(
319    sb: &Superblock,
320    groups: &[BlockGroupDescriptor],
321    is_dir: bool,
322    hint_group: u32,
323    mut bitmap_reader: F,
324) -> Result<InodeAllocationPlan>
325where
326    F: FnMut(u64) -> Result<Vec<u8>>,
327{
328    let ngroups = groups.len() as u32;
329    if ngroups == 0 {
330        return Err(Error::Corrupt("no block groups"));
331    }
332
333    let start_group = if is_dir {
334        orlov_select_group(groups, hint_group)
335    } else {
336        hint_group.min(ngroups.saturating_sub(1))
337    };
338
339    for step in 0..ngroups {
340        let gi = (start_group + step) % ngroups;
341        let bgd = &groups[gi as usize];
342        if bgd.free_inodes_count == 0 {
343            continue;
344        }
345
346        let max_bits = sb.inodes_per_group;
347        let bitmap_bytes: Vec<u8> = if bgd.flags().contains(BgdFlags::INODE_UNINIT) {
348            vec![0u8; sb.block_size() as usize]
349        } else {
350            bitmap_reader(bgd.inode_bitmap)?
351        };
352
353        // THE RESERVED INODES ARE NOT AVAILABLE.
354        //
355        // Inodes below `s_first_ino` are the filesystem's own: 2 is the
356        // root directory, 8 the journal. On a filesystem mke2fs wrote
357        // their bits are set, so scanning from zero skipped them by
358        // accident -- but the bitmap is bytes off the disk, and one
359        // with those bits clear (or a group 0 flagged INODE_UNINIT,
360        // which is read as an all-zero bitmap without being read at
361        // all) handed out inode 2. `apply_create` then writes the new
362        // file's inode image over the root directory's.
363        let floor = if gi == 0 {
364            sb.first_inode.saturating_sub(1)
365        } else {
366            0
367        };
368        let Some(bit_start) = find_first_free(&bitmap_bytes, floor, max_bits) else {
369            continue;
370        };
371
372        // Inode numbers are 1-based: first inode in group 0 is inode 1.
373        // Checked, because `inodes_per_group` is a superblock field and
374        // the product wrapped in release -- a group-2 allocation came
375        // back as inode 2 and was written over the root inode while
376        // group 2's counters were debited.
377        let inode = u64::from(gi)
378            .checked_mul(u64::from(sb.inodes_per_group))
379            .and_then(|base| base.checked_add(u64::from(bit_start) + 1))
380            .filter(|n| *n <= u64::from(sb.inodes_count))
381            .ok_or(Error::Corrupt(
382                "the group's inode range does not fit in the filesystem",
383            ))? as u32;
384
385        return Ok(InodeAllocationPlan {
386            inode,
387            is_dir,
388            bitmap: BitmapWrite {
389                bitmap_block: bgd.inode_bitmap,
390                bit_start,
391                count: 1,
392                set: true,
393            },
394            bgd: BgdCounterUpdate {
395                group_idx: gi,
396                free_blocks_delta: 0,
397                free_inodes_delta: -1,
398                used_dirs_delta: if is_dir { 1 } else { 0 },
399            },
400            sb: SuperblockCounterUpdate {
401                free_blocks_delta: 0,
402                free_inodes_delta: -1,
403            },
404        });
405    }
406
407    Err(Error::Corrupt("no group has a free inode"))
408}
409
410/// Orlov group selection (simplified). Chooses the group among the ngroups
411/// starting at `hint` that currently has the fewest directories AND at least
412/// average free inodes. If no group is clearly "good", falls back to `hint`.
413fn orlov_select_group(groups: &[BlockGroupDescriptor], hint: u32) -> u32 {
414    let ngroups = groups.len() as u32;
415    if ngroups == 0 {
416        return 0;
417    }
418    let hint = hint.min(ngroups.saturating_sub(1));
419
420    let total_free_inodes: u64 = groups.iter().map(|g| g.free_inodes_count as u64).sum();
421    let total_used_dirs: u64 = groups.iter().map(|g| g.used_dirs_count as u64).sum();
422    let avg_free_inodes = total_free_inodes / ngroups as u64;
423    let avg_used_dirs = total_used_dirs / ngroups as u64;
424
425    // Walk all groups from hint and pick the first that has more free inodes
426    // than the average AND fewer used dirs than the average. Fall back to the
427    // group with the most free inodes overall.
428    let mut best: Option<u32> = None;
429    let mut best_score: i64 = i64::MIN;
430    for step in 0..ngroups {
431        let gi = (hint + step) % ngroups;
432        let g = &groups[gi as usize];
433        let fi = g.free_inodes_count as i64;
434        let ud = g.used_dirs_count as i64;
435        // Score: bonus if above avg inodes and below avg dirs.
436        let mut score = fi - ud;
437        if fi >= avg_free_inodes as i64 {
438            score += 1000;
439        }
440        if ud <= avg_used_dirs as i64 {
441            score += 500;
442        }
443        if score > best_score && g.free_inodes_count > 0 {
444            best_score = score;
445            best = Some(gi);
446        }
447    }
448    best.unwrap_or(hint)
449}
450
451// ---------------------------------------------------------------------------
452// Plan application helpers — pure functions that mutate caller-owned buffers.
453// The actual disk writes happen in E11 (journaled writes).
454// ---------------------------------------------------------------------------
455
456/// Apply a [`BitmapWrite`] to a bitmap buffer in place.
457pub fn apply_bitmap_write(buf: &mut [u8], w: &BitmapWrite) {
458    for b in 0..w.count {
459        let idx = (w.bit_start + b) as usize;
460        let byte = idx / 8;
461        let mask = 1u8 << (idx % 8);
462        if byte >= buf.len() {
463            break;
464        }
465        if w.set {
466            buf[byte] |= mask;
467        } else {
468            buf[byte] &= !mask;
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    fn mk_sb(
478        block_size: u32,
479        blocks_per_group: u32,
480        inodes_per_group: u32,
481        total_blocks: u64,
482    ) -> Superblock {
483        // Minimal superblock constructed from a synthetic buffer.
484        let mut raw = vec![0u8; crate::superblock::SUPERBLOCK_SIZE];
485        // magic
486        raw[0x38..0x3A].copy_from_slice(&(crate::superblock::EXT4_MAGIC).to_le_bytes());
487        raw[0x00..0x04].copy_from_slice(&(inodes_per_group * 4).to_le_bytes());
488        raw[0x04..0x08].copy_from_slice(&(total_blocks as u32).to_le_bytes());
489        raw[0x14..0x18].copy_from_slice(&1u32.to_le_bytes()); // first_data_block
490        raw[0x18..0x1C]
491            .copy_from_slice(&(block_size.trailing_zeros().saturating_sub(10)).to_le_bytes());
492        raw[0x20..0x24].copy_from_slice(&blocks_per_group.to_le_bytes());
493        raw[0x28..0x2C].copy_from_slice(&inodes_per_group.to_le_bytes());
494        raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level=1 so inode_size read
495        raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
496        raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); // desc_size
497        crate::superblock::Superblock::parse(raw).unwrap()
498    }
499
500    fn mk_bgd(
501        free_blocks: u32,
502        free_inodes: u32,
503        used_dirs: u32,
504        flags: u16,
505    ) -> BlockGroupDescriptor {
506        BlockGroupDescriptor {
507            block_bitmap: 100,
508            inode_bitmap: 200,
509            inode_table: 300,
510            free_blocks_count: free_blocks,
511            free_inodes_count: free_inodes,
512            used_dirs_count: used_dirs,
513            flags,
514            itable_unused: 0,
515            block_bitmap_csum: 0,
516            inode_bitmap_csum: 0,
517            checksum: 0,
518        }
519    }
520
521    /// `bit_is_set` answers "not set" for an index past the end of the
522    /// buffer, which reads as free -- and `max_bits` comes from
523    /// `s_blocks_per_group` / `s_inodes_per_group`, superblock fields
524    /// that nothing bounds to what a bitmap block can hold.
525    ///
526    /// The plan then names a real inode or block while the write that
527    /// marks it used silently does nothing (the byte is past the
528    /// buffer), and the counters are debited anyway. The next
529    /// allocation returns the same one: every file on the same block,
530    /// every new inode over the last.
531    #[test]
532    fn a_bit_outside_the_bitmap_is_not_a_free_bit() {
533        // One byte of bitmap, every bit used, but a group claiming
534        // 2^31 bits.
535        let full = vec![0xFFu8];
536        assert_eq!(find_first_free(&full, 0, 1 << 31), None);
537        // And with room in the byte, only the bits that are there.
538        let partly = vec![0x0Fu8];
539        assert_eq!(find_first_free(&partly, 0, 1 << 31), Some(4));
540        assert_eq!(find_first_free(&partly, 8, 1 << 31), None);
541        // An empty bitmap has no free bits, however many are claimed.
542        assert_eq!(find_first_free(&[], 0, 1 << 31), None);
543    }
544
545    /// Inodes below `s_first_ino` are the filesystem's own: 2 is the
546    /// root directory, 8 the journal. On a filesystem mke2fs wrote,
547    /// their bits are set, so scanning from zero skipped them by
548    /// accident -- but the bitmap is bytes off the disk, and a group 0
549    /// flagged INODE_UNINIT is read as an all-zero bitmap without being
550    /// read at all.
551    #[test]
552    fn a_reserved_inode_is_never_allocated() {
553        let sb = mk_sb(1024, 8192, 128, 1024);
554        // Group 0 declared uninitialised, so its bitmap is taken as
555        // all-free without a device read.
556        let groups = vec![mk_bgd(100, 128, 0, BgdFlags::INODE_UNINIT.bits())];
557        assert_eq!(
558            sb.first_inode,
559            crate::superblock::GOOD_OLD_FIRST_INODE,
560            "the fixture superblock leaves s_first_ino zero; the floor comes from the parser"
561        );
562        let plan = plan_inode_allocation(&sb, &groups, false, 0, |_| {
563            panic!("an uninitialised group is not read")
564        })
565        .expect("a group with free inodes");
566        assert!(
567            plan.inode >= sb.first_inode,
568            "allocated inode {} , where the first non-reserved one is {} -- \
569             writing it puts the new file's inode over the root directory's",
570            plan.inode,
571            sb.first_inode
572        );
573
574        // Group 1 has no reserved inodes, so it starts at its first bit.
575        let groups = vec![
576            mk_bgd(100, 0, 0, 0),
577            mk_bgd(100, 128, 0, BgdFlags::INODE_UNINIT.bits()),
578        ];
579        let plan = plan_inode_allocation(&sb, &groups, false, 1, |_| unreachable!()).unwrap();
580        assert_eq!(plan.inode, 128 + 1);
581    }
582
583    #[test]
584    fn find_first_free_walks_bits() {
585        let buf = vec![0xFF, 0x0F, 0x00]; // bits 0..11 set, 12..23 free
586        assert_eq!(find_first_free(&buf, 0, 24), Some(12));
587        assert_eq!(find_first_free(&buf, 20, 24), Some(20));
588    }
589
590    #[test]
591    fn find_first_free_word_aligned_fast_path() {
592        // 16 bytes = 128 bits. First 64 bits all set; bit 80 is the first free.
593        let mut buf = vec![0xFFu8; 8];
594        buf.extend_from_slice(&[0xFFu8; 2]); // bits 64..79 set
595        buf.push(0x00); // bit 80..87 free → first free is 80
596        buf.extend_from_slice(&[0xFFu8; 5]);
597        assert_eq!(find_first_free(&buf, 0, 128), Some(80));
598    }
599
600    #[test]
601    fn find_first_free_all_ones_in_range() {
602        // Whole range is allocated; must return None without overflow.
603        let buf = vec![0xFFu8; 32]; // 256 bits, all set
604        assert_eq!(find_first_free(&buf, 0, 256), None);
605        assert_eq!(find_first_free(&buf, 63, 256), None);
606        assert_eq!(find_first_free(&buf, 64, 256), None);
607    }
608
609    #[test]
610    fn find_first_free_respects_max_bits_mid_word() {
611        // Two zero bits starting at 64; max_bits caps at 65 → bit 64 valid, 65 out.
612        let mut buf = vec![0xFFu8; 8]; // bits 0..63 set
613        buf.push(0x00); // bits 64..71 free
614        buf.extend_from_slice(&[0xFFu8; 7]);
615        assert_eq!(find_first_free(&buf, 0, 65), Some(64));
616        assert_eq!(find_first_free(&buf, 0, 64), None);
617    }
618
619    #[test]
620    fn find_first_free_unaligned_start_matches_per_bit() {
621        // Reference: every result must agree with the simple per-bit implementation.
622        let buf: Vec<u8> = (0..128u8).collect(); // mixed pattern
623        let max = (buf.len() as u32) * 8;
624        for start in [0u32, 1, 7, 8, 63, 64, 65, 127, 200, 511] {
625            let fast = find_first_free(&buf, start, max);
626            let slow = {
627                let mut i = start;
628                loop {
629                    if i >= max {
630                        break None;
631                    }
632                    if !bit_is_set(&buf, i) {
633                        break Some(i);
634                    }
635                    i += 1;
636                }
637            };
638            assert_eq!(fast, slow, "start={start}");
639        }
640    }
641
642    #[test]
643    fn find_free_run_handles_gaps() {
644        // byte 0: bits 0,1 set; bits 2..=7 free. Byte 1: all set. Byte 2: all free.
645        let buf = vec![0b0000_0011, 0xFF, 0x00];
646        // Shortest run: bits 2..=7 = run of 6. A request for 5 fits at bit 2.
647        assert_eq!(find_free_run(&buf, 0, 24, 5), Some(2));
648        // A request for 7 cannot fit in bits 2..7 (only 6 free) — next run is byte 2.
649        assert_eq!(find_free_run(&buf, 0, 24, 7), Some(16));
650    }
651
652    #[test]
653    fn find_free_run_exact_fit() {
654        let buf = vec![0x00];
655        assert_eq!(find_free_run(&buf, 0, 8, 8), Some(0));
656    }
657
658    #[test]
659    fn find_free_run_rejects_too_short() {
660        let buf = vec![0xFE]; // bit 0 free, bits 1..7 used
661        assert_eq!(find_free_run(&buf, 0, 8, 2), None);
662    }
663
664    #[test]
665    fn block_allocation_uses_first_group_with_room() {
666        let sb = mk_sb(4096, 32768, 8192, 65536);
667        let g0 = mk_bgd(100, 8000, 0, 0);
668        let g1 = mk_bgd(20000, 8000, 0, 0);
669        let groups = vec![g0, g1];
670        let read = |_block: u64| -> Result<Vec<u8>> { Ok(vec![0u8; 4096]) };
671        let plan = plan_block_allocation(&sb, &groups, 10, 0, read).unwrap();
672        assert_eq!(plan.first_block, 1); // first_data_block=1, group 0 bit 0
673        assert_eq!(plan.count, 10);
674        assert_eq!(plan.bgd.group_idx, 0);
675        assert_eq!(plan.bgd.free_blocks_delta, -10);
676        assert_eq!(plan.sb.free_blocks_delta, -10);
677    }
678
679    #[test]
680    fn block_allocation_skips_full_group() {
681        let sb = mk_sb(4096, 32768, 8192, 65536);
682        let g0 = mk_bgd(5, 8000, 0, 0); // not enough for 10-block run
683        let g1 = mk_bgd(20000, 8000, 0, 0);
684        let groups = vec![g0, g1];
685        let read = |_b| Ok(vec![0u8; 4096]);
686        let plan = plan_block_allocation(&sb, &groups, 10, 0, read).unwrap();
687        assert_eq!(plan.bgd.group_idx, 1);
688        // Block 1 + group1_offset
689        assert_eq!(plan.first_block, 1 + 32768);
690    }
691
692    #[test]
693    fn block_allocation_honours_block_uninit_flag() {
694        let sb = mk_sb(4096, 32768, 8192, 65536);
695        // group 0 is UNINIT → treated as all-free without reading bitmap
696        let g0 = mk_bgd(32768, 8000, 0, BgdFlags::BLOCK_UNINIT.bits());
697        let groups = vec![g0];
698        let mut call_count = 0;
699        let read = |_b| {
700            call_count += 1;
701            Ok(vec![0xFFu8; 4096])
702        };
703        let plan = plan_block_allocation(&sb, &groups, 4, 0, read).unwrap();
704        assert_eq!(plan.count, 4);
705        assert_eq!(call_count, 0, "UNINIT group should not read bitmap");
706    }
707
708    /// Inode numbers are one-based, and group 0's first *available*
709    /// one is `s_first_ino`.
710    ///
711    /// This used to assert inode 1, on an all-free bitmap that no real
712    /// filesystem has: mke2fs sets the reserved bits, so scanning from
713    /// zero returned 11 anyway and the assertion only held for the
714    /// synthetic bitmap here. It is the reserved range the allocator
715    /// must not enter, not the bits that happen to be set.
716    #[test]
717    fn inode_allocation_returns_one_based_number() {
718        let sb = mk_sb(4096, 32768, 8192, 65536);
719        let g0 = mk_bgd(1000, 8000, 0, 0);
720        let groups = vec![g0];
721        let read = |_b| Ok(vec![0u8; 4096]);
722        let plan = plan_inode_allocation(&sb, &groups, false, 0, read).unwrap();
723        assert_eq!(
724            plan.inode, sb.first_inode,
725            "group 0's first available inode is s_first_ino, not inode 1"
726        );
727        assert_eq!(plan.bitmap.bit_start, sb.first_inode - 1, "one-based");
728        assert!(!plan.is_dir);
729        assert_eq!(plan.bgd.used_dirs_delta, 0);
730    }
731
732    #[test]
733    fn inode_allocation_dir_bumps_used_dirs() {
734        let sb = mk_sb(4096, 32768, 8192, 65536);
735        let g0 = mk_bgd(1000, 8000, 0, 0);
736        let groups = vec![g0];
737        let read = |_b| Ok(vec![0u8; 4096]);
738        let plan = plan_inode_allocation(&sb, &groups, true, 0, read).unwrap();
739        assert!(plan.is_dir);
740        assert_eq!(plan.bgd.used_dirs_delta, 1);
741        assert_eq!(plan.bgd.free_inodes_delta, -1);
742    }
743
744    #[test]
745    fn orlov_prefers_group_with_fewer_dirs() {
746        // g1 has fewer dirs and more free inodes — should win the Orlov beauty contest.
747        let groups = vec![
748            mk_bgd(100, 500, 30, 0),
749            mk_bgd(100, 1000, 2, 0),
750            mk_bgd(100, 800, 10, 0),
751        ];
752        assert_eq!(orlov_select_group(&groups, 0), 1);
753    }
754
755    #[test]
756    fn apply_bitmap_write_sets_and_clears_bits() {
757        let mut buf = vec![0u8; 2];
758        apply_bitmap_write(
759            &mut buf,
760            &BitmapWrite {
761                bitmap_block: 0,
762                bit_start: 0,
763                count: 10,
764                set: true,
765            },
766        );
767        assert_eq!(buf, vec![0xFF, 0x03]);
768        apply_bitmap_write(
769            &mut buf,
770            &BitmapWrite {
771                bitmap_block: 0,
772                bit_start: 5,
773                count: 3,
774                set: false,
775            },
776        );
777        assert_eq!(buf, vec![0b0001_1111, 0x03]);
778    }
779
780    // --- blocks_in_group ---
781
782    #[test]
783    fn blocks_in_group_full_groups_return_blocks_per_group() {
784        // 3 full groups of 32768 each: total = 3*32768 + 1 (first_data_block=1).
785        let sb = mk_sb(4096, 32768, 8192, 3 * 32768 + 1);
786        // Groups 0 and 1 are not the last group, so they return blocks_per_group.
787        assert_eq!(blocks_in_group(&sb, 0), 32768);
788        assert_eq!(blocks_in_group(&sb, 1), 32768);
789    }
790
791    #[test]
792    fn blocks_in_group_last_group_exact_multiple_returns_full() {
793        // usable = 2*32768, first_data_block=1 → blocks_count = 2*32768+1
794        // remainder = (2*32768+1-1) % 32768 = 65536 % 32768 = 0 → full group
795        let sb = mk_sb(4096, 32768, 8192, 2 * 32768 + 1);
796        assert_eq!(blocks_in_group(&sb, 1), 32768);
797    }
798
799    #[test]
800    fn blocks_in_group_short_last_group() {
801        // 1 full group + 100 extra blocks: total = 32768 + 100 + 1 = 32869
802        let sb = mk_sb(4096, 32768, 8192, 32769 + 100);
803        assert_eq!(blocks_in_group(&sb, 0), 32768); // full
804        assert_eq!(blocks_in_group(&sb, 1), 100); // short last group
805    }
806}