Skip to main content

littlefs_rust/writer/
editor.rs

1use super::*;
2
3impl ImageEditor {
4    pub fn open(image: Vec<u8>, cfg: Config) -> Result<Self> {
5        let fs = Filesystem::mount(&image, cfg)?;
6        let used_blocks = fs.used_blocks()?;
7        let root = MetadataPair::read(&image, cfg, [0, 1])?;
8        Ok(Self {
9            cfg,
10            image,
11            root,
12            used_blocks,
13        })
14    }
15
16    pub fn used_blocks(&self) -> &[bool] {
17        &self.used_blocks
18    }
19
20    /// Creates a new file in an imported image.
21    ///
22    /// The editor appends a normal littlefs CREATE/name/struct commit to the
23    /// target metadata pair, choosing the create id by lexical order so later
24    /// entries shift as they would in a directory log written by C. Small files
25    /// are stored inline; larger files allocate CTZ blocks from the scanned
26    /// reachable-block map and erase them before programming.
27    pub fn create_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
28        let parts = components(path)?;
29        let (name, parents) = split_parent(&parts)?;
30        if !parents.is_empty() {
31            return self.create_file_in_directory(parents, name, data);
32        }
33        if name.len() > DEFAULT_NAME_MAX as usize {
34            return Err(Error::Unsupported);
35        }
36
37        let files = self.root.files()?;
38        if files.iter().any(|file| file.name == name) {
39            return Err(Error::Unsupported);
40        }
41        let id = root_create_id(&files, name)?;
42        let name_entry = CommitEntry::new(
43            Tag::new(LFS_TYPE_REG, id, checked_u10(name.len())?),
44            name.as_bytes(),
45        );
46
47        if data.len() <= DEFAULT_ATTR_MAX as usize {
48            let entries = [
49                CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
50                name_entry,
51                CommitEntry::new(
52                    Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
53                    data,
54                ),
55            ];
56            self.append_root_entries(&entries)?;
57            return Ok(self);
58        }
59
60        let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
61        let blocks = allocator.alloc_ctz_blocks(data.len())?;
62        let storage = FileStorage::Ctz(CtzFile::new(data, blocks));
63        storage.erase_blocks(&mut self.image, self.cfg)?;
64        storage.write_blocks(&mut self.image, self.cfg)?;
65        let entries = [
66            CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
67            name_entry,
68            storage_struct_entry(id, &storage)?,
69        ];
70        self.append_root_entries(&entries)?;
71        self.used_blocks = allocator.into_used();
72        Ok(self)
73    }
74
75    fn create_file_in_directory(
76        &mut self,
77        parents: &[&str],
78        name: &str,
79        data: &[u8],
80    ) -> Result<&mut Self> {
81        if name.len() > DEFAULT_NAME_MAX as usize {
82            return Err(Error::Unsupported);
83        }
84
85        let parent = self.resolve_directory(parents)?;
86        let files = self.files_in_pair_chain(&parent)?;
87        if files.iter().any(|file| file.name == name) {
88            return Err(Error::Unsupported);
89        }
90        let id = dir_create_id(&files, name)?;
91        let name_entry = CommitEntry::new(
92            Tag::new(LFS_TYPE_REG, id, checked_u10(name.len())?),
93            name.as_bytes(),
94        );
95
96        if data.len() <= DEFAULT_ATTR_MAX as usize {
97            let entries = [
98                CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
99                name_entry,
100                CommitEntry::new(
101                    Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
102                    data,
103                ),
104            ];
105            self.append_pair_entries(&parent, &entries)?;
106            return Ok(self);
107        }
108
109        let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
110        let blocks = allocator.alloc_ctz_blocks(data.len())?;
111        let storage = FileStorage::Ctz(CtzFile::new(data, blocks));
112        storage.erase_blocks(&mut self.image, self.cfg)?;
113        storage.write_blocks(&mut self.image, self.cfg)?;
114        let entries = [
115            CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
116            name_entry,
117            storage_struct_entry(id, &storage)?,
118        ];
119        self.append_pair_entries(&parent, &entries)?;
120        self.used_blocks = allocator.into_used();
121        Ok(self)
122    }
123
124    /// Creates a new root-level directory in an imported image.
125    ///
126    /// The directory receives a freshly allocated metadata pair containing a
127    /// valid empty commit, then the root pair gets a CREATE/name/DIRSTRUCT
128    /// commit that makes the child reachable. This mirrors the two pieces C
129    /// needs to open the directory after remounting.
130    pub fn create_dir(&mut self, path: &str) -> Result<&mut Self> {
131        let parts = components(path)?;
132        let (name, parents) = split_parent(&parts)?;
133        if !parents.is_empty() {
134            return self.create_dir_in_directory(parents, name);
135        }
136        if name.len() > DEFAULT_NAME_MAX as usize {
137            return Err(Error::Unsupported);
138        }
139
140        let files = self.root.files()?;
141        if files.iter().any(|file| file.name == name) {
142            return Err(Error::Unsupported);
143        }
144        let id = root_create_id(&files, name)?;
145        let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
146        let pair = allocator.alloc_pair()?;
147        Directory::new(pair, self.cfg, FilesystemOptions::default())
148            .write_empty_pair(&mut self.image, self.cfg)?;
149
150        let mut pair_payload = Vec::with_capacity(8);
151        pair_payload.extend_from_slice(&pair[0].to_le_bytes());
152        pair_payload.extend_from_slice(&pair[1].to_le_bytes());
153        let entries = [
154            CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
155            CommitEntry::new(
156                Tag::new(LFS_TYPE_DIR, id, checked_u10(name.len())?),
157                name.as_bytes(),
158            ),
159            CommitEntry::new(Tag::new(LFS_TYPE_DIRSTRUCT, id, 8), &pair_payload),
160        ];
161        self.append_root_entries(&entries)?;
162        self.used_blocks = allocator.into_used();
163        Ok(self)
164    }
165
166    fn create_dir_in_directory(&mut self, parents: &[&str], name: &str) -> Result<&mut Self> {
167        if name.len() > DEFAULT_NAME_MAX as usize {
168            return Err(Error::Unsupported);
169        }
170
171        let parent = self.resolve_directory(parents)?;
172        let files = self.files_in_pair_chain(&parent)?;
173        if files.iter().any(|file| file.name == name) {
174            return Err(Error::Unsupported);
175        }
176        let id = dir_create_id(&files, name)?;
177        let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
178        let pair = allocator.alloc_pair()?;
179        Directory::new(pair, self.cfg, FilesystemOptions::default())
180            .write_empty_pair(&mut self.image, self.cfg)?;
181
182        let mut pair_payload = Vec::with_capacity(8);
183        pair_payload.extend_from_slice(&pair[0].to_le_bytes());
184        pair_payload.extend_from_slice(&pair[1].to_le_bytes());
185        let entries = [
186            CommitEntry::new(Tag::new(LFS_TYPE_CREATE, id, 0), &[]),
187            CommitEntry::new(
188                Tag::new(LFS_TYPE_DIR, id, checked_u10(name.len())?),
189                name.as_bytes(),
190            ),
191            CommitEntry::new(Tag::new(LFS_TYPE_DIRSTRUCT, id, 8), &pair_payload),
192        ];
193        self.append_pair_entries(&parent, &entries)?;
194        self.used_blocks = allocator.into_used();
195        Ok(self)
196    }
197
198    /// Appends an inline struct update for an existing root-level file.
199    ///
200    /// Imported-image editing starts with this narrow operation because it does
201    /// not need new data blocks or parent DIRSTRUCT rewrites. The update is
202    /// still a real littlefs metadata commit appended to the active root side,
203    /// so upstream C must be able to mount the edited image and read the new
204    /// bytes.
205    pub fn update_inline_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
206        if data.len() > DEFAULT_ATTR_MAX as usize {
207            return Err(Error::Unsupported);
208        }
209
210        let parts = components(path)?;
211        let (name, parents) = split_parent(&parts)?;
212        if !parents.is_empty() {
213            let parent = self.resolve_directory(parents)?;
214            let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
215            let entry = CommitEntry::new(
216                Tag::new(LFS_TYPE_INLINESTRUCT, id, checked_u10(data.len())?),
217                data,
218            );
219            self.append_pair_entries(&pair, &[entry])?;
220            return Ok(self);
221        }
222
223        let id = self.root_file_id_from_name(name)?;
224        self.apply_root_edit(RootEdit::Storage {
225            id,
226            storage: FileStorage::Inline(data.to_vec()),
227        })?;
228        Ok(self)
229    }
230
231    pub fn update_file(&mut self, path: &str, data: &[u8]) -> Result<&mut Self> {
232        if data.len() <= DEFAULT_ATTR_MAX as usize {
233            return self.update_inline_file(path, data);
234        }
235
236        let mut allocator = FreshAllocator::from_used(self.cfg, self.used_blocks.clone())?;
237        let blocks = allocator.alloc_ctz_blocks(data.len())?;
238        let storage = FileStorage::Ctz(CtzFile::new(data, blocks));
239        storage.erase_blocks(&mut self.image, self.cfg)?;
240        storage.write_blocks(&mut self.image, self.cfg)?;
241
242        let parts = components(path)?;
243        let (name, parents) = split_parent(&parts)?;
244        if !parents.is_empty() {
245            let parent = self.resolve_directory(parents)?;
246            let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
247            let entry = storage_struct_entry(id, &storage)?;
248            self.append_pair_entries(&pair, &[entry])?;
249            self.used_blocks = allocator.into_used();
250            return Ok(self);
251        }
252
253        let id = self.root_file_id_from_name(name)?;
254        self.apply_root_edit(RootEdit::Storage { id, storage })?;
255        self.used_blocks = allocator.into_used();
256        Ok(self)
257    }
258
259    pub fn update_attr(&mut self, path: &str, attr_type: u8, data: &[u8]) -> Result<&mut Self> {
260        if data.len() > DEFAULT_ATTR_MAX as usize {
261            return Err(Error::Unsupported);
262        }
263
264        let parts = components(path)?;
265        let (name, parents) = split_parent(&parts)?;
266        if !parents.is_empty() {
267            let parent = self.resolve_directory(parents)?;
268            let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
269            let entry = CommitEntry::new(
270                Tag::new(
271                    LFS_TYPE_USERATTR + u16::from(attr_type),
272                    id,
273                    checked_u10(data.len())?,
274                ),
275                data,
276            );
277            self.append_pair_entries(&pair, &[entry])?;
278            return Ok(self);
279        }
280
281        let id = self.root_file_id(path)?;
282        self.apply_root_edit(RootEdit::Attr {
283            id,
284            attr_type,
285            value: Some(data.to_vec()),
286        })?;
287        Ok(self)
288    }
289
290    pub fn delete_attr(&mut self, path: &str, attr_type: u8) -> Result<&mut Self> {
291        let parts = components(path)?;
292        let (name, parents) = split_parent(&parts)?;
293        if !parents.is_empty() {
294            let parent = self.resolve_directory(parents)?;
295            let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
296            let entry = CommitEntry::new(
297                Tag::new(LFS_TYPE_USERATTR + u16::from(attr_type), id, 0x3ff),
298                &[],
299            );
300            self.append_pair_entries(&pair, &[entry])?;
301            return Ok(self);
302        }
303
304        let id = self.root_file_id(path)?;
305        self.apply_root_edit(RootEdit::Attr {
306            id,
307            attr_type,
308            value: None,
309        })?;
310        Ok(self)
311    }
312
313    pub fn delete_file(&mut self, path: &str) -> Result<&mut Self> {
314        let parts = components(path)?;
315        let (name, parents) = split_parent(&parts)?;
316        if !parents.is_empty() {
317            let parent = self.resolve_directory(parents)?;
318            let (pair, id) = self.find_file_in_pair_chain(&parent, name)?;
319            let entry = CommitEntry::new(Tag::new(LFS_TYPE_DELETE, id, 0), &[]);
320            self.append_pair_entries(&pair, &[entry])?;
321            return Ok(self);
322        }
323
324        let id = self.root_file_id(path)?;
325        self.apply_root_edit(RootEdit::Delete { id })?;
326        Ok(self)
327    }
328
329    pub fn delete_dir(&mut self, path: &str) -> Result<&mut Self> {
330        let parts = components(path)?;
331        let (name, parents) = split_parent(&parts)?;
332        if !parents.is_empty() {
333            let parent = self.resolve_directory(parents)?;
334            let (pair, file) = self.find_file_record_in_pair_chain(&parent, name)?;
335            let FileData::Directory(child) = file.data else {
336                return Err(Error::Unsupported);
337            };
338            if file.ty != crate::types::FileType::Dir {
339                return Err(Error::Unsupported);
340            }
341            let child = MetadataPair::read(&self.image, self.cfg, child)?;
342            if !self.files_in_pair_chain(&child)?.is_empty() {
343                return Err(Error::Unsupported);
344            }
345            let entry = CommitEntry::new(Tag::new(LFS_TYPE_DELETE, file.id, 0), &[]);
346            self.append_pair_entries(&pair, &[entry])?;
347            return Ok(self);
348        }
349
350        let file = self
351            .root
352            .files()?
353            .into_iter()
354            .find(|file| file.name == name)
355            .ok_or(Error::NotFound)?;
356        let FileData::Directory(child) = file.data else {
357            return Err(Error::Unsupported);
358        };
359        if file.ty != crate::types::FileType::Dir {
360            return Err(Error::Unsupported);
361        }
362        let child = MetadataPair::read(&self.image, self.cfg, child)?;
363        if !self.files_in_pair_chain(&child)?.is_empty() {
364            return Err(Error::Unsupported);
365        }
366        self.apply_root_edit(RootEdit::Delete { id: file.id })?;
367        Ok(self)
368    }
369
370    fn apply_root_edit(&mut self, edit: RootEdit) -> Result<()> {
371        let entries = edit.commit_entries()?;
372        match self.append_root_entries(&entries) {
373            Ok(()) => Ok(()),
374            Err(Error::NoSpace) => self.compact_root_with_edit(&edit),
375            Err(err) => Err(err),
376        }
377    }
378
379    fn append_root_entries(&mut self, entries: &[CommitEntry]) -> Result<()> {
380        self.append_pair_entries(&self.root.clone(), entries)?;
381        self.root = MetadataPair::read(&self.image, self.cfg, [0, 1])?;
382        Ok(())
383    }
384
385    fn append_pair_entries(&mut self, pair: &MetadataPair, entries: &[CommitEntry]) -> Result<()> {
386        let block = image_block_mut(&mut self.image, self.cfg, pair.active_block)?;
387        let mut commit = MetadataCommitWriter::append(block, METADATA_PROG_SIZE, pair.state)?;
388        commit.write_entries(entries)?;
389        commit.finish()?;
390        Ok(())
391    }
392
393    fn compact_root_with_edit(&mut self, edit: &RootEdit) -> Result<()> {
394        let mut entries = self.root_entries_for_compaction()?;
395        edit.apply_to_root_entries(&mut entries)?;
396
397        let alternate = if self.root.active_block == self.root.pair[0] {
398            self.root.pair[1]
399        } else {
400            self.root.pair[0]
401        };
402        {
403            let block = image_block_mut(&mut self.image, self.cfg, alternate)?;
404            block.fill(0xff);
405            let root = RootCommit::from_entries(self.cfg, FilesystemOptions::default(), &entries)?;
406            root.write_into_rev(
407                block,
408                self.cfg,
409                METADATA_PROG_SIZE,
410                self.root.rev.wrapping_add(1),
411            )?;
412        }
413
414        self.root = MetadataPair::read(&self.image, self.cfg, [0, 1])?;
415        Ok(())
416    }
417
418    fn root_entries_for_compaction(&self) -> Result<BTreeMap<String, RootEntry>> {
419        let mut entries = BTreeMap::new();
420        for file in self.root.files()? {
421            match file.data {
422                FileData::Inline(data) if file.ty == crate::types::FileType::File => {
423                    entries.insert(
424                        file.name,
425                        RootEntry::File(InlineFile {
426                            storage: FileStorage::Inline(data),
427                            attrs: file.attrs,
428                        }),
429                    );
430                }
431                FileData::Ctz { head, size } if file.ty == crate::types::FileType::File => {
432                    entries.insert(
433                        file.name,
434                        RootEntry::File(InlineFile {
435                            storage: FileStorage::ExistingCtz { head, size },
436                            attrs: file.attrs,
437                        }),
438                    );
439                }
440                FileData::Directory(pair) if file.ty == crate::types::FileType::Dir => {
441                    entries.insert(
442                        file.name,
443                        RootEntry::Dir(Directory::new(
444                            pair,
445                            self.cfg,
446                            FilesystemOptions::default(),
447                        )),
448                    );
449                }
450                _ => return Err(Error::Corrupt),
451            }
452        }
453        Ok(entries)
454    }
455
456    fn root_file_id(&self, path: &str) -> Result<u16> {
457        let parts = components(path)?;
458        let (name, parents) = split_parent(&parts)?;
459        if !parents.is_empty() {
460            return Err(Error::Unsupported);
461        }
462        self.root_file_id_from_name(name)
463    }
464
465    fn root_file_id_from_name(&self, name: &str) -> Result<u16> {
466        let file = self
467            .root
468            .files()?
469            .into_iter()
470            .find(|file| file.name == name)
471            .ok_or(Error::NotFound)?;
472        if file.ty != crate::types::FileType::File {
473            return Err(Error::Unsupported);
474        }
475        Ok(file.id)
476    }
477
478    fn resolve_directory(&self, path: &[&str]) -> Result<MetadataPair> {
479        let mut pair = self.root.clone();
480        for component in path {
481            let (dir_pair, file) = self.find_file_record_in_pair_chain(&pair, component)?;
482            match file.data {
483                FileData::Directory(child) if file.ty == crate::types::FileType::Dir => {
484                    pair = MetadataPair::read(&self.image, self.cfg, child)?;
485                }
486                _ => {
487                    let _ = dir_pair;
488                    return Err(Error::Unsupported);
489                }
490            }
491        }
492        Ok(pair)
493    }
494
495    fn find_file_in_pair_chain(
496        &self,
497        pair: &MetadataPair,
498        name: &str,
499    ) -> Result<(MetadataPair, u16)> {
500        let (pair, file) = self.find_file_record_in_pair_chain(pair, name)?;
501        if file.ty != crate::types::FileType::File {
502            return Err(Error::Unsupported);
503        }
504        Ok((pair, file.id))
505    }
506
507    fn find_file_record_in_pair_chain(
508        &self,
509        pair: &MetadataPair,
510        name: &str,
511    ) -> Result<(MetadataPair, crate::metadata::FileRecord)> {
512        let mut current = pair.clone();
513        let mut seen = Vec::<[u32; 2]>::new();
514        loop {
515            if seen.contains(&current.pair) {
516                return Err(Error::Corrupt);
517            }
518            seen.push(current.pair);
519            if let Some(file) = current.files()?.into_iter().find(|file| file.name == name) {
520                return Ok((current, file));
521            }
522            match current.hardtail()? {
523                Some(next) if next != [crate::format::LFS_NULL, crate::format::LFS_NULL] => {
524                    current = MetadataPair::read(&self.image, self.cfg, next)?;
525                }
526                _ => return Err(Error::NotFound),
527            }
528        }
529    }
530
531    fn files_in_pair_chain(&self, pair: &MetadataPair) -> Result<Vec<crate::metadata::FileRecord>> {
532        let mut files = Vec::new();
533        let mut current = pair.clone();
534        let mut seen = Vec::<[u32; 2]>::new();
535        loop {
536            if seen.contains(&current.pair) {
537                return Err(Error::Corrupt);
538            }
539            seen.push(current.pair);
540            files.extend(current.files()?);
541            match current.hardtail()? {
542                Some(next) if next != [crate::format::LFS_NULL, crate::format::LFS_NULL] => {
543                    current = MetadataPair::read(&self.image, self.cfg, next)?;
544                }
545                _ => return Ok(files),
546            }
547        }
548    }
549
550    pub fn into_bytes(self) -> Vec<u8> {
551        self.image
552    }
553}