Skip to main content

fstool/fs/littlefs/
hosted.rs

1//! The hosted half of the littlefs backend: the `Filesystem`
2//! implementation, built on a heap.
3//!
4//! The format itself is described in the [module
5//! docs](crate::fs::littlefs); this file is what a machine with an
6//! allocator does with it. A volume is opened over a
7//! [`BlockDevice`](crate::block::BlockDevice) rather than a
8//! [`FlashDriver`](super::FlashDriver), metadata pairs are replayed into
9//! `Vec<Entry>` views, and the block allocator holds an exact in-use bitmap
10//! for the whole volume — all of which is right for building and converting
11//! images and wrong for a microcontroller, which is what
12//! [`Volume`](super::Volume) is for.
13//!
14//! ## What this backend does
15//!
16//! [`LittleFs::format`] lays down a fresh volume, and [`LittleFs::open`]
17//! mounts an existing one; both return a fully mutable handle. Every
18//! mutation is written through immediately as a real littlefs commit —
19//! there is no build-once mode and no in-memory image, so `create -t
20//! littlefs`, `repack`, `add`/`rm` and `open_file_rw` all drive the same
21//! code path and a re-opened image keeps working exactly like a fresh one.
22//!
23//! Each commit rewrites the whole metadata pair (a *compaction*) into its
24//! stale block rather than appending to the live one. That is the same
25//! operation littlefs performs whenever a block fills up, so the result is
26//! always a volume a stock littlefs can mount and keep appending to — at
27//! the cost of writing a block per metadata change, which is the right
28//! trade for an image tool.
29//!
30//! ## Metadata mapping
31//!
32//! littlefs stores no POSIX metadata at all: no mode, owner, timestamps,
33//! symlinks or device nodes. Modes are therefore synthesised on read
34//! (`0o755` for directories, `0o644` for files) and dropped on write, and
35//! [`Filesystem::create_symlink`] / [`Filesystem::create_device`] report
36//! [`Error::Unsupported`] so a `repack` sink skips those entries rather
37//! than silently mangling them. littlefs *user attributes* are surfaced as
38//! extended attributes named `user.littlefs.<type>`, where `<type>` is the
39//! attribute's 8-bit type in decimal.
40
41use crate::io::Read;
42use crate::path::Path;
43use ::alloc::boxed::Box;
44use ::alloc::collections::BTreeMap;
45use ::alloc::collections::VecDeque;
46use ::alloc::format;
47use ::alloc::string::{String, ToString};
48use ::alloc::vec;
49use ::alloc::vec::Vec;
50
51use crate::block::BlockDevice;
52use crate::fs::{
53    DirEntry, EntryKind, FileAttrs, FileMeta, FileSource, Filesystem, MutationCapability, StatFs,
54    XattrPair,
55};
56use crate::{Error, Result};
57
58// These were sibling modules when this file was the module root; it is now
59// one half of it, so they are mounted here by path. They reach into
60// `LittleFs`'s internals, so they must stay its descendants.
61#[path = "alloc.rs"]
62mod alloc;
63#[path = "ctz.rs"]
64mod ctz;
65#[path = "mdir.rs"]
66mod mdir;
67#[path = "rw.rs"]
68mod rw;
69#[path = "size_plan.rs"]
70mod size_plan;
71#[cfg(test)]
72#[path = "tests.rs"]
73mod tests;
74
75// The on-disk tag format and the disk-version constants are the two things
76// both halves of the backend speak, and they live in the module root. The
77// re-export keeps `super::tag` resolving for the submodules above, which
78// were written when this file was that root.
79pub(super) use super::{
80    DISK_VERSION_2_0, DISK_VERSION_2_1, FILE_MAX, MAGIC, SUPERBLOCK_PAIR, index, tag,
81};
82
83pub use size_plan::LittleFsSizePlan;
84
85use self::alloc::Alloc;
86use mdir::{Entry, Geom, Mdir, Struct};
87
88/// Prefix of the extended-attribute names littlefs user attributes are
89/// surfaced under; the 8-bit attribute type follows in decimal.
90const XATTR_PREFIX: &str = "user.littlefs.";
91
92/// Format-time options for [`LittleFs::format`].
93#[derive(Debug, Clone)]
94pub struct LittleFsFormatOpts {
95    /// Logical block size — the flash erase-block size. littlefs stores it
96    /// in the superblock; 4 KiB is the common default.
97    pub block_size: u32,
98    /// Number of blocks. `None` fills the device.
99    pub block_count: Option<u32>,
100    /// Program (page) alignment. Commits are padded to it so that a real
101    /// littlefs can append in place.
102    pub prog_size: u32,
103    /// On-disk version to write: [`DISK_VERSION_2_1`] (default) or
104    /// [`DISK_VERSION_2_0`] for targets running a pre-2.1 littlefs.
105    pub disk_version: u32,
106    /// Longest file name the volume accepts.
107    pub name_max: u32,
108    /// Largest file kept inline in its directory's metadata instead of
109    /// being written out as a CTZ skip-list. `None` picks littlefs's own
110    /// default of an eighth of a block.
111    pub inline_max: Option<u32>,
112}
113
114impl Default for LittleFsFormatOpts {
115    fn default() -> Self {
116        Self {
117            block_size: 4096,
118            block_count: None,
119            prog_size: 256,
120            disk_version: DISK_VERSION_2_1,
121            name_max: 255,
122            inline_max: None,
123        }
124    }
125}
126
127/// A mounted littlefs volume.
128pub struct LittleFs {
129    geom: Geom,
130    version: u32,
131    name_max: u32,
132    file_max: u32,
133    attr_max: u32,
134    inline_max: u32,
135    root: [u32; 2],
136    /// In-use bitmap, built on first allocation and maintained exactly from
137    /// then on. `None` until something needs to allocate.
138    alloc: Option<Alloc>,
139    cache: MdirCache,
140}
141
142/// Small LRU over parsed metadata pairs. Directory operations walk the same
143/// pairs repeatedly (a lookup, then an insert, then a commit), and every
144/// walk would otherwise re-read and re-parse a block.
145struct MdirCache {
146    map: BTreeMap<[u32; 2], Mdir>,
147    order: VecDeque<[u32; 2]>,
148    cap: usize,
149}
150
151impl MdirCache {
152    fn new(cap: usize) -> Self {
153        Self {
154            map: BTreeMap::new(),
155            order: VecDeque::new(),
156            cap,
157        }
158    }
159
160    /// Cache key: a pair addresses the same metadata whichever way round it
161    /// is written, and a commit swaps the two halves.
162    fn key(pair: [u32; 2]) -> [u32; 2] {
163        if pair[0] <= pair[1] {
164            pair
165        } else {
166            [pair[1], pair[0]]
167        }
168    }
169
170    fn get(&self, pair: [u32; 2]) -> Option<&Mdir> {
171        self.map.get(&Self::key(pair))
172    }
173
174    fn put(&mut self, mdir: Mdir) {
175        let k = Self::key(mdir.pair);
176        if self.map.insert(k, mdir).is_none() {
177            self.order.push_back(k);
178            while self.order.len() > self.cap {
179                if let Some(old) = self.order.pop_front() {
180                    self.map.remove(&old);
181                }
182            }
183        }
184    }
185
186    fn remove(&mut self, pair: [u32; 2]) {
187        let k = Self::key(pair);
188        self.map.remove(&k);
189        self.order.retain(|p| *p != k);
190    }
191}
192
193/// What a path resolved to.
194enum Resolved {
195    /// The root directory, which has no entry of its own.
196    Root,
197    /// An entry at `id` in the metadata pair `mdir`.
198    Entry { mdir: Mdir, id: usize },
199}
200
201impl LittleFs {
202    /// Format a fresh volume on `dev`.
203    pub fn format(dev: &mut dyn BlockDevice, opts: &LittleFsFormatOpts) -> Result<Self> {
204        let block_size = opts.block_size;
205        // 128 bytes is the floor at which a CTZ block can still hold its
206        // skip pointers (the spec's bound is 104); everything else in
207        // littlefs assumes a power-of-two erase block.
208        if block_size < 128 || !block_size.is_power_of_two() {
209            return Err(Error::InvalidArgument(format!(
210                "littlefs: block_size {block_size} must be a power of two and at least 128"
211            )));
212        }
213        let prog_size = opts.prog_size.max(1);
214        if !prog_size.is_power_of_two() || prog_size > block_size {
215            return Err(Error::InvalidArgument(format!(
216                "littlefs: prog_size {prog_size} must be a power of two no larger than the block size"
217            )));
218        }
219        if opts.disk_version != DISK_VERSION_2_0 && opts.disk_version != DISK_VERSION_2_1 {
220            return Err(Error::InvalidArgument(format!(
221                "littlefs: unsupported disk version {:#010x} (use 2.0 or 2.1)",
222                opts.disk_version
223            )));
224        }
225
226        let avail = (dev.total_size() / block_size as u64).min(u32::MAX as u64) as u32;
227        let block_count = opts.block_count.unwrap_or(avail);
228        if block_count > avail {
229            return Err(Error::InvalidArgument(format!(
230                "littlefs: block_count {block_count} exceeds the {avail} blocks the device holds"
231            )));
232        }
233        // The superblock pair plus room for a directory pair and some data.
234        if block_count < 4 {
235            return Err(Error::InvalidArgument(
236                "littlefs: a volume needs at least 4 blocks".into(),
237            ));
238        }
239        if opts.name_max == 0 || opts.name_max > tag::MAX_SIZE as u32 {
240            return Err(Error::InvalidArgument(format!(
241                "littlefs: name_max {} must be between 1 and {}",
242                opts.name_max,
243                tag::MAX_SIZE
244            )));
245        }
246
247        let geom = Geom {
248            block_size,
249            block_count,
250            prog_size,
251            fcrc: opts.disk_version >= DISK_VERSION_2_1,
252        };
253        let attr_max = tag::MAX_SIZE as u32;
254        let inline_max = pick_inline_max(&geom, opts.inline_max)?;
255
256        let mut fs = Self {
257            geom,
258            version: opts.disk_version,
259            name_max: opts.name_max,
260            file_max: FILE_MAX,
261            attr_max,
262            inline_max,
263            root: SUPERBLOCK_PAIR,
264            alloc: None,
265            cache: MdirCache::new(32),
266        };
267
268        // The root pair is written twice, exactly as `lfs_format` does: the
269        // second compaction lands in the other block so that *both* halves
270        // of the pair are valid littlefs commits, leaving nothing of an
271        // older filesystem behind for a fetch to trip over.
272        let mut root = Mdir::empty([SUPERBLOCK_PAIR[1], SUPERBLOCK_PAIR[0]]);
273        root.entries.push(Entry {
274            kind: tag::TYPE_SUPERBLOCK as u8,
275            name: MAGIC.to_vec(),
276            data: Some(Struct::Inline(fs.superblock_bytes())),
277            attrs: Vec::new(),
278        });
279        fs.commit(dev, &mut root)?;
280        fs.commit(dev, &mut root)?;
281
282        // Claim the superblock pair up front so nothing else can hand it out.
283        let mut a = Alloc::new(block_count);
284        a.mark(SUPERBLOCK_PAIR[0]);
285        a.mark(SUPERBLOCK_PAIR[1]);
286        fs.alloc = Some(a);
287        Ok(fs)
288    }
289
290    /// Mount an existing volume.
291    pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
292        // The superblock's inline struct always sits at a fixed offset in
293        // the first commit of block 0 — that's the only way to learn the
294        // block size, which everything else needs.
295        let mut head = [0u8; 44];
296        let n = head.len().min(dev.total_size() as usize);
297        dev.read_at(0, &mut head[..n])?;
298        if &head[8..16] != MAGIC {
299            return Err(Error::InvalidImage(
300                "littlefs: no \"littlefs\" magic at offset 8".into(),
301            ));
302        }
303        let version = tag::le32(&head[20..24]);
304        let block_size = tag::le32(&head[24..28]);
305        let block_count = tag::le32(&head[28..32]);
306        if version >> 16 != 2 {
307            return Err(Error::Unsupported(format!(
308                "littlefs: on-disk version {}.{} (only v2 is supported)",
309                version >> 16,
310                version & 0xffff
311            )));
312        }
313        if version & 0xffff > 1 {
314            return Err(Error::Unsupported(format!(
315                "littlefs: on-disk version 2.{} is newer than 2.1",
316                version & 0xffff
317            )));
318        }
319        if !(128..=16 * 1024 * 1024).contains(&block_size) || block_count == 0 {
320            return Err(Error::InvalidImage(format!(
321                "littlefs: implausible geometry ({block_size}-byte blocks × {block_count})"
322            )));
323        }
324        if (block_size as u64).saturating_mul(block_count as u64) > dev.total_size() {
325            return Err(Error::InvalidImage(format!(
326                "littlefs: volume claims {block_count} × {block_size}-byte blocks but the device holds {} bytes",
327                dev.total_size()
328            )));
329        }
330
331        let geom = Geom {
332            block_size,
333            block_count,
334            prog_size: 1,
335            fcrc: version >= DISK_VERSION_2_1,
336        };
337        let mut fs = Self {
338            geom,
339            version,
340            name_max: tag::le32(&head[32..36]),
341            file_max: tag::le32(&head[36..40]),
342            attr_max: tag::le32(&head[40..44]),
343            inline_max: 0,
344            root: SUPERBLOCK_PAIR,
345            alloc: None,
346            cache: MdirCache::new(32),
347        };
348        if fs.name_max == 0 || fs.name_max > tag::MAX_SIZE as u32 {
349            fs.name_max = 255;
350        }
351        if fs.file_max == 0 {
352            fs.file_max = FILE_MAX;
353        }
354        if fs.attr_max == 0 || fs.attr_max > tag::MAX_SIZE as u32 {
355            fs.attr_max = tag::MAX_SIZE as u32;
356        }
357
358        // Walk the superblock chain: the last pair still carrying a
359        // superblock entry is the root directory. littlefs grows this chain
360        // as the root is rewritten, to spread erase cycles.
361        let mut pair = Some(SUPERBLOCK_PAIR);
362        let mut hops = 0u32;
363        while let Some(p) = pair {
364            let m = mdir::fetch(dev, &fs.geom, p)?;
365            if m.entries
366                .first()
367                .is_some_and(|e| e.kind == tag::TYPE_SUPERBLOCK as u8)
368            {
369                fs.root = m.pair;
370                // A commit's forward-CRC records the program size its
371                // writer used; reusing it keeps our commits aligned the way
372                // the volume's creator intended.
373                if let Some(p) = m.fcrc_size
374                    && p.is_power_of_two()
375                    && p <= block_size
376                {
377                    fs.geom.prog_size = p;
378                }
379            }
380            pair = m.tail;
381            hops += 1;
382            if hops > block_count {
383                return Err(Error::InvalidImage(
384                    "littlefs: cycle in the metadata-pair list".into(),
385                ));
386            }
387        }
388        if fs.geom.prog_size == 1 {
389            fs.geom.prog_size = 256.min(block_size / 4).max(1);
390        }
391        fs.inline_max = pick_inline_max(&fs.geom, None)?;
392        fs.cache = MdirCache::new(32);
393        Ok(fs)
394    }
395
396    /// Volume geometry: `(block size, block count)`.
397    pub fn geometry(&self) -> (u32, u32) {
398        (self.geom.block_size, self.geom.block_count)
399    }
400
401    /// On-disk version, as `(major, minor)`.
402    pub fn version(&self) -> (u16, u16) {
403        ((self.version >> 16) as u16, (self.version & 0xffff) as u16)
404    }
405
406    /// Largest file kept inline in metadata rather than written as a CTZ
407    /// skip-list.
408    pub fn inline_max(&self) -> u32 {
409        self.inline_max
410    }
411
412    /// Program (page) alignment commits are padded to. Not recorded in
413    /// the superblock — it is a property of the target flash — so for an
414    /// opened image this is recovered from the forward-CRC of the last
415    /// commit, falling back to a sensible default when the image carries
416    /// none.
417    pub fn program_size(&self) -> u32 {
418        self.geom.prog_size
419    }
420
421    /// Blocks currently in use.
422    pub fn used_blocks(&mut self, dev: &mut dyn BlockDevice) -> Result<u32> {
423        Ok(self.allocator(dev)?.used())
424    }
425
426    /// The 24-byte superblock configuration record.
427    fn superblock_bytes(&self) -> Vec<u8> {
428        let mut b = Vec::with_capacity(24);
429        for v in [
430            self.version,
431            self.geom.block_size,
432            self.geom.block_count,
433            self.name_max,
434            self.file_max,
435            self.attr_max,
436        ] {
437            b.extend_from_slice(&v.to_le_bytes());
438        }
439        b
440    }
441
442    // ---- metadata pairs -------------------------------------------------
443
444    /// Fetch a metadata pair, through the cache.
445    fn fetch(&mut self, dev: &mut dyn BlockDevice, pair: [u32; 2]) -> Result<Mdir> {
446        if let Some(m) = self.cache.get(pair) {
447            return Ok(m.clone());
448        }
449        let m = mdir::fetch(dev, &self.geom, pair)?;
450        self.cache.put(m.clone());
451        Ok(m)
452    }
453
454    /// Write `mdir` back as a fresh compaction, splitting it across further
455    /// pairs first if its contents no longer fit one metadata block.
456    fn commit(&mut self, dev: &mut dyn BlockDevice, mdir: &mut Mdir) -> Result<()> {
457        while mdir::needs_split(&self.geom, mdir) {
458            let at = mdir::split_point(&self.geom, mdir);
459            if at == 0 {
460                return Err(Error::InvalidArgument(
461                    "littlefs: a single entry is too large for a metadata block".into(),
462                ));
463            }
464            let mut tail = self.new_pair(dev)?;
465            tail.entries = mdir.entries.split_off(at);
466            tail.tail = mdir.tail;
467            tail.hard = mdir.hard;
468            self.commit(dev, &mut tail)?;
469            // The overflow pair becomes the continuation of this directory,
470            // which also keeps it threaded on the filesystem-wide list.
471            mdir.tail = Some(tail.pair);
472            mdir.hard = true;
473        }
474
475        mdir.rev = mdir.rev.wrapping_add(1);
476        let target = mdir.pair[1];
477        mdir::write_compaction(dev, &self.geom, mdir, target, mdir.rev)?;
478        // The block we just wrote is now the live half of the pair.
479        mdir.pair.swap(0, 1);
480        self.cache.put(mdir.clone());
481        Ok(())
482    }
483
484    /// Allocate a metadata pair that isn't on disk yet.
485    ///
486    /// The revision count is seeded from the block we will *not* write
487    /// first, so that our commit always outranks whatever an earlier
488    /// filesystem left in the other half of the pair.
489    fn new_pair(&mut self, dev: &mut dyn BlockDevice) -> Result<Mdir> {
490        let pair = self.allocator(dev)?.take_pair()?;
491        let mut m = Mdir::empty(pair);
492        m.rev = mdir::read_rev(dev, &self.geom, pair[0]).unwrap_or(0);
493        Ok(m)
494    }
495
496    // ---- allocation -----------------------------------------------------
497
498    /// The in-use bitmap, built by traversing the volume the first time
499    /// anything needs to allocate.
500    fn allocator(&mut self, dev: &mut dyn BlockDevice) -> Result<&mut Alloc> {
501        if self.alloc.is_none() {
502            let a = self.scan_used(dev)?;
503            self.alloc = Some(a);
504        }
505        Ok(self.alloc.as_mut().expect("just built"))
506    }
507
508    /// Walk every metadata pair on the threaded list and every file's
509    /// skip-list, marking the blocks they occupy.
510    fn scan_used(&mut self, dev: &mut dyn BlockDevice) -> Result<Alloc> {
511        let geom = self.geom;
512        let mut a = Alloc::new(geom.block_count);
513        let mut next = Some(SUPERBLOCK_PAIR);
514        let mut hops = 0u32;
515        while let Some(pair) = next {
516            let m = self.fetch(dev, pair)?;
517            a.mark(m.pair[0]);
518            a.mark(m.pair[1]);
519            for e in &m.entries {
520                if let Some(Struct::Ctz { head, size }) = &e.data {
521                    ctz::traverse(dev, &geom, *head, *size, &mut |b| a.mark(b))?;
522                }
523            }
524            next = m.tail;
525            hops += 1;
526            if hops > geom.block_count {
527                return Err(Error::InvalidImage(
528                    "littlefs: cycle in the metadata-pair list".into(),
529                ));
530            }
531        }
532        Ok(a)
533    }
534
535    /// Release every block a file's data occupies.
536    fn free_data(&mut self, dev: &mut dyn BlockDevice, data: &Struct) -> Result<()> {
537        let Struct::Ctz { head, size } = data else {
538            return Ok(());
539        };
540        let geom = self.geom;
541        let mut blocks = Vec::new();
542        ctz::traverse(dev, &geom, *head, *size, &mut |b| blocks.push(b))?;
543        let a = self.allocator(dev)?;
544        for b in blocks {
545            a.free(b);
546        }
547        Ok(())
548    }
549
550    // ---- path resolution ------------------------------------------------
551
552    /// Resolve a path, erroring when it doesn't exist.
553    fn resolve(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<Resolved> {
554        self.try_resolve(dev, path)?.ok_or_else(|| {
555            Error::InvalidArgument(format!("littlefs: no such path {:?}", path.display()))
556        })
557    }
558
559    /// Resolve a path, returning `None` when the final component is absent.
560    fn try_resolve(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<Option<Resolved>> {
561        let comps = components(path)?;
562        let mut dir = self.root;
563        let mut out = Resolved::Root;
564        for (i, name) in comps.iter().enumerate() {
565            let Some((mdir, id)) = self.find_in_dir(dev, dir, name.as_bytes())? else {
566                return Ok(None);
567            };
568            if i + 1 < comps.len() {
569                dir = match &mdir.entries[id].data {
570                    Some(Struct::Dir(p)) => *p,
571                    _ => {
572                        return Err(Error::InvalidArgument(format!(
573                            "littlefs: {name:?} is not a directory"
574                        )));
575                    }
576                };
577            }
578            out = Resolved::Entry { mdir, id };
579        }
580        Ok(Some(out))
581    }
582
583    /// The metadata pair a directory's entries start at.
584    fn dir_head(&self, r: &Resolved) -> Result<[u32; 2]> {
585        match r {
586            Resolved::Root => Ok(self.root),
587            Resolved::Entry { mdir, id } => match &mdir.entries[*id].data {
588                Some(Struct::Dir(p)) => Ok(*p),
589                _ => Err(Error::InvalidArgument(
590                    "littlefs: not a directory".to_string(),
591                )),
592            },
593        }
594    }
595
596    /// Resolve `path`'s parent directory to the pair its entries start at.
597    fn parent_head(
598        &mut self,
599        dev: &mut dyn BlockDevice,
600        path: &Path,
601    ) -> Result<([u32; 2], String)> {
602        let comps = components(path)?;
603        let (name, parents) = comps
604            .split_last()
605            .ok_or_else(|| Error::InvalidArgument("littlefs: empty path".into()))?;
606        let mut dir = self.root;
607        for p in parents {
608            let Some((mdir, id)) = self.find_in_dir(dev, dir, p.as_bytes())? else {
609                return Err(Error::InvalidArgument(format!(
610                    "littlefs: no such directory {p:?}"
611                )));
612            };
613            dir = match &mdir.entries[id].data {
614                Some(Struct::Dir(pair)) => *pair,
615                _ => {
616                    return Err(Error::InvalidArgument(format!(
617                        "littlefs: {p:?} is not a directory"
618                    )));
619                }
620            };
621        }
622        Ok((dir, (*name).to_string()))
623    }
624
625    /// Find `name` in the directory whose chain starts at `head`.
626    fn find_in_dir(
627        &mut self,
628        dev: &mut dyn BlockDevice,
629        head: [u32; 2],
630        name: &[u8],
631    ) -> Result<Option<(Mdir, usize)>> {
632        for m in self.chain(dev, head)? {
633            if let Some(id) = m.find(name) {
634                return Ok(Some((m, id)));
635            }
636        }
637        Ok(None)
638    }
639
640    /// Every metadata pair of one directory, following its hard tails.
641    fn chain(&mut self, dev: &mut dyn BlockDevice, head: [u32; 2]) -> Result<Vec<Mdir>> {
642        let mut out = Vec::new();
643        let mut pair = Some(head);
644        while let Some(p) = pair {
645            let m = self.fetch(dev, p)?;
646            pair = if m.hard { m.tail } else { None };
647            out.push(m);
648            if out.len() as u32 > self.geom.block_count {
649                return Err(Error::InvalidImage(
650                    "littlefs: cycle in a directory's metadata chain".into(),
651                ));
652            }
653        }
654        Ok(out)
655    }
656
657    /// The metadata pair whose tail points at `pair` — its predecessor on
658    /// the filesystem-wide threaded list.
659    fn find_pred(&mut self, dev: &mut dyn BlockDevice, pair: [u32; 2]) -> Result<Mdir> {
660        let key = MdirCache::key(pair);
661        let mut next = Some(SUPERBLOCK_PAIR);
662        let mut hops = 0u32;
663        while let Some(p) = next {
664            let m = self.fetch(dev, p)?;
665            if m.tail.map(MdirCache::key) == Some(key) {
666                return Ok(m);
667            }
668            next = m.tail;
669            hops += 1;
670            if hops > self.geom.block_count {
671                break;
672            }
673        }
674        Err(Error::InvalidImage(
675            "littlefs: metadata pair is not on the threaded list".into(),
676        ))
677    }
678
679    // ---- mutation -------------------------------------------------------
680
681    /// Insert an entry into a directory, keeping the chain in name order
682    /// (littlefs sorts directory entries by their raw bytes).
683    fn insert_entry(
684        &mut self,
685        dev: &mut dyn BlockDevice,
686        head: [u32; 2],
687        entry: Entry,
688    ) -> Result<()> {
689        let mut pair = head;
690        loop {
691            let mut m = self.fetch(dev, pair)?;
692            // The superblock shares the root's pair as id 0 and takes no
693            // part in the ordering, so entries start after it.
694            let start = m.entries.iter().take_while(|e| !e.is_file()).count();
695            let pos = m.entries[start..]
696                .iter()
697                .position(|e| e.name.as_slice() > entry.name.as_slice())
698                .map(|p| p + start);
699            match pos {
700                Some(p) => {
701                    m.entries.insert(p, entry);
702                    return self.commit(dev, &mut m);
703                }
704                None => match (m.hard, m.tail) {
705                    (true, Some(t)) => pair = t,
706                    _ => {
707                        m.entries.push(entry);
708                        return self.commit(dev, &mut m);
709                    }
710                },
711            }
712        }
713    }
714
715    /// Shared body of `create_file` / `create_file_streaming`.
716    fn write_file(
717        &mut self,
718        dev: &mut dyn BlockDevice,
719        path: &Path,
720        body: &mut dyn Read,
721        len: u64,
722    ) -> Result<()> {
723        let (head, name) = self.parent_head(dev, path)?;
724        self.check_name(&name)?;
725        if len > self.file_max as u64 {
726            return Err(Error::InvalidArgument(format!(
727                "littlefs: {len} bytes exceeds the volume's {}-byte file limit",
728                self.file_max
729            )));
730        }
731
732        let existing = self.find_in_dir(dev, head, name.as_bytes())?;
733        if let Some((m, id)) = &existing
734            && m.entries[*id].kind == tag::TYPE_DIR as u8
735        {
736            return Err(Error::InvalidArgument(format!(
737                "littlefs: {name:?} already exists as a directory"
738            )));
739        }
740
741        let data = self.write_data(dev, body, len)?;
742        match existing {
743            // Replacing a file keeps its id — only the struct changes.
744            Some((mut m, id)) => {
745                if let Some(old) = m.entries[id].data.clone() {
746                    self.free_data(dev, &old)?;
747                }
748                m.entries[id].data = Some(data);
749                self.commit(dev, &mut m)
750            }
751            None => self.insert_entry(
752                dev,
753                head,
754                Entry {
755                    kind: tag::TYPE_REG as u8,
756                    name: name.into_bytes(),
757                    data: Some(data),
758                    attrs: Vec::new(),
759                },
760            ),
761        }
762    }
763
764    /// Stream `len` bytes of file data into the volume, inlining it when it
765    /// is small enough to live in the directory's metadata.
766    fn write_data(
767        &mut self,
768        dev: &mut dyn BlockDevice,
769        body: &mut dyn Read,
770        len: u64,
771    ) -> Result<Struct> {
772        if len <= self.inline_max as u64 {
773            let mut buf = vec![0u8; len as usize];
774            body.read_exact(&mut buf)?;
775            return Ok(Struct::Inline(buf));
776        }
777        let geom = self.geom;
778        let mut src = ctz::ReaderSource { body };
779        let alloc = self.allocator(dev)?;
780        let head =
781            ctz::write_blocks(dev, &geom, alloc, 0, None, 0, &mut src, len)?.ok_or_else(|| {
782                Error::InvalidArgument("littlefs: empty skip-list for a non-empty file".into())
783            })?;
784        Ok(Struct::Ctz {
785            head,
786            size: len as u32,
787        })
788    }
789
790    /// Create a directory: a fresh metadata pair, threaded onto the
791    /// filesystem-wide list right after its parent's last pair, plus an
792    /// entry pointing at it.
793    fn make_dir(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<()> {
794        let (head, name) = self.parent_head(dev, path)?;
795        self.check_name(&name)?;
796        if let Some((m, id)) = self.find_in_dir(dev, head, name.as_bytes())? {
797            return if m.entries[id].kind == tag::TYPE_DIR as u8 {
798                Ok(())
799            } else {
800                Err(Error::InvalidArgument(format!(
801                    "littlefs: {name:?} already exists"
802                )))
803            };
804        }
805
806        let mut dir = self.new_pair(dev)?;
807        // Splice the new pair into the threaded list behind the parent's
808        // last pair, so a traversal still reaches every metadata block.
809        let pred_pair = self
810            .chain(dev, head)?
811            .last()
812            .expect("a directory always has at least one pair")
813            .pair;
814        let mut pred = self.fetch(dev, pred_pair)?;
815        dir.tail = pred.tail;
816        dir.hard = false;
817        self.commit(dev, &mut dir)?;
818        pred.tail = Some(dir.pair);
819        pred.hard = false;
820        self.commit(dev, &mut pred)?;
821
822        self.insert_entry(
823            dev,
824            head,
825            Entry {
826                kind: tag::TYPE_DIR as u8,
827                name: name.into_bytes(),
828                data: Some(Struct::Dir(dir.pair)),
829                attrs: Vec::new(),
830            },
831        )
832    }
833
834    /// Remove a file or an empty directory.
835    fn remove_path(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<()> {
836        let Resolved::Entry { mdir, id } = self.resolve(dev, path)? else {
837            return Err(Error::InvalidArgument(
838                "littlefs: cannot remove the root directory".into(),
839            ));
840        };
841        let entry = mdir.entries[id].clone();
842
843        if entry.kind == tag::TYPE_DIR as u8 {
844            let head = match &entry.data {
845                Some(Struct::Dir(p)) => *p,
846                _ => {
847                    return Err(Error::InvalidImage(
848                        "littlefs: directory entry without a metadata pair".into(),
849                    ));
850                }
851            };
852            let chain = self.chain(dev, head)?;
853            if chain.iter().any(|m| m.entries.iter().any(Entry::is_file)) {
854                return Err(Error::InvalidArgument(format!(
855                    "littlefs: directory {:?} is not empty",
856                    path.display()
857                )));
858            }
859
860            // Drop the entry first; the predecessor may well be the very
861            // pair we just rewrote, so it has to be re-read afterwards.
862            let mut parent = mdir;
863            parent.entries.remove(id);
864            self.commit(dev, &mut parent)?;
865
866            let last = chain.last().expect("chain is never empty");
867            let mut pred = self.find_pred(dev, head)?;
868            pred.tail = last.tail;
869            pred.hard = last.hard;
870            // Global state lives as a per-pair delta whose XOR across the
871            // volume is the filesystem's state, so a dropped pair's delta
872            // has to be carried over rather than lost.
873            for m in &chain {
874                if let Some(g) = m.gdelta {
875                    let mut acc = pred.gdelta.unwrap_or([0u8; 12]);
876                    for (a, b) in acc.iter_mut().zip(g.iter()) {
877                        *a ^= *b;
878                    }
879                    pred.gdelta = if acc == [0u8; 12] { None } else { Some(acc) };
880                }
881            }
882            self.commit(dev, &mut pred)?;
883
884            for m in &chain {
885                self.cache.remove(m.pair);
886                let a = self.allocator(dev)?;
887                a.free(m.pair[0]);
888                a.free(m.pair[1]);
889            }
890            return Ok(());
891        }
892
893        if let Some(data) = &entry.data {
894            self.free_data(dev, data)?;
895        }
896        let mut parent = mdir;
897        parent.entries.remove(id);
898        self.commit(dev, &mut parent)
899    }
900
901    /// Reject names littlefs can't store.
902    fn check_name(&self, name: &str) -> Result<()> {
903        if name.is_empty() {
904            return Err(Error::InvalidArgument("littlefs: empty name".into()));
905        }
906        if name.len() > self.name_max as usize {
907            return Err(Error::InvalidArgument(format!(
908                "littlefs: name {name:?} is longer than the volume's {}-byte limit",
909                self.name_max
910            )));
911        }
912        Ok(())
913    }
914
915    /// Directory listing shared by `list` and the FUSE-facing helpers.
916    fn list_dir(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<DirEntry>> {
917        let r = self.resolve(dev, path)?;
918        let head = self.dir_head(&r)?;
919        let mut out = Vec::new();
920        for m in self.chain(dev, head)? {
921            for (id, e) in m.entries.iter().enumerate().filter(|(_, e)| e.is_file()) {
922                out.push(DirEntry {
923                    name: String::from_utf8_lossy(&e.name).into_owned(),
924                    inode: synthetic_inode(m.pair, id, e),
925                    kind: entry_kind(e),
926                    size: entry_size(e),
927                });
928            }
929        }
930        Ok(out)
931    }
932
933    /// Locate a file's contents for reading.
934    fn file_source(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<rw::Source> {
935        let Resolved::Entry { mdir, id } = self.resolve(dev, path)? else {
936            return Err(Error::InvalidArgument(
937                "littlefs: the root is not a file".into(),
938            ));
939        };
940        let e = &mdir.entries[id];
941        if e.kind != tag::TYPE_REG as u8 {
942            return Err(Error::InvalidArgument(format!(
943                "littlefs: {:?} is not a regular file",
944                path.display()
945            )));
946        }
947        Ok(match &e.data {
948            Some(Struct::Inline(d)) => rw::Source::Inline(d.clone()),
949            Some(Struct::Ctz { head, size }) => rw::Source::Ctz {
950                head: *head,
951                size: *size,
952            },
953            _ => rw::Source::Inline(Vec::new()),
954        })
955    }
956}
957
958/// littlefs's own default: a file is inlined while it fits in an eighth of
959/// a metadata block, bounded by what a single tag can carry.
960fn pick_inline_max(geom: &Geom, requested: Option<u32>) -> Result<u32> {
961    let ceiling = (tag::MAX_SIZE as u32).min(geom.split_limit() as u32 / 2);
962    let v = requested.unwrap_or_else(|| (geom.block_size / 8).min(ceiling));
963    if v > ceiling {
964        return Err(Error::InvalidArgument(format!(
965            "littlefs: inline_max {v} exceeds the {ceiling} bytes a {}-byte block can inline",
966            geom.block_size
967        )));
968    }
969    Ok(v)
970}
971
972/// Split a path into its components, rejecting anything that would escape
973/// the volume root.
974///
975/// Both separators are accepted, as the FAT backend does: the trait hands
976/// us `&Path`s that callers build with `PathBuf::join` — including the
977/// trait's own default `total_file_bytes` walker — and on Windows that
978/// joins with a backslash. The cost is that a littlefs name *containing* a
979/// backslash can't be addressed by path, which matches how FAT behaves and
980/// is not a name any real volume uses.
981fn components(path: &Path) -> Result<Vec<&str>> {
982    let s = path
983        .to_str()
984        .ok_or_else(|| Error::InvalidArgument("littlefs: non-UTF-8 path".into()))?;
985    let mut out: Vec<&str> = Vec::new();
986    for c in s.split(['/', '\\']) {
987        match c {
988            "" | "." => {}
989            ".." => {
990                if out.pop().is_none() {
991                    return Err(Error::InvalidArgument(
992                        "littlefs: path escapes the root".into(),
993                    ));
994                }
995            }
996            other => out.push(other),
997        }
998    }
999    Ok(out)
1000}
1001
1002fn entry_kind(e: &Entry) -> EntryKind {
1003    if e.kind == tag::TYPE_DIR as u8 {
1004        EntryKind::Dir
1005    } else {
1006        EntryKind::Regular
1007    }
1008}
1009
1010fn entry_size(e: &Entry) -> u64 {
1011    match &e.data {
1012        Some(Struct::Inline(d)) => d.len() as u64,
1013        Some(Struct::Ctz { size, .. }) => *size as u64,
1014        _ => 0,
1015    }
1016}
1017
1018/// littlefs has no inode numbers, so we synthesise a stable one: a
1019/// directory is identified by the first block of its own metadata pair, a
1020/// file by its id within its parent's pair (ids stop at 0xfe, so eight bits
1021/// are enough). Callers use these only to tell entries apart — FUSE node
1022/// ids, and cycle detection in tree walks.
1023fn synthetic_inode(pair: [u32; 2], id: usize, e: &Entry) -> u32 {
1024    match &e.data {
1025        Some(Struct::Dir(p)) => p[0].max(1),
1026        _ => 0x8000_0000 | (pair[0].wrapping_shl(8) & 0x7fff_ff00) | (id as u32 & 0xff),
1027    }
1028}
1029
1030/// Map a littlefs user-attribute type to its extended-attribute name.
1031fn xattr_name(kind: u8) -> String {
1032    format!("{XATTR_PREFIX}{kind}")
1033}
1034
1035/// Parse an extended-attribute name back into a littlefs attribute type.
1036fn xattr_type(name: &str) -> Result<u8> {
1037    name.strip_prefix(XATTR_PREFIX)
1038        .and_then(|n| n.parse::<u8>().ok())
1039        .ok_or_else(|| {
1040            Error::Unsupported(format!(
1041                "littlefs: only {XATTR_PREFIX}<0-255> attributes can be stored (got {name:?})"
1042            ))
1043        })
1044}
1045
1046impl Filesystem for LittleFs {
1047    fn streams_immediately(&self) -> bool {
1048        true
1049    }
1050
1051    fn create_file(
1052        &mut self,
1053        dev: &mut dyn BlockDevice,
1054        path: &Path,
1055        src: FileSource,
1056        _meta: FileMeta,
1057    ) -> Result<()> {
1058        let (mut reader, len) = src.open()?;
1059        self.write_file(dev, path, &mut reader, len)
1060    }
1061
1062    fn create_file_streaming(
1063        &mut self,
1064        dev: &mut dyn BlockDevice,
1065        path: &Path,
1066        body: &mut dyn Read,
1067        len: u64,
1068        _meta: FileMeta,
1069    ) -> Result<()> {
1070        self.write_file(dev, path, body, len)
1071    }
1072
1073    fn create_dir(
1074        &mut self,
1075        dev: &mut dyn BlockDevice,
1076        path: &Path,
1077        _meta: FileMeta,
1078    ) -> Result<()> {
1079        if components(path)?.is_empty() {
1080            return Ok(()); // the root always exists
1081        }
1082        self.make_dir(dev, path)
1083    }
1084
1085    fn create_symlink(
1086        &mut self,
1087        _dev: &mut dyn BlockDevice,
1088        _path: &Path,
1089        _target: &Path,
1090        _meta: FileMeta,
1091    ) -> Result<()> {
1092        Err(Error::Unsupported(
1093            "littlefs: the format has no symbolic links".into(),
1094        ))
1095    }
1096
1097    fn create_device(
1098        &mut self,
1099        _dev: &mut dyn BlockDevice,
1100        _path: &Path,
1101        _kind: crate::fs::DeviceKind,
1102        _major: u32,
1103        _minor: u32,
1104        _meta: FileMeta,
1105    ) -> Result<()> {
1106        Err(Error::Unsupported(
1107            "littlefs: the format has no device nodes".into(),
1108        ))
1109    }
1110
1111    fn remove(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<()> {
1112        self.remove_path(dev, path)
1113    }
1114
1115    fn list(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<DirEntry>> {
1116        self.list_dir(dev, path)
1117    }
1118
1119    fn read_file<'a>(
1120        &'a mut self,
1121        dev: &'a mut dyn BlockDevice,
1122        path: &Path,
1123    ) -> Result<Box<dyn Read + 'a>> {
1124        let src = self.file_source(dev, path)?;
1125        Ok(Box::new(rw::FileReader::new(dev, self.geom, src)))
1126    }
1127
1128    fn open_file_ro<'a>(
1129        &'a mut self,
1130        dev: &'a mut dyn BlockDevice,
1131        path: &Path,
1132    ) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
1133        let src = self.file_source(dev, path)?;
1134        Ok(Box::new(rw::FileReader::new(dev, self.geom, src)))
1135    }
1136
1137    fn open_file_rw<'a>(
1138        &'a mut self,
1139        dev: &'a mut dyn BlockDevice,
1140        path: &Path,
1141        flags: crate::fs::OpenFlags,
1142        meta: Option<FileMeta>,
1143    ) -> Result<Box<dyn crate::fs::FileHandle + 'a>> {
1144        rw::open_rw(self, dev, path, flags, meta)
1145    }
1146
1147    fn truncate(&mut self, dev: &mut dyn BlockDevice, path: &Path, new_size: u64) -> Result<()> {
1148        rw::truncate(self, dev, path, new_size)
1149    }
1150
1151    fn rename(
1152        &mut self,
1153        dev: &mut dyn BlockDevice,
1154        old_path: &Path,
1155        new_path: &Path,
1156    ) -> Result<()> {
1157        let Resolved::Entry { mdir, id } = self.resolve(dev, old_path)? else {
1158            return Err(Error::InvalidArgument(
1159                "littlefs: cannot rename the root directory".into(),
1160            ));
1161        };
1162        let entry = mdir.entries[id].clone();
1163        let (dst_head, name) = self.parent_head(dev, new_path)?;
1164        self.check_name(&name)?;
1165        if self.find_in_dir(dev, dst_head, name.as_bytes())?.is_some() {
1166            return Err(Error::InvalidArgument(format!(
1167                "littlefs: {:?} already exists",
1168                new_path.display()
1169            )));
1170        }
1171
1172        // Drop the old entry first so a name moving within one directory
1173        // doesn't briefly exist twice — the pair is rewritten either way.
1174        let mut src = mdir;
1175        src.entries.remove(id);
1176        self.commit(dev, &mut src)?;
1177        self.insert_entry(
1178            dev,
1179            dst_head,
1180            Entry {
1181                kind: entry.kind,
1182                name: name.into_bytes(),
1183                data: entry.data,
1184                attrs: entry.attrs,
1185            },
1186        )
1187    }
1188
1189    fn getattr(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<FileAttrs> {
1190        let r = self.resolve(dev, path)?;
1191        let (kind, size, inode) = match &r {
1192            Resolved::Root => (EntryKind::Dir, 0, self.root[0].max(1)),
1193            Resolved::Entry { mdir, id } => {
1194                let e = &mdir.entries[*id];
1195                (
1196                    entry_kind(e),
1197                    entry_size(e),
1198                    synthetic_inode(mdir.pair, *id, e),
1199                )
1200            }
1201        };
1202        // littlefs stores no permissions, owners or timestamps; these are
1203        // the values a littlefs FUSE mount reports too.
1204        Ok(FileAttrs {
1205            kind,
1206            mode: if kind == EntryKind::Dir { 0o755 } else { 0o644 },
1207            uid: 0,
1208            gid: 0,
1209            size,
1210            blocks: size.div_ceil(512),
1211            nlink: if kind == EntryKind::Dir { 2 } else { 1 },
1212            atime: 0,
1213            mtime: 0,
1214            ctime: 0,
1215            rdev: 0,
1216            inode,
1217        })
1218    }
1219
1220    fn list_xattrs(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<XattrPair>> {
1221        let Resolved::Entry { mdir, id } = self.resolve(dev, path)? else {
1222            return Ok(Vec::new());
1223        };
1224        Ok(mdir.entries[id]
1225            .attrs
1226            .iter()
1227            .map(|(k, v)| XattrPair {
1228                name: xattr_name(*k),
1229                value: v.clone(),
1230            })
1231            .collect())
1232    }
1233
1234    fn set_xattr(
1235        &mut self,
1236        dev: &mut dyn BlockDevice,
1237        path: &Path,
1238        name: &str,
1239        value: &[u8],
1240    ) -> Result<()> {
1241        let kind = xattr_type(name)?;
1242        if value.len() > self.attr_max as usize {
1243            return Err(Error::InvalidArgument(format!(
1244                "littlefs: attribute value of {} bytes exceeds the volume's {}-byte limit",
1245                value.len(),
1246                self.attr_max
1247            )));
1248        }
1249        let Resolved::Entry { mut mdir, id } = self.resolve(dev, path)? else {
1250            return Err(Error::InvalidArgument(
1251                "littlefs: the root has no attributes".into(),
1252            ));
1253        };
1254        let attrs = &mut mdir.entries[id].attrs;
1255        attrs.retain(|(k, _)| *k != kind);
1256        attrs.push((kind, value.to_vec()));
1257        attrs.sort_by_key(|(k, _)| *k);
1258        self.commit(dev, &mut mdir)
1259    }
1260
1261    fn remove_xattr(&mut self, dev: &mut dyn BlockDevice, path: &Path, name: &str) -> Result<()> {
1262        let kind = xattr_type(name)?;
1263        let Resolved::Entry { mut mdir, id } = self.resolve(dev, path)? else {
1264            return Err(Error::InvalidArgument(
1265                "littlefs: the root has no attributes".into(),
1266            ));
1267        };
1268        mdir.entries[id].attrs.retain(|(k, _)| *k != kind);
1269        self.commit(dev, &mut mdir)
1270    }
1271
1272    fn statfs(&mut self, dev: &mut dyn BlockDevice) -> Result<StatFs> {
1273        let used = self.allocator(dev)?.used() as u64;
1274        let total = self.geom.block_count as u64;
1275        Ok(StatFs {
1276            block_size: self.geom.block_size,
1277            blocks: total,
1278            blocks_free: total.saturating_sub(used),
1279            blocks_avail: total.saturating_sub(used),
1280            inodes: 0,
1281            inodes_free: 0,
1282            name_max: self.name_max,
1283        })
1284    }
1285
1286    fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
1287        // Every mutation is already committed; this just pushes the block
1288        // device's own buffers out.
1289        dev.sync()
1290    }
1291
1292    fn mutation_capability(&self) -> MutationCapability {
1293        MutationCapability::Mutable
1294    }
1295}
1296
1297impl crate::fs::FilesystemFactory for LittleFs {
1298    type FormatOpts = LittleFsFormatOpts;
1299
1300    fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
1301        LittleFs::format(dev, opts)
1302    }
1303
1304    fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
1305        LittleFs::open(dev)
1306    }
1307
1308    fn size_plan(opts: &Self::FormatOpts) -> Option<Box<dyn crate::fs::FsSizePlan>> {
1309        Some(Box::new(LittleFsSizePlan::new(opts)))
1310    }
1311}