Skip to main content

littlefs_rust/writer/
builder.rs

1use super::*;
2
3impl ImageBuilder {
4    /// Creates a builder for a fresh littlefs image.
5    ///
6    /// The writer currently emits one root metadata pair and no data blocks, so
7    /// it only accepts geometries large enough to contain the root pair. More
8    /// constraints will move into a richer writer config once CTZ files and a
9    /// real allocator appear.
10    pub fn new(cfg: Config) -> Result<Self> {
11        Self::new_with_options(cfg, FilesystemOptions::default())
12    }
13
14    /// Creates a fresh-image builder with explicit littlefs-style options.
15    ///
16    /// The options decide which limits are recorded in the superblock and which
17    /// program boundary the metadata commit writer pads to. They intentionally
18    /// share validation with mounted formatting so a hand-built test image does
19    /// not quietly use a shape the top-level formatter would reject.
20    pub fn new_with_options(cfg: Config, options: FilesystemOptions) -> Result<Self> {
21        let options = options.validate(cfg)?;
22        if cfg.block_size < 64 || cfg.block_count < 2 {
23            return Err(Error::InvalidConfig);
24        }
25        Ok(Self {
26            cfg,
27            options,
28            entries: BTreeMap::new(),
29            visible_entries: BTreeMap::new(),
30            update_commits: Vec::new(),
31            allocator: FreshAllocator::new(cfg),
32        })
33    }
34
35    pub(crate) fn empty_root_block_with_options(
36        cfg: Config,
37        options: FilesystemOptions,
38    ) -> Result<Vec<u8>> {
39        let builder = Self::new_with_options(cfg, options)?;
40        let root = RootCommit::from_builder(&builder)?;
41        let mut block = vec![0xff; cfg.block_size];
42        root.write_into(&mut block, cfg, builder.options.prog_size)?;
43        Ok(block)
44    }
45
46    /// Adds or replaces an inline file.
47    ///
48    /// Root files and files inside already-created directories are supported.
49    /// The writer resolves the parent directory first, then inserts the file in
50    /// that directory's sorted id space.
51    pub fn add_inline_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
52        if !self.update_commits.is_empty() {
53            return Err(Error::Unsupported);
54        }
55        let parts = components(path)?;
56        let (name, parents) = split_parent(&parts)?;
57        if !parents.is_empty() {
58            return self.add_directory_inline_file(parents, name, data);
59        }
60        if name.len() > self.options.name_max as usize
61            || data.len()
62                > self
63                    .options
64                    .inline_threshold(self.cfg, self.options.attr_max)
65        {
66            return Err(Error::Unsupported);
67        }
68        if matches!(self.entries.get(name), Some(RootEntry::Dir(_))) {
69            return Err(Error::Unsupported);
70        }
71
72        self.entries.insert(
73            name.to_string(),
74            RootEntry::File(InlineFile {
75                storage: FileStorage::Inline(data.to_vec()),
76                attrs: BTreeMap::new(),
77            }),
78        );
79        self.visible_entries
80            .insert(name.to_string(), RootKind::File);
81        Ok(self)
82    }
83
84    /// Adds a CTZ file.
85    ///
86    /// CTZ data blocks are allocated monotonically in the fresh image and are
87    /// linked with littlefs skip pointers. Root files and files inside existing
88    /// directories share the same CTZ data-block writer.
89    pub fn add_ctz_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
90        if !self.update_commits.is_empty() {
91            return Err(Error::Unsupported);
92        }
93        let parts = components(path)?;
94        let (name, parents) = split_parent(&parts)?;
95        if name.len() > self.options.name_max as usize {
96            return Err(Error::Unsupported);
97        }
98        if !parents.is_empty() {
99            self.directory(parents)?;
100            let blocks = self.allocator.alloc_ctz_blocks(data.len())?;
101            let parent = self.directory_mut(parents)?;
102            return parent.add_ctz_file(name, data, blocks).map(|_| self);
103        }
104        if matches!(self.entries.get(name), Some(RootEntry::Dir(_))) {
105            return Err(Error::Unsupported);
106        }
107        let blocks = self.allocator.alloc_ctz_blocks(data.len())?;
108        self.entries.insert(
109            name.to_string(),
110            RootEntry::File(InlineFile {
111                storage: FileStorage::Ctz(CtzFile::new(data, blocks)),
112                attrs: BTreeMap::new(),
113            }),
114        );
115        self.visible_entries
116            .insert(name.to_string(), RootKind::File);
117        Ok(self)
118    }
119
120    /// Adds an empty directory before append-style updates are emitted.
121    ///
122    /// Each directory receives a fresh metadata pair from the builder's
123    /// monotonic allocator.
124    pub fn create_dir(&mut self, path: &str) -> Result<&mut Self> {
125        if !self.update_commits.is_empty() {
126            return Err(Error::Unsupported);
127        }
128        let parts = components(path)?;
129        let (name, parents) = split_parent(&parts)?;
130        if name.len() > self.options.name_max as usize {
131            return Err(Error::Unsupported);
132        }
133        if !parents.is_empty() {
134            self.directory(parents)?;
135            let pair = self.allocator.alloc_pair()?;
136            let parent = self.directory_mut(parents)?;
137            return parent.create_dir(name, pair).map(|_| self);
138        }
139        if self.entries.contains_key(name) {
140            return Err(Error::Unsupported);
141        }
142        let pair = self.allocator.alloc_pair()?;
143        self.entries.insert(
144            name.to_string(),
145            RootEntry::Dir(Directory::new(pair, self.cfg, self.options)),
146        );
147        self.visible_entries.insert(name.to_string(), RootKind::Dir);
148        Ok(self)
149    }
150
151    /// Sets a user attribute on an already-added root-level inline file.
152    ///
153    /// littlefs encodes user attributes as metadata tags with type
154    /// `LFS_TYPE_USERATTR + attr_type`. Keeping this method tied to files that
155    /// already exist avoids accidentally emitting orphan attributes that C would
156    /// ignore or reject in later operations.
157    pub fn set_attr(&mut self, path: &str, attr_type: u8, data: &[u8]) -> Result<&mut Self> {
158        if !self.update_commits.is_empty() {
159            return Err(Error::Unsupported);
160        }
161        let parts = components(path)?;
162        let (name, parents) = split_parent(&parts)?;
163        if data.len() > self.options.attr_max as usize {
164            return Err(Error::Unsupported);
165        }
166        if !parents.is_empty() {
167            let parent = self.directory_mut(parents)?;
168            return parent.set_attr(name, attr_type, data).map(|_| self);
169        }
170
171        let entry = self.entries.get_mut(name).ok_or(Error::NotFound)?;
172        let RootEntry::File(file) = entry else {
173            return Err(Error::Unsupported);
174        };
175        file.attrs.insert(attr_type, data.to_vec());
176        Ok(self)
177    }
178
179    /// Appends a later root metadata commit that overwrites an inline file.
180    ///
181    /// The original create/name tags remain in the first commit, while this
182    /// method writes a later struct tag for the same id. littlefs's normal
183    /// supersede rules make the later inline payload visible.
184    pub fn update_inline_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
185        let parts = components(path)?;
186        let (name, parents) = split_parent(&parts)?;
187        if !parents.is_empty() {
188            let parent = self.directory_mut(parents)?;
189            return parent.update_inline_file(name, data).map(|_| self);
190        }
191        if data.len()
192            > self
193                .options
194                .inline_threshold(self.cfg, self.options.attr_max)
195        {
196            return Err(Error::Unsupported);
197        }
198        let id = root_entry_id(&self.visible_entries, name)?;
199        match self.visible_entries.get(name) {
200            Some(RootKind::File) => {}
201            Some(RootKind::Dir) => return Err(Error::Unsupported),
202            None => return Err(Error::NotFound),
203        }
204
205        self.push_root_storage_update(id, FileStorage::Inline(data.to_vec()));
206        Ok(self)
207    }
208
209    /// Updates an existing file, selecting inline or CTZ storage by payload size.
210    ///
211    /// A later littlefs struct tag supersedes the previous one even when the
212    /// struct type changes. That lets this builder model inline-to-CTZ and
213    /// CTZ-to-inline conversion as ordinary append commits. Old CTZ blocks are
214    /// left unreachable until the allocator learns real free-space tracking.
215    pub fn update_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
216        if data.len()
217            <= self
218                .options
219                .inline_threshold(self.cfg, self.options.attr_max)
220        {
221            return self.update_inline_file(path, data);
222        }
223
224        let parts = components(path)?;
225        let (name, parents) = split_parent(&parts)?;
226        if !parents.is_empty() {
227            let parent = self.directory(parents)?;
228            child_file_id(&parent.visible_entries, name)?;
229            let blocks = self.allocator.alloc_ctz_blocks(data.len())?;
230            let parent = self.directory_mut(parents)?;
231            return parent
232                .update_storage(name, FileStorage::Ctz(CtzFile::new(data, blocks)))
233                .map(|_| self);
234        }
235
236        let id = root_entry_id(&self.visible_entries, name)?;
237        match self.visible_entries.get(name) {
238            Some(RootKind::File) => {}
239            Some(RootKind::Dir) => return Err(Error::Unsupported),
240            None => return Err(Error::NotFound),
241        }
242
243        let blocks = self.allocator.alloc_ctz_blocks(data.len())?;
244        self.push_root_storage_update(id, FileStorage::Ctz(CtzFile::new(data, blocks)));
245        Ok(self)
246    }
247
248    fn push_root_storage_update(&mut self, id: u16, storage: FileStorage) {
249        self.update_commits.push(RootUpdateCommit {
250            id,
251            storage: Some(storage),
252            attrs: BTreeMap::new(),
253            delete_file: false,
254        });
255    }
256
257    /// Appends a later root metadata commit that overwrites a user attribute.
258    ///
259    /// Empty attributes are represented as normal zero-length userattr tags.
260    /// Deletion uses a different tag shape and is handled by `delete_attr`.
261    pub fn update_attr(&mut self, path: &str, attr_type: u8, data: &[u8]) -> Result<&mut Self> {
262        let parts = components(path)?;
263        let (name, parents) = split_parent(&parts)?;
264        if data.len() > self.options.attr_max as usize {
265            return Err(Error::Unsupported);
266        }
267        if !parents.is_empty() {
268            let parent = self.directory_mut(parents)?;
269            return parent.update_attr(name, attr_type, data).map(|_| self);
270        }
271        let id = root_entry_id(&self.visible_entries, name)?;
272        match self.visible_entries.get(name) {
273            Some(RootKind::File) => {}
274            Some(RootKind::Dir) => return Err(Error::Unsupported),
275            None => return Err(Error::NotFound),
276        }
277
278        let mut attrs = BTreeMap::new();
279        attrs.insert(attr_type, Some(data.to_vec()));
280        self.update_commits.push(RootUpdateCommit {
281            id,
282            storage: None,
283            attrs,
284            delete_file: false,
285        });
286        Ok(self)
287    }
288
289    /// Appends a later root metadata commit that deletes a user attribute.
290    ///
291    /// C's `lfs_removeattr` is just a normal metadata commit with a userattr tag
292    /// whose size field is `0x3ff`. In littlefs tag terminology that means
293    /// "delete the latest matching tag" rather than "write a 1023-byte value".
294    pub fn delete_attr(&mut self, path: &str, attr_type: u8) -> Result<&mut Self> {
295        let parts = components(path)?;
296        let (name, parents) = split_parent(&parts)?;
297        if !parents.is_empty() {
298            let parent = self.directory_mut(parents)?;
299            return parent.delete_attr(name, attr_type).map(|_| self);
300        }
301        let id = root_entry_id(&self.visible_entries, name)?;
302        match self.visible_entries.get(name) {
303            Some(RootKind::File) => {}
304            Some(RootKind::Dir) => return Err(Error::Unsupported),
305            None => return Err(Error::NotFound),
306        }
307
308        let mut attrs = BTreeMap::new();
309        attrs.insert(attr_type, None);
310        self.update_commits.push(RootUpdateCommit {
311            id,
312            storage: None,
313            attrs,
314            delete_file: false,
315        });
316        Ok(self)
317    }
318
319    /// Appends a later root metadata commit that deletes a root inline file.
320    ///
321    /// File deletion is a splice operation in littlefs. The `LFS_TYPE_DELETE`
322    /// tag removes the current id and shifts later ids down by one, so the
323    /// builder updates `visible_entries` immediately. Later update/delete calls
324    /// therefore compute ids against the same state that C will see.
325    pub fn delete_file(&mut self, path: &str) -> Result<&mut Self> {
326        let parts = components(path)?;
327        let (name, parents) = split_parent(&parts)?;
328        if !parents.is_empty() {
329            let parent = self.directory_mut(parents)?;
330            return parent.delete_file(name).map(|_| self);
331        }
332        let id = root_entry_id(&self.visible_entries, name)?;
333        if self.visible_entries.remove(name).is_none() {
334            return Err(Error::NotFound);
335        }
336
337        self.update_commits.push(RootUpdateCommit {
338            id,
339            storage: None,
340            attrs: BTreeMap::new(),
341            delete_file: true,
342        });
343        Ok(self)
344    }
345
346    /// Appends a parent-directory commit that removes an empty directory.
347    ///
348    /// littlefs uses the same splice/delete tag for files and directories in a
349    /// parent directory. The safety rule lives above the tag writer: only a
350    /// directory whose final visible child map is empty may be unlinked here.
351    /// Its old metadata pair remains allocated but unreachable until a real
352    /// free-space tracker exists.
353    pub fn delete_dir(&mut self, path: &str) -> Result<&mut Self> {
354        let parts = components(path)?;
355        let (name, parents) = split_parent(&parts)?;
356        if !parents.is_empty() {
357            let parent = self.directory_mut(parents)?;
358            return parent.delete_dir(name).map(|_| self);
359        }
360
361        let id = root_entry_id(&self.visible_entries, name)?;
362        match self.visible_entries.get(name) {
363            Some(RootKind::Dir) => {}
364            Some(RootKind::File) => return Err(Error::Unsupported),
365            None => return Err(Error::NotFound),
366        }
367        let RootEntry::Dir(dir) = self.entries.get(name).ok_or(Error::Corrupt)? else {
368            return Err(Error::Unsupported);
369        };
370        if !dir.is_empty_for_delete() {
371            return Err(Error::Unsupported);
372        }
373
374        self.visible_entries.remove(name);
375        self.update_commits.push(RootUpdateCommit {
376            id,
377            storage: None,
378            attrs: BTreeMap::new(),
379            delete_file: true,
380        });
381        Ok(self)
382    }
383
384    /// Builds the erased flash image and writes the root metadata commit.
385    ///
386    /// Blocks start as `0xff`, the erased NOR-flash state. We then program only
387    /// block 0 with a complete metadata commit. A valid single side of the root
388    /// pair is sufficient for both littlefs and our parser; block 1 remains an
389    /// erased power-loss-style alternate copy.
390    pub fn build(&self) -> Result<Vec<u8>> {
391        let image_len = self
392            .cfg
393            .block_size
394            .checked_mul(self.cfg.block_count)
395            .ok_or(Error::InvalidConfig)?;
396        let mut image = vec![0xff; image_len];
397        for entry in self.entries.values() {
398            match entry {
399                RootEntry::Dir(dir) => dir.write_empty_pair(&mut image, self.cfg)?,
400                RootEntry::File(file) => {
401                    file.storage.write_blocks(&mut image, self.cfg)?;
402                }
403            }
404        }
405        for update in &self.update_commits {
406            update.write_blocks(&mut image, self.cfg)?;
407        }
408        if let Err(err) = self.write_root_log(&mut image) {
409            if err != Error::NoSpace {
410                return Err(err);
411            }
412
413            // If the append log does not fit, compact the final visible root
414            // state to the other side of the root pair. This is not full
415            // littlefs wear-leveling yet, but it proves the essential recovery
416            // shape: C chooses the newer valid revision and sees only compacted
417            // state.
418            let compacted = self.compacted_root_entries()?;
419            let root = RootCommit::from_entries(self.cfg, self.options, &compacted)?;
420            root.write_into_rev(
421                &mut image[self.cfg.block_size..2 * self.cfg.block_size],
422                self.cfg,
423                self.options.prog_size,
424                2,
425            )?;
426        }
427        Ok(image)
428    }
429
430    fn write_root_log(&self, image: &mut [u8]) -> Result<()> {
431        let root = RootCommit::from_builder(self)?;
432        let mut state = root.write_into(
433            &mut image[0..self.cfg.block_size],
434            self.cfg,
435            self.options.prog_size,
436        )?;
437        for update in &self.update_commits {
438            state = update.write_into(
439                &mut image[0..self.cfg.block_size],
440                self.cfg,
441                self.options.prog_size,
442                state,
443            )?;
444        }
445        Ok(())
446    }
447
448    fn compacted_root_entries(&self) -> Result<BTreeMap<String, RootEntry>> {
449        let mut entries = self.entries.clone();
450        for update in &self.update_commits {
451            let key = root_key_for_id(&entries, update.id)?.to_string();
452            if update.delete_file {
453                entries.remove(&key);
454                continue;
455            }
456
457            let entry = entries.get_mut(&key).ok_or(Error::Corrupt)?;
458            let RootEntry::File(file) = entry else {
459                return Err(Error::Unsupported);
460            };
461            if let Some(storage) = &update.storage {
462                file.storage = storage.clone();
463            }
464            for (attr_type, attr) in &update.attrs {
465                match attr {
466                    Some(data) => {
467                        file.attrs.insert(*attr_type, data.clone());
468                    }
469                    None => {
470                        file.attrs.remove(attr_type);
471                    }
472                }
473            }
474        }
475        Ok(entries)
476    }
477}
478
479impl ImageBuilder {
480    fn add_directory_inline_file(
481        &mut self,
482        parents: &[&str],
483        name: &str,
484        data: &[u8],
485    ) -> Result<&mut Self> {
486        let parent = self.directory_mut(parents)?;
487        parent.add_inline_file(name, data)?;
488        Ok(self)
489    }
490
491    fn directory(&self, path: &[&str]) -> Result<&Directory> {
492        let (name, rest) = path.split_first().ok_or(Error::Unsupported)?;
493        let entry = self.entries.get(*name).ok_or(Error::NotFound)?;
494        let RootEntry::Dir(dir) = entry else {
495            return Err(Error::Unsupported);
496        };
497        if rest.is_empty() {
498            Ok(dir)
499        } else {
500            dir.directory(rest)
501        }
502    }
503
504    fn directory_mut(&mut self, path: &[&str]) -> Result<&mut Directory> {
505        let (name, rest) = path.split_first().ok_or(Error::Unsupported)?;
506        let entry = self.entries.get_mut(*name).ok_or(Error::NotFound)?;
507        let RootEntry::Dir(dir) = entry else {
508            return Err(Error::Unsupported);
509        };
510        if rest.is_empty() {
511            Ok(dir)
512        } else {
513            dir.directory_mut(rest)
514        }
515    }
516}