Skip to main content

littlefs_rust/
writer.rs

1use ::alloc::{
2    collections::BTreeMap,
3    string::{String, ToString},
4    vec,
5    vec::Vec,
6};
7
8mod alloc;
9mod block;
10mod builder;
11mod commits;
12mod ctz;
13mod editor;
14mod tree;
15
16use self::{alloc::FreshAllocator, block::image_block_mut, ctz::CtzFile};
17use crate::{
18    commit::{CommitEntry, CommitState, MetadataCommitWriter, checked_u10},
19    format::{
20        LFS_TYPE_CREATE, LFS_TYPE_CTZSTRUCT, LFS_TYPE_DELETE, LFS_TYPE_DIR, LFS_TYPE_DIRSTRUCT,
21        LFS_TYPE_INLINESTRUCT, LFS_TYPE_REG, LFS_TYPE_SUPERBLOCK, LFS_TYPE_USERATTR, Tag,
22    },
23    fs::Filesystem,
24    metadata::{FileData, MetadataPair},
25    path::components,
26    types::{Config, Error, FilesystemOptions, Result},
27};
28
29/// Disk version written by the current upstream littlefs release in this repo.
30///
31/// Pinning this value in one place matters because littlefs may decide to
32/// rewrite older superblocks during mount. C-oracle tests should inspect the
33/// Rust-written bytes directly, not an upgraded image rewritten by C.
34const DISK_VERSION: u32 = 0x0002_0001;
35
36const DEFAULT_NAME_MAX: u32 = 255;
37const DEFAULT_ATTR_MAX: u32 = 1_022;
38const METADATA_PROG_SIZE: usize = 16;
39
40/// Explicit full-image builder for host-side fixtures and interop tests.
41///
42/// Mounted block-device users should prefer `Filesystem::format_device` and
43/// `Filesystem::mount_device_mut`. This builder is still useful when a caller
44/// already wants a complete image in memory.
45#[derive(Debug, Clone)]
46pub struct ImageBuilder {
47    cfg: Config,
48    options: FilesystemOptions,
49    entries: BTreeMap<String, RootEntry>,
50    visible_entries: BTreeMap<String, RootKind>,
51    update_commits: Vec<RootUpdateCommit>,
52    allocator: FreshAllocator,
53}
54
55/// Explicit full-image editor for already materialized littlefs images.
56///
57/// This is an offline/host-side utility. Mounted APIs must not call it as a
58/// hidden fallback because it owns a complete image `Vec`.
59#[derive(Debug, Clone)]
60pub struct ImageEditor {
61    cfg: Config,
62    image: Vec<u8>,
63    root: MetadataPair,
64    used_blocks: Vec<bool>,
65}
66
67#[derive(Debug, Clone)]
68enum RootEdit {
69    Storage {
70        id: u16,
71        storage: FileStorage,
72    },
73    Attr {
74        id: u16,
75        attr_type: u8,
76        value: Option<Vec<u8>>,
77    },
78    Delete {
79        id: u16,
80    },
81}
82
83#[derive(Debug, Clone)]
84enum RootEntry {
85    File(InlineFile),
86    Dir(Directory),
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90enum RootKind {
91    File,
92    Dir,
93}
94
95#[derive(Debug, Clone)]
96struct InlineFile {
97    storage: FileStorage,
98    attrs: BTreeMap<u8, Vec<u8>>,
99}
100
101#[derive(Debug, Clone)]
102enum FileStorage {
103    Inline(Vec<u8>),
104    Ctz(CtzFile),
105    ExistingCtz { head: u32, size: u32 },
106}
107
108#[derive(Debug, Clone)]
109struct Directory {
110    pair: [u32; 2],
111    cfg: Config,
112    options: FilesystemOptions,
113    entries: BTreeMap<String, DirectoryEntry>,
114    visible_entries: BTreeMap<String, RootKind>,
115    update_commits: Vec<DirUpdateCommit>,
116}
117
118#[derive(Debug, Clone)]
119enum DirectoryEntry {
120    File(InlineFile),
121    Dir(Directory),
122}
123
124#[derive(Debug, Clone)]
125struct DirUpdateCommit {
126    id: u16,
127    storage: Option<FileStorage>,
128    attrs: BTreeMap<u8, Option<Vec<u8>>>,
129    delete_file: bool,
130}
131
132#[derive(Debug, Clone)]
133struct RootUpdateCommit {
134    id: u16,
135    storage: Option<FileStorage>,
136    attrs: BTreeMap<u8, Option<Vec<u8>>>,
137    delete_file: bool,
138}
139
140#[derive(Debug)]
141struct RootCommit {
142    entries: Vec<CommitEntry>,
143}
144
145fn superblock_payload(cfg: Config, options: FilesystemOptions) -> Vec<u8> {
146    let mut payload = Vec::with_capacity(24);
147    for word in [
148        DISK_VERSION,
149        cfg.block_size as u32,
150        cfg.block_count as u32,
151        options.name_max,
152        options.file_max,
153        options.attr_max,
154    ] {
155        payload.extend_from_slice(&word.to_le_bytes());
156    }
157    payload
158}
159
160fn storage_struct_entry(id: u16, storage: &FileStorage) -> Result<CommitEntry> {
161    match storage {
162        FileStorage::Inline(data) => Ok(CommitEntry::new(
163            Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
164            data,
165        )),
166        FileStorage::Ctz(ctz) => {
167            let mut payload = Vec::with_capacity(8);
168            payload.extend_from_slice(&ctz.head()?.to_le_bytes());
169            payload.extend_from_slice(&(ctz.len() as u32).to_le_bytes());
170            Ok(CommitEntry::new(
171                Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
172                &payload,
173            ))
174        }
175        FileStorage::ExistingCtz { head, size } => {
176            let mut payload = Vec::with_capacity(8);
177            payload.extend_from_slice(&head.to_le_bytes());
178            payload.extend_from_slice(&size.to_le_bytes());
179            Ok(CommitEntry::new(
180                Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
181                &payload,
182            ))
183        }
184    }
185}
186
187fn split_parent<'a>(parts: &'a [&'a str]) -> Result<(&'a str, &'a [&'a str])> {
188    let (name, parents) = parts.split_last().ok_or(Error::Unsupported)?;
189    Ok((*name, parents))
190}
191
192fn root_entry_id<T>(entries: &BTreeMap<String, T>, name: &str) -> Result<u16> {
193    // Root entry ids are assigned by lexical order after the superblock's
194    // permanent id 0. Updates must reuse that same id; otherwise C would attach
195    // the new struct/attr to a different directory entry.
196    for (index, existing) in entries.keys().enumerate() {
197        if existing == name {
198            let id = u16::try_from(index + 1).map_err(|_| Error::Unsupported)?;
199            return if id < 0x3ff {
200                Ok(id)
201            } else {
202                Err(Error::Unsupported)
203            };
204        }
205    }
206    Err(Error::NotFound)
207}
208
209fn root_key_for_id<T>(entries: &BTreeMap<String, T>, id: u16) -> Result<&str> {
210    if id == 0 {
211        return Err(Error::Unsupported);
212    }
213    entries
214        .keys()
215        .nth(id as usize - 1)
216        .map(|key| key.as_str())
217        .ok_or(Error::Corrupt)
218}
219
220fn root_create_id(files: &[crate::metadata::FileRecord], name: &str) -> Result<u16> {
221    let id = files
222        .iter()
223        .filter(|file| file.name.as_str() < name)
224        .count()
225        .checked_add(1)
226        .ok_or(Error::Unsupported)?;
227    let id = u16::try_from(id).map_err(|_| Error::Unsupported)?;
228    if id < 0x3ff {
229        Ok(id)
230    } else {
231        Err(Error::Unsupported)
232    }
233}
234
235fn dir_create_id(files: &[crate::metadata::FileRecord], name: &str) -> Result<u16> {
236    let id = files
237        .iter()
238        .filter(|file| file.name.as_str() < name)
239        .count();
240    let id = u16::try_from(id).map_err(|_| Error::Unsupported)?;
241    if id < 0x3ff {
242        Ok(id)
243    } else {
244        Err(Error::Unsupported)
245    }
246}
247
248fn dir_key_for_id<T>(entries: &BTreeMap<String, T>, id: u16) -> Result<&str> {
249    entries
250        .keys()
251        .nth(id as usize)
252        .map(|key| key.as_str())
253        .ok_or(Error::Corrupt)
254}
255
256fn directory_entries(
257    entries_by_name: &BTreeMap<String, DirectoryEntry>,
258) -> Result<Vec<CommitEntry>> {
259    let mut entries = Vec::new();
260    for (index, (name, entry)) in entries_by_name.iter().enumerate() {
261        let id = u16::try_from(index).map_err(|_| Error::Unsupported)?;
262        if id >= 0x3ff {
263            return Err(Error::Unsupported);
264        }
265        entries.push(CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]));
266        match entry {
267            DirectoryEntry::File(file) => {
268                entries.push(CommitEntry::new(
269                    Tag::new(LFS_TYPE_REG, id, checked_u10(name.len())?),
270                    name.as_bytes(),
271                ));
272                match &file.storage {
273                    FileStorage::Inline(data) => {
274                        entries.push(CommitEntry::new(
275                            Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
276                            data,
277                        ));
278                    }
279                    FileStorage::Ctz(ctz) => {
280                        let mut payload = Vec::with_capacity(8);
281                        payload.extend_from_slice(&ctz.head()?.to_le_bytes());
282                        payload.extend_from_slice(&(ctz.len() as u32).to_le_bytes());
283                        entries.push(CommitEntry::new(
284                            Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
285                            &payload,
286                        ));
287                    }
288                    FileStorage::ExistingCtz { head, size } => {
289                        let mut payload = Vec::with_capacity(8);
290                        payload.extend_from_slice(&head.to_le_bytes());
291                        payload.extend_from_slice(&size.to_le_bytes());
292                        entries.push(CommitEntry::new(
293                            Tag::new(LFS_TYPE_CTZSTRUCT, id, 8),
294                            &payload,
295                        ));
296                    }
297                }
298                for (attr_type, attr) in &file.attrs {
299                    entries.push(CommitEntry::new(
300                        Tag::new(
301                            LFS_TYPE_USERATTR + u16::from(*attr_type),
302                            id,
303                            checked_u10(attr.len())?,
304                        ),
305                        attr,
306                    ));
307                }
308            }
309            DirectoryEntry::Dir(dir) => {
310                entries.push(CommitEntry::new(
311                    Tag::new(LFS_TYPE_DIR, id, checked_u10(name.len())?),
312                    name.as_bytes(),
313                ));
314                let mut pair = Vec::with_capacity(8);
315                pair.extend_from_slice(&dir.pair[0].to_le_bytes());
316                pair.extend_from_slice(&dir.pair[1].to_le_bytes());
317                entries.push(CommitEntry::new(Tag::new(LFS_TYPE_DIRSTRUCT, id, 8), &pair));
318            }
319        }
320    }
321    Ok(entries)
322}
323
324fn child_file_id(entries: &BTreeMap<String, RootKind>, name: &str) -> Result<u16> {
325    for (index, (existing, kind)) in entries.iter().enumerate() {
326        if existing == name {
327            if *kind != RootKind::File {
328                return Err(Error::Unsupported);
329            }
330            let id = u16::try_from(index).map_err(|_| Error::Unsupported)?;
331            return if id < 0x3ff {
332                Ok(id)
333            } else {
334                Err(Error::Unsupported)
335            };
336        }
337    }
338    Err(Error::NotFound)
339}
340
341fn child_dir_id(entries: &BTreeMap<String, RootKind>, name: &str) -> Result<u16> {
342    for (index, (existing, kind)) in entries.iter().enumerate() {
343        if existing == name {
344            if *kind != RootKind::Dir {
345                return Err(Error::Unsupported);
346            }
347            let id = u16::try_from(index).map_err(|_| Error::Unsupported)?;
348            return if id < 0x3ff {
349                Ok(id)
350            } else {
351                Err(Error::Unsupported)
352            };
353        }
354    }
355    Err(Error::NotFound)
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::Filesystem;
362
363    #[test]
364    fn built_image_mounts_with_rust_reader() {
365        let mut builder = ImageBuilder::new(Config {
366            block_size: 512,
367            block_count: 64,
368        })
369        .expect("builder");
370        builder
371            .add_inline_file("/hello.txt", b"hello from rust\n")
372            .expect("add file")
373            .set_attr("/hello.txt", 0x42, b"greeting")
374            .expect("set attr");
375
376        let image = builder.build().expect("build image");
377        let fs = Filesystem::mount(
378            &image,
379            Config {
380                block_size: 512,
381                block_count: 64,
382            },
383        )
384        .expect("mount generated image");
385
386        assert_eq!(
387            fs.read_file("/hello.txt").expect("read generated file"),
388            b"hello from rust\n"
389        );
390        assert_eq!(
391            fs.read_attr("/hello.txt", 0x42)
392                .expect("read generated attr"),
393            b"greeting"
394        );
395    }
396}