Skip to main content

littlefs_rust/
fs.rs

1use alloc::{
2    boxed::Box,
3    collections::BTreeMap,
4    format,
5    rc::Rc,
6    string::{String, ToString},
7    vec::Vec,
8};
9use core::cell::{Cell, RefCell};
10
11use crate::{
12    allocator::BlockAllocator,
13    block_device::BlockDevice,
14    cache::BlockCache,
15    commit::{CommitEntry, MetadataCommitWriter, checked_u10},
16    format::{
17        LFS_NULL, LFS_TYPE_CREATE, LFS_TYPE_CTZSTRUCT, LFS_TYPE_DELETE, LFS_TYPE_DIR,
18        LFS_TYPE_DIRSTRUCT, LFS_TYPE_HARDTAIL, LFS_TYPE_INLINESTRUCT, LFS_TYPE_MOVESTATE,
19        LFS_TYPE_REG, LFS_TYPE_SOFTTAIL, LFS_TYPE_SUPERBLOCK, LFS_TYPE_USERATTR, Tag, ctz, le32,
20        npw2, popc,
21    },
22    metadata::{FileData, FileRecord, GlobalState, MetadataPair, MetadataTail, StorageRef},
23    path::{components, join_path, normalize_dir_path},
24    types::{
25        Config, DirEntry, DirectoryUsage, Error, FileType, FilesystemLimits, FilesystemOptions,
26        FsInfo, Result, WalkEntry,
27    },
28    writer::ImageBuilder,
29};
30
31mod handles;
32mod mutable;
33mod read;
34
35const SUPPORTED_DISK_VERSION: u32 = 0x0002_0001;
36
37/// Read-only view over a littlefs disk image or block device.
38///
39/// Slice-backed mounts are still valuable for fixtures, corruption tests, and
40/// C-oracle artifacts that already exist as complete images. The block-device
41/// mount path, however, deliberately borrows the device and reads blocks on
42/// demand, so normal read-only user semantics do not require copying a whole
43/// flash image into RAM.
44#[derive(Debug, Clone)]
45pub struct Filesystem<'a> {
46    image: ImageStorage<'a>,
47    cfg: Config,
48    root: MetadataPair,
49    info: FsInfo,
50    options: FilesystemOptions,
51    global_state: GlobalState,
52    allocation_seed: u32,
53}
54
55/// Mutable mounted filesystem shell backed by a real block device.
56///
57/// The mounted view owns the device and keeps a read-only parser view pointed
58/// at that same device through a small block cache. Mounted mutations commit
59/// through the block-device/cache layer only. Unsupported native transaction
60/// shapes return `Error::Unsupported` instead of falling back to a hidden
61/// capacity-sized image allocation.
62#[derive(Debug)]
63pub struct FilesystemMut<D: BlockDevice + 'static> {
64    fs: Filesystem<'static>,
65    device: Box<D>,
66    cache: BlockCache,
67    allocator: BlockAllocator,
68    block_cycles: Option<u32>,
69}
70
71/// Create-only file writer for the mounted mutable API.
72///
73/// Small files stay buffered so callers can seek before close. Once sequential
74/// writes cross the inline threshold, the handle streams CTZ data blocks before
75/// publishing the close-time metadata commit.
76pub struct FileWriter<'fs, D: BlockDevice + 'static> {
77    fs: &'fs mut FilesystemMut<D>,
78    path: String,
79    data: Vec<u8>,
80    pos: usize,
81    stream: Option<StreamingWrite>,
82}
83
84// Dirty file handles keep these writeback plans until a flush fully succeeds.
85// Cloning lets `FileHandle::flush` attempt the transaction while preserving the
86// original plan for retry if a block-device sync fails after bytes were written.
87#[derive(Clone)]
88struct StreamingWrite {
89    allocator: BlockAllocator,
90    blocks: Vec<u32>,
91    current: Option<StreamingBlock>,
92    len: usize,
93    target: StreamingTarget,
94}
95
96#[derive(Clone)]
97struct StreamingBlock {
98    block: u32,
99    bytes: Vec<u8>,
100    off: usize,
101    mode: StreamingBlockMode,
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105enum StreamingBlockMode {
106    /// A freshly allocated CTZ block. It must be erased before programming and
107    /// appended to the logical CTZ block list once durable.
108    New,
109    /// The last block of an existing CTZ file. Appends may program its erased
110    /// tail bytes in place, but the block is already part of `blocks`.
111    ExistingTail,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115enum StreamingTarget {
116    Create,
117    Replace,
118}
119
120#[derive(Clone)]
121struct MergeWrite {
122    original_len: usize,
123    patches: Vec<FilePatch>,
124}
125
126#[derive(Clone)]
127struct FilePatch {
128    off: usize,
129    data: Vec<u8>,
130}
131
132#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
133pub struct FileOptions {
134    read: bool,
135    write: bool,
136    create: bool,
137    create_new: bool,
138    truncate: bool,
139    append: bool,
140}
141
142/// Option-driven mounted file handle.
143///
144/// Writable handles buffer only when they need random-write merge semantics.
145/// New files, truncating replacements, and CTZ appends can switch to the
146/// streaming CTZ writer, which programs data blocks before the exposing
147/// metadata commit. Read-only handles keep only the current offset and ask the
148/// mounted snapshot to fill caller buffers on demand.
149pub struct FileHandle<'fs, D: BlockDevice + 'static> {
150    fs: &'fs mut FilesystemMut<D>,
151    path: String,
152    /// Buffered logical contents for handles that may write, append, or
153    /// truncate. Empty for pure read-only streaming handles.
154    data: Vec<u8>,
155    /// Current logical file position, shared by both the buffered and streaming
156    /// paths.
157    pos: usize,
158    /// Last known logical file length. For buffered writers this tracks
159    /// `data.len()`; for streaming readers it records the current `stat`
160    /// result so seek/read decisions do not need to preload the file.
161    len: usize,
162    /// True when reads should be served by `Filesystem::read_file_at` instead
163    /// of the buffered `data` vector.
164    stream_read: bool,
165    /// Immutable source captured when a pure read-only handle is opened.
166    /// Holding the resolved file data here avoids repeating path lookup and
167    /// metadata folding on every small `read` call.
168    stream_source: Option<FileData>,
169    /// Streaming mode to use if this handle crosses the CTZ threshold. Missing
170    /// files create a new directory entry; existing files replace their current
171    /// struct with a new CTZ head after data blocks are durable.
172    stream_target: StreamingTarget,
173    /// Streaming CTZ state for newly-created files, truncating replacements,
174    /// and CTZ append handles.
175    stream: Option<StreamingWrite>,
176    /// Patch list for write-only partial overwrites of an existing CTZ file.
177    /// At flush time the handle streams a replacement CTZ chain by reading old
178    /// data in small chunks and overlaying these patches. This is not C's exact
179    /// file cache, but it avoids buffering the whole file for the common
180    /// "overwrite a range in a large file" shape.
181    merge: Option<MergeWrite>,
182    readable: bool,
183    writable: bool,
184    dirty: bool,
185}
186
187/// Stream-like directory handle for the public read API.
188///
189/// The handle borrows the mounted filesystem and stores only a directory head
190/// plus cursor position. Reads fold metadata on demand instead of keeping an
191/// owned `Vec<DirEntry>` snapshot, which keeps large directory handles bounded
192/// by metadata scratch state rather than entry count.
193pub struct DirHandle<'fs, 'a> {
194    fs: &'fs Filesystem<'a>,
195    head: [u32; 2],
196    pos: usize,
197}
198
199enum ImageStorage<'a> {
200    Borrowed(&'a [u8]),
201    Owned(Vec<u8>),
202    Device {
203        device: &'a dyn BlockDevice,
204        cache: Rc<ReadBlockCache>,
205    },
206}
207
208impl core::fmt::Debug for ImageStorage<'_> {
209    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210        match self {
211            Self::Borrowed(image) => f
212                .debug_tuple("Borrowed")
213                .field(&format_args!("{} bytes", image.len()))
214                .finish(),
215            Self::Owned(image) => f
216                .debug_tuple("Owned")
217                .field(&format_args!("{} bytes", image.len()))
218                .finish(),
219            Self::Device { .. } => f.debug_tuple("Device").field(&"<block-device>").finish(),
220        }
221    }
222}
223
224impl<'a> Clone for ImageStorage<'a> {
225    fn clone(&self) -> Self {
226        match self {
227            Self::Borrowed(image) => Self::Borrowed(*image),
228            Self::Owned(image) => Self::Owned(image.clone()),
229            Self::Device { device, cache } => Self::Device {
230                device: *device,
231                cache: cache.clone(),
232            },
233        }
234    }
235}
236
237#[derive(Debug)]
238struct ReadBlockCache {
239    slots: RefCell<Vec<ReadBlockCacheSlot>>,
240    next: Cell<usize>,
241    chunk_size: usize,
242}
243
244#[derive(Debug)]
245struct ReadBlockCacheSlot {
246    block: Option<u32>,
247    off: usize,
248    len: usize,
249    data: Vec<u8>,
250}
251
252impl ReadBlockCache {
253    fn new(cache_size: usize, slots: usize) -> Self {
254        let slots = core::cmp::max(slots, 1);
255        let cache_size = core::cmp::max(cache_size, 1);
256        Self {
257            slots: RefCell::new(
258                (0..slots)
259                    .map(|_| ReadBlockCacheSlot {
260                        block: None,
261                        off: 0,
262                        len: 0,
263                        data: alloc::vec![0xff; cache_size],
264                    })
265                    .collect(),
266            ),
267            next: Cell::new(0),
268            chunk_size: cache_size,
269        }
270    }
271
272    fn read(
273        &self,
274        device: &dyn BlockDevice,
275        cfg: Config,
276        block: u32,
277        off: usize,
278        out: &mut [u8],
279    ) -> Result<()> {
280        if off.checked_add(out.len()).ok_or(Error::OutOfBounds)? > cfg.block_size {
281            return Err(Error::OutOfBounds);
282        }
283
284        let mut copied = 0usize;
285        while copied < out.len() {
286            let absolute = off + copied;
287            let chunk_off = (absolute / self.chunk_size) * self.chunk_size;
288            let chunk_len = core::cmp::min(self.chunk_size, cfg.block_size - chunk_off);
289            let in_chunk = absolute - chunk_off;
290            let len = core::cmp::min(out.len() - copied, chunk_len - in_chunk);
291            self.read_chunk(
292                device,
293                block,
294                chunk_off,
295                chunk_len,
296                in_chunk,
297                &mut out[copied..copied + len],
298            )?;
299            copied += len;
300        }
301        Ok(())
302    }
303
304    fn read_chunk(
305        &self,
306        device: &dyn BlockDevice,
307        block: u32,
308        off: usize,
309        chunk_len: usize,
310        in_chunk: usize,
311        out: &mut [u8],
312    ) -> Result<()> {
313        let mut slots = self.slots.borrow_mut();
314        if let Some(slot) = slots
315            .iter()
316            .find(|slot| slot.block == Some(block) && slot.off == off && slot.len == chunk_len)
317        {
318            out.copy_from_slice(&slot.data[in_chunk..in_chunk + out.len()]);
319            return Ok(());
320        }
321
322        let slot_index = self.next.get() % slots.len();
323        self.next.set((slot_index + 1) % slots.len());
324        let slot = &mut slots[slot_index];
325        if slot.data.len() < chunk_len {
326            slot.data.resize(chunk_len, 0xff);
327        }
328        device.read(block, off, &mut slot.data[..chunk_len])?;
329        slot.block = Some(block);
330        slot.off = off;
331        slot.len = chunk_len;
332        out.copy_from_slice(&slot.data[in_chunk..in_chunk + out.len()]);
333        Ok(())
334    }
335}
336
337fn ctz_data_start(index: usize) -> Result<usize> {
338    if index == 0 {
339        Ok(0)
340    } else {
341        let skips = index.trailing_zeros() as usize + 1;
342        skips.checked_mul(4).ok_or(Error::NoSpace)
343    }
344}
345
346fn program_nor_bytes(block: &mut [u8], off: usize, data: &[u8]) -> Result<()> {
347    let end = off.checked_add(data.len()).ok_or(Error::NoSpace)?;
348    if end > block.len() {
349        return Err(Error::NoSpace);
350    }
351    for (dst, src) in block[off..end].iter_mut().zip(data) {
352        *dst &= *src;
353    }
354    Ok(())
355}
356
357fn root_create_id(files: &[FileRecord], name: &str) -> Result<u16> {
358    let id = files
359        .iter()
360        .filter(|file| file.name.as_str() < name)
361        .count()
362        .checked_add(1)
363        .ok_or(Error::Unsupported)?;
364    let id = u16::try_from(id).map_err(|_| Error::Unsupported)?;
365    if id < 0x3ff {
366        Ok(id)
367    } else {
368        Err(Error::Unsupported)
369    }
370}
371
372fn dir_create_id(files: &[FileRecord], name: &str) -> Result<u16> {
373    let id = files
374        .iter()
375        .filter(|file| file.name.as_str() < name)
376        .count();
377    let id = u16::try_from(id).map_err(|_| Error::Unsupported)?;
378    if id < 0x3ff {
379        Ok(id)
380    } else {
381        Err(Error::Unsupported)
382    }
383}