Skip to main content

littlefs_rust/fs/
read.rs

1use super::*;
2
3impl<'a> ImageStorage<'a> {
4    fn full_len_is_available(&self, cfg: Config) -> bool {
5        let Some(expected) = cfg.block_size.checked_mul(cfg.block_count) else {
6            return false;
7        };
8        match self {
9            ImageStorage::Borrowed(image) => image.len() >= expected,
10            ImageStorage::Owned(image) => image.len() >= expected,
11            ImageStorage::Device { device, .. } => {
12                let device_cfg = device.config();
13                device_cfg.block_size == cfg.block_size && device_cfg.block_count >= cfg.block_count
14            }
15        }
16    }
17
18    fn read(&self, cfg: Config, block: u32, off: usize, out: &mut [u8]) -> Result<()> {
19        if block as usize >= cfg.block_count {
20            return Err(Error::OutOfBounds);
21        }
22        if off.checked_add(out.len()).ok_or(Error::OutOfBounds)? > cfg.block_size {
23            return Err(Error::OutOfBounds);
24        }
25
26        match self {
27            ImageStorage::Borrowed(image) => {
28                let start = block as usize * cfg.block_size + off;
29                let end = start + out.len();
30                out.copy_from_slice(image.get(start..end).ok_or(Error::OutOfBounds)?);
31                Ok(())
32            }
33            ImageStorage::Owned(image) => {
34                let start = block as usize * cfg.block_size + off;
35                let end = start + out.len();
36                out.copy_from_slice(image.get(start..end).ok_or(Error::OutOfBounds)?);
37                Ok(())
38            }
39            ImageStorage::Device { device, cache } => cache.read(*device, cfg, block, off, out),
40        }
41    }
42
43    fn read_block(&self, cfg: Config, block: u32, out: &mut [u8]) -> Result<()> {
44        if out.len() != cfg.block_size {
45            return Err(Error::InvalidConfig);
46        }
47        self.read(cfg, block, 0, out)
48    }
49}
50
51impl<'a> Filesystem<'a> {
52    /// Mounts an existing littlefs image by reading the root metadata pair
53    /// `{0, 1}` and validating the superblock entry.
54    pub fn mount(image: &'a [u8], cfg: Config) -> Result<Self> {
55        Self::mount_with_options(image, cfg, FilesystemOptions::default())
56    }
57
58    /// Mounts an existing image with explicit littlefs-style options.
59    pub fn mount_with_options(
60        image: &'a [u8],
61        cfg: Config,
62        options: FilesystemOptions,
63    ) -> Result<Self> {
64        Self::mount_storage(ImageStorage::Borrowed(image), cfg, options)
65    }
66
67    /// Mounts a read-only view by borrowing a block device.
68    ///
69    /// Unlike the fixture-oriented `mount(&[u8])` path, this does not copy the
70    /// complete device into an owned `Vec`. Metadata and CTZ data blocks are
71    /// fetched through `BlockDevice::read` as user operations need them. The
72    /// returned filesystem therefore cannot outlive the device reference.
73    pub fn mount_device<D: BlockDevice + 'a>(device: &'a D) -> Result<Self> {
74        Self::mount_device_with_options(device, FilesystemOptions::default())
75    }
76
77    /// Mounts a read-only block-device view with explicit cache and limit
78    /// options. The limit fields are used like C littlefs' mount-time maximums:
79    /// an image whose superblock advertises larger limits is rejected.
80    pub fn mount_device_with_options<D: BlockDevice + 'a>(
81        device: &'a D,
82        options: FilesystemOptions,
83    ) -> Result<Self> {
84        Self::mount_device_cached(device, options, 4)
85    }
86
87    pub(crate) fn mount_device_cached<D: BlockDevice + 'a>(
88        device: &'a D,
89        options: FilesystemOptions,
90        cache_slots: usize,
91    ) -> Result<Self> {
92        let cfg = device.config();
93        let options = options.validate(cfg)?;
94        Self::mount_storage(
95            ImageStorage::Device {
96                device,
97                cache: Rc::new(ReadBlockCache::new(
98                    options.cache_size_for(cfg),
99                    cache_slots,
100                )),
101            },
102            cfg,
103            options,
104        )
105    }
106
107    fn mount_storage(
108        image: ImageStorage<'a>,
109        cfg: Config,
110        options: FilesystemOptions,
111    ) -> Result<Self> {
112        let options = options.validate(cfg)?;
113        if cfg.block_size < 16 || cfg.block_count < 2 {
114            return Err(Error::InvalidConfig);
115        }
116        if !image.full_len_is_available(cfg) {
117            return Err(Error::OutOfBounds);
118        }
119
120        let root = Self::read_pair_from_storage(&image, cfg, [0, 1])?;
121
122        // The superblock is not a special block. It is a normal metadata entry
123        // rooted in the first metadata pair, and the magic string is stored in
124        // the name tag with id 0.
125        let name = root.find(Tag::new(LFS_TYPE_SUPERBLOCK, 0, 8))?;
126        if name.data != b"littlefs" {
127            return Err(Error::Corrupt);
128        }
129
130        let sb = root.find(Tag::new(LFS_TYPE_INLINESTRUCT, 0, 24))?;
131        if sb.data.len() != 24 {
132            return Err(Error::Corrupt);
133        }
134        let info = FsInfo {
135            disk_version: le32(&sb.data[0..4])?,
136            block_size: le32(&sb.data[4..8])?,
137            block_count: le32(&sb.data[8..12])?,
138            name_max: le32(&sb.data[12..16])?,
139            file_max: le32(&sb.data[16..20])?,
140            attr_max: le32(&sb.data[20..24])?,
141        };
142
143        if info.disk_version != SUPPORTED_DISK_VERSION {
144            return Err(Error::Unsupported);
145        }
146        if info.block_size as usize != cfg.block_size {
147            return Err(Error::InvalidConfig);
148        }
149        if info.block_count as usize > cfg.block_count {
150            return Err(Error::InvalidConfig);
151        }
152        if info.name_max > options.name_max
153            || info.file_max > options.file_max
154            || info.attr_max > options.attr_max
155        {
156            return Err(Error::InvalidConfig);
157        }
158
159        let global_state = Self::collect_global_state(&image, cfg, &root)?;
160        let allocation_seed = Self::collect_allocation_seed(&image, cfg, &root)?;
161
162        Ok(Self {
163            image,
164            cfg,
165            root,
166            info,
167            options,
168            global_state,
169            allocation_seed,
170        })
171    }
172
173    pub fn info(&self) -> &FsInfo {
174        &self.info
175    }
176
177    pub fn options(&self) -> FilesystemOptions {
178        self.options
179    }
180
181    pub fn limits(&self) -> FilesystemLimits {
182        FilesystemLimits {
183            block_size: self.info.block_size,
184            block_count: self.info.block_count,
185            name_max: self.info.name_max,
186            file_max: self.info.file_max,
187            attr_max: self.info.attr_max,
188        }
189    }
190
191    pub(super) fn global_state(&self) -> GlobalState {
192        self.global_state
193    }
194
195    pub(super) fn allocation_seed(&self) -> u32 {
196        self.allocation_seed
197    }
198
199    pub(super) fn inline_threshold(&self) -> usize {
200        self.options.inline_threshold(self.cfg, self.info.attr_max)
201    }
202
203    pub(super) fn read_pair(&self, pair: [u32; 2]) -> Result<MetadataPair> {
204        Self::read_pair_from_storage(&self.image, self.cfg, pair)
205    }
206
207    fn read_pair_from_storage(
208        image: &ImageStorage<'_>,
209        cfg: Config,
210        pair: [u32; 2],
211    ) -> Result<MetadataPair> {
212        MetadataPair::read_from(cfg, pair, |block, out| image.read_block(cfg, block, out))
213    }
214
215    pub fn directory_usage(&self, path: &str) -> Result<DirectoryUsage> {
216        let pair = self.resolve_dir(path)?;
217        self.directory_usage_from_pair(&pair)
218    }
219
220    pub fn root_entries(&self) -> Result<Vec<DirEntry>> {
221        self.read_dir("/")
222    }
223
224    pub fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
225        let pair = self.resolve_dir(path)?;
226        self.entries_in_pair(&pair)
227    }
228
229    /// Visits directory entries without allocating the public result `Vec`.
230    ///
231    /// The parser still folds one metadata pair at a time internally, but
232    /// callers that only need to stream names or count entries no longer have
233    /// to materialize the complete directory listing. `read_dir` is intentionally
234    /// kept as the convenient collecting wrapper.
235    pub fn read_dir_with<F>(&self, path: &str, mut visitor: F) -> Result<()>
236    where
237        F: FnMut(DirEntry) -> Result<()>,
238    {
239        let pair = self.resolve_dir(path)?;
240        self.for_each_file_in_pair_chain(&pair, |file| visitor(file.dir_entry()))
241    }
242
243    pub fn open_dir<'fs>(&'fs self, path: &str) -> Result<DirHandle<'fs, 'a>> {
244        let pair = self.resolve_dir(path)?;
245        Ok(DirHandle {
246            fs: self,
247            head: pair.pair,
248            pos: 0,
249        })
250    }
251
252    pub fn stat(&self, path: &str) -> Result<DirEntry> {
253        // littlefs reports the root as a directory even though it is not named
254        // by a normal directory entry.
255        if path.is_empty() || path == "/" {
256            return Ok(DirEntry {
257                name: alloc::string::String::new(),
258                ty: FileType::Dir,
259                size: 0,
260            });
261        }
262
263        self.resolve_dir_entry(path)
264    }
265
266    pub fn read_file(&self, path: &str) -> Result<Vec<u8>> {
267        let file = self.resolve_file_no_attrs(path)?;
268        if file.ty != FileType::File {
269            return Err(Error::IsDir);
270        }
271
272        match file.data {
273            FileData::Inline(data) => Ok(data),
274            FileData::Ctz { head, size } => self.read_ctz(head, size),
275            FileData::Directory(_) => Err(Error::IsDir),
276        }
277    }
278
279    /// Reads a file into a caller-provided buffer and returns the byte count.
280    ///
281    /// This is the embedded-facing full-file read shape. It uses the normal
282    /// metadata/path machinery internally, but file contents do not require
283    /// allocating a result `Vec`. If the buffer is too small, no partial-read
284    /// contract is promised; callers get `NoSpace` and can retry with the size
285    /// reported by `stat`.
286    pub fn read_file_into(&self, path: &str, out: &mut [u8]) -> Result<usize> {
287        let file = self.resolve_file_no_attrs(path)?;
288        if file.ty != FileType::File {
289            return Err(Error::IsDir);
290        }
291
292        let size = file.dir_entry().size as usize;
293        if out.len() < size {
294            return Err(Error::NoSpace);
295        }
296
297        match file.data {
298            FileData::Inline(data) => {
299                out[..data.len()].copy_from_slice(&data);
300                Ok(data.len())
301            }
302            FileData::Ctz { head, size } => self.read_ctz_into(head, size, out),
303            FileData::Directory(_) => Err(Error::IsDir),
304        }
305    }
306
307    /// Reads a range from a file without materializing the whole file.
308    ///
309    /// This is the read-only counterpart to the mutable file-handle streaming
310    /// path. It follows littlefs' visible read semantics: offsets at or beyond
311    /// EOF return zero, and short tail reads return only the remaining bytes.
312    pub fn read_file_at(&self, path: &str, offset: usize, out: &mut [u8]) -> Result<usize> {
313        let file = self.resolve_file_no_attrs(path)?;
314        if file.ty != FileType::File {
315            return Err(Error::IsDir);
316        }
317
318        let size = file.dir_entry().size as usize;
319        if offset >= size || out.is_empty() {
320            return Ok(0);
321        }
322        let n = core::cmp::min(out.len(), size - offset);
323        match file.data {
324            FileData::Inline(data) => {
325                out[..n].copy_from_slice(&data[offset..offset + n]);
326                Ok(n)
327            }
328            FileData::Ctz { head, size } => self.read_ctz_range(head, size, offset, &mut out[..n]),
329            FileData::Directory(_) => Err(Error::IsDir),
330        }
331    }
332
333    pub(super) fn read_file_data_at(
334        &self,
335        data: &FileData,
336        offset: usize,
337        out: &mut [u8],
338    ) -> Result<usize> {
339        let size = match data {
340            FileData::Inline(data) => data.len(),
341            FileData::Ctz { size, .. } => *size as usize,
342            FileData::Directory(_) => return Err(Error::IsDir),
343        };
344        if offset >= size || out.is_empty() {
345            return Ok(0);
346        }
347        let n = core::cmp::min(out.len(), size - offset);
348        match data {
349            FileData::Inline(data) => {
350                out[..n].copy_from_slice(&data[offset..offset + n]);
351                Ok(n)
352            }
353            FileData::Ctz { head, size } => {
354                self.read_ctz_range(*head, *size, offset, &mut out[..n])
355            }
356            FileData::Directory(_) => Err(Error::IsDir),
357        }
358    }
359
360    pub fn read_attr(&self, path: &str, attr_type: u8) -> Result<Vec<u8>> {
361        self.resolve_attr(path, attr_type)?.ok_or(Error::NotFound)
362    }
363
364    /// Reads a user attribute into caller-owned storage.
365    ///
366    /// This mirrors `read_file_into`: a missing attr returns `NotFound`, and a
367    /// too-small output buffer returns `NoSpace` without copying a truncated
368    /// value. The return value is the number of bytes written.
369    pub fn read_attr_into(&self, path: &str, attr_type: u8, out: &mut [u8]) -> Result<usize> {
370        self.resolve_attr_into(path, attr_type, out)?
371            .ok_or(Error::NotFound)
372    }
373
374    pub fn walk(&self, path: &str) -> Result<Vec<WalkEntry>> {
375        let base = normalize_dir_path(path)?;
376        let mut out = Vec::new();
377        self.walk_with(&base, |entry| {
378            out.push(entry);
379            Ok(())
380        })?;
381        Ok(out)
382    }
383
384    /// Recursively visits entries without allocating a full-tree result.
385    ///
386    /// Each directory is still sorted with a short-lived local vector so the
387    /// callback order matches `walk`. Avoiding a single full recursive result is
388    /// enough for embedded callers that stream a manifest or stop early after a
389    /// match.
390    pub fn walk_with<F>(&self, path: &str, mut visitor: F) -> Result<()>
391    where
392        F: FnMut(WalkEntry) -> Result<()>,
393    {
394        let base = normalize_dir_path(path)?;
395        self.walk_dir_with(&base, &mut visitor)
396    }
397
398    /// Scans the mounted image and returns a bitmap of blocks reachable from
399    /// metadata pairs, hardtail chains, directory structs, and CTZ files.
400    ///
401    /// This is intentionally conservative: both sides of every metadata pair
402    /// are marked used even if only one side currently contains the newest
403    /// valid commit. That matches littlefs allocation semantics and gives the
404    /// future mount-and-mutate writer a safe starting point.
405    pub fn used_blocks(&self) -> Result<Vec<bool>> {
406        let mut used = alloc::vec![false; self.cfg.block_count];
407        let mut seen_pairs = Vec::new();
408        self.mark_pair_tree(&self.root, &mut used, &mut seen_pairs)?;
409        Ok(used)
410    }
411
412    fn walk_dir_with<F>(&self, path: &str, visitor: &mut F) -> Result<()>
413    where
414        F: FnMut(WalkEntry) -> Result<()>,
415    {
416        let mut entries = self.read_dir(path)?;
417        entries.sort_by(|a, b| a.name.cmp(&b.name));
418
419        for entry in entries {
420            let child_path = join_path(path, &entry.name);
421            visitor(WalkEntry {
422                path: child_path.clone(),
423                entry: entry.clone(),
424            })?;
425            if entry.ty == FileType::Dir {
426                self.walk_dir_with(&child_path, visitor)?;
427            }
428        }
429
430        Ok(())
431    }
432
433    pub(super) fn resolve_dir(&self, path: &str) -> Result<MetadataPair> {
434        // Resolve one path component at a time with targeted metadata lookup.
435        // Directory listing still materializes a public Vec for callers, but
436        // `stat("/a/b")` should not allocate every entry in `/` just to find
437        // `a`, then allocate every entry in `/a` just to find `b`.
438        if path.is_empty() || path == "/" {
439            return Ok(self.root.clone());
440        }
441
442        let mut pair = self.root.clone();
443        for component in components(path)? {
444            let file = self.find_file_in_pair_chain_no_attrs(&pair, component)?;
445            match file.data {
446                FileData::Directory(child) if file.ty == FileType::Dir => {
447                    pair = self.read_pair(child)?;
448                }
449                _ => return Err(Error::NotDir),
450            }
451        }
452        Ok(pair)
453    }
454
455    fn resolve_dir_entry(&self, path: &str) -> Result<DirEntry> {
456        let parts = components(path)?;
457        let (name, parents) = parts.split_last().ok_or(Error::InvalidPath)?;
458
459        let mut pair = self.root.clone();
460        for component in parents {
461            let file = self.find_file_in_pair_chain_no_attrs(&pair, component)?;
462            match file.data {
463                FileData::Directory(child) if file.ty == FileType::Dir => {
464                    pair = self.read_pair(child)?;
465                }
466                _ => return Err(Error::NotDir),
467            }
468        }
469
470        self.find_dir_entry_in_pair_chain(&pair, name)
471    }
472
473    pub(super) fn resolve_file_no_attrs(&self, path: &str) -> Result<FileRecord> {
474        let parts = components(path)?;
475        let (name, parents) = parts.split_last().ok_or(Error::InvalidPath)?;
476
477        let mut pair = self.root.clone();
478        for component in parents {
479            let file = self.find_file_in_pair_chain_no_attrs(&pair, component)?;
480            match file.data {
481                FileData::Directory(child) if file.ty == FileType::Dir => {
482                    pair = self.read_pair(child)?;
483                }
484                _ => return Err(Error::NotDir),
485            }
486        }
487
488        self.find_file_in_pair_chain_no_attrs(&pair, name)
489    }
490
491    fn resolve_attr(&self, path: &str, attr_type: u8) -> Result<Option<Vec<u8>>> {
492        let parts = components(path)?;
493        let (name, parents) = parts.split_last().ok_or(Error::InvalidPath)?;
494
495        let mut pair = self.root.clone();
496        for component in parents {
497            let file = self.find_file_in_pair_chain_no_attrs(&pair, component)?;
498            match file.data {
499                FileData::Directory(child) if file.ty == FileType::Dir => {
500                    pair = self.read_pair(child)?;
501                }
502                _ => return Err(Error::NotDir),
503            }
504        }
505
506        self.find_attr_in_pair_chain(&pair, name, attr_type)
507    }
508
509    fn resolve_attr_into(
510        &self,
511        path: &str,
512        attr_type: u8,
513        out: &mut [u8],
514    ) -> Result<Option<usize>> {
515        let parts = components(path)?;
516        let (name, parents) = parts.split_last().ok_or(Error::InvalidPath)?;
517
518        let mut pair = self.root.clone();
519        for component in parents {
520            let file = self.find_file_in_pair_chain_no_attrs(&pair, component)?;
521            match file.data {
522                FileData::Directory(child) if file.ty == FileType::Dir => {
523                    pair = self.read_pair(child)?;
524                }
525                _ => return Err(Error::NotDir),
526            }
527        }
528
529        self.find_attr_in_pair_chain_into(&pair, name, attr_type, out)
530    }
531
532    fn entries_in_pair(&self, pair: &MetadataPair) -> Result<Vec<DirEntry>> {
533        let mut entries = Vec::new();
534        self.for_each_file_in_pair_chain(pair, |file| {
535            entries.push(file.dir_entry());
536            Ok(())
537        })?;
538        Ok(entries)
539    }
540
541    pub(super) fn dir_entry_at(&self, head: [u32; 2], index: usize) -> Result<Option<DirEntry>> {
542        let mut current = if head == self.root.pair {
543            self.root.clone()
544        } else {
545            self.read_pair(head)?
546        };
547        let mut seen = Vec::<[u32; 2]>::new();
548        let mut remaining = index;
549
550        loop {
551            if seen.contains(&current.pair) {
552                return Err(Error::Corrupt);
553            }
554            seen.push(current.pair);
555
556            let mut found = None;
557            let mut local_index = 0usize;
558            current.fold_dir(|_id, file| {
559                if found.is_none() && local_index == remaining {
560                    found = Some(file.dir_entry());
561                }
562                local_index += 1;
563                Ok(())
564            })?;
565            if found.is_some() {
566                return Ok(found);
567            }
568            remaining = remaining.saturating_sub(local_index);
569
570            match current.hardtail()? {
571                Some(next) if next != [LFS_NULL, LFS_NULL] => {
572                    current = self.read_pair(next)?;
573                }
574                _ => return Ok(None),
575            }
576        }
577    }
578
579    pub(super) fn files_in_pair_chain(&self, pair: &MetadataPair) -> Result<Vec<FileRecord>> {
580        let mut files = Vec::new();
581        self.for_each_file_in_pair_chain(pair, |file| {
582            files.push(file);
583            Ok(())
584        })?;
585        Ok(files)
586    }
587
588    pub(super) fn for_each_file_in_pair_chain<F>(
589        &self,
590        pair: &MetadataPair,
591        mut visitor: F,
592    ) -> Result<()>
593    where
594        F: FnMut(FileRecord) -> Result<()>,
595    {
596        let mut current = pair.clone();
597        let mut seen = Vec::<[u32; 2]>::new();
598
599        loop {
600            // Hardtail loops should never exist on a valid filesystem, but bad
601            // images must not turn traversal into an infinite loop.
602            if seen.contains(&current.pair) {
603                return Err(Error::Corrupt);
604            }
605            seen.push(current.pair);
606            current.fold_dir(|_id, file| visitor(file))?;
607
608            match current.hardtail()? {
609                Some(next) if next != [LFS_NULL, LFS_NULL] => {
610                    current = self.read_pair(next)?;
611                }
612                _ => break,
613            }
614        }
615
616        Ok(())
617    }
618
619    fn find_file_in_pair_chain_no_attrs(
620        &self,
621        pair: &MetadataPair,
622        name: &str,
623    ) -> Result<FileRecord> {
624        let mut current = pair.clone();
625        let mut seen = Vec::<[u32; 2]>::new();
626
627        loop {
628            if seen.contains(&current.pair) {
629                return Err(Error::Corrupt);
630            }
631            seen.push(current.pair);
632
633            if let Some(file) = current.find_name_no_attrs(name)? {
634                return Ok(file);
635            }
636
637            match current.hardtail()? {
638                Some(next) if next != [LFS_NULL, LFS_NULL] => {
639                    current = self.read_pair(next)?;
640                }
641                _ => return Err(Error::NotFound),
642            }
643        }
644    }
645
646    fn find_dir_entry_in_pair_chain(&self, pair: &MetadataPair, name: &str) -> Result<DirEntry> {
647        let mut current = pair.clone();
648        let mut seen = Vec::<[u32; 2]>::new();
649
650        loop {
651            if seen.contains(&current.pair) {
652                return Err(Error::Corrupt);
653            }
654            seen.push(current.pair);
655
656            if let Some(entry) = current.find_dir_entry(name)? {
657                return Ok(entry);
658            }
659
660            match current.hardtail()? {
661                Some(next) if next != [LFS_NULL, LFS_NULL] => {
662                    current = self.read_pair(next)?;
663                }
664                _ => return Err(Error::NotFound),
665            }
666        }
667    }
668
669    fn find_attr_in_pair_chain(
670        &self,
671        pair: &MetadataPair,
672        name: &str,
673        attr_type: u8,
674    ) -> Result<Option<Vec<u8>>> {
675        let mut current = pair.clone();
676        let mut seen = Vec::<[u32; 2]>::new();
677
678        loop {
679            if seen.contains(&current.pair) {
680                return Err(Error::Corrupt);
681            }
682            seen.push(current.pair);
683
684            if let Some(attr) = current.find_attr(name, attr_type)? {
685                return Ok(Some(attr));
686            }
687
688            match current.hardtail()? {
689                Some(next) if next != [LFS_NULL, LFS_NULL] => {
690                    current = self.read_pair(next)?;
691                }
692                _ => return Ok(None),
693            }
694        }
695    }
696
697    fn find_attr_in_pair_chain_into(
698        &self,
699        pair: &MetadataPair,
700        name: &str,
701        attr_type: u8,
702        out: &mut [u8],
703    ) -> Result<Option<usize>> {
704        let mut current = pair.clone();
705        let mut seen = Vec::<[u32; 2]>::new();
706
707        loop {
708            if seen.contains(&current.pair) {
709                return Err(Error::Corrupt);
710            }
711            seen.push(current.pair);
712
713            if let Some(len) = current.copy_attr_into(name, attr_type, out)? {
714                return Ok(Some(len));
715            }
716
717            match current.hardtail()? {
718                Some(next) if next != [LFS_NULL, LFS_NULL] => {
719                    current = self.read_pair(next)?;
720                }
721                _ => return Ok(None),
722            }
723        }
724    }
725
726    fn directory_usage_from_pair(&self, pair: &MetadataPair) -> Result<DirectoryUsage> {
727        let mut entry_count = 0usize;
728        let mut metadata_pair_count = 0usize;
729        let mut current = pair.clone();
730        let mut seen = Vec::<[u32; 2]>::new();
731        let mut is_split = false;
732        loop {
733            if seen.contains(&current.pair) {
734                return Err(Error::Corrupt);
735            }
736            seen.push(current.pair);
737            metadata_pair_count += 1;
738            entry_count += current.file_count()?;
739            match current.hardtail()? {
740                Some(next) if next != [LFS_NULL, LFS_NULL] => {
741                    is_split = true;
742                    current = self.read_pair(next)?;
743                }
744                _ => {
745                    return Ok(DirectoryUsage {
746                        entry_count,
747                        metadata_pair_count,
748                        is_split,
749                        append_bytes_used: current.state.off,
750                        append_bytes_remaining: self
751                            .cfg
752                            .block_size
753                            .saturating_sub(current.state.off),
754                    });
755                }
756            }
757        }
758    }
759
760    fn mark_pair_tree(
761        &self,
762        pair: &MetadataPair,
763        used: &mut [bool],
764        seen_pairs: &mut Vec<[u32; 2]>,
765    ) -> Result<()> {
766        let files = self.mark_pair_chain(pair, used, seen_pairs)?;
767        for file in files {
768            match file.data {
769                FileData::Directory(child) if file.ty == FileType::Dir => {
770                    let child = self.read_pair(child)?;
771                    self.mark_pair_tree(&child, used, seen_pairs)?;
772                }
773                FileData::Ctz { head, size } if file.ty == FileType::File => {
774                    self.mark_ctz_blocks(head, size, used)?;
775                }
776                _ => {}
777            }
778        }
779        Ok(())
780    }
781
782    fn mark_pair_chain(
783        &self,
784        pair: &MetadataPair,
785        used: &mut [bool],
786        seen_pairs: &mut Vec<[u32; 2]>,
787    ) -> Result<Vec<FileRecord>> {
788        let mut files = Vec::new();
789        let mut current = pair.clone();
790
791        loop {
792            if seen_pairs.contains(&current.pair) {
793                return Err(Error::Corrupt);
794            }
795            seen_pairs.push(current.pair);
796            self.mark_block(current.pair[0], used)?;
797            self.mark_block(current.pair[1], used)?;
798            files.extend(current.files()?);
799
800            match current.hardtail()? {
801                Some(next) if next != [LFS_NULL, LFS_NULL] => {
802                    current = self.read_pair(next)?;
803                }
804                _ => break,
805            }
806        }
807
808        Ok(files)
809    }
810
811    fn mark_ctz_blocks(&self, head: u32, size: u32, used: &mut [bool]) -> Result<()> {
812        let mut pos = 0u32;
813        while pos < size {
814            let (block, off) = self.ctz_find(head, size, pos)?;
815            if block == LFS_NULL {
816                return Err(Error::Corrupt);
817            }
818            self.mark_block(block, used)?;
819
820            let off = off as usize;
821            if off >= self.cfg.block_size {
822                return Err(Error::Corrupt);
823            }
824            let diff = core::cmp::min((size - pos) as usize, self.cfg.block_size - off);
825            pos += diff as u32;
826        }
827        Ok(())
828    }
829
830    fn mark_block(&self, block: u32, used: &mut [bool]) -> Result<()> {
831        let slot = used.get_mut(block as usize).ok_or(Error::OutOfBounds)?;
832        if *slot {
833            return Err(Error::Corrupt);
834        }
835        *slot = true;
836        Ok(())
837    }
838
839    fn collect_global_state(
840        image: &ImageStorage<'_>,
841        cfg: Config,
842        root: &MetadataPair,
843    ) -> Result<GlobalState> {
844        if root.tail()?.is_some() {
845            return Self::collect_global_state_thread(image, cfg, root);
846        }
847        let mut global_state = GlobalState::default();
848        let mut seen_pairs = Vec::new();
849        Self::collect_global_state_pair_tree(image, cfg, root, &mut seen_pairs, &mut global_state)?;
850        Ok(global_state)
851    }
852
853    fn collect_allocation_seed(
854        image: &ImageStorage<'_>,
855        cfg: Config,
856        root: &MetadataPair,
857    ) -> Result<u32> {
858        if root.tail()?.is_some() {
859            return Self::collect_allocation_seed_thread(image, cfg, root);
860        }
861        let mut seed = 0;
862        let mut seen_pairs = Vec::new();
863        Self::collect_allocation_seed_pair_tree(image, cfg, root, &mut seen_pairs, &mut seed)?;
864        Ok(seed)
865    }
866
867    fn collect_allocation_seed_thread(
868        image: &ImageStorage<'_>,
869        cfg: Config,
870        root: &MetadataPair,
871    ) -> Result<u32> {
872        let mut seed = 0;
873        let mut current = root.clone();
874        let mut seen = Vec::<[u32; 2]>::new();
875        loop {
876            if seen.contains(&current.pair) {
877                break;
878            }
879            seen.push(current.pair);
880            current.fold_commit_crcs_into_seed(&mut seed)?;
881
882            let Some(tail) = current.tail()? else {
883                break;
884            };
885            if tail.pair == [LFS_NULL, LFS_NULL] {
886                break;
887            }
888            current = Self::read_pair_from_storage(image, cfg, tail.pair)?;
889        }
890        Ok(seed)
891    }
892
893    fn collect_allocation_seed_pair_tree(
894        image: &ImageStorage<'_>,
895        cfg: Config,
896        pair: &MetadataPair,
897        seen_pairs: &mut Vec<[u32; 2]>,
898        seed: &mut u32,
899    ) -> Result<()> {
900        if seen_pairs.contains(&pair.pair) {
901            return Err(Error::Corrupt);
902        }
903        seen_pairs.push(pair.pair);
904        pair.fold_commit_crcs_into_seed(seed)?;
905        for file in pair.files()? {
906            if let FileData::Directory(child) = file.data
907                && file.ty == FileType::Dir
908            {
909                let child = Self::read_pair_from_storage(image, cfg, child)?;
910                Self::collect_allocation_seed_pair_tree(image, cfg, &child, seen_pairs, seed)?;
911            }
912        }
913        Ok(())
914    }
915
916    fn collect_global_state_thread(
917        image: &ImageStorage<'_>,
918        cfg: Config,
919        root: &MetadataPair,
920    ) -> Result<GlobalState> {
921        let mut global_state = GlobalState::default();
922        let mut current = root.clone();
923        let mut seen = Vec::<[u32; 2]>::new();
924        loop {
925            if seen.contains(&current.pair) {
926                // Keep read-only mount tolerant of a malformed hardtail cycle
927                // so the operation that actually follows the directory chain
928                // reports the corruption. This mirrors the older visible-tree
929                // global-state fold and keeps corruption errors localized to
930                // the user-visible traversal that needs the chain.
931                break;
932            }
933            seen.push(current.pair);
934            global_state.xor(current.global_state_delta()?);
935
936            let Some(tail) = current.tail()? else {
937                break;
938            };
939            if tail.pair == [LFS_NULL, LFS_NULL] {
940                break;
941            }
942            current = Self::read_pair_from_storage(image, cfg, tail.pair)?;
943        }
944        Ok(global_state)
945    }
946
947    fn collect_global_state_pair_tree(
948        image: &ImageStorage<'_>,
949        cfg: Config,
950        pair: &MetadataPair,
951        seen_pairs: &mut Vec<[u32; 2]>,
952        global_state: &mut GlobalState,
953    ) -> Result<()> {
954        let files =
955            Self::collect_global_state_pair_chain(image, cfg, pair, seen_pairs, global_state)?;
956        for file in files {
957            if let FileData::Directory(child) = file.data
958                && file.ty == FileType::Dir
959            {
960                let child = Self::read_pair_from_storage(image, cfg, child)?;
961                Self::collect_global_state_pair_tree(image, cfg, &child, seen_pairs, global_state)?;
962            }
963        }
964        Ok(())
965    }
966
967    fn collect_global_state_pair_chain(
968        image: &ImageStorage<'_>,
969        cfg: Config,
970        pair: &MetadataPair,
971        seen_pairs: &mut Vec<[u32; 2]>,
972        global_state: &mut GlobalState,
973    ) -> Result<Vec<FileRecord>> {
974        let mut files = Vec::new();
975        let mut current = pair.clone();
976        loop {
977            if seen_pairs.contains(&current.pair) {
978                // Cross-parent directory rename currently has a final-state
979                // path that can briefly make the same child pair reachable
980                // through both old and new parents while the old entry is
981                // being unlinked. Global-state aggregation should not reject
982                // that mount-time intermediate or XOR the same pair twice.
983                return Ok(Vec::new());
984            }
985            seen_pairs.push(current.pair);
986            global_state.xor(current.global_state_delta()?);
987            files.extend(current.files()?);
988
989            match current.hardtail()? {
990                Some(next) if next != [LFS_NULL, LFS_NULL] => {
991                    current = Self::read_pair_from_storage(image, cfg, next)?;
992                }
993                _ => break,
994            }
995        }
996        Ok(files)
997    }
998
999    fn read_ctz(&self, head: u32, size: u32) -> Result<Vec<u8>> {
1000        let mut out = alloc::vec![0; size as usize];
1001        self.read_ctz_range(head, size, 0, &mut out)?;
1002        Ok(out)
1003    }
1004
1005    fn read_ctz_into(&self, head: u32, size: u32, out: &mut [u8]) -> Result<usize> {
1006        if out.len() < size as usize {
1007            return Err(Error::NoSpace);
1008        }
1009
1010        self.read_ctz_range(head, size, 0, &mut out[..size as usize])
1011    }
1012
1013    /// Reads a logical byte range from a CTZ file.
1014    ///
1015    /// CTZ lookup can jump directly to the block containing `offset`, so range
1016    /// reads should not scan every earlier data block. This is especially
1017    /// important for file handles that issue many small random reads.
1018    fn read_ctz_range(&self, head: u32, size: u32, offset: usize, out: &mut [u8]) -> Result<usize> {
1019        if offset > size as usize {
1020            return Ok(0);
1021        }
1022        let end = core::cmp::min(size as usize, offset.saturating_add(out.len()));
1023        let mut pos = u32::try_from(offset).map_err(|_| Error::OutOfBounds)?;
1024        let mut dst = 0usize;
1025        while pos < size && (pos as usize) < end {
1026            let (block, off) = self.ctz_find(head, size, pos)?;
1027            if block == LFS_NULL {
1028                return Err(Error::Corrupt);
1029            }
1030
1031            let off = off as usize;
1032            if off >= self.cfg.block_size {
1033                return Err(Error::Corrupt);
1034            }
1035            let diff = core::cmp::min((size - pos) as usize, self.cfg.block_size - off);
1036            let logical_start = pos as usize;
1037            let logical_end = logical_start + diff;
1038            if logical_end > offset {
1039                let copy_start = core::cmp::max(logical_start, offset);
1040                let copy_end = core::cmp::min(logical_end, end);
1041                let src_start = off + (copy_start - logical_start);
1042                let len = copy_end - copy_start;
1043                self.image
1044                    .read(self.cfg, block, src_start, &mut out[dst..dst + len])?;
1045                dst += len;
1046            }
1047            pos += diff as u32;
1048        }
1049
1050        Ok(dst)
1051    }
1052
1053    fn ctz_find(&self, mut head: u32, size: u32, pos: u32) -> Result<(u32, u32)> {
1054        if size == 0 {
1055            return Ok((LFS_NULL, 0));
1056        }
1057
1058        let mut last = size - 1;
1059        let mut target_pos = pos;
1060        let mut current = self.ctz_index(&mut last)?;
1061        let target = self.ctz_index(&mut target_pos)?;
1062
1063        while current > target {
1064            // Each CTZ block contains skip pointers at the front. The pointer
1065            // chosen here mirrors `lfs_ctz_find`: jump back by the largest
1066            // usable power-of-two distance toward the target block index.
1067            let skip = core::cmp::min(npw2(current - target + 1) - 1, ctz(current));
1068            let ptr_off = (4 * skip) as usize;
1069            if ptr_off + 4 > self.cfg.block_size {
1070                return Err(Error::Corrupt);
1071            }
1072            let mut ptr = [0u8; 4];
1073            self.image.read(self.cfg, head, ptr_off, &mut ptr)?;
1074            head = le32(&ptr)?;
1075            current -= 1 << skip;
1076        }
1077
1078        Ok((head, target_pos))
1079    }
1080
1081    fn ctz_index(&self, off: &mut u32) -> Result<u32> {
1082        // littlefs reserves space at the front of CTZ blocks for skip pointers.
1083        // This helper maps a file byte offset to the logical CTZ block index
1084        // and rewrites `off` to the offset within that physical block.
1085        let size = *off;
1086        let b = self
1087            .cfg
1088            .block_size
1089            .checked_sub(8)
1090            .ok_or(Error::InvalidConfig)? as u32;
1091        let mut i = size / b;
1092        if i == 0 {
1093            return Ok(0);
1094        }
1095
1096        i = (size - 4 * (popc(i - 1) + 2)) / b;
1097        *off = size - b * i - 4 * popc(i);
1098        Ok(i)
1099    }
1100}
1101
1102impl Filesystem<'static> {
1103    /// Formats a block device as an empty littlefs image.
1104    ///
1105    /// The formatted bytes are programmed into the caller's real block device
1106    /// with erase/program/sync operations, so tests exercise the public device
1107    /// boundary instead of only comparing an in-memory `Vec`.
1108    pub fn format_device<D: BlockDevice>(device: &mut D) -> Result<()> {
1109        Self::format_device_with_options(device, FilesystemOptions::default())
1110    }
1111
1112    /// Formats a block device using explicit littlefs-style options.
1113    pub fn format_device_with_options<D: BlockDevice>(
1114        device: &mut D,
1115        options: FilesystemOptions,
1116    ) -> Result<()> {
1117        let cfg = device.config();
1118        let options = options.validate(cfg)?;
1119        let root = ImageBuilder::empty_root_block_with_options(cfg, options)?;
1120        let mut cache = BlockCache::new_with_cache_size(cfg, options.cache_size_for(cfg))?;
1121        for block in 0..cfg.block_count {
1122            cache.erase(device, block as u32)?;
1123        }
1124        cache.prog(device, 0, 0, &root)?;
1125        cache.sync(device)
1126    }
1127
1128    /// Mounts a block device for mutation.
1129    ///
1130    /// Write methods will be added to `FilesystemMut` in small C-verified
1131    /// milestones. Today this establishes the API ownership model and ensures
1132    /// callers can format, mount, inspect, sync, and recover the underlying
1133    /// device without dropping down to image bytes.
1134    pub fn mount_device_mut<D: BlockDevice + 'static>(device: D) -> Result<FilesystemMut<D>> {
1135        FilesystemMut::mount(device)
1136    }
1137
1138    /// Mounts a block device for mutation with explicit littlefs-style options.
1139    pub fn mount_device_mut_with_options<D: BlockDevice + 'static>(
1140        device: D,
1141        options: FilesystemOptions,
1142    ) -> Result<FilesystemMut<D>> {
1143        FilesystemMut::mount_with_options(device, options)
1144    }
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::*;
1150    use crate::format::LFS_TYPE_MOVESTATE;
1151
1152    fn append_movestate(image: &mut [u8], cfg: Config, pair: &MetadataPair, delta: GlobalState) {
1153        let start = pair.active_block as usize * cfg.block_size;
1154        let end = start + cfg.block_size;
1155        let block = image.get_mut(start..end).expect("metadata block slice");
1156        let mut payload = Vec::new();
1157        payload.extend_from_slice(&delta.tag.to_le_bytes());
1158        payload.extend_from_slice(&delta.pair[0].to_le_bytes());
1159        payload.extend_from_slice(&delta.pair[1].to_le_bytes());
1160        let entry = CommitEntry::new(Tag::new(LFS_TYPE_MOVESTATE, 0x3ff, 12), &payload);
1161        let mut writer =
1162            MetadataCommitWriter::append(block, 16, pair.state).expect("append movestate commit");
1163        writer
1164            .write_entries(&[entry])
1165            .expect("write movestate entry");
1166        writer.finish().expect("finish movestate commit");
1167    }
1168
1169    #[test]
1170    fn mount_aggregates_global_state_from_visible_pair_tree() {
1171        let cfg = Config {
1172            block_size: 512,
1173            block_count: 32,
1174        };
1175        let mut builder = ImageBuilder::new(cfg).expect("builder");
1176        builder.create_dir("/docs").expect("create child directory");
1177        builder
1178            .add_inline_file("/docs/a.txt", b"a")
1179            .expect("create child file");
1180        let mut image = builder.build().expect("build image");
1181        let fs = Filesystem::mount(&image, cfg).expect("mount initial image");
1182        let root = fs.root.clone();
1183        let docs = fs.resolve_dir("/docs").expect("resolve child dir");
1184
1185        let root_delta = GlobalState {
1186            tag: Tag::new(LFS_TYPE_DELETE, 7, 0).0,
1187            pair: [0x10, 0x11],
1188        };
1189        let docs_delta = GlobalState {
1190            tag: Tag::new(LFS_TYPE_DELETE, 3, 0).0,
1191            pair: [0x05, 0x15],
1192        };
1193        append_movestate(&mut image, cfg, &root, root_delta);
1194        append_movestate(&mut image, cfg, &docs, docs_delta);
1195
1196        let fs = Filesystem::mount(&image, cfg).expect("mount image with global state");
1197        assert_eq!(
1198            fs.global_state(),
1199            GlobalState {
1200                tag: root_delta.tag ^ docs_delta.tag,
1201                pair: [
1202                    root_delta.pair[0] ^ docs_delta.pair[0],
1203                    root_delta.pair[1] ^ docs_delta.pair[1],
1204                ],
1205            }
1206        );
1207    }
1208}