littlefs2-rust 0.1.1

Pure Rust littlefs implementation with a mounted block-device API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
use alloc::vec::Vec;

use crate::types::{Config, Error, Result};

/// Minimal mounted block allocator.
///
/// This is not littlefs's final lookahead allocator yet. It deliberately keeps
/// a simple in-memory used-block bitmap plus a next-fit cursor so mounted
/// native writes can allocate without rescanning every reachable metadata/CTZ
/// block on each file growth. Native writes clone/commit the allocator state
/// only after the corresponding metadata transaction succeeds, and operations
/// that free visible storage rebuild it from the freshly remounted filesystem.
#[derive(Debug, Clone)]
pub(crate) struct BlockAllocator {
    cfg: Config,
    used: Vec<bool>,
    bad: Option<Vec<bool>>,
    lookahead: Vec<bool>,
    lookahead_start: usize,
    next: usize,
}

impl BlockAllocator {
    #[cfg(test)]
    pub(crate) fn from_used(cfg: Config, used: Vec<bool>) -> Result<Self> {
        Self::from_used_with_start(cfg, used, 0)
    }

    /// Builds an allocator whose first lookahead scan begins at `start`.
    ///
    /// On mount, C littlefs derives this start point from a CRC seed over
    /// committed metadata blocks. Accepting an explicit start keeps the simple
    /// bitmap allocator testable while letting mounted filesystems avoid the
    /// old Rust-native behavior of always reusing the lowest free block after a
    /// remount.
    pub(crate) fn from_used_with_start(cfg: Config, used: Vec<bool>, start: usize) -> Result<Self> {
        Self::from_used_with_bad_blocks(cfg, used, start, &[])
    }

    /// Rebuilds the allocator from a freshly visible used-block graph while
    /// preserving blocks that failed with `Error::Corrupt` in the current
    /// mounted session.
    ///
    /// littlefs does not expose a portable persistent bad-block table in the
    /// on-disk format, but it also must not immediately reuse an allocation
    /// candidate that just reported a bad-block style failure. Keeping the
    /// reservation in the live allocator is a pragmatic middle ground: failed
    /// unreachable blocks stay unavailable for the rest of the mount, while a
    /// future remount starts from the disk-visible graph just like C.
    pub(crate) fn from_used_with_bad_blocks(
        cfg: Config,
        mut used: Vec<bool>,
        start: usize,
        bad: &[bool],
    ) -> Result<Self> {
        if used.len() != cfg.block_count {
            return Err(Error::InvalidConfig);
        }
        if cfg.block_count == 0 {
            return Err(Error::InvalidConfig);
        }
        let mut bad_blocks = None;
        if !bad.is_empty() {
            if bad.len() != cfg.block_count {
                return Err(Error::InvalidConfig);
            }
            let mut blocks = alloc::vec![false; cfg.block_count];
            blocks.copy_from_slice(bad);
            for (slot, is_bad) in used.iter_mut().zip(&blocks) {
                if *is_bad {
                    *slot = true;
                }
            }
            bad_blocks = Some(blocks);
        }
        let start = start % cfg.block_count;
        let mut allocator = Self {
            cfg,
            used,
            bad: bad_blocks,
            lookahead: alloc::vec![false; core::cmp::min(cfg.block_count, 64)],
            lookahead_start: start,
            next: start,
        };
        allocator.reload_lookahead(start);
        Ok(allocator)
    }

    pub(crate) fn alloc_block(&mut self) -> Result<u32> {
        let blocks = self.alloc_blocks(1)?;
        Ok(blocks[0])
    }

    pub(crate) fn alloc_pair(&mut self) -> Result<[u32; 2]> {
        let blocks = self.alloc_blocks(2)?;
        // C littlefs allocates a fresh `lfs_mdir_t` backwards internally, then
        // writes the first committed directory block to pair[1] and swaps the
        // pair after the commit sticks. This allocator returns the published
        // post-commit order that callers store in DIRSTRUCT/SOFTTAIL records.
        Ok([blocks[0], blocks[1]])
    }

    pub(crate) fn alloc_ctz_blocks(&mut self, size: usize) -> Result<Vec<u32>> {
        if size == 0 {
            return Err(Error::Unsupported);
        }
        let needed = ctz_block_count(size, self.cfg.block_size)?;
        self.alloc_blocks(needed)
    }

