Skip to main content

sequential_storage/
lib.rs

1#![cfg_attr(not(any(test, doctest, feature = "std", fuzzing)), no_std)]
2#![warn(missing_docs)]
3#![doc = include_str!("../README.md")]
4#![allow(clippy::cast_possible_truncation)]
5
6use core::num::NonZeroUsize;
7use core::{
8    fmt::Debug,
9    marker::PhantomData,
10    ops::{Deref, DerefMut, Range},
11};
12use embedded_storage_async::nor_flash::NorFlash;
13use map::SerializationError;
14
15#[cfg(feature = "alloc")]
16mod alloc_impl;
17#[cfg(feature = "arrayvec")]
18mod arrayvec_impl;
19pub mod cache;
20#[cfg(feature = "heapless-09")]
21mod heapless_09_impl;
22#[cfg(feature = "heapless")]
23mod heapless_impl;
24mod item;
25pub mod map;
26pub mod queue;
27
28#[cfg(any(test, doctest, feature = "_test"))]
29/// An in-memory flash type that can be used for mocking.
30pub mod mock_flash;
31
32/// The biggest wordsize we support.
33///
34/// Stm32 internal flash has 256-bit words, so 32 bytes.
35/// Many flashes have 4-byte or 1-byte words.
36const MAX_WORD_SIZE: usize = 32;
37
38/// The generic object that manages the flash.
39/// This is mostly an internal type.
40///
41/// To create real instances, call:
42/// - map: [`map::MapStorage::new`]
43/// - queue: [`queue::QueueStorage::new`]
44///
45/// You can [`Self::destroy`] this type to get back the flash and the cache.
46struct GenericStorage<S: NorFlash, C: CacheImpl<KEY>, KEY> {
47    flash: S,
48    flash_range: Range<u32>,
49    cache: C,
50    _phantom: PhantomData<KEY>,
51}
52
53impl<S: NorFlash, C: CacheImpl<KEY>, KEY> GenericStorage<S, C, KEY> {
54    /// Resets the flash in the entire given flash range.
55    ///
56    /// This is just a thin helper function as it just calls the flash's erase function.
57    pub async fn erase_all(&mut self) -> Result<(), Error<S::Error>> {
58        self.flash
59            .erase(self.flash_range.start, self.flash_range.end)
60            .await
61            .map_err(|e| Error::Storage {
62                value: e,
63                #[cfg(feature = "_test")]
64                backtrace: std::backtrace::Backtrace::capture(),
65            })
66    }
67
68    /// Get the minimal overhead size per stored item for the given flash type.
69    ///
70    /// The associated data of each item is additionally padded to a full flash word size, but that's not part of this number.\
71    /// This means the full item length is `returned number + (data length).next_multiple_of(S::WORD_SIZE)`.
72    #[must_use]
73    pub const fn item_overhead_size() -> u32 {
74        item::ItemHeader::data_address::<S>(0)
75    }
76
77    async fn try_general_repair(&mut self) -> Result<(), Error<S::Error>> {
78        // Loop through the pages and get their state. If one returns the corrupted error,
79        // the page is likely half-erased. Fix for that is to re-erase again to hopefully finish the job.
80        for page_index in self.get_pages(0) {
81            if matches!(
82                self.get_page_state(page_index).await,
83                Err(Error::Corrupted { .. })
84            ) {
85                self.open_page(page_index).await?;
86            }
87        }
88
89        #[cfg(fuzzing)]
90        eprintln!("General repair has been called");
91
92        Ok(())
93    }
94
95    /// Find the first page that is in the given page state.
96    ///
97    /// The search starts at `starting_page_index` (and wraps around back to 0 if required)
98    async fn find_first_page(
99        &mut self,
100        starting_page_index: usize,
101        page_state: PageState,
102    ) -> Result<Option<usize>, Error<S::Error>> {
103        for page_index in self.get_pages(starting_page_index) {
104            if page_state == self.get_page_state_cached(page_index).await? {
105                return Ok(Some(page_index));
106            }
107        }
108
109        Ok(None)
110    }
111
112    fn page_count(&self) -> NonZeroUsize {
113        let page_count = self.flash_range.len() / S::ERASE_SIZE;
114        // Do a max 1 on the page count to prevent a panic. We know it's never 0 because it's checked in the constructor, but the compiler doesn't know
115        NonZeroUsize::new(page_count.max(1)).unwrap()
116    }
117
118    /// Get all pages in the flash range from the given start to end (that might wrap back to 0)
119    fn get_pages(
120        &self,
121        starting_page_index: usize,
122    ) -> impl DoubleEndedIterator<Item = usize> + use<S, C, KEY> {
123        let page_count = self.page_count();
124        (0..page_count.get()).map(move |index| (index + starting_page_index) % page_count)
125    }
126
127    /// Get the next page index (wrapping around to 0 if required)
128    fn next_page(&self, page_index: usize) -> usize {
129        let page_count = self.page_count();
130        (page_index + 1) % page_count
131    }
132
133    /// Get the previous page index (wrapping around to the biggest page if required)
134    fn previous_page(&self, page_index: usize) -> usize {
135        let page_count = self.page_count();
136
137        match page_index.checked_sub(1) {
138            Some(new_page_index) => new_page_index,
139            None => page_count.get() - 1,
140        }
141    }
142
143    /// Get the state of the page located at the given index
144    async fn get_page_state_cached(
145        &mut self,
146        page_index: usize,
147    ) -> Result<PageState, Error<S::Error>> {
148        if let Some(cached_page_state) = self.cache.get_page_state(page_index) {
149            if cfg!(feature = "_check-cache") {
150                let discovered_state = self.get_page_state(page_index).await?;
151                assert_eq!(
152                    cached_page_state, discovered_state,
153                    "At page index: {page_index}",
154                );
155            }
156
157            return Ok(cached_page_state);
158        }
159
160        let discovered_state = self.get_page_state(page_index).await?;
161
162        // Not dirty because nothing changed and nothing can be inconsistent
163        self.cache
164            .notice_page_state(page_index, discovered_state, false);
165
166        Ok(discovered_state)
167    }
168
169    /// Get the state of the page located at the given index
170    async fn get_page_state(&mut self, page_index: usize) -> Result<PageState, Error<S::Error>> {
171        let page_address = calculate_page_address::<S>(self.flash_range.clone(), page_index);
172        /// We only care about the data in the first byte to aid shutdown/cancellation.
173        /// But we also don't want it to be too too definitive because we want to survive the occasional bitflip.
174        /// So only half of the byte needs to be zero.
175        const HALF_MARKER_BITS: u32 = 4;
176
177        let mut buffer = [0; MAX_WORD_SIZE];
178        self.flash
179            .read(page_address, &mut buffer[..S::READ_SIZE])
180            .await
181            .map_err(|e| Error::Storage {
182                value: e,
183                #[cfg(feature = "_test")]
184                backtrace: std::backtrace::Backtrace::capture(),
185            })?;
186        let start_marked = buffer[..S::READ_SIZE]
187            .iter()
188            .map(|marker_byte| marker_byte.count_zeros())
189            .sum::<u32>()
190            >= HALF_MARKER_BITS;
191
192        self.flash
193            .read(
194                page_address + (S::ERASE_SIZE - S::READ_SIZE) as u32,
195                &mut buffer[..S::READ_SIZE],
196            )
197            .await
198            .map_err(|e| Error::Storage {
199                value: e,
200                #[cfg(feature = "_test")]
201                backtrace: std::backtrace::Backtrace::capture(),
202            })?;
203        let end_marked = buffer[..S::READ_SIZE]
204            .iter()
205            .map(|marker_byte| marker_byte.count_zeros())
206            .sum::<u32>()
207            >= HALF_MARKER_BITS;
208
209        let discovered_state = match (start_marked, end_marked) {
210            (true, true) => PageState::Closed,
211            (true, false) => PageState::PartialOpen,
212            // Probably an interrupted erase
213            (false, true) => {
214                return Err(Error::Corrupted {
215                    #[cfg(feature = "_test")]
216                    backtrace: std::backtrace::Backtrace::capture(),
217                });
218            }
219            (false, false) => PageState::Open,
220        };
221
222        Ok(discovered_state)
223    }
224
225    /// Erase the page to open it again
226    async fn open_page(&mut self, page_index: usize) -> Result<(), Error<S::Error>> {
227        self.cache
228            .notice_page_state(page_index, PageState::Open, true);
229
230        let page_address = calculate_page_address::<S>(self.flash_range.clone(), page_index);
231        let page_end_address =
232            calculate_page_end_address::<S>(self.flash_range.clone(), page_index);
233
234        self.flash
235            .erase(page_address, page_end_address)
236            .await
237            .map_err(|e| Error::Storage {
238                value: e,
239                #[cfg(feature = "_test")]
240                backtrace: std::backtrace::Backtrace::capture(),
241            })?;
242
243        Ok(())
244    }
245
246    /// Fully closes a page by writing both the start and end marker
247    async fn close_page(&mut self, page_index: usize) -> Result<(), Error<S::Error>> {
248        let current_state = self.partial_close_page(page_index).await?;
249
250        if current_state != PageState::PartialOpen {
251            return Ok(());
252        }
253
254        self.cache
255            .notice_page_state(page_index, PageState::Closed, true);
256
257        let buffer = AlignedBuf([MARKER; MAX_WORD_SIZE]);
258        let page_end_address =
259            calculate_page_end_address::<S>(self.flash_range.clone(), page_index)
260                - S::WORD_SIZE as u32;
261        // Close the end marker
262        self.flash
263            .write(page_end_address, &buffer[..S::WORD_SIZE])
264            .await
265            .map_err(|e| Error::Storage {
266                value: e,
267                #[cfg(feature = "_test")]
268                backtrace: std::backtrace::Backtrace::capture(),
269            })?;
270
271        Ok(())
272    }
273
274    /// Partially close a page by writing the start marker
275    async fn partial_close_page(
276        &mut self,
277        page_index: usize,
278    ) -> Result<PageState, Error<S::Error>> {
279        let current_state = self.get_page_state_cached(page_index).await?;
280
281        if current_state != PageState::Open {
282            return Ok(current_state);
283        }
284
285        let new_state = match current_state {
286            PageState::Closed => PageState::Closed,
287            PageState::PartialOpen | PageState::Open => PageState::PartialOpen,
288        };
289
290        self.cache.notice_page_state(page_index, new_state, true);
291
292        let buffer = AlignedBuf([MARKER; MAX_WORD_SIZE]);
293        let page_start_address = calculate_page_address::<S>(self.flash_range.clone(), page_index);
294        // Close the start marker
295        self.flash
296            .write(page_start_address, &buffer[..S::WORD_SIZE])
297            .await
298            .map_err(|e| Error::Storage {
299                value: e,
300                #[cfg(feature = "_test")]
301                backtrace: std::backtrace::Backtrace::capture(),
302            })?;
303
304        Ok(new_state)
305    }
306
307    #[cfg(any(test, feature = "std", fuzzing))]
308    /// Print all items in flash to the returned string
309    pub async fn print_items(&mut self) -> String {
310        use crate::NorFlashExt;
311        use std::fmt::Write;
312
313        let mut buf = [0; 1024 * 16];
314
315        let mut s = String::new();
316
317        writeln!(s, "Items in flash:").unwrap();
318
319        for page_index in self.get_pages(0) {
320            writeln!(
321                s,
322                "  Page {page_index} ({}):",
323                match self.get_page_state(page_index).await {
324                    Ok(value) => format!("{value:?}"),
325                    Err(e) => format!("Error ({e:?})"),
326                },
327            )
328            .unwrap();
329            let page_data_start =
330                crate::calculate_page_address::<S>(self.flash_range.clone(), page_index)
331                    + S::WORD_SIZE as u32;
332            let page_data_end =
333                crate::calculate_page_end_address::<S>(self.flash_range.clone(), page_index)
334                    - S::WORD_SIZE as u32;
335
336            let mut it = crate::item::ItemHeaderIter::new(page_data_start, page_data_end);
337            while let (Some(header), item_address) =
338                it.traverse(&mut self.flash, |_, _| false).await.unwrap()
339            {
340                let next_item_address = header.next_item_address::<S>(item_address);
341                let maybe_item = match header
342                    .read_item(&mut self.flash, &mut buf, item_address, page_data_end)
343                    .await
344                {
345                    Ok(maybe_item) => maybe_item,
346                    Err(e) => {
347                        writeln!(
348                            s,
349                            "   Item COULD NOT BE READ at {item_address}..{next_item_address}"
350                        )
351                        .unwrap();
352
353                        println!("{s}");
354                        panic!("{e:?}");
355                    }
356                };
357
358                writeln!(
359                    s,
360                    "   Item {maybe_item:?} at {item_address}..{next_item_address}"
361                )
362                .unwrap();
363            }
364        }
365
366        s
367    }
368
369    /// Destroy the instance to get back the flash and the cache.
370    ///
371    /// The cache can be passed to a new storage instance, but only for the same flash region and if nothing has changed in flash.
372    pub fn destroy(self) -> (S, C) {
373        (self.flash, self.cache)
374    }
375
376    /// Get a reference to the flash. Mutating the memory is at your own risk.
377    pub const fn flash(&mut self) -> &mut S {
378        &mut self.flash
379    }
380
381    /// Get the flash range being used
382    pub const fn flash_range(&self) -> Range<u32> {
383        self.flash_range.start..self.flash_range.end
384    }
385}
386
387/// Round up the the given number to align with the wordsize of the flash.
388/// If the number is already aligned, it is not changed.
389const fn round_up_to_alignment<S: NorFlash>(value: u32) -> u32 {
390    value.next_multiple_of(S::WORD_SIZE as u32)
391}
392
393/// Round up the the given number to align with the wordsize of the flash.
394/// If the number is already aligned, it is not changed.
395const fn round_up_to_alignment_usize<S: NorFlash>(value: usize) -> usize {
396    value.next_multiple_of(S::WORD_SIZE)
397}
398
399/// Round down the the given number to align with the wordsize of the flash.
400/// If the number is already aligned, it is not changed.
401const fn round_down_to_alignment<S: NorFlash>(value: u32) -> u32 {
402    let alignment = S::WORD_SIZE as u32;
403    (value / alignment) * alignment
404}
405
406/// Round down the the given number to align with the wordsize of the flash.
407/// If the number is already aligned, it is not changed.
408const fn round_down_to_alignment_usize<S: NorFlash>(value: usize) -> usize {
409    round_down_to_alignment::<S>(value as u32) as usize
410}
411
412/// Calculate the first address of the given page
413const fn calculate_page_address<S: NorFlash>(flash_range: Range<u32>, page_index: usize) -> u32 {
414    flash_range.start + (S::ERASE_SIZE * page_index) as u32
415}
416
417/// Calculate the last address (exclusive) of the given page
418const fn calculate_page_end_address<S: NorFlash>(
419    flash_range: Range<u32>,
420    page_index: usize,
421) -> u32 {
422    flash_range.start + (S::ERASE_SIZE * (page_index + 1)) as u32
423}
424
425/// Get the page index from any address located inside that page
426const fn calculate_page_index<S: NorFlash>(flash_range: Range<u32>, address: u32) -> usize {
427    (address - flash_range.start) as usize / S::ERASE_SIZE
428}
429
430const fn calculate_page_size<S: NorFlash>() -> usize {
431    // Page minus the two page status words
432    S::ERASE_SIZE - S::WORD_SIZE * 2
433}
434
435/// The marker being used for page states
436const MARKER: u8 = 0;
437
438/// The state of a page
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440#[cfg_attr(feature = "defmt", derive(defmt::Format))]
441pub enum PageState {
442    /// This page was fully written and has now been sealed
443    Closed,
444    /// This page has been written to, but may have some space left over
445    PartialOpen,
446    /// This page is fully erased
447    Open,
448}
449
450#[allow(dead_code)]
451impl PageState {
452    /// Returns `true` if the page state is [`Closed`].
453    ///
454    /// [`Closed`]: PageState::Closed
455    #[must_use]
456    fn is_closed(self) -> bool {
457        matches!(self, Self::Closed)
458    }
459
460    /// Returns `true` if the page state is [`PartialOpen`].
461    ///
462    /// [`PartialOpen`]: PageState::PartialOpen
463    #[must_use]
464    fn is_partial_open(self) -> bool {
465        matches!(self, Self::PartialOpen)
466    }
467
468    /// Returns `true` if the page state is [`Open`].
469    ///
470    /// [`Open`]: PageState::Open
471    #[must_use]
472    fn is_open(self) -> bool {
473        matches!(self, Self::Open)
474    }
475}
476
477/// The main error type
478#[non_exhaustive]
479#[derive(Debug)]
480#[cfg_attr(feature = "defmt", derive(defmt::Format))]
481pub enum Error<S> {
482    /// An error in the storage (flash)
483    Storage {
484        /// The error value
485        value: S,
486        #[cfg(feature = "_test")]
487        /// Backtrace made at the construction of the error
488        backtrace: std::backtrace::Backtrace,
489    },
490    /// The item cannot be stored anymore because the storage is full.
491    FullStorage,
492    /// It's been detected that the memory is likely corrupted.
493    /// You may want to erase the memory to recover.
494    Corrupted {
495        #[cfg(feature = "_test")]
496        /// Backtrace made at the construction of the error
497        backtrace: std::backtrace::Backtrace,
498    },
499    /// There's a bug in the logic of the crate. Please report!
500    /// This would otherwise have been a panic
501    LogicBug {
502        #[cfg(feature = "_test")]
503        /// Backtrace made at the construction of the error
504        backtrace: std::backtrace::Backtrace,
505    },
506    /// A provided buffer was to big to be used
507    BufferTooBig,
508    /// A provided buffer was to small to be used (usize is size needed)
509    BufferTooSmall(usize),
510    /// A serialization error (from the key or value)
511    SerializationError(SerializationError),
512    /// The item does not fit in flash, ever.
513    /// This is different from [`Error::FullStorage`] because this item is too big to fit even in empty flash.
514    ///
515    /// See the readme for more info about the constraints on item sizes.
516    ItemTooBig,
517}
518
519impl<S> From<SerializationError> for Error<S> {
520    fn from(v: SerializationError) -> Self {
521        Self::SerializationError(v)
522    }
523}
524
525impl<S: PartialEq> PartialEq for Error<S> {
526    fn eq(&self, other: &Self) -> bool {
527        match (self, other) {
528            (Self::Storage { value: l_value, .. }, Self::Storage { value: r_value, .. }) => {
529                l_value == r_value
530            }
531            (Self::BufferTooSmall(l0), Self::BufferTooSmall(r0)) => l0 == r0,
532            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
533        }
534    }
535}
536
537impl<S> core::fmt::Display for Error<S>
538where
539    S: core::fmt::Display,
540{
541    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
542        match self {
543            Error::Storage { value, .. } => write!(f, "Storage error: {value}"),
544            Error::FullStorage => write!(f, "Storage is full"),
545            #[cfg(not(feature = "_test"))]
546            Error::Corrupted { .. } => write!(f, "Storage is corrupted"),
547            #[cfg(feature = "_test")]
548            Error::Corrupted { backtrace } => write!(f, "Storage is corrupted\n{backtrace}"),
549            #[cfg(not(feature = "_test"))]
550            Error::LogicBug { .. } => write!(f, "Logic bug"),
551            #[cfg(feature = "_test")]
552            Error::LogicBug { backtrace } => write!(f, "Logic bug\n{backtrace}"),
553            Error::BufferTooBig => write!(f, "A provided buffer was to big to be used"),
554            Error::BufferTooSmall(needed) => write!(
555                f,
556                "A provided buffer was to small to be used. Needed was {needed}"
557            ),
558            Error::SerializationError(value) => write!(f, "Map value error: {value}"),
559            Error::ItemTooBig => write!(f, "The item is too big to fit in the flash"),
560        }
561    }
562}
563
564impl<S> core::error::Error for Error<S> where S: core::fmt::Display + core::fmt::Debug {}
565
566// Type representing buffer aligned to 4 byte boundary.
567#[repr(align(4))]
568pub(crate) struct AlignedBuf<const SIZE: usize>(pub(crate) [u8; SIZE]);
569impl<const SIZE: usize> Deref for AlignedBuf<SIZE> {
570    type Target = [u8];
571    fn deref(&self) -> &Self::Target {
572        &self.0
573    }
574}
575
576impl<const SIZE: usize> DerefMut for AlignedBuf<SIZE> {
577    fn deref_mut(&mut self) -> &mut Self::Target {
578        &mut self.0
579    }
580}
581
582/// Extension trait to get the overall word size, which is the largest of the write and read word size
583trait NorFlashExt {
584    /// The largest of the write and read word size
585    const WORD_SIZE: usize;
586}
587
588impl<S: NorFlash> NorFlashExt for S {
589    const WORD_SIZE: usize = {
590        assert_read_write_sizes(Self::WRITE_SIZE, Self::READ_SIZE);
591
592        if Self::WRITE_SIZE > Self::READ_SIZE {
593            Self::WRITE_SIZE
594        } else {
595            Self::READ_SIZE
596        }
597    };
598}
599
600#[track_caller]
601const fn assert_read_write_sizes(write_size: usize, read_size: usize) {
602    assert!(
603        write_size.is_multiple_of(read_size) || read_size.is_multiple_of(write_size),
604        "Only flash with read and write sizes that are multiple of each other are supported"
605    );
606}
607
608macro_rules! run_with_auto_repair {
609    (function = $function:expr, repair = $repair_function:expr) => {
610        match $function {
611            Err(Error::Corrupted {
612                #[cfg(feature = "_test")]
613                    backtrace: _backtrace,
614                ..
615            }) => {
616                #[cfg(all(feature = "_test", fuzzing))]
617                eprintln!(
618                    "### Encountered curruption! Repairing now. Originated from:\n{_backtrace:#}"
619                );
620                $repair_function;
621                $function
622            }
623            val => val,
624        }
625    };
626}
627
628pub(crate) use run_with_auto_repair;
629
630use crate::cache::CacheImpl;
631
632#[cfg(test)]
633mod tests {
634    use crate::cache::Cache;
635
636    use super::*;
637    use futures_test::test;
638
639    type MockFlash = mock_flash::MockFlashBase<4, 4, 64>;
640
641    async fn write_aligned(
642        flash: &mut MockFlash,
643        offset: u32,
644        bytes: &[u8],
645    ) -> Result<(), mock_flash::MockFlashError> {
646        let mut buf = AlignedBuf([0; 256]);
647        buf[..bytes.len()].copy_from_slice(bytes);
648        flash.write(offset, &buf[..bytes.len()]).await
649    }
650
651    #[test]
652    async fn test_find_pages() {
653        // Page setup:
654        // 0: closed
655        // 1: closed
656        // 2: partial-open
657        // 3: open
658
659        let mut flash = MockFlash::default();
660
661        // Page 0 markers
662        write_aligned(&mut flash, 0x000, &[MARKER, 0, 0, 0])
663            .await
664            .unwrap();
665        write_aligned(&mut flash, 0x100 - 4, &[0, 0, 0, MARKER])
666            .await
667            .unwrap();
668        // Page 1 markers
669        write_aligned(&mut flash, 0x100, &[MARKER, 0, 0, 0])
670            .await
671            .unwrap();
672        write_aligned(&mut flash, 0x200 - 4, &[0, 0, 0, MARKER])
673            .await
674            .unwrap();
675        // Page 2 markers
676        write_aligned(&mut flash, 0x200, &[MARKER, 0, 0, 0])
677            .await
678            .unwrap();
679
680        let mut storage = GenericStorage {
681            flash: flash,
682            flash_range: 0x000..0x400,
683            cache: Cache::new_uncached(),
684            _phantom: PhantomData::<()>,
685        };
686
687        assert_eq!(
688            storage.find_first_page(0, PageState::Open).await.unwrap(),
689            Some(3)
690        );
691        assert_eq!(
692            storage
693                .find_first_page(0, PageState::PartialOpen)
694                .await
695                .unwrap(),
696            Some(2)
697        );
698        assert_eq!(
699            storage
700                .find_first_page(1, PageState::PartialOpen)
701                .await
702                .unwrap(),
703            Some(2)
704        );
705        assert_eq!(
706            storage
707                .find_first_page(2, PageState::PartialOpen)
708                .await
709                .unwrap(),
710            Some(2)
711        );
712        assert_eq!(
713            storage.find_first_page(3, PageState::Open).await.unwrap(),
714            Some(3)
715        );
716
717        storage.flash_range = 0x000..0x200;
718        assert_eq!(
719            storage
720                .find_first_page(0, PageState::PartialOpen)
721                .await
722                .unwrap(),
723            None
724        );
725        storage.flash_range = 0x000..0x400;
726
727        assert_eq!(
728            storage.find_first_page(0, PageState::Closed).await.unwrap(),
729            Some(0)
730        );
731        assert_eq!(
732            storage.find_first_page(1, PageState::Closed).await.unwrap(),
733            Some(1)
734        );
735        assert_eq!(
736            storage.find_first_page(2, PageState::Closed).await.unwrap(),
737            Some(0)
738        );
739        assert_eq!(
740            storage.find_first_page(3, PageState::Closed).await.unwrap(),
741            Some(0)
742        );
743
744        storage.flash_range = 0x200..0x400;
745        assert_eq!(
746            storage.find_first_page(0, PageState::Closed).await.unwrap(),
747            None
748        );
749    }
750
751    #[test]
752    async fn read_write_sizes() {
753        assert_read_write_sizes(1, 1);
754        assert_read_write_sizes(1, 4);
755        assert_read_write_sizes(4, 4);
756        assert_read_write_sizes(4, 1);
757    }
758}