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