    pub(crate) fn reserve_bad_block(&mut self, block: u32) -> Result<()> {
        let block = block as usize;
        if block >= self.cfg.block_count {
            return Err(Error::OutOfBounds);
        }
        let bad = self
            .bad
            .get_or_insert_with(|| alloc::vec![false; self.cfg.block_count]);
        bad[block] = true;
        self.used[block] = true;
        if self.lookahead_slot(block).is_some() {
            self.set_lookahead_used(block)?;
        }
        Ok(())
    }

    pub(crate) fn bad_blocks(&self) -> &[bool] {
        self.bad.as_deref().unwrap_or(&[])
    }

    fn alloc_blocks(&mut self, needed: usize) -> Result<Vec<u32>> {
        if needed == 0 {
            return Ok(Vec::new());
        }
        if needed > self.cfg.block_count {
            return Err(Error::NoSpace);
        }

        let mut blocks = Vec::with_capacity(needed);
        let mut scanned = 0usize;
        let mut cursor = self.next;
        while scanned < self.cfg.block_count {
            if self.lookahead_slot(cursor).is_none() {
                self.reload_lookahead(cursor);
            }

            if !self.lookahead_used(cursor)? {
                self.used[cursor] = true;
                self.set_lookahead_used(cursor)?;
                blocks.push(u32::try_from(cursor).map_err(|_| Error::NoSpace)?);
                if blocks.len() == needed {
                    self.next = (cursor + 1) % self.cfg.block_count;
                    return Ok(blocks);
                }
            }
            cursor = (cursor + 1) % self.cfg.block_count;
            scanned += 1;
        }

        // Roll back any reservations from this failed allocation attempt. This
        // keeps metadata-pair allocation from consuming one leftover block when
        // a two-block pair cannot be satisfied.
        for block in blocks {
            let block = block as usize;
            self.used[block] = false;
            if self.lookahead_slot(block).is_some() {
                self.set_lookahead_free(block)?;
            }
        }
        Err(Error::NoSpace)
    }

    fn reload_lookahead(&mut self, start: usize) {
        self.lookahead_start = start;
        for (i, slot) in self.lookahead.iter_mut().enumerate() {
            let block = (start + i) % self.cfg.block_count;
            *slot = self.used[block];
        }
    }

    fn lookahead_slot(&self, block: usize) -> Option<usize> {
        let size = self.lookahead.len();
        if size == 0 {
            return None;
        }
        let end = self.lookahead_start + size;
        if self.lookahead_start <= block && block < end {
            Some(block - self.lookahead_start)
        } else if end > self.cfg.block_count && block < end % self.cfg.block_count {
            Some(self.cfg.block_count - self.lookahead_start + block)
        } else {
            None
        }
    }

    fn lookahead_used(&self, block: usize) -> Result<bool> {
        let slot = self.lookahead_slot(block).ok_or(Error::Corrupt)?;
        Ok(self.lookahead[slot])
    }

    fn set_lookahead_used(&mut self, block: usize) -> Result<()> {
        let slot = self.lookahead_slot(block).ok_or(Error::Corrupt)?;
        self.lookahead[slot] = true;
        Ok(())
    }

    fn set_lookahead_free(&mut self, block: usize) -> Result<()> {
        let slot = self.lookahead_slot(block).ok_or(Error::Corrupt)?;
        self.lookahead[slot] = false;
        Ok(())
    }

    #[cfg(test)]
    fn lookahead_window_for_test(&self) -> (usize, Vec<bool>) {
        (self.lookahead_start, self.lookahead.clone())
    }
}

fn ctz_block_count(size: usize, block_size: usize) -> Result<usize> {
    let mut remaining = size;
    let mut index = 0usize;
    while remaining > 0 {
        let start = ctz_data_start(index)?;
        let capacity = block_size.checked_sub(start).ok_or(Error::InvalidConfig)?;
        if capacity == 0 {
            return Err(Error::InvalidConfig);
        }
        remaining = remaining.saturating_sub(capacity);
        index = index.checked_add(1).ok_or(Error::NoSpace)?;
    }
    Ok(index)
}

fn ctz_data_start(index: usize) -> Result<usize> {
    if index == 0 {
        Ok(0)
    } else {
        let skips = index.trailing_zeros() as usize + 1;
        skips.checked_mul(4).ok_or(Error::NoSpace)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pair_allocation_rolls_back_when_two_blocks_do_not_fit() {
        let cfg = Config {
            block_size: 512,
            block_count: 5,
        };
        let mut used = vec![true, true, true, true, false];
        let mut allocator = BlockAllocator::from_used(cfg, used.clone()).expect("allocator");
        assert_eq!(
            allocator
                .alloc_pair()
                .expect_err("one free block cannot satisfy a metadata pair"),
            Error::NoSpace
        );

        used[4] = false;
        assert_eq!(
            allocator
                .alloc_ctz_blocks(32)
                .expect("failed pair allocation must not consume the leftover block"),
            vec![4]
        );
    }

    #[test]
    fn ctz_allocation_uses_next_fit_cursor() {
        let cfg = Config {
            block_size: 512,
            block_count: 8,
        };
        let used = vec![true, true, false, false, false, false, false, false];
        let mut allocator = BlockAllocator::from_used(cfg, used).expect("allocator");
        assert_eq!(allocator.alloc_ctz_blocks(32).expect("first"), vec![2]);
        assert_eq!(allocator.alloc_ctz_blocks(32).expect("second"), vec![3]);
    }

    #[test]
    fn allocator_refreshes_lookahead_window_while_scanning() {
        let cfg = Config {
            block_size: 512,
            block_count: 96,
        };
        let mut used = vec![true; 96];
        used[70] = false;
        let mut allocator = BlockAllocator::from_used(cfg, used).expect("allocator");

        assert_eq!(
            allocator.alloc_block().expect("block outside first window"),
            70
        );
        let (start, window) = allocator.lookahead_window_for_test();
        assert_eq!(start, 64);
        assert!(window[6], "allocated block should be marked in lookahead");
    }

    #[test]
    fn failed_pair_allocation_rolls_back_lookahead_bits() {
        let cfg = Config {
            block_size: 512,
            block_count: 96,
        };
        let mut used = vec![true; 96];
        used[70] = false;
        let mut allocator = BlockAllocator::from_used(cfg, used).expect("allocator");

        assert_eq!(
            allocator
                .alloc_pair()
                .expect_err("single free block cannot satisfy a pair"),
            Error::NoSpace
        );
        assert_eq!(
            allocator
                .alloc_block()
                .expect("rolled-back block remains free"),
            70
        );
    }

    #[test]
    fn pair_allocation_uses_c_published_metadata_pair_order() {
        let cfg = Config {
            block_size: 512,
            block_count: 8,
        };
        let used = vec![true, true, false, false, false, false, false, false];
        let mut allocator = BlockAllocator::from_used(cfg, used).expect("allocator");

        assert_eq!(
            allocator.alloc_pair().expect("first mounted pair"),
            [2, 3],
            "callers expect the post-compaction pair order stored in C DIRSTRUCT records"
        );
    }

    #[test]
    fn seeded_allocator_starts_scan_at_mount_seed() {
        let cfg = Config {
            block_size: 512,
            block_count: 16,
        };
        let mut used = vec![true; 16];
        used[2] = false;
        used[11] = false;
        let mut allocator =
            BlockAllocator::from_used_with_start(cfg, used, 9).expect("seeded allocator");

        assert_eq!(
            allocator.alloc_block().expect("first block after seed"),
            11,
            "the mount seed should choose the first free block at or after the seeded lookahead start"
        );
        assert_eq!(
            allocator.alloc_block().expect("wrap to earlier free block"),
            2,
            "after exhausting the seeded window the scan wraps around the block device"
        );
    }

    #[test]
    fn bad_block_reservations_survive_rebuild_inputs() {
        let cfg = Config {
            block_size: 512,
            block_count: 8,
        };
        let mut used = vec![false; 8];
        used[0] = true;
        used[1] = true;
        let mut bad = vec![false; 8];
        bad[3] = true;
        let mut allocator =
            BlockAllocator::from_used_with_bad_blocks(cfg, used, 2, &bad).expect("allocator");

        assert_eq!(
            allocator.alloc_block().expect("first non-bad free block"),
            2
        );
        assert_eq!(allocator.alloc_block().expect("skip session bad block"), 4);
    }

    #[test]
    fn reserving_bad_block_updates_current_lookahead() {
        let cfg = Config {
            block_size: 512,
            block_count: 8,
        };
        let mut used = vec![false; 8];
        used[0] = true;
        used[1] = true;
        let mut allocator = BlockAllocator::from_used(cfg, used).expect("allocator");

        allocator.reserve_bad_block(2).expect("reserve bad block");
        assert_eq!(
            allocator.alloc_block().expect("reserved block is skipped"),
            3
        );
    }
}