// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Provides a multi-purpose data-structure. //! //! # Description //! //! The `Store` data-structure permits to iterate, find, insert, delete, and replace entries in a //! multi-set. The mutable operations (insert, delete, and replace) are atomic, in the sense that if //! power is lost during the operation, then the operation might either succeed or fail but the //! store remains in a coherent state. The data-structure is flash-efficient, in the sense that it //! tries to minimize the number of times a page is erased. //! //! An _entry_ is made of a _tag_, which is a number, and a _data_, which is a slice of bytes. The //! tag is stored efficiently by using unassigned bits of the entry header and footer. For example, //! it can be used to decide how to deserialize the data. It is not necessary to use tags since a //! prefix of the data could be used to decide how to deserialize the rest. //! //! Entries can also be associated to a set of _keys_. The find operation permits to retrieve all //! entries associated to a given key. The same key can be associated to multiple entries and the //! same entry can be associated to multiple keys. //! //! # Storage //! //! The data-structure is parametric over its storage which must implement the `Storage` trait. //! There are currently 2 implementations of this trait: //! - `SyscallStorage` using the `embedded_flash` syscall API for production builds. //! - `BufferStorage` using a heap-allocated buffer for testing. //! //! # Configuration //! //! The data-structure can be configured with the `StoreConfig` trait. By implementing this trait, //! the number of possible tags and the association between keys and entries are defined. //! //! # Properties //! //! The data-structure provides the following properties: //! - When an operation returns success, then the represented multi-set is updated accordingly. For //! example, an inserted entry can be found without alteration until replaced or deleted. //! - When an operation returns an error, the resulting multi-set state is described in the error //! documentation. //! - When power is lost before an operation returns, the operation will either succeed or be //! rolled-back on the next initialization. So the multi-set would be either left unchanged or //! updated accordingly. //! //! Those properties rely on the following assumptions: //! - Writing a word to flash is atomic. When power is lost, the word is either fully written or not //! written at all. //! - Reading a word from flash is deterministic. When power is lost while writing or erasing a word //! (erasing a page containing that word), reading that word repeatedly returns the same result //! (until it is written or its page is erased). //! - To decide whether a page has been erased, it is enough to test if all its bits are equal to 1. //! //! The properties may still hold outside those assumptions but with weaker probabilities as the //! usage diverges from the assumptions. //! //! # Implementation //! //! The store is a page-aligned sequence of bits. It matches the following grammar: //! //! ```text //! Store := Page* //! Page := PageHeader (Entry | InternalEntry)* Padding(page) //! PageHeader := // must fit in one word //! initialized:1 //! erase_count:erase_bits //! compacting:1 //! new_page:page_bits //! Padding(word) //! Entry := Header Data Footer //! // Let X be the byte (word-aligned for sensitive queries) following `length` in `Info`. //! Header := Info[..X] // must fit in one word //! Footer := Info[X..] // must fit in one word //! Info := //! present=0 //! deleted:1 //! internal=1 //! replace:1 //! sensitive:1 //! length:byte_bits //! tag:tag_bits //! [ // present if `replace` is 0 //! replace_page:page_bits //! replace_byte:byte_bits //! ] //! [Padding(bit)] // until `complete` is the last bit of a different word than `present` //! committed:1 //! complete=0 //! InternalEntry := //! present=0 //! deleted:1 //! internal=0 //! old_page:page_bits //! saved_erase_count:erase_bits //! Padding(word) //! Padding(X) := 1* until X-aligned //! ``` //! //! For bit flags, a value of 0 means true and a value of 1 means false. So when erased, bits are //! false. They can be set to true by writing 0. //! //! The `Entry` rule is for user entries and the `InternalEntry` rule is for internal entries of the //! store. Currently, there is only one kind of internal entry: an entry to erase the page being //! compacted. //! //! The `Header` and `Footer` rules are computed from the `Info` rule. An entry could simply be the //! concatenation of internal metadata and the user data. However, to optimize the size in flash, we //! splice the user data in the middle of the metadata. The reason is that we can only write twice //! the same word and for replace entries we need to write the deleted bit and the committed bit //! independently. Also, this is important for the complete bit to be the last written bit (since //! slices are written to flash from low to high addresses). Here is the representation of a //! specific replace entry for a specific configuration: //! //! ```text //! page_bits=6 //! byte_bits=9 //! tag_bits=5 //! //! byte.bit name //! 0.0 present //! 0.1 deleted //! 0.2 internal //! 0.3 replace //! 0.4 sensitive //! 0.5 length (9 bits) //! 1.6 tag (least significant 2 bits out of 5) //! (the header ends at the first byte boundary after `length`) //! 2.0 <user data> (2 bytes in this example) //! (the footer starts immediately after the user data) //! 4.0 tag (most significant 3 bits out of 5) //! 4.3 replace_page (6 bits) //! 5.1 replace_byte (9 bits) //! 6.2 padding (make sure the 2 properties below hold) //! 7.6 committed //! 7.7 complete (on a different word than `present`) //! 8.0 <end> (word-aligned) //! ``` //! //! The store should always contain at least one blank page, so that it is always possible to //! compact. // TODO(cretin): We don't need inner padding for insert entries. The store format can be: // InsertEntry | ReplaceEntry | InternalEntry (maybe rename to EraseEntry) // InsertEntry padding is until `complete` is the last bit of a word. // ReplaceEntry padding is until `complete` is the last bit of a different word than `present`. // TODO(cretin): Add checksum (may play the same role as the completed bit) and recovery strategy? // TODO(cretin): Add corruption (deterministic but undetermined reads) to fuzzing. // TODO(cretin): Add more complex transactions? (this does not seem necessary yet) // TODO(cretin): Add possibility to shred an entry (force compact page after delete)? mod bitfield; mod format; use self::format::{Format, IsReplace}; #[cfg(feature = "std")] use super::BufferStorage; use super::{Index, Storage}; use alloc::collections::BTreeMap; use alloc::vec::Vec; /// Configures a store. pub trait StoreConfig { /// How entries are keyed. /// /// To disable keys, this may be defined to `()` or even better a custom empty enum. type Key: Ord; /// Number of entry tags. /// /// All tags must be smaller than this value. /// /// To disable tags, this function should return `1`. The only valid tag would then be `0`. fn num_tags(&self) -> usize; /// Specifies the set of keys of an entry. /// /// If keys are not used, this function can immediately return. Otherwise, it should call /// `associate_key` for each key that should be associated to `entry`. fn keys(&self, entry: StoreEntry, associate_key: impl FnMut(Self::Key)); } /// Errors returned by store operations. #[derive(Debug, PartialEq, Eq)] pub enum StoreError { /// The operation could not proceed because the store is full. StoreFull, /// The operation could not proceed because the provided tag is invalid. InvalidTag, /// The operation could not proceed because the preconditions do not hold. InvalidPrecondition, } /// The position of an entry in the store. #[cfg_attr(feature = "std", derive(Debug))] #[derive(Copy, Clone)] pub struct StoreIndex { /// The index of this entry in the storage. index: Index, /// The generation at which this index is valid. /// /// See the documentation of the field with the same name in the `Store` struct. generation: usize, } /// A user entry. #[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))] #[derive(Copy, Clone)] pub struct StoreEntry<'a> { /// The tag of the entry. /// /// Must be smaller than the configured number of tags. pub tag: usize, /// The data of the entry. pub data: &'a [u8], /// Whether the data is sensitive. /// /// Sensitive data is overwritten with zeroes when the entry is deleted. pub sensitive: bool, } /// Implements a configurable multi-set on top of any storage. pub struct Store<S: Storage, C: StoreConfig> { storage: S, config: C, format: Format, /// The index of the blank page reserved for compaction. blank_page: usize, /// Counts the number of compactions since the store creation. /// /// A `StoreIndex` is valid only if they originate from the same generation. This is checked by /// operations that take a `StoreIndex` as argument. generation: usize, } impl<S: Storage, C: StoreConfig> Store<S, C> { /// Creates a new store. /// /// Initializes the storage if it is fresh (filled with `0xff`). Rolls-back or completes an /// operation if the store was powered off in the middle of that operation. In other words, /// operations are atomic. /// /// # Errors /// /// Returns `None` if `storage` and/or `config` are not supported. pub fn new(storage: S, config: C) -> Option<Store<S, C>> { let format = Format::new(&storage, &config)?; let blank_page = format.num_pages; let mut store = Store { storage, config, format, blank_page, generation: 0, }; // Finish any ongoing page compaction. store.recover_compact_page(); // Finish or roll-back any other entry-level operations. store.recover_entry_operations(); // Initialize uninitialized pages. store.initialize_storage(); Some(store) } /// Iterates over all entries in the store. pub fn iter(&self) -> impl Iterator<Item = (StoreIndex, StoreEntry)> { Iter::new(self).filter_map(move |(index, entry)| { if self.format.is_alive(entry) { Some(( StoreIndex { index, generation: self.generation, }, StoreEntry { tag: self.format.get_tag(entry), data: self.format.get_data(entry), sensitive: self.format.is_sensitive(entry), }, )) } else { None } }) } /// Iterates over all entries matching a key in the store. pub fn find_all<'a>( &'a self, key: &'a C::Key, ) -> impl Iterator<Item = (StoreIndex, StoreEntry)> + 'a { self.iter().filter(move |&(_, entry)| { let mut has_match = false; self.config.keys(entry, |k| has_match |= key == &k); has_match }) } /// Returns the first entry matching a key in the store. /// /// This is a convenience function for when at most one entry should match the key. /// /// # Panics /// /// In debug mode, panics if more than one entry matches the key. pub fn find_one<'a>(&'a self, key: &'a C::Key) -> Option<(StoreIndex, StoreEntry<'a>)> { let mut iter = self.find_all(key); let first = iter.next()?; let has_only_one_element = iter.next().is_none(); debug_assert!(has_only_one_element); Some(first) } /// Deletes an entry from the store. pub fn delete(&mut self, index: StoreIndex) -> Result<(), StoreError> { if self.generation != index.generation { return Err(StoreError::InvalidPrecondition); } self.delete_index(index.index); Ok(()) } /// Replaces an entry with another with the same tag in the store. /// /// This operation (like others) is atomic. If it returns successfully, then the old entry is /// deleted and the new is inserted. If it fails, the old entry is not deleted and the new entry /// is not inserted. If power is lost during the operation, during next startup, the operation /// is either rolled-back (like in case of failure) or completed (like in case of success). /// /// # Errors /// /// Returns: /// - `StoreFull` if the new entry does not fit in the store. /// - `InvalidTag` if the tag of the new entry is not smaller than the configured number of /// tags. pub fn replace(&mut self, old: StoreIndex, new: StoreEntry) -> Result<(), StoreError> { if self.generation != old.generation { return Err(StoreError::InvalidPrecondition); } self.format.validate_entry(new)?; let mut old_index = old.index; // Find a slot. let entry_len = self.replace_len(new.sensitive, new.data.len()); let index = self.find_slot_for_write(entry_len, Some(&mut old_index))?; // Build a new entry replacing the old one. let entry = self.format.build_entry(Some(old_index), new); debug_assert_eq!(entry.len(), entry_len); // Write the new entry. self.write_entry(index, &entry); // Commit the new entry, which both deletes the old entry and commits the new one. self.commit_index(index); Ok(()) } /// Inserts an entry in the store. /// /// # Errors /// /// Returns: /// - `StoreFull` if the new entry does not fit in the store. /// - `InvalidTag` if the tag of the new entry is not smaller than the configured number of /// tags. pub fn insert(&mut self, entry: StoreEntry) -> Result<(), StoreError> { self.format.validate_entry(entry)?; // Build entry. let entry = self.format.build_entry(None, entry); // Find a slot. let index = self.find_slot_for_write(entry.len(), None)?; // Write entry. self.write_entry(index, &entry); Ok(()) } /// Returns the byte cost of a replace operation. /// /// Computes the length in bytes that would be used in the storage if a replace operation is /// executed provided the data of the new entry has `length` bytes and whether this data is /// sensitive. pub fn replace_len(&self, sensitive: bool, length: usize) -> usize { self.format .entry_size(IsReplace::Replace, sensitive, length) } /// Returns the byte cost of an insert operation. /// /// Computes the length in bytes that would be used in the storage if an insert operation is /// executed provided the data of the inserted entry has `length` bytes and whether this data is /// sensitive. pub fn insert_len(&self, sensitive: bool, length: usize) -> usize { self.format.entry_size(IsReplace::Insert, sensitive, length) } /// Returns the erase count of all pages. /// /// The value at index `page` of the result is the number of times page `page` was erased. This /// number is an underestimate in case power was lost when this page was erased. pub fn compaction_info(&self) -> Vec<usize> { let mut info = Vec::with_capacity(self.format.num_pages); for page in 0..self.format.num_pages { let (page_header, _) = self.read_page_header(page); let erase_count = self.format.get_erase_count(page_header); info.push(erase_count); } info } /// Completes any ongoing page compaction. fn recover_compact_page(&mut self) { for page in 0..self.format.num_pages { let (page_header, _) = self.read_page_header(page); if self.format.is_compacting(page_header) { let new_page = self.format.get_new_page(page_header); self.compact_page(page, new_page); } } } /// Rolls-back or completes any ongoing operation. fn recover_entry_operations(&mut self) { for page in 0..self.format.num_pages { let (page_header, mut index) = self.read_page_header(page); if !self.format.is_initialized(page_header) { // Skip uninitialized pages. continue; } while index.byte < self.format.page_size { let entry_index = index; let entry = self.read_entry(index); index.byte += entry.len(); if !self.format.is_present(entry) { // Reached the end of the page. } else if self.format.is_deleted(entry) { // Wipe sensitive data if needed. self.wipe_sensitive_data(entry_index); } else if self.format.is_internal(entry) { // Finish page compaction. self.erase_page(entry_index); } else if !self.format.is_complete(entry) { // Roll-back incomplete operations. self.delete_index(entry_index); } else if !self.format.is_committed(entry) { // Finish complete but uncommitted operations. self.commit_index(entry_index) } } } } /// Initializes uninitialized pages. fn initialize_storage(&mut self) { for page in 0..self.format.num_pages { let (header, index) = self.read_page_header(page); if self.format.is_initialized(header) { // Update blank page. let first_entry = self.read_entry(index); if !self.format.is_present(first_entry) { self.blank_page = page; } } else { // We set the erase count to zero the very first time we initialize a page. self.initialize_page(page, 0); } } debug_assert!(self.blank_page != self.format.num_pages); } /// Marks an entry as deleted. /// /// The provided index must point to the beginning of an entry. fn delete_index(&mut self, index: Index) { self.update_word(index, |format, word| format.set_deleted(word)); self.wipe_sensitive_data(index); } /// Wipes the data of a sensitive entry. /// /// If the entry at the provided index is sensitive, overwrites the data with zeroes. Otherwise, /// does nothing. fn wipe_sensitive_data(&mut self, mut index: Index) { let entry = self.read_entry(index); debug_assert!(self.format.is_present(entry)); debug_assert!(self.format.is_deleted(entry)); if self.format.is_internal(entry) || !self.format.is_sensitive(entry) { // No need to wipe the data. return; } let gap = self.format.entry_gap(entry); let data = gap.slice(entry); if data.iter().all(|&byte| byte == 0x00) { // The data is already wiped. return; } index.byte += gap.start; self.storage .write_slice(index, &vec![0; gap.length]) .unwrap(); } /// Finds a page with enough free space. /// /// Returns an index to the free space of a page which can hold an entry of `length` bytes. If /// necessary, pages may be compacted to free space. In that case, if provided, the `old_index` /// is updated according to compaction. fn find_slot_for_write( &mut self, length: usize, mut old_index: Option<&mut Index>, ) -> Result<Index, StoreError> { loop { if let Some(index) = self.choose_slot_for_write(length) { return Ok(index); } match self.choose_page_for_compact() { None => return Err(StoreError::StoreFull), Some(page) => { let blank_page = self.blank_page; // Compact the chosen page and update the old index to point to the entry in the // new page if it happened to be in the old page. This is essentially a way to // avoid index invalidation due to compaction. let map = self.compact_page(page, blank_page); if let Some(old_index) = &mut old_index { map_index(page, blank_page, &map, old_index); } } } } } /// Returns whether a page has enough free space. /// /// Returns an index to the free space of a page with smallest free space that may hold `length` /// bytes. fn choose_slot_for_write(&self, length: usize) -> Option<Index> { Iter::new(self) .filter(|(index, entry)| { index.page != self.blank_page && !self.format.is_present(entry) && length <= entry.len() }) .min_by_key(|(_, entry)| entry.len()) .map(|(index, _)| index) } /// Returns the page that should be compacted. fn choose_page_for_compact(&self) -> Option<usize> { // TODO(cretin): This could be optimized by using some cost function depending on: // - the erase count // - the length of the free space // - the length of the alive entries // We want to minimize this cost. We could also take into account the length of the entry we // want to write to bound the number of compaction before failing with StoreFull. // // We should also make sure that all pages (including if they have no deleted entries and no // free space) are eventually compacted (ideally to a heavily used page) to benefit from the // low erase count of those pages. (0..self.format.num_pages) .map(|page| (page, self.page_info(page))) .filter(|&(page, ref info)| { page != self.blank_page && info.erase_count < self.format.max_page_erases && info.deleted_length > self.format.internal_entry_size() }) .min_by(|(_, lhs_info), (_, rhs_info)| lhs_info.compare_for_compaction(rhs_info)) .map(|(page, _)| page) } fn page_info(&self, page: usize) -> PageInfo { let (page_header, mut index) = self.read_page_header(page); let mut info = PageInfo { erase_count: self.format.get_erase_count(page_header), deleted_length: 0, free_length: 0, }; while index.byte < self.format.page_size { let entry = self.read_entry(index); index.byte += entry.len(); if !self.format.is_present(entry) { debug_assert_eq!(info.free_length, 0); info.free_length = entry.len(); } else if self.format.is_deleted(entry) { info.deleted_length += entry.len(); } } debug_assert_eq!(index.page, page); info } fn read_slice(&self, index: Index, length: usize) -> &[u8] { self.storage.read_slice(index, length).unwrap() } /// Reads an entry (with header and footer) at a given index. /// /// If no entry is present, returns the free space up to the end of the page. fn read_entry(&self, index: Index) -> &[u8] { let first_byte = self.read_slice(index, 1); let max_length = self.format.page_size - index.byte; let mut length = if !self.format.is_present(first_byte) { max_length } else if self.format.is_internal(first_byte) { self.format.internal_entry_size() } else { // We don't know if the entry is sensitive or not, but it doesn't matter here. We just // need to read the replace, sensitive, and length fields. let header = self.read_slice(index, self.format.header_size(false)); let replace = self.format.is_replace(header); let sensitive = self.format.is_sensitive(header); let length = self.format.get_length(header); self.format.entry_size(replace, sensitive, length) }; // Truncate the length to fit the page. This can only happen in case of corruption or // partial writes. length = core::cmp::min(length, max_length); self.read_slice(index, length) } /// Reads a page header. /// /// Also returns the index after the page header. fn read_page_header(&self, page: usize) -> (&[u8], Index) { let mut index = Index { page, byte: 0 }; let page_header = self.read_slice(index, self.format.page_header_size()); index.byte += page_header.len(); (page_header, index) } /// Updates a word at a given index. /// /// The `update` function is called with the word at `index`. The input value is the current /// value of the word. The output value is the value that will be written. It should only change /// bits from 1 to 0. fn update_word(&mut self, index: Index, update: impl FnOnce(&Format, &mut [u8])) { let word_size = self.format.word_size; let mut word = self.read_slice(index, word_size).to_vec(); update(&self.format, &mut word); self.storage.write_slice(index, &word).unwrap(); } fn write_entry(&mut self, index: Index, entry: &[u8]) { self.storage.write_slice(index, entry).unwrap(); } /// Initializes a page by writing the page header. /// /// If the page is not erased, it is first erased. fn initialize_page(&mut self, page: usize, erase_count: usize) { let index = Index { page, byte: 0 }; let page = self.read_slice(index, self.format.page_size); if !page.iter().all(|&byte| byte == 0xff) { self.storage.erase_page(index.page).unwrap(); } self.update_word(index, |format, header| { format.set_initialized(header); format.set_erase_count(header, erase_count); }); self.blank_page = index.page; } /// Commits a replace entry. /// /// Deletes the old entry and commits the new entry. fn commit_index(&mut self, mut index: Index) { let entry = self.read_entry(index); index.byte += entry.len(); let word_size = self.format.word_size; debug_assert!(entry.len() >= 2 * word_size); match self.format.is_replace(entry) { IsReplace::Replace => { let delete_index = self.format.get_replace_index(entry); self.delete_index(delete_index); } IsReplace::Insert => debug_assert!(false), }; index.byte -= word_size; self.update_word(index, |format, word| format.set_committed(word)); } /// Compacts a page to an other. /// /// Returns the mapping from the alive entries in the old page to their index in the new page. fn compact_page(&mut self, old_page: usize, new_page: usize) -> BTreeMap<usize, usize> { // Write the old page as being compacted to the new page. let mut erase_count = 0; self.update_word( Index { page: old_page, byte: 0, }, |format, header| { erase_count = format.get_erase_count(header); format.set_compacting(header); format.set_new_page(header, new_page); }, ); // Copy alive entries from the old page to the new page. let page_header_size = self.format.page_header_size(); let mut old_index = Index { page: old_page, byte: page_header_size, }; let mut new_index = Index { page: new_page, byte: page_header_size, }; let mut map = BTreeMap::new(); while old_index.byte < self.format.page_size { let old_entry = self.read_entry(old_index); let old_entry_index = old_index.byte; old_index.byte += old_entry.len(); if !self.format.is_alive(old_entry) { continue; } let previous_mapping = map.insert(old_entry_index, new_index.byte); debug_assert!(previous_mapping.is_none()); // We need to copy the old entry because it is in the storage and we are going to write // to the storage. Rust cannot tell that both entries don't overlap. let old_entry = old_entry.to_vec(); self.write_entry(new_index, &old_entry); new_index.byte += old_entry.len(); } // Save the old page index and erase count to the new page. let erase_index = new_index; let erase_entry = self.format.build_erase_entry(old_page, erase_count); self.write_entry(new_index, &erase_entry); // Erase the page. self.erase_page(erase_index); // Increase generation. self.generation += 1; map } /// Commits an internal entry. /// /// The only kind of internal entry is to erase a page, which first erases the page, then /// initializes it with the saved erase count, and finally deletes the internal entry. fn erase_page(&mut self, erase_index: Index) { let erase_entry = self.read_entry(erase_index); debug_assert!(self.format.is_present(erase_entry)); debug_assert!(!self.format.is_deleted(erase_entry)); debug_assert!(self.format.is_internal(erase_entry)); let old_page = self.format.get_old_page(erase_entry); let erase_count = self.format.get_saved_erase_count(erase_entry) + 1; // Erase the page. self.storage.erase_page(old_page).unwrap(); // Initialize the page. self.initialize_page(old_page, erase_count); // Delete the internal entry. self.delete_index(erase_index); } } // Those functions are not meant for production. #[cfg(feature = "std")] impl<C: StoreConfig> Store<BufferStorage, C> { /// Takes a snapshot of the storage after a given amount of word operations. pub fn arm_snapshot(&mut self, delay: usize) { self.storage.arm_snapshot(delay); } /// Unarms and returns the snapshot or the delay remaining. pub fn get_snapshot(&mut self) -> Result<Box<[u8]>, usize> { self.storage.get_snapshot() } /// Takes a snapshot of the storage. pub fn take_snapshot(&self) -> Box<[u8]> { self.storage.take_snapshot() } /// Returns the storage. pub fn get_storage(self) -> Box<[u8]> { self.storage.get_storage() } /// Erases and initializes a page with a given erase count. pub fn set_erase_count(&mut self, page: usize, erase_count: usize) { self.initialize_page(page, erase_count); } /// Returns whether all deleted sensitive entries have been wiped. pub fn deleted_entries_are_wiped(&self) -> bool { for (_, entry) in Iter::new(self) { if !self.format.is_present(entry) || !self.format.is_deleted(entry) || self.format.is_internal(entry) || !self.format.is_sensitive(entry) { continue; } let gap = self.format.entry_gap(entry); let data = gap.slice(entry); if !data.iter().all(|&byte| byte == 0x00) { return false; } } true } } /// Maps an index from an old page to a new page if needed. fn map_index(old_page: usize, new_page: usize, map: &BTreeMap<usize, usize>, index: &mut Index) { if index.page == old_page { index.page = new_page; index.byte = *map.get(&index.byte).unwrap(); } } /// Page information for compaction. struct PageInfo { /// How many times the page was erased. erase_count: usize, /// Cumulative length of deleted entries (including header and footer). deleted_length: usize, /// Length of the free space. free_length: usize, } impl PageInfo { /// Returns whether a page should be compacted before another. fn compare_for_compaction(&self, rhs: &PageInfo) -> core::cmp::Ordering { self.erase_count .cmp(&rhs.erase_count) .then(rhs.deleted_length.cmp(&self.deleted_length)) .then(self.free_length.cmp(&rhs.free_length)) } } /// Iterates over all entries (including free space) of a store. struct Iter<'a, S: Storage, C: StoreConfig> { store: &'a Store<S, C>, index: Index, } impl<'a, S: Storage, C: StoreConfig> Iter<'a, S, C> { fn new(store: &'a Store<S, C>) -> Iter<'a, S, C> { let index = Index { page: 0, byte: store.format.page_header_size(), }; Iter { store, index } } } impl<'a, S: Storage, C: StoreConfig> Iterator for Iter<'a, S, C> { type Item = (Index, &'a [u8]); fn next(&mut self) -> Option<(Index, &'a [u8])> { if self.index.byte == self.store.format.page_size { self.index.page += 1; self.index.byte = self.store.format.page_header_size(); } if self.index.page == self.store.format.num_pages { return None; } let index = self.index; let entry = self.store.read_entry(self.index); self.index.byte += entry.len(); Some((index, entry)) } } #[cfg(test)] mod tests { use super::super::{BufferOptions, BufferStorage}; use super::*; struct Config; const WORD_SIZE: usize = 4; const PAGE_SIZE: usize = 8 * WORD_SIZE; const NUM_PAGES: usize = 3; impl StoreConfig for Config { type Key = u8; fn num_tags(&self) -> usize { 1 } fn keys(&self, entry: StoreEntry, mut add: impl FnMut(u8)) { assert_eq!(entry.tag, 0); if !entry.data.is_empty() { add(entry.data[0]); } } } fn new_buffer(storage: Box<[u8]>) -> BufferStorage { let options = BufferOptions { word_size: WORD_SIZE, page_size: PAGE_SIZE, max_word_writes: 2, max_page_erases: 2, strict_write: true, }; BufferStorage::new(storage, options) } fn new_store() -> Store<BufferStorage, Config> { let storage = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); Store::new(new_buffer(storage), Config).unwrap() } #[test] fn insert_ok() { let mut store = new_store(); assert_eq!(store.iter().count(), 0); let tag = 0; let key = 1; let data = &[key, 2]; let entry = StoreEntry { tag, data, sensitive: false, }; store.insert(entry).unwrap(); assert_eq!(store.iter().count(), 1); assert_eq!(store.find_one(&key).unwrap().1, entry); } #[test] fn insert_sensitive_ok() { let mut store = new_store(); let tag = 0; let key = 1; let data = &[key, 4]; let entry = StoreEntry { tag, data, sensitive: true, }; store.insert(entry).unwrap(); assert_eq!(store.iter().count(), 1); assert_eq!(store.find_one(&key).unwrap().1, entry); } #[test] fn delete_ok() { let mut store = new_store(); let tag = 0; let key = 1; let entry = StoreEntry { tag, data: &[key, 2], sensitive: false, }; store.insert(entry).unwrap(); assert_eq!(store.find_all(&key).count(), 1); let (index, _) = store.find_one(&key).unwrap(); store.delete(index).unwrap(); assert_eq!(store.find_all(&key).count(), 0); assert_eq!(store.iter().count(), 0); } #[test] fn delete_sensitive_ok() { let mut store = new_store(); let tag = 0; let key = 1; let entry = StoreEntry { tag, data: &[key, 2], sensitive: true, }; store.insert(entry).unwrap(); assert_eq!(store.find_all(&key).count(), 1); let (index, _) = store.find_one(&key).unwrap(); store.delete(index).unwrap(); assert_eq!(store.find_all(&key).count(), 0); assert_eq!(store.iter().count(), 0); assert!(store.deleted_entries_are_wiped()); } #[test] fn insert_until_full() { let mut store = new_store(); let tag = 0; let mut key = 0; while store .insert(StoreEntry { tag, data: &[key, 0], sensitive: false, }) .is_ok() { key += 1; } assert!(key > 0); } #[test] fn compact_ok() { let mut store = new_store(); let tag = 0; let mut key = 0; while store .insert(StoreEntry { tag, data: &[key, 0], sensitive: false, }) .is_ok() { key += 1; } let (index, _) = store.find_one(&0).unwrap(); store.delete(index).unwrap(); store .insert(StoreEntry { tag: 0, data: &[key, 0], sensitive: false, }) .unwrap(); for k in 1..=key { assert_eq!(store.find_all(&k).count(), 1); } } #[test] fn reboot_ok() { let mut store = new_store(); let tag = 0; let key = 1; let data = &[key, 2]; let entry = StoreEntry { tag, data, sensitive: false, }; store.insert(entry).unwrap(); // Reboot the store. let store = store.get_storage(); let store = Store::new(new_buffer(store), Config).unwrap(); assert_eq!(store.iter().count(), 1); assert_eq!(store.find_one(&key).unwrap().1, entry); } #[test] fn replace_atomic() { let tag = 0; let key = 1; let old_entry = StoreEntry { tag, data: &[key, 2, 3, 4, 5, 6], sensitive: false, }; let new_entry = StoreEntry { tag, data: &[key, 7, 8, 9], sensitive: false, }; let mut delay = 0; loop { let mut store = new_store(); store.insert(old_entry).unwrap(); store.arm_snapshot(delay); let (index, _) = store.find_one(&key).unwrap(); store.replace(index, new_entry).unwrap(); let (complete, store) = match store.get_snapshot() { Err(_) => (true, store.get_storage()), Ok(store) => (false, store), }; let store = Store::new(new_buffer(store), Config).unwrap(); assert_eq!(store.iter().count(), 1); assert_eq!(store.find_all(&key).count(), 1); let (_, cur_entry) = store.find_one(&key).unwrap(); assert!((cur_entry == old_entry && !complete) || cur_entry == new_entry); if complete { break; } delay += 1; } } #[test] fn compact_atomic() { let tag = 0; let mut delay = 0; loop { let mut store = new_store(); let mut key = 0; while store .insert(StoreEntry { tag, data: &[key, 0], sensitive: false, }) .is_ok() { key += 1; } let (index, _) = store.find_one(&0).unwrap(); store.delete(index).unwrap(); let (index, _) = store.find_one(&1).unwrap(); store.arm_snapshot(delay); store .replace( index, StoreEntry { tag, data: &[1, 1], sensitive: false, }, ) .unwrap(); let (complete, store) = match store.get_snapshot() { Err(_) => (true, store.get_storage()), Ok(store) => (false, store), }; let store = Store::new(new_buffer(store), Config).unwrap(); assert_eq!(store.iter().count(), key as usize - 1); for k in 2..key { assert_eq!(store.find_all(&k).count(), 1); assert_eq!( store.find_one(&k).unwrap().1, StoreEntry { tag, data: &[k, 0], sensitive: false, } ); } assert_eq!(store.find_all(&1).count(), 1); let (_, entry) = store.find_one(&1).unwrap(); assert_eq!(entry.tag, tag); assert!((entry.data == [1, 0] && !complete) || entry.data == [1, 1]); if complete { break; } delay += 1; } } #[test] fn invalid_tag() { let mut store = new_store(); let entry = StoreEntry { tag: 1, data: &[], sensitive: false, }; assert_eq!(store.insert(entry), Err(StoreError::InvalidTag)); } #[test] fn invalid_length() { let mut store = new_store(); let entry = StoreEntry { tag: 0, data: &[0; PAGE_SIZE], sensitive: false, }; assert_eq!(store.insert(entry), Err(StoreError::StoreFull)); } }