rudelblinken_filesystem/
lib.rs

1//! A zero-copy flash filesystem optimized for embedded systems
2//!
3//! `rudelblinken-filesystem` implements a flash-friendly filesystem designed for resource-constrained
4//! embedded devices. Key features include:
5//!
6//! - **Zero-copy access**: Files are memory-mapped for direct, efficient access
7//! - **Flash-optimized**: Implements wear leveling and flash-aware write patterns
8//! - **Safe concurrency**: Reference counting enables safe concurrent access with reader/writer separation. Deferred deletion ensure data integrity
9//! - **Resource efficient**: Minimal RAM overhead during normal operation
10//!
11//! The filesystem provides direct memory-mapped access to file contents while maintaining safety
12//! through a custom reference counting system. Multiple readers can access files concurrently
13//! while writers get exclusive access. Files are only deleted once all references are dropped.
14//!
15//! Designed specifically for flash storage, the implementation uses block-aligned operations,
16//! respects write limitations, and implements basic wear leveling.
17//!
18#![warn(missing_docs)]
19#![feature(adt_const_params)]
20#![feature(box_as_ptr)]
21#![feature(box_vec_non_null)]
22#![feature(allocator_api)]
23#![feature(doc_cfg)]
24#[cfg_attr(
25    feature = "simulated",
26    doc = r##"
27
28# Examples
29
30Create an in-memory filesystem for testing:
31
32```
33use rudelblinken_filesystem::storage::simulated::SimulatedStorage;
34use rudelblinken_filesystem::Filesystem;
35
36let storage = SimulatedStorage::new();
37// TODO: Improve the interface to allow storages with lifetimes
38let static_storage_ref = unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&storage) };
39let mut filesystem = Filesystem::new(static_storage_ref);
40```
41"##
42)]
43use file::{CommitFileContentError, File, FileState, WriteFileToStorageError};
44use file_information::FileInformation;
45use file_metadata::FileMetadata;
46use std::{collections::BTreeMap, io::Write, ops::Bound::Included};
47use storage::{EraseStorageError, Storage};
48use thiserror::Error;
49
50mod file;
51mod file_information;
52mod file_metadata;
53/// Storage traits and implementations
54pub mod storage;
55
56/// Errors that can occur when finding free space
57#[derive(Error, Debug, Clone)]
58pub enum FindFreeSpaceError {
59    /// Error in filesystem structure
60    #[error("Error in filesystem structure")]
61    FilesystemError,
62    /// No free space
63    #[error("No free space")]
64    NoFreeSpace,
65    /// Not enough space
66    #[error("Not enough space")]
67    NotEnoughSpace,
68}
69
70/// Errors that can occur when writing a file
71#[derive(Error, Debug)]
72pub enum FilesystemWriteError {
73    /// Error while finding free space
74    #[error(transparent)]
75    FindFreeSpaceError(#[from] FindFreeSpaceError),
76    /// Error while writing file to storage
77    #[error(transparent)]
78    WriteFileToStorageError(#[from] WriteFileToStorageError),
79    /// Some kind of io error
80    #[error(transparent)]
81    IoError(#[from] std::io::Error),
82    /// Error while committing file content
83    #[error(transparent)]
84    CommitFileContentError(#[from] CommitFileContentError),
85}
86
87/// Errors that can occur when deleting a file
88#[derive(Error, Debug)]
89pub enum FilesystemDeleteError {
90    /// Error while erasing storage
91    #[error(transparent)]
92    EraseStorageError(#[from] EraseStorageError),
93    /// The file does not exist
94    #[error("The file does not exist")]
95    FileNotFound,
96}
97
98///  A struct representing the filesystem backed by a generic storage type `T`.
99///
100/// # Type Parameters
101///
102/// * `T` - A type that implements the `Storage` trait and is `'static`, `Send`, and `Sync`.
103pub struct Filesystem<T: Storage + 'static + Send + Sync> {
104    storage: &'static T,
105    files: Vec<FileInformation<T>>,
106}
107
108impl<T: Storage + 'static + Send + Sync> Filesystem<T> {
109    /// Retrieves the first block number from the storage metadata.
110    fn get_first_block(&self) -> Result<u16, std::io::Error> {
111        let first_block_slice: Box<[u8; 2]> = self
112            .storage
113            .read_metadata("first_block")?
114            .try_into()
115            .unwrap();
116        Ok(u16::from_le_bytes(*first_block_slice))
117    }
118    /// Sets the first block number in the storage metadata.
119    fn set_first_block(&mut self, first_block: u16) -> Result<(), std::io::Error> {
120        self.storage
121            .write_metadata("first_block", &first_block.to_le_bytes())?;
122        Ok(())
123    }
124
125    /// Creates a new filesystem instance on top of the provided storage.
126    ///
127    /// # Initialization Process
128    /// 1. Reads or initializes the first block pointer from metadata
129    /// 2. Scans through blocks starting at first_block
130    /// 3. Reconstructs file list from valid file headers
131    /// 4. Erases corrupted blocks (non-0xFF when invalid)
132    ///
133    /// # Arguments
134    /// * `storage` - Static reference to storage implementing the Storage trait
135    ///
136    /// # Returns
137    /// A new `Filesystem` instance with the reconstructed file list
138    pub fn new(storage: &'static T) -> Self {
139        let mut filesystem = Self {
140            storage,
141            files: Vec::new(),
142        };
143        let first_block = filesystem.get_first_block();
144        let first_block = first_block.unwrap_or_else(|_| {
145            filesystem.set_first_block(0).unwrap();
146            0
147        });
148
149        let mut block_number = 0;
150
151        while block_number < T::BLOCKS {
152            let current_block_number = (block_number + first_block as u32) % T::BLOCKS;
153            let file_information = FileInformation::from_storage(
154                filesystem.storage,
155                current_block_number * T::BLOCK_SIZE,
156            );
157            let file_information = match file_information {
158                Ok(file_information) => file_information,
159                Err(_) => {
160                    block_number += 1;
161                    let Ok(current_block) = filesystem
162                        .storage
163                        .read(current_block_number * T::BLOCK_SIZE, T::BLOCK_SIZE)
164                    else {
165                        continue;
166                    };
167                    if current_block.iter().any(|b| *b != 0xff) {
168                        println!(
169                            "Erasing block {} because it is not zeroed",
170                            current_block_number
171                        );
172                        filesystem
173                            .storage
174                            .erase(current_block_number * T::BLOCK_SIZE, T::BLOCK_SIZE)
175                            .unwrap();
176                    };
177                    continue;
178                }
179            };
180            block_number += ((file_information.length + 64) / T::BLOCK_SIZE) + 1;
181            filesystem.files.push(file_information);
182        }
183
184        filesystem
185    }
186
187    /// Finds a file by name and returns a reference to it.
188    pub fn read_file(&self, name: &str) -> Option<File<T, { FileState::Weak }>> {
189        let file = self
190            .files
191            .iter()
192            .find(|file| file.name == name && file.valid())?;
193        Some(file.read())
194    }
195
196    /// Find a free space in storage of at least the given length.
197    ///
198    /// For now the space is guaranteed to start at a block boundary
199    fn find_free_space(&self, length: u32) -> Result<u32, FindFreeSpaceError> {
200        let mut free_ranges: BTreeMap<u16, u16> = Default::default();
201        free_ranges.insert(0, T::BLOCKS as u16 * 2);
202
203        for file in &self.files {
204            let start_block = (file.address / T::BLOCK_SIZE) as u16;
205            let length_in_blocks =
206                (file.length + size_of::<FileMetadata>() as u32).div_ceil(T::BLOCK_SIZE) as u16;
207            let end_block = start_block + length_in_blocks;
208
209            let Some((&surrounding_start, &surrounding_length)) = free_ranges
210                .range((Included(0), Included(start_block)))
211                .last()
212            else {
213                // There should always be a surrounding free range
214                return Err(FindFreeSpaceError::FilesystemError);
215            };
216
217            let space_before = start_block - surrounding_start;
218            let space_after = (surrounding_start + surrounding_length) - (end_block);
219
220            match (space_before, space_after) {
221                (0, 0) => {
222                    free_ranges.remove(&surrounding_start);
223                }
224                (0, space_after) => {
225                    free_ranges.remove(&surrounding_start);
226                    free_ranges.insert(end_block, space_after);
227                }
228                (space_before, 0) => {
229                    free_ranges.insert(start_block, space_before);
230                }
231                (space_before, space_after) => {
232                    free_ranges.insert(start_block, space_before);
233                    free_ranges.insert(end_block, space_after);
234                }
235            }
236        }
237
238        // Fix the last entry for wraparound
239        let last_free_space_start = free_ranges.last_key_value().map_or(0, |(start, _)| *start);
240        let wraparound_length: i64 = last_free_space_start as i64 - T::BLOCKS as i64;
241        if wraparound_length >= 0 {
242            let wraparound_length = wraparound_length as u16;
243            let Some((0, &first_range_length)) = free_ranges.first_key_value() else {
244                // If there is wraparound on the last file, there needs to be enough space at the start of the storage to accomodate that overlap
245                return Err(FindFreeSpaceError::FilesystemError);
246            };
247            free_ranges.remove(&0);
248            let new_first_range_length = first_range_length - wraparound_length;
249            if new_first_range_length > 0 {
250                free_ranges.insert(wraparound_length, new_first_range_length);
251            }
252            free_ranges.insert(
253                last_free_space_start,
254                T::BLOCKS as u16 - last_free_space_start,
255            );
256        }
257
258        let Some(longest_range) = free_ranges
259            .into_iter()
260            .max_by(|(_, length_a), (_, length_b)| length_a.cmp(length_b))
261            .map(|(a, b)| (a as u32, b as u32))
262        else {
263            return Err(FindFreeSpaceError::NoFreeSpace);
264        };
265
266        if (longest_range.1 * T::BLOCK_SIZE) < length {
267            return Err(FindFreeSpaceError::NotEnoughSpace);
268        }
269
270        let longest_range_start = longest_range.0 % (T::BLOCKS);
271
272        Ok(longest_range_start * T::BLOCK_SIZE)
273    }
274
275    /// Write a file to storage.
276    pub fn write_file(
277        &mut self,
278        name: &str,
279        content: &[u8],
280        _hash: &[u8; 32],
281    ) -> Result<(), FilesystemWriteError> {
282        let mut writer = self.get_file_writer(name, content.len() as u32, _hash)?;
283
284        writer.write_all(content)?;
285        writer.commit()?;
286        Ok(())
287    }
288
289    /// Get a writer that allows writing a file over time.
290    ///
291    /// The file can only be read after the content was finished
292    pub fn get_file_writer(
293        &mut self,
294        name: &str,
295        length: u32,
296        _hash: &[u8; 32],
297    ) -> Result<File<T, { FileState::Writer }>, FilesystemWriteError> {
298        self.cleanup_files();
299        let free_location = self.find_free_space(length + size_of::<FileMetadata>() as u32)?;
300
301        let (file, writer) =
302            FileInformation::to_storage(self.storage, free_location, length, name)?;
303        self.files.push(file);
304        Ok(writer)
305    }
306
307    /// Delete a file
308    ///
309    /// The file will only be deleted once there are no strong references to its content left. Strong references can be obtained by calling upgrade on the content of a file
310    pub fn delete_file(&mut self, filename: &str) -> Result<(), FilesystemDeleteError> {
311        let Some((index, _)) = self
312            .files
313            .iter()
314            .enumerate()
315            .find(|(_, file)| file.name == filename)
316        else {
317            return Err(FilesystemDeleteError::FileNotFound);
318        };
319        let file = &mut self.files[index];
320        if !file.marked_for_deletion() {
321            file.mark_for_deletion().unwrap();
322        }
323        if file.deleted() {
324            self.files.swap_remove(index);
325        }
326        Ok(())
327    }
328
329    /// Remove all files with no remaining strong pointers
330    fn cleanup_files(&mut self) {
331        let mut remove_indices: Vec<usize> = Vec::new();
332        for index in 0..self.files.len() {
333            if self.files[index].deleted() {
334                remove_indices.push(index);
335            }
336        }
337        for index in remove_indices.into_iter().rev() {
338            self.files.swap_remove(index);
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use crate::storage::simulated::SimulatedStorage;
346
347    use super::*;
348
349    #[test]
350    fn writing_and_reading_a_simple_file_works() {
351        let owned_storage = SimulatedStorage::new();
352        let storage =
353            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
354        let mut filesystem = Filesystem::new(storage);
355        let file = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
356        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
357        let result = filesystem.read_file("fancy").unwrap();
358        assert_eq!(result.upgrade().unwrap().as_ref(), file);
359    }
360
361    #[test]
362    fn can_read_a_file_from_an_old_storage() {
363        let owned_storage = SimulatedStorage::new();
364        let storage =
365            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
366        let file = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
367        let mut filesystem = Filesystem::new(storage);
368        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
369        let filesystem = Filesystem::new(storage);
370        let result = filesystem.read_file("fancy").unwrap();
371        assert_eq!(result.upgrade().unwrap().as_ref(), file);
372    }
373
374    #[test]
375    fn writing_multiple_files() {
376        let owned_storage = SimulatedStorage::new();
377        let storage =
378            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
379        let mut filesystem = Filesystem::new(storage);
380        let file = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
381        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
382        filesystem.write_file("fancy2", &file, &[0u8; 32]).unwrap();
383        let result = filesystem.read_file("fancy").unwrap();
384        assert_eq!(result.upgrade().unwrap().as_ref(), file);
385        let result = filesystem.read_file("fancy2").unwrap();
386        assert_eq!(result.upgrade().unwrap().as_ref(), file);
387    }
388
389    #[test]
390    fn deleting_a_file_works() {
391        let owned_storage = SimulatedStorage::new();
392        let storage =
393            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
394        let mut filesystem = Filesystem::new(storage);
395        let file = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
396        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
397        filesystem.delete_file("fancy").unwrap();
398        let None = filesystem.read_file("fancy") else {
399            panic!("Should not be able to read a deleted file");
400        };
401    }
402
403    #[test]
404    fn deleting_a_file_actually_works() {
405        let owned_storage = SimulatedStorage::new();
406        let storage =
407            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
408        let mut filesystem = Filesystem::new(storage);
409        let file = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
410        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
411        filesystem.delete_file("fancy").unwrap();
412        let None = filesystem.read_file("fancy") else {
413            panic!("Should not be able to read a deleted file");
414        };
415
416        let filesystem = Filesystem::new(storage);
417        let None = filesystem.read_file("fancy") else {
418            panic!("Should not be able to read a deleted file");
419        };
420    }
421
422    #[test]
423    fn file_cant_be_upgraded_if_it_has_been_deleted_and_there_are_only_weak_references() {
424        let owned_storage = SimulatedStorage::new();
425        let storage =
426            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
427        let mut filesystem = Filesystem::new(storage);
428        let content = vec![0; SimulatedStorage::SIZE as usize - size_of::<FileMetadata>()];
429        filesystem
430            .write_file("fancy", &content, &[0u8; 32])
431            .unwrap();
432        let file = filesystem.read_file("fancy").unwrap();
433        let weak_ref = file;
434        filesystem.delete_file("fancy").unwrap();
435        let None = weak_ref.upgrade() else {
436            panic!("Should not be able to upgrade deleted file");
437        };
438        let None = filesystem.read_file("fancy") else {
439            panic!("Should not be able to read a deleted file");
440        };
441    }
442
443    #[test]
444    fn no_new_references_can_be_created_to_a_file_marked_for_deletion() {
445        let owned_storage = SimulatedStorage::new();
446        let storage =
447            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
448        let mut filesystem = Filesystem::new(storage);
449        let content = vec![0; SimulatedStorage::SIZE as usize - size_of::<FileMetadata>()];
450        filesystem
451            .write_file("fancy", &content, &[0u8; 32])
452            .unwrap();
453        let file = filesystem.read_file("fancy").unwrap();
454        let strong_ref = file.upgrade().unwrap();
455        filesystem.delete_file("fancy").unwrap();
456        let None = filesystem.read_file("fancy").unwrap().upgrade() else {
457            panic!(
458                "Should not be able to create a new reference to a file marked for deletion file"
459            );
460        };
461        // Strong ref still has the same correct content
462        assert_eq!(strong_ref.as_ref(), content);
463    }
464
465    #[test]
466    fn writing_a_maximum_size_file_works() {
467        let owned_storage = SimulatedStorage::new();
468        let storage =
469            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
470        let mut filesystem = Filesystem::new(storage);
471        let file = [0u8; SimulatedStorage::SIZE as usize - size_of::<FileMetadata>()];
472        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
473        let result = filesystem.read_file("fancy").unwrap();
474        assert_eq!(result.upgrade().unwrap().as_ref(), file);
475    }
476
477    #[test]
478    fn deleting_a_file_makes_space_for_a_new_file() {
479        let owned_storage = SimulatedStorage::new();
480        let storage =
481            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
482        let mut filesystem = Filesystem::new(storage);
483        let file = [0u8; SimulatedStorage::SIZE as usize - size_of::<FileMetadata>()];
484        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
485        filesystem.delete_file("fancy").unwrap();
486        filesystem.write_file("fancy2", &file, &[0u8; 32]).unwrap();
487        let result = filesystem.read_file("fancy2").unwrap();
488        assert_eq!(result.upgrade().unwrap().as_ref(), file);
489    }
490
491    #[test]
492    fn deleting_a_file_does_not_make_space_for_a_new_file_if_there_are_still_strong_references_to_its_content(
493    ) {
494        let owned_storage = SimulatedStorage::new();
495        let storage =
496            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
497        let mut filesystem = Filesystem::new(storage);
498        let file = [0u8; SimulatedStorage::SIZE as usize - size_of::<FileMetadata>()];
499        filesystem.write_file("fancy", &file, &[0u8; 32]).unwrap();
500        let fancy_file = filesystem.read_file("fancy").unwrap();
501        let strong_ref = fancy_file.upgrade().unwrap();
502        filesystem.delete_file("fancy").unwrap();
503        let Err(_) = filesystem.write_file("fancy2", &file, &[0u8; 32]) else {
504            panic!("Should fail because the file was not yet deleted");
505        };
506        assert_eq!(strong_ref.as_ref(), file);
507        drop(strong_ref);
508        // Should work now, because we dropped the last strong reference
509        filesystem.write_file("fancy2", &file, &[0u8; 32]).unwrap();
510    }
511
512    #[test]
513    fn writing_a_file_thats_too_big_fails() {
514        let owned_storage = SimulatedStorage::new();
515        let storage =
516            unsafe { std::mem::transmute::<_, &'static SimulatedStorage>(&owned_storage) };
517        let mut filesystem = Filesystem::new(storage);
518        let file = [0u8; SimulatedStorage::SIZE as usize + 1];
519        let Err(_) = filesystem.write_file("fancy", &file, &[0u8; 32]) else {
520            panic!("Should fail when there is not enough space");
521        };
522    }
523}