Skip to main content

littlefs_rust/
block_device.rs

1use alloc::{boxed::Box, vec::Vec};
2
3use crate::types::{Config, Error, Result};
4
5#[cfg(feature = "std")]
6use std::{
7    fs::{File, OpenOptions},
8    io::{Read, Seek, SeekFrom, Write},
9    path::Path,
10    sync::Mutex,
11};
12
13/// Minimal synchronous block-device interface for littlefs experiments.
14///
15/// The trait deliberately mirrors littlefs's block-level vocabulary instead of
16/// exposing a raw byte slice. `read` and `prog` operate inside one logical
17/// block, `erase` resets an entire block, and `sync` gives later writable
18/// backends a place to flush durable state.
19pub trait BlockDevice {
20    fn config(&self) -> Config;
21
22    fn read(&self, block: u32, off: usize, out: &mut [u8]) -> Result<()>;
23
24    fn prog(&mut self, block: u32, off: usize, data: &[u8]) -> Result<()>;
25
26    fn erase(&mut self, block: u32) -> Result<()>;
27
28    fn sync(&mut self) -> Result<()> {
29        Ok(())
30    }
31}
32
33/// In-memory NOR-flash block device used by tests and early integrations.
34///
35/// Blocks start erased to `0xff`. Programming uses `old & new`, so accidental
36/// attempts to turn a programmed zero bit back into one are visible in the
37/// stored bytes just as they would be on NOR flash. This is intentionally a
38/// real block device implementation for tests, not a mock of filesystem
39/// behavior.
40#[derive(Debug, Clone)]
41pub struct MemoryBlockDevice {
42    cfg: Config,
43    storage: Vec<u8>,
44}
45
46/// File-backed block device for black-box tests and desktop experiments.
47///
48/// This backend performs direct local-file reads and writes. `sync` forwards to
49/// the host file's data-sync operation so file-handle flush/sync tests exercise
50/// a real backend boundary. Like `MemoryBlockDevice`, `prog` preserves NOR
51/// semantics by reading the current bytes, applying `old & new`, and writing
52/// the result back to the file.
53#[cfg(feature = "std")]
54#[derive(Debug)]
55pub struct FileBlockDevice {
56    cfg: Config,
57    file: Mutex<File>,
58}
59
60impl MemoryBlockDevice {
61    pub fn new_erased(cfg: Config) -> Result<Self> {
62        let len = image_len(cfg)?;
63        Ok(Self {
64            cfg,
65            storage: alloc::vec![0xff; len],
66        })
67    }
68
69    pub fn from_bytes(cfg: Config, bytes: &[u8]) -> Result<Self> {
70        let len = image_len(cfg)?;
71        if bytes.len() != len {
72            return Err(Error::InvalidConfig);
73        }
74        Ok(Self {
75            cfg,
76            storage: bytes.to_vec(),
77        })
78    }
79
80    pub fn as_bytes(&self) -> &[u8] {
81        &self.storage
82    }
83
84    fn block_range(&self, block: u32, off: usize, len: usize) -> Result<core::ops::Range<usize>> {
85        let block = block as usize;
86        if block >= self.cfg.block_count {
87            return Err(Error::OutOfBounds);
88        }
89        let end_off = off.checked_add(len).ok_or(Error::OutOfBounds)?;
90        if end_off > self.cfg.block_size {
91            return Err(Error::OutOfBounds);
92        }
93        let start = block
94            .checked_mul(self.cfg.block_size)
95            .and_then(|base| base.checked_add(off))
96            .ok_or(Error::OutOfBounds)?;
97        let end = start.checked_add(len).ok_or(Error::OutOfBounds)?;
98        Ok(start..end)
99    }
100}
101
102#[cfg(feature = "std")]
103impl FileBlockDevice {
104    pub fn create_erased<P: AsRef<Path>>(path: P, cfg: Config) -> Result<Self> {
105        let len = image_len(cfg)?;
106        let mut file = OpenOptions::new()
107            .read(true)
108            .write(true)
109            .create(true)
110            .truncate(true)
111            .open(path)
112            .map_err(|_| Error::Io)?;
113
114        let erased = alloc::vec![0xff; cfg.block_size];
115        for _ in 0..cfg.block_count {
116            file.write_all(&erased).map_err(|_| Error::Io)?;
117        }
118        file.set_len(len as u64).map_err(|_| Error::Io)?;
119        Ok(Self {
120            cfg,
121            file: Mutex::new(file),
122        })
123    }
124
125    pub fn open<P: AsRef<Path>>(path: P, cfg: Config) -> Result<Self> {
126        let len = image_len(cfg)? as u64;
127        let file = OpenOptions::new()
128            .read(true)
129            .write(true)
130            .open(path)
131            .map_err(|_| Error::Io)?;
132        if file.metadata().map_err(|_| Error::Io)?.len() != len {
133            return Err(Error::InvalidConfig);
134        }
135        Ok(Self {
136            cfg,
137            file: Mutex::new(file),
138        })
139    }
140
141    pub fn from_bytes<P: AsRef<Path>>(path: P, cfg: Config, bytes: &[u8]) -> Result<Self> {
142        let len = image_len(cfg)?;
143        if bytes.len() != len {
144            return Err(Error::InvalidConfig);
145        }
146
147        let mut file = OpenOptions::new()
148            .read(true)
149            .write(true)
150            .create(true)
151            .truncate(true)
152            .open(path)
153            .map_err(|_| Error::Io)?;
154        file.write_all(bytes).map_err(|_| Error::Io)?;
155        Ok(Self {
156            cfg,
157            file: Mutex::new(file),
158        })
159    }
160
161    fn block_range(&self, block: u32, off: usize, len: usize) -> Result<core::ops::Range<usize>> {
162        let block = block as usize;
163        if block >= self.cfg.block_count {
164            return Err(Error::OutOfBounds);
165        }
166        let end_off = off.checked_add(len).ok_or(Error::OutOfBounds)?;
167        if end_off > self.cfg.block_size {
168            return Err(Error::OutOfBounds);
169        }
170        let start = block
171            .checked_mul(self.cfg.block_size)
172            .and_then(|base| base.checked_add(off))
173            .ok_or(Error::OutOfBounds)?;
174        let end = start.checked_add(len).ok_or(Error::OutOfBounds)?;
175        Ok(start..end)
176    }
177}
178
179impl BlockDevice for MemoryBlockDevice {
180    fn config(&self) -> Config {
181        self.cfg
182    }
183
184    fn read(&self, block: u32, off: usize, out: &mut [u8]) -> Result<()> {
185        let range = self.block_range(block, off, out.len())?;
186        out.copy_from_slice(&self.storage[range]);
187        Ok(())
188    }
189
190    fn prog(&mut self, block: u32, off: usize, data: &[u8]) -> Result<()> {
191        let range = self.block_range(block, off, data.len())?;
192        for (dst, src) in self.storage[range].iter_mut().zip(data) {
193            *dst &= *src;
194        }
195        Ok(())
196    }
197
198    fn erase(&mut self, block: u32) -> Result<()> {
199        let range = self.block_range(block, 0, self.cfg.block_size)?;
200        self.storage[range].fill(0xff);
201        Ok(())
202    }
203}
204
205impl<D: BlockDevice + ?Sized> BlockDevice for Box<D> {
206    fn config(&self) -> Config {
207        (**self).config()
208    }
209
210    fn read(&self, block: u32, off: usize, out: &mut [u8]) -> Result<()> {
211        (**self).read(block, off, out)
212    }
213
214    fn prog(&mut self, block: u32, off: usize, data: &[u8]) -> Result<()> {
215        (**self).prog(block, off, data)
216    }
217
218    fn erase(&mut self, block: u32) -> Result<()> {
219        (**self).erase(block)
220    }
221
222    fn sync(&mut self) -> Result<()> {
223        (**self).sync()
224    }
225}
226
227#[cfg(feature = "std")]
228impl BlockDevice for FileBlockDevice {
229    fn config(&self) -> Config {
230        self.cfg
231    }
232
233    fn read(&self, block: u32, off: usize, out: &mut [u8]) -> Result<()> {
234        let range = self.block_range(block, off, out.len())?;
235        let mut file = self.file.lock().map_err(|_| Error::Io)?;
236        file.seek(SeekFrom::Start(range.start as u64))
237            .map_err(|_| Error::Io)?;
238        file.read_exact(out).map_err(|_| Error::Io)
239    }
240
241    fn prog(&mut self, block: u32, off: usize, data: &[u8]) -> Result<()> {
242        let range = self.block_range(block, off, data.len())?;
243        let mut file = self.file.lock().map_err(|_| Error::Io)?;
244        let chunk_size = self.cfg.cache_size();
245        let mut old = alloc::vec![0xff; chunk_size];
246        let mut copied = 0usize;
247        while copied < data.len() {
248            let n = core::cmp::min(chunk_size, data.len() - copied);
249            let file_off = range.start + copied;
250            file.seek(SeekFrom::Start(file_off as u64))
251                .map_err(|_| Error::Io)?;
252            file.read_exact(&mut old[..n]).map_err(|_| Error::Io)?;
253            for (dst, src) in old[..n].iter_mut().zip(&data[copied..copied + n]) {
254                *dst &= *src;
255            }
256            file.seek(SeekFrom::Start(file_off as u64))
257                .map_err(|_| Error::Io)?;
258            file.write_all(&old[..n]).map_err(|_| Error::Io)?;
259            copied += n;
260        }
261        Ok(())
262    }
263
264    fn erase(&mut self, block: u32) -> Result<()> {
265        let range = self.block_range(block, 0, self.cfg.block_size)?;
266        let mut file = self.file.lock().map_err(|_| Error::Io)?;
267        let chunk_size = self.cfg.cache_size();
268        let erased = alloc::vec![0xff; chunk_size];
269        let mut copied = 0usize;
270        while copied < self.cfg.block_size {
271            let n = core::cmp::min(chunk_size, self.cfg.block_size - copied);
272            file.seek(SeekFrom::Start((range.start + copied) as u64))
273                .map_err(|_| Error::Io)?;
274            file.write_all(&erased[..n]).map_err(|_| Error::Io)?;
275            copied += n;
276        }
277        Ok(())
278    }
279
280    fn sync(&mut self) -> Result<()> {
281        let file = self.file.lock().map_err(|_| Error::Io)?;
282        file.sync_data().map_err(|_| Error::Io)
283    }
284}
285
286fn image_len(cfg: Config) -> Result<usize> {
287    if cfg.block_size == 0 || cfg.block_count == 0 {
288        return Err(Error::InvalidConfig);
289    }
290    cfg.block_size
291        .checked_mul(cfg.block_count)
292        .ok_or(Error::InvalidConfig)
293}