├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── LICENCE-APACHE ├── LICENCE-MIT ├── README.md ├── example ├── .cargo │ └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── memory.x └── src │ └── main.rs ├── fuzz.sh ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── map.rs │ ├── queue.rs │ └── random-flash.rs └── src ├── alloc_impl.rs ├── arrayvec_impl.rs ├── cache ├── key_pointers.rs ├── mod.rs ├── page_pointers.rs ├── page_states.rs └── tests.rs ├── heapless_impl.rs ├── item.rs ├── lib.rs ├── map.rs ├── mock_flash.rs └── queue.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | # Run on any PR 8 | pull_request: 9 | 10 | jobs: 11 | fmt: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: dtolnay/rust-toolchain@stable 16 | - run: cargo fmt --all -- --check 17 | 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: dtolnay/rust-toolchain@stable 23 | - run: cargo test --features arrayvec 24 | 25 | clippy: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: dtolnay/rust-toolchain@stable 30 | - run: cargo clippy -- -D warnings 31 | 32 | fuzz: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: dtolnay/rust-toolchain@stable 37 | - uses: cargo-bins/cargo-binstall@main 38 | 39 | - name: Install cargo fuzz 40 | run: cargo binstall cargo-fuzz --no-confirm --target x86_64-unknown-linux-gnu 41 | 42 | - name: Smoke-test fuzz targets 43 | run: | 44 | cargo fuzz build --sanitizer none 45 | for target in $(cargo fuzz list) ; do 46 | cargo fuzz run --sanitizer none $target -- -max_total_time=10 47 | done 48 | 49 | binary-size: 50 | runs-on: ubuntu-latest 51 | permissions: 52 | actions: read 53 | pull-requests: write 54 | steps: 55 | - uses: dtolnay/rust-toolchain@nightly 56 | - uses: actions/cache@v3 57 | id: cache-cargo 58 | with: 59 | path: | 60 | ~/.cargo/bin/ 61 | ~/.cargo/registry/index/ 62 | ~/.cargo/registry/cache/ 63 | ~/.cargo/git/db/ 64 | ./example/target/ 65 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 66 | 67 | - run: rustup target add thumbv7em-none-eabihf 68 | - run: rustup component add rust-src llvm-tools 69 | - if: steps.cache-cargo.outputs.cache-hit != 'true' 70 | run: cargo install cargo-binutils 71 | 72 | - name: Check out the repo with the full git history 73 | uses: actions/checkout@v3 74 | with: 75 | fetch-depth: '0' 76 | - name: Build new binary 77 | working-directory: ./example 78 | run: | 79 | echo 'RESULT<> $GITHUB_OUTPUT 80 | cargo size --release >> $GITHUB_OUTPUT 81 | echo 'EOF' >> $GITHUB_OUTPUT 82 | id: new-size 83 | - name: Save binary 84 | run: | 85 | mv ./example/target/thumbv7em-none-eabihf/release/example ./example/target/thumbv7em-none-eabihf/release/original.elf 86 | - name: If it's a PR checkout the base commit 87 | if: ${{ github.event.pull_request }} 88 | run: git checkout ${{ github.event.pull_request.base.sha }} 89 | - name: Rebuild with the base commit 90 | if: ${{ github.event.pull_request }} 91 | working-directory: ./example 92 | run: cargo build --release 93 | - name: Run Bloaty to compare both output files 94 | if: ${{ github.event.pull_request }} 95 | id: bloaty-comparison 96 | uses: carlosperate/bloaty-action@v1 97 | with: 98 | bloaty-args: ./example/target/thumbv7em-none-eabihf/release/original.elf -- ./example/target/thumbv7em-none-eabihf/release/example 99 | output-to-summary: true 100 | - name: Add a PR comment with the bloaty diff 101 | if: ${{ github.event.pull_request }} 102 | continue-on-error: true 103 | uses: actions/github-script@v7 104 | with: 105 | script: | 106 | github.rest.issues.createComment({ 107 | issue_number: context.issue.number, 108 | owner: context.repo.owner, 109 | repo: context.repo.repo, 110 | body: `## PR build size\n \`\`\`\n${{ join(steps.new-size.outputs.*, '\n') }}\n\`\`\`\n ### Diff\n\`\`\`\n${{ steps.bloaty-comparison.outputs.bloaty-output-encoded }}\`\`\`\n` 111 | }) 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "Cargo.toml", 4 | "fuzz/Cargo.toml", 5 | "example/Cargo.toml" 6 | ] 7 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | (DD-MM-YY) 4 | 5 | ## Unreleased 6 | 7 | - Move to edition 2024 8 | - Add a const assert to make sure flash `WRITE_SIZE` and `READ_SIZE` are multiples of each other 9 | 10 | ## 4.0.1 17-01-25 11 | 12 | - Swapped two checks so some fringe situations now rightly return corrupted data errors instead of wrongly user errors 13 | - Added random data input fuzzing 14 | - You're still meant to run this crate on erased flash! 15 | 16 | ## 4.0.0 28-12-24 17 | 18 | - Added item iterator for map. 19 | - *Breaking:* Added `Value` impls for `bool`, `Option`, and `[T: Value; N]`. 20 | *This can break existing code because it changes type inference, be mindfull of that!* 21 | 22 | ## 3.0.1 25-07-24 23 | 24 | - Add `defmt` attributes to cache types. 25 | 26 | ## 3.0.0 17-07-24 27 | 28 | - *Breaking:* Map keys are now always passed by reference. This avoids extra cloning and memory use for bigger keys. 29 | - Added `space_left` function for queue 30 | - Added a new `map::remove_all_items()` API to remove all stored items in flash. 31 | 32 | This release is 'disk'-compatible with 2.0 33 | 34 | ## 2.0.2 07-05-24 35 | 36 | - Added check for too big items that won't ever fit in flash so it returns a good clear error. 37 | 38 | ## 2.0.1 06-05-24 39 | 40 | - Implemented the `get_len` function for all built-in key types 41 | 42 | ## 2.0.0 06-05-24 43 | 44 | - *Breaking:* Made the cache API a bit more strict. Caches now always have to be passed as a mutable reference. 45 | The API before would lead to a lot of extra unncesessary binary size. 46 | - *Breaking:* Removed the `StorageItem` trait in favor of two separate `Key` and `Value` traits. This helps cut 47 | binary size and is closer to what users of the map APIs were expecting. 48 | - *Breaking:* The error type is no longer generic over the Item error. That error variant has been renamed `SerializationError` 49 | and carries a predefined error subtype. 50 | - Added `erase_all` function as a helper to erase the flash in a region. 51 | - *Breaking:* Changed the way that queue iteration works. Now there's an `iter` function instead of two separate `peek_many` and `pop_many` functions. The new iter returns an entry from which you can get the data that was just peeked. If you want to pop it, then call the pop function on the entry. 52 | - Added `arrayvec` feature that when activated impls the `Key` trait for `ArrayVec` and `ArrayString`. 53 | 54 | ## 1.0.0 01-03-24 55 | 56 | - *Breaking:* Corruption repair is automatic now! The repair functions have been made private. 57 | - *Breaking:* There's now only one error type. `MapError` has been retired and the main error type now carries 58 | the Item error as well. The queue uses `Infallable` as the item error type. 59 | - *Breaking:* The feature `defmt` has been renamed `defmt-03` to avoid a future breaking change. 60 | - Added `std` feature that implements the error trait for the error enum 61 | - This release is flash compatible downto version 0.7 62 | 63 | ## 0.9.1 13-02-24 64 | 65 | - Added `remove_item` to map 66 | 67 | ## 0.9.0 11-02-24 68 | 69 | - *Breaking:* Storage item key must now also be clone 70 | - Added KeyPointerCache which significantly helps out the map 71 | 72 | ## 0.8.1 07-02-24 73 | 74 | - Added new PagePointerCache that caches more than the PageStateCache. See the readme for more details. 75 | 76 | ## 0.8.0 05-12-24 77 | 78 | - *Breaking:* The item to store is now passed by reference to Map `store_item` 79 | - *Breaking:* Added cache options to the functions to speed up reading the state of the flash. 80 | To retain the old behaviour you can pass the `NoCache` type as the cache parameter. 81 | - Removed defmt logging since that wasn't being maintained. The format impl for the errors remain. 82 | 83 | ## 0.7.0 10-01-24 84 | 85 | - *Breaking:* Data CRC has been upgraded to 32-bit from 16-bit. Turns out 16-bit has too many collisions. 86 | This increases the item header size from 6 to 8. The CRC was also moved to the front of the header to 87 | aid with shutdown/cancellation issues. 88 | - When the state is corrupted, many issues can now be repaired with the repair functions in the map and queue modules 89 | - Made changes to the entire crate to better survive shutoffs 90 | - *Breaking:* Convert API to async first supporting the traits from embedded-storage-async. Flash 91 | drivers supporting `sequential-storage` can be wrapped using 92 | [BlockingAsync](https://docs.embassy.dev/embassy-embedded-hal/git/default/adapter/struct.BlockingAsync.html), and a 93 | simple [blocking executor](https://docs.rs/futures/0.3.30/futures/executor/fn.block_on.html) can be used to call the 94 | API from a non-async function. 95 | 96 | ## 0.6.2 - 22-12-23 97 | 98 | - Small bug fixes and refactorings including an off-by-one error. Found with added fuzzing from ([#13](https://github.com/tweedegolf/sequential-storage/pull/13)) 99 | 100 | ## 0.6.1 - 16-12-23 101 | 102 | - Added queue peek_many and pop_many ([#12](https://github.com/tweedegolf/sequential-storage/pull/12)) 103 | 104 | ## 0.6.0 - 21-11-23 105 | 106 | - *Breaking:* Internal overhaul of the code. Both map and queue now use the same `item` module to store and read their data with. 107 | - *Breaking:* Map serialization is no longer done in a stack buffer, but in the buffer provided by the user. 108 | - *Breaking:* Map StorageItemError trait has been removed. 109 | - Added CRC protection of the user data. If user data is corrupted, it will now be skipped as if it wasn't stored. 110 | If you think it should be reported to the user, let me know and propose an API for that! 111 | - Read word size is no longer required to be 1. It can now be 1-32. 112 | 113 | ## 0.5.0 - 13-11-23 114 | 115 | - *Breaking:* Map `store_item` item no longer uses a ram buffer to temporarily store erased items in. 116 | Instead it keeps an extra open page so it can copy from one page to another page directly. 117 | This means the minimum page count for map is now 2. 118 | 119 | ## 0.4.2 - 13-11-23 120 | 121 | - Map no longer erases the flash when corrupted to self-recover. It now just returns an error so the user can choose what to do. 122 | 123 | ## 0.4.1 - 26-09-23 124 | 125 | - Flipped one of the error messages in `queue::pop` and `queue::peek` from `BufferTooBig` to `BufferTooSmall` because that's a lot clearer 126 | - Massive performance bug fixed for the queue. Before it had to read all pages from the start until the first pop or peek data was found. 127 | Now empty pages are erased which solves this issue. 128 | 129 | ## 0.4.0 - 04-07-23 130 | 131 | - Fixed the queue implementation for devices with a write size of 1 132 | - *Breaking:* The internal storage format for queue has changed, so is incompatible with existing stored memory. The max size has come down to 0x7FFE. 133 | 134 | ## 0.3.0 - 30-06-23 135 | 136 | - Added new queue implementation with `push`, `peek` and `pop` which requires multiwrite flash 137 | - *Breaking:* the map implementation now moved to its own module. You'll need to change your imports. 138 | 139 | ## 0.2.2 - 11-05-23 140 | 141 | - Optimized reading items from flash which reduces the amount of reads by ~30% for small items. 142 | 143 | ## 0.2.1 - 19-01-23 144 | 145 | - Added defmt behind a feature flag. When enabled, the error type implements Format 146 | 147 | ## 0.2.0 - 13-01-23 148 | 149 | - Fixed a scenario where an infinite recursion could lead to a stackoverflow. 150 | If there's no more space to fit all items, you'll now get an error instead. 151 | - Made the error non-exhaustive so that next time this update wouldn't be a breaking one. 152 | 153 | ## 0.1.0 - 12-01-23 154 | 155 | - Initial release 156 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sequential-storage" 3 | version = "4.0.1" 4 | edition = "2024" 5 | license = "MIT OR Apache-2.0" 6 | description = "A crate for storing data in flash with minimal erase cycles." 7 | homepage = "https://github.com/tweedegolf/sequential-storage" 8 | repository = "https://github.com/tweedegolf/sequential-storage" 9 | readme = "README.md" 10 | keywords = ["no_std", "embedded", "flash", "storage"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | embedded-storage-async = "0.4.1" 16 | defmt = { version = "0.3", optional = true } 17 | futures = { version = "0.3.30", features = ["executor"], optional = true } 18 | approx = { version = "0.5.1", optional = true } 19 | arrayvec = { version = "0.7.4", default-features = false, optional = true } 20 | heapless = { version = "0.8.0", optional = true } 21 | 22 | [dev-dependencies] 23 | approx = "0.5.1" 24 | futures = { version = "0.3.30", features = ["executor"] } 25 | futures-test = "0.3.30" 26 | 27 | [features] 28 | defmt-03 = ["dep:defmt"] 29 | std = [] 30 | # Enable the implementation of the map Key trait for ArrayVec and ArrayString 31 | arrayvec = ["dep:arrayvec"] 32 | alloc = [] 33 | heapless = ["dep:heapless"] 34 | _test = ["dep:futures", "dep:approx", "std", "arrayvec", "alloc", "heapless"] 35 | 36 | [lints.rust] 37 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing_repro)'] } 38 | -------------------------------------------------------------------------------- /LICENCE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENCE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dion Dokter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sequential-storage 2 | 3 | [![crates.io](https://img.shields.io/crates/v/sequential-storage.svg)](https://crates.io/crates/sequential-storage) [![Documentation](https://docs.rs/sequential-storage/badge.svg)](https://docs.rs/sequential-storage) 4 | 5 | A crate for storing data in flash with minimal erase cycles. 6 | 7 | There are two datastructures: 8 | 9 | - Map: A key-value pair store 10 | - Queue: A fifo store 11 | 12 | Each live in their own module. See the module documentation for more info and examples. 13 | 14 | ***Note:** Make sure not to mix the datastructures in flash!* 15 | You can't e.g. fetch a key-value item from a flash region where you pushed to the queue. 16 | 17 | To search for data, the crate first searches for the flash page that is likeliest to contain it and 18 | then performs a linear scan over the data, skipping data blocks where it can. 19 | 20 | ## Status 21 | 22 | This crate is used in production in multiple projects inside and outside of Tweede golf. 23 | 24 | The in-flash representation is not (yet?) stable. This too follows semver. 25 | 26 | - A breaking change to the in-flash representation will lead to a major version bump 27 | - A feature addition will lead to a minor version bump 28 | - This is always backward-compatible. So data created by e.g. `1.0.0` can be used by `1.1.0`. 29 | - This may not be forward-compatible. So data created by e.g. `1.0.1` may not be usable by `1.0.0`. 30 | - After 1.0, patch releases only fix bugs and don't change the in-flash representation 31 | 32 | For any update, consult the changelog to see what changed. Any externally noticable changes are recorded there. 33 | 34 | The `_test` feature of the crate is considered private. It and anything it enables is not covered by semver. 35 | 36 | The performance of any of the cache types is not covered by semver either. If you use this crate in a 37 | performance sensitive application, then make sure that everything remains working even when no cache is used. 38 | That way you've covered the worst-case execution time for that part of your application. 39 | 40 | A cache performance regression might be a bug though. Open an issue to discus your situation if you find a regression. 41 | 42 | ## MSRV 43 | 44 | This crate has no further guarantees other than being able to run on the latest stable compiler. 45 | Increasing the MSRV is not seen as a breaking change semver-wise. 46 | If you find yourself in trouble with this, feel free to open an issue. 47 | 48 | ## Example 49 | 50 | See the `map` and `queue` module level documentation for examples. 51 | 52 | ## Features 53 | 54 | - Key value datastore (Map) 55 | - User defined key and value types 56 | - Great for creating configs 57 | - Fifo queue datastore (Queue) 58 | - Store byte slices in memory 59 | - Great for caching data 60 | - Simple APIs: 61 | - Just call the function you think you need 62 | - No need to worry about anything else 63 | - Item header CRC protected 64 | - Item data CRC protected 65 | - Power-fail safe 66 | - The system is always fine or fully recoverable 67 | - Corrupted items are ignored 68 | - Optional caching to speed things up 69 | - Wear leveling 70 | - Pages are used cyclically, so all pages get erased an equal amount 71 | - Built on [`embedded-storage`](https://github.com/rust-embedded-community/embedded-storage) 72 | - This is the only required dependency 73 | 74 | If you're looking for an alternative with different tradeoffs, take a look at [ekv](https://github.com/embassy-rs/ekv). 75 | 76 | ***Note:** The crate uses futures for its operations. These futures write to flash. If a future is cancelled, this can lead* 77 | *to a corrupted flash state, so cancelling is at your own risc. If this happens, the state will be repaired.* 78 | *In any case, the thing you tried to store or erase might or might not have fully happened.* 79 | 80 | ### Corruption repair 81 | 82 | When corruption is found while an operation is going on, the crate will automatically try to repair it. 83 | Some corruption leads to unrecoverable data and sadly that cannot be repaired. 84 | However, the repair will make sure that the flash state is recovered so any next operation should succeed. 85 | 86 | If any function still returns the corrupted error, that means that a repair wasn't able to fix the state. 87 | In that case please open an issue! 88 | 89 | ### Caching 90 | 91 | There are various cache options that speed up the operations. 92 | By default (no cache) all state is stored in flash and the state has to be fully read every time. 93 | Instead, we can optionally store some state in ram. 94 | 95 | These numbers are taken from the test cases in the cache module: 96 | 97 | | Name | RAM bytes | Map # flash reads | Map flash bytes read | Queue # flash reads | Queue flash bytes read | 98 | | ---------------: | -------------------------------------------: | ----------------: | -------------------: | ------------------: | ---------------------: | 99 | | NoCache | 0 | 100% | 100% | 100% | 100% | 100 | | PageStateCache | 1 * num pages | 77% | 97% | 51% | 90% | 101 | | PagePointerCache | 9 * num pages | 70% | 89% | 35% | 61% | 102 | | KeyPointerCache | 9 * num pages + (sizeof(KEY) + 4) * num keys | 6.2% | 8.2% | - | - | 103 | 104 | #### Takeaways 105 | 106 | - PageStateCache 107 | - Mostly tackles number of reads 108 | - Very cheap in RAM, so easy win 109 | - PagePointerCache 110 | - Very efficient for the queue 111 | - Minimum cache level that makes a dent in the map 112 | - KeyPointerCache 113 | - Awesome savings! 114 | - Numbers are less good if there are more keys than the cache can store 115 | - Same as PagePointerCache when used for queue 116 | 117 | ## Inner workings 118 | 119 | To save on erase cycles, this crate only really appends data to the pages. Exactly how this is done depends 120 | on whether you use the map or the queue. 121 | 122 | To do all this there are two concepts: 123 | 124 | ### Page & page state 125 | 126 | The flash is divided up in multiple pages. 127 | A page can be in three states: 128 | 129 | 1. Open - This page is in the erased state 130 | 2. Partial open - This page has been written to, but is not full yet 131 | 3. Closed - This page has been fully written to 132 | 133 | The state of a page is encoded into the first and the last word of a page. 134 | If both words are `FF` (erased), then the page is open. 135 | If the first word is written with the marker, then the page is partial open. 136 | If both words are written, then the page is closed. 137 | 138 | ### Items 139 | 140 | All data is stored as an item. 141 | 142 | An item consists of a header containing the data length, a CRC for that length, a data CRC, and some data. 143 | An item is considered erased when its data CRC field is 0. 144 | 145 | *NOTE: This means the data itself is still stored on the flash when it's considered erased.* 146 | *Depending on your usecase, this might not be secure* 147 | 148 | The length is a u16, so any item cannot be longer than 0xFFFF or `page size - the item header (padded to word boundary) - page state (2 words)`. 149 | 150 | ### Inner workings for map 151 | 152 | The map stores every key-value as an item. Every new value is appended at the last partial open page 153 | or the first open after the last closed page. 154 | 155 | Once a page is full it will be closed and the next page will need to store the item. 156 | However, we need to erase an old page at some point. Because we don't want to lose any data, 157 | all items on the to-be-erased page are checked. If an item does not have a newer value than what is found on 158 | the page, it will be copied over from the to-be-erased page to the current partial-open page. 159 | This way no data is lost (as long as the flash is big enough to contain all data). 160 | 161 | ### Inner workings for queue 162 | 163 | When pushing, the youngest spot to place the item is searched for. 164 | If it doesn't fit, it will return an error or erase an old page if you specified it could. 165 | You should only lose data when you give permission. 166 | 167 | Peeking and popping look at the oldest data it can find. 168 | When popping, the item is also erased. 169 | 170 | When using peek_many, you can look at all data from oldest to newest. 171 | -------------------------------------------------------------------------------- /example/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | # replace nRF82840_xxAA with your chip as listed in `probe-rs chip list` 3 | runner = "probe-rs run --chip nRF52840_xxAA" 4 | 5 | [build] 6 | target = "thumbv7em-none-eabihf" 7 | 8 | [env] 9 | DEFMT_LOG = "trace" 10 | 11 | [unstable] 12 | build-std = ["panic_abort", "core"] 13 | build-std-features = ["panic_immediate_abort"] 14 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /example/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 10 | 11 | [[package]] 12 | name = "az" 13 | version = "1.2.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" 16 | 17 | [[package]] 18 | name = "bare-metal" 19 | version = "0.2.5" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" 22 | dependencies = [ 23 | "rustc_version", 24 | ] 25 | 26 | [[package]] 27 | name = "bitfield" 28 | version = "0.13.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "1.3.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "2.5.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 43 | 44 | [[package]] 45 | name = "bytemuck" 46 | version = "1.15.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" 49 | 50 | [[package]] 51 | name = "byteorder" 52 | version = "1.5.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "1.0.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 61 | 62 | [[package]] 63 | name = "cortex-m" 64 | version = "0.7.7" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" 67 | dependencies = [ 68 | "bare-metal", 69 | "bitfield", 70 | "critical-section", 71 | "embedded-hal 0.2.7", 72 | "volatile-register", 73 | ] 74 | 75 | [[package]] 76 | name = "cortex-m-rt" 77 | version = "0.7.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "ee84e813d593101b1723e13ec38b6ab6abbdbaaa4546553f5395ed274079ddb1" 80 | dependencies = [ 81 | "cortex-m-rt-macros", 82 | ] 83 | 84 | [[package]] 85 | name = "cortex-m-rt-macros" 86 | version = "0.7.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7" 89 | dependencies = [ 90 | "proc-macro2", 91 | "quote", 92 | "syn 1.0.109", 93 | ] 94 | 95 | [[package]] 96 | name = "critical-section" 97 | version = "1.1.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" 100 | 101 | [[package]] 102 | name = "crunchy" 103 | version = "0.2.2" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 106 | 107 | [[package]] 108 | name = "darling" 109 | version = "0.20.8" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" 112 | dependencies = [ 113 | "darling_core", 114 | "darling_macro", 115 | ] 116 | 117 | [[package]] 118 | name = "darling_core" 119 | version = "0.20.8" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" 122 | dependencies = [ 123 | "fnv", 124 | "ident_case", 125 | "proc-macro2", 126 | "quote", 127 | "strsim", 128 | "syn 2.0.58", 129 | ] 130 | 131 | [[package]] 132 | name = "darling_macro" 133 | version = "0.20.8" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" 136 | dependencies = [ 137 | "darling_core", 138 | "quote", 139 | "syn 2.0.58", 140 | ] 141 | 142 | [[package]] 143 | name = "defmt" 144 | version = "0.3.6" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "3939552907426de152b3c2c6f51ed53f98f448babd26f28694c95f5906194595" 147 | dependencies = [ 148 | "bitflags 1.3.2", 149 | "defmt-macros", 150 | ] 151 | 152 | [[package]] 153 | name = "defmt-macros" 154 | version = "0.3.7" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "18bdc7a7b92ac413e19e95240e75d3a73a8d8e78aa24a594c22cbb4d44b4bbda" 157 | dependencies = [ 158 | "defmt-parser", 159 | "proc-macro-error", 160 | "proc-macro2", 161 | "quote", 162 | "syn 2.0.58", 163 | ] 164 | 165 | [[package]] 166 | name = "defmt-parser" 167 | version = "0.3.4" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f" 170 | dependencies = [ 171 | "thiserror", 172 | ] 173 | 174 | [[package]] 175 | name = "defmt-rtt" 176 | version = "0.4.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "609923761264dd99ed9c7d209718cda4631c5fe84668e0f0960124cbb844c49f" 179 | dependencies = [ 180 | "critical-section", 181 | "defmt", 182 | ] 183 | 184 | [[package]] 185 | name = "document-features" 186 | version = "0.2.8" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" 189 | dependencies = [ 190 | "litrs", 191 | ] 192 | 193 | [[package]] 194 | name = "embassy-embedded-hal" 195 | version = "0.1.0" 196 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 197 | dependencies = [ 198 | "defmt", 199 | "embassy-futures", 200 | "embassy-sync", 201 | "embassy-time", 202 | "embedded-hal 0.2.7", 203 | "embedded-hal 1.0.0", 204 | "embedded-hal-async", 205 | "embedded-storage", 206 | "embedded-storage-async", 207 | "nb 1.1.0", 208 | ] 209 | 210 | [[package]] 211 | name = "embassy-executor" 212 | version = "0.5.0" 213 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 214 | dependencies = [ 215 | "cortex-m", 216 | "critical-section", 217 | "defmt", 218 | "document-features", 219 | "embassy-executor-macros", 220 | ] 221 | 222 | [[package]] 223 | name = "embassy-executor-macros" 224 | version = "0.4.1" 225 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 226 | dependencies = [ 227 | "darling", 228 | "proc-macro2", 229 | "quote", 230 | "syn 2.0.58", 231 | ] 232 | 233 | [[package]] 234 | name = "embassy-futures" 235 | version = "0.1.1" 236 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 237 | 238 | [[package]] 239 | name = "embassy-hal-internal" 240 | version = "0.1.0" 241 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 242 | dependencies = [ 243 | "cortex-m", 244 | "critical-section", 245 | "defmt", 246 | "num-traits", 247 | ] 248 | 249 | [[package]] 250 | name = "embassy-nrf" 251 | version = "0.1.0" 252 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 253 | dependencies = [ 254 | "bitflags 2.5.0", 255 | "cfg-if", 256 | "cortex-m", 257 | "cortex-m-rt", 258 | "critical-section", 259 | "defmt", 260 | "document-features", 261 | "embassy-embedded-hal", 262 | "embassy-hal-internal", 263 | "embassy-sync", 264 | "embassy-usb-driver", 265 | "embedded-hal 0.2.7", 266 | "embedded-hal 1.0.0", 267 | "embedded-hal-async", 268 | "embedded-io", 269 | "embedded-io-async", 270 | "embedded-storage", 271 | "embedded-storage-async", 272 | "fixed", 273 | "nrf51-pac", 274 | "nrf52805-pac", 275 | "nrf52810-pac", 276 | "nrf52811-pac", 277 | "nrf52820-pac", 278 | "nrf52832-pac", 279 | "nrf52833-pac", 280 | "nrf52840-pac", 281 | "nrf5340-app-pac", 282 | "nrf5340-net-pac", 283 | "nrf9160-pac", 284 | "rand_core", 285 | ] 286 | 287 | [[package]] 288 | name = "embassy-sync" 289 | version = "0.5.0" 290 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 291 | dependencies = [ 292 | "cfg-if", 293 | "critical-section", 294 | "defmt", 295 | "embedded-io-async", 296 | "futures-util", 297 | "heapless", 298 | ] 299 | 300 | [[package]] 301 | name = "embassy-time" 302 | version = "0.3.0" 303 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 304 | dependencies = [ 305 | "cfg-if", 306 | "critical-section", 307 | "document-features", 308 | "embassy-time-driver", 309 | "embassy-time-queue-driver", 310 | "embedded-hal 0.2.7", 311 | "embedded-hal 1.0.0", 312 | "embedded-hal-async", 313 | "futures-util", 314 | "heapless", 315 | ] 316 | 317 | [[package]] 318 | name = "embassy-time-driver" 319 | version = "0.1.0" 320 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 321 | dependencies = [ 322 | "document-features", 323 | ] 324 | 325 | [[package]] 326 | name = "embassy-time-queue-driver" 327 | version = "0.1.0" 328 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 329 | 330 | [[package]] 331 | name = "embassy-usb-driver" 332 | version = "0.1.0" 333 | source = "git+https://github.com/embassy-rs/embassy.git?rev=029636e#029636e6fc06b484898c5935957b33b60a9c88b2" 334 | dependencies = [ 335 | "defmt", 336 | ] 337 | 338 | [[package]] 339 | name = "embedded-hal" 340 | version = "0.2.7" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" 343 | dependencies = [ 344 | "nb 0.1.3", 345 | "void", 346 | ] 347 | 348 | [[package]] 349 | name = "embedded-hal" 350 | version = "1.0.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" 353 | 354 | [[package]] 355 | name = "embedded-hal-async" 356 | version = "1.0.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" 359 | dependencies = [ 360 | "embedded-hal 1.0.0", 361 | ] 362 | 363 | [[package]] 364 | name = "embedded-io" 365 | version = "0.6.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 368 | 369 | [[package]] 370 | name = "embedded-io-async" 371 | version = "0.6.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" 374 | dependencies = [ 375 | "embedded-io", 376 | ] 377 | 378 | [[package]] 379 | name = "embedded-storage" 380 | version = "0.3.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" 383 | 384 | [[package]] 385 | name = "embedded-storage-async" 386 | version = "0.4.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" 389 | dependencies = [ 390 | "embedded-storage", 391 | ] 392 | 393 | [[package]] 394 | name = "example" 395 | version = "0.1.0" 396 | dependencies = [ 397 | "cortex-m", 398 | "cortex-m-rt", 399 | "defmt", 400 | "defmt-rtt", 401 | "embassy-embedded-hal", 402 | "embassy-executor", 403 | "embassy-nrf", 404 | "embedded-storage", 405 | "embedded-storage-async", 406 | "panic-probe", 407 | "sequential-storage", 408 | ] 409 | 410 | [[package]] 411 | name = "fixed" 412 | version = "1.27.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048" 415 | dependencies = [ 416 | "az", 417 | "bytemuck", 418 | "half", 419 | "typenum", 420 | ] 421 | 422 | [[package]] 423 | name = "fnv" 424 | version = "1.0.7" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 427 | 428 | [[package]] 429 | name = "futures-core" 430 | version = "0.3.30" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 433 | 434 | [[package]] 435 | name = "futures-task" 436 | version = "0.3.30" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 439 | 440 | [[package]] 441 | name = "futures-util" 442 | version = "0.3.30" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 445 | dependencies = [ 446 | "futures-core", 447 | "futures-task", 448 | "pin-project-lite", 449 | "pin-utils", 450 | ] 451 | 452 | [[package]] 453 | name = "half" 454 | version = "2.4.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 457 | dependencies = [ 458 | "cfg-if", 459 | "crunchy", 460 | ] 461 | 462 | [[package]] 463 | name = "hash32" 464 | version = "0.3.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 467 | dependencies = [ 468 | "byteorder", 469 | ] 470 | 471 | [[package]] 472 | name = "heapless" 473 | version = "0.8.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 476 | dependencies = [ 477 | "hash32", 478 | "stable_deref_trait", 479 | ] 480 | 481 | [[package]] 482 | name = "ident_case" 483 | version = "1.0.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 486 | 487 | [[package]] 488 | name = "litrs" 489 | version = "0.4.1" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 492 | 493 | [[package]] 494 | name = "nb" 495 | version = "0.1.3" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" 498 | dependencies = [ 499 | "nb 1.1.0", 500 | ] 501 | 502 | [[package]] 503 | name = "nb" 504 | version = "1.1.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" 507 | 508 | [[package]] 509 | name = "nrf51-pac" 510 | version = "0.12.2" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "137f187dc6ee482e27312086bd3c3a83e1c273512782cf131a61957f72fc4219" 513 | dependencies = [ 514 | "cortex-m", 515 | "cortex-m-rt", 516 | "vcell", 517 | ] 518 | 519 | [[package]] 520 | name = "nrf52805-pac" 521 | version = "0.12.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "a2da657648039d59f4de6bc31b948dd3a5d03b32529a4d5d19d9e2dd9d4bfa6c" 524 | dependencies = [ 525 | "cortex-m", 526 | "cortex-m-rt", 527 | "vcell", 528 | ] 529 | 530 | [[package]] 531 | name = "nrf52810-pac" 532 | version = "0.12.2" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "c26b12d5af17a9f4bb9a06ca9a1f814bca3d67bc8715b23f8dc230b09a227666" 535 | dependencies = [ 536 | "cortex-m", 537 | "cortex-m-rt", 538 | "vcell", 539 | ] 540 | 541 | [[package]] 542 | name = "nrf52811-pac" 543 | version = "0.12.2" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "4179b2a7ed0b2fd5e109d0fab9b4fc55b3936b2a4916a9306d22e5bc8dc1fd8f" 546 | dependencies = [ 547 | "cortex-m", 548 | "cortex-m-rt", 549 | "vcell", 550 | ] 551 | 552 | [[package]] 553 | name = "nrf52820-pac" 554 | version = "0.12.2" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "e4791cff995e6419a5ad1aebc3b3c9539d79125ca85eb5bfd2cff9b470b81071" 557 | dependencies = [ 558 | "cortex-m", 559 | "cortex-m-rt", 560 | "vcell", 561 | ] 562 | 563 | [[package]] 564 | name = "nrf52832-pac" 565 | version = "0.12.2" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "0242b685c9c15648fb803e155628f42ace457478b2cb930868f40cae2db925e0" 568 | dependencies = [ 569 | "cortex-m", 570 | "cortex-m-rt", 571 | "vcell", 572 | ] 573 | 574 | [[package]] 575 | name = "nrf52833-pac" 576 | version = "0.12.2" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "10e1358255b360cdc816dd7b6ef81be8c8499c0998277e5249bed222bd0f5241" 579 | dependencies = [ 580 | "cortex-m", 581 | "cortex-m-rt", 582 | "vcell", 583 | ] 584 | 585 | [[package]] 586 | name = "nrf52840-pac" 587 | version = "0.12.2" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "30713f36f1be02e5bc9abefa30eae4a1f943d810f199d4923d3ad062d1be1b3d" 590 | dependencies = [ 591 | "cortex-m", 592 | "cortex-m-rt", 593 | "vcell", 594 | ] 595 | 596 | [[package]] 597 | name = "nrf5340-app-pac" 598 | version = "0.12.2" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "7c88824573cd150fe9f27c1a48cea31a8cb24d3322df488875775143618c087a" 601 | dependencies = [ 602 | "cortex-m", 603 | "cortex-m-rt", 604 | "vcell", 605 | ] 606 | 607 | [[package]] 608 | name = "nrf5340-net-pac" 609 | version = "0.12.2" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "e5c03e44df22fe5888109fe42e523162c7059adf4d30860f4f73ecc8b1fc16fe" 612 | dependencies = [ 613 | "cortex-m", 614 | "cortex-m-rt", 615 | "vcell", 616 | ] 617 | 618 | [[package]] 619 | name = "nrf9160-pac" 620 | version = "0.12.2" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "7344d74afb5684e00c48d175cad9619f36d629cfb0687d33b4d1bb86fba688f4" 623 | dependencies = [ 624 | "cortex-m", 625 | "cortex-m-rt", 626 | "vcell", 627 | ] 628 | 629 | [[package]] 630 | name = "num-traits" 631 | version = "0.2.18" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 634 | dependencies = [ 635 | "autocfg", 636 | ] 637 | 638 | [[package]] 639 | name = "panic-probe" 640 | version = "0.3.1" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "aa6fa5645ef5a760cd340eaa92af9c1ce131c8c09e7f8926d8a24b59d26652b9" 643 | dependencies = [ 644 | "cortex-m", 645 | "defmt", 646 | ] 647 | 648 | [[package]] 649 | name = "pin-project-lite" 650 | version = "0.2.14" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 653 | 654 | [[package]] 655 | name = "pin-utils" 656 | version = "0.1.0" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 659 | 660 | [[package]] 661 | name = "proc-macro-error" 662 | version = "1.0.4" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 665 | dependencies = [ 666 | "proc-macro-error-attr", 667 | "proc-macro2", 668 | "quote", 669 | "syn 1.0.109", 670 | "version_check", 671 | ] 672 | 673 | [[package]] 674 | name = "proc-macro-error-attr" 675 | version = "1.0.4" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 678 | dependencies = [ 679 | "proc-macro2", 680 | "quote", 681 | "version_check", 682 | ] 683 | 684 | [[package]] 685 | name = "proc-macro2" 686 | version = "1.0.79" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 689 | dependencies = [ 690 | "unicode-ident", 691 | ] 692 | 693 | [[package]] 694 | name = "quote" 695 | version = "1.0.35" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 698 | dependencies = [ 699 | "proc-macro2", 700 | ] 701 | 702 | [[package]] 703 | name = "rand_core" 704 | version = "0.6.4" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 707 | 708 | [[package]] 709 | name = "rustc_version" 710 | version = "0.2.3" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 713 | dependencies = [ 714 | "semver", 715 | ] 716 | 717 | [[package]] 718 | name = "semver" 719 | version = "0.9.0" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 722 | dependencies = [ 723 | "semver-parser", 724 | ] 725 | 726 | [[package]] 727 | name = "semver-parser" 728 | version = "0.7.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 731 | 732 | [[package]] 733 | name = "sequential-storage" 734 | version = "4.0.1" 735 | dependencies = [ 736 | "defmt", 737 | "embedded-storage-async", 738 | ] 739 | 740 | [[package]] 741 | name = "stable_deref_trait" 742 | version = "1.2.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 745 | 746 | [[package]] 747 | name = "strsim" 748 | version = "0.10.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 751 | 752 | [[package]] 753 | name = "syn" 754 | version = "1.0.109" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 757 | dependencies = [ 758 | "proc-macro2", 759 | "quote", 760 | "unicode-ident", 761 | ] 762 | 763 | [[package]] 764 | name = "syn" 765 | version = "2.0.58" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" 768 | dependencies = [ 769 | "proc-macro2", 770 | "quote", 771 | "unicode-ident", 772 | ] 773 | 774 | [[package]] 775 | name = "thiserror" 776 | version = "1.0.58" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 779 | dependencies = [ 780 | "thiserror-impl", 781 | ] 782 | 783 | [[package]] 784 | name = "thiserror-impl" 785 | version = "1.0.58" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 788 | dependencies = [ 789 | "proc-macro2", 790 | "quote", 791 | "syn 2.0.58", 792 | ] 793 | 794 | [[package]] 795 | name = "typenum" 796 | version = "1.17.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 799 | 800 | [[package]] 801 | name = "unicode-ident" 802 | version = "1.0.12" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 805 | 806 | [[package]] 807 | name = "vcell" 808 | version = "0.1.3" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" 811 | 812 | [[package]] 813 | name = "version_check" 814 | version = "0.9.4" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 817 | 818 | [[package]] 819 | name = "void" 820 | version = "1.0.2" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 823 | 824 | [[package]] 825 | name = "volatile-register" 826 | version = "0.2.2" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" 829 | dependencies = [ 830 | "vcell", 831 | ] 832 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", rev = "029636e", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "defmt"] } 8 | embassy-nrf = { git = "https://github.com/embassy-rs/embassy.git", rev = "029636e", features = ["defmt", "nrf52840"] } 9 | embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy.git", rev = "029636e", default-features = false } 10 | 11 | defmt = "0.3" 12 | defmt-rtt = "0.4" 13 | 14 | cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } 15 | cortex-m-rt = "0.7.0" 16 | panic-probe = { version = "0.3", features = ["print-defmt"] } 17 | embedded-storage = "0.3.1" 18 | embedded-storage-async = "0.4.1" 19 | 20 | sequential-storage = { path = "../", features = ["defmt-03"] } 21 | 22 | [profile.release] 23 | lto = true 24 | debug = true 25 | opt-level = "z" 26 | panic = "abort" 27 | codegen-units = 1 28 | incremental = false 29 | -------------------------------------------------------------------------------- /example/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 35 | } 36 | -------------------------------------------------------------------------------- /example/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* NOTE 1 K = 1 KiBi = 1024 bytes */ 4 | FLASH : ORIGIN = 0x00000000, LENGTH = 1024K - 32K 5 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 6 | } -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use core::ops::Range; 5 | 6 | use defmt::unwrap; 7 | use embassy_executor::Spawner; 8 | use embedded_storage_async::nor_flash::MultiwriteNorFlash; 9 | use sequential_storage::cache::NoCache; 10 | use {defmt_rtt as _, panic_probe as _}; 11 | 12 | #[embassy_executor::main] 13 | async fn main(_spawner: Spawner) { 14 | let p = embassy_nrf::init(Default::default()); 15 | 16 | let flash = embassy_nrf::nvmc::Nvmc::new(p.NVMC); 17 | let mut flash = embassy_embedded_hal::adapter::BlockingAsync::new(flash); 18 | 19 | const QUEUE_FLASH_RANGE: Range = 0xF8000..0xFC000; 20 | const MAP_FLASH_RANGE: Range = 0xFC000..0x100000; 21 | 22 | run_queue(&mut flash, QUEUE_FLASH_RANGE).await; 23 | run_map(&mut flash, MAP_FLASH_RANGE).await; 24 | 25 | defmt::info!("All went ok!"); 26 | cortex_m::asm::bkpt(); 27 | } 28 | 29 | async fn run_queue( 30 | flash: &mut impl MultiwriteNorFlash, 31 | flash_range: Range, 32 | ) { 33 | unwrap!(sequential_storage::erase_all(flash, flash_range.clone()).await); 34 | 35 | let mut data_buffer = [0; 32]; 36 | 37 | unwrap!( 38 | sequential_storage::queue::push( 39 | flash, 40 | flash_range.clone(), 41 | &mut NoCache::new(), 42 | &[0, 0, 0, 0], 43 | false 44 | ) 45 | .await 46 | ); 47 | 48 | let peeked = unwrap!( 49 | sequential_storage::queue::peek( 50 | flash, 51 | flash_range.clone(), 52 | &mut NoCache::new(), 53 | &mut data_buffer, 54 | ) 55 | .await 56 | ); 57 | 58 | defmt::assert_eq!(peeked, Some(&mut [0u8, 0, 0, 0][..])); 59 | 60 | let popped = unwrap!( 61 | sequential_storage::queue::pop( 62 | flash, 63 | flash_range.clone(), 64 | &mut NoCache::new(), 65 | &mut data_buffer, 66 | ) 67 | .await 68 | ); 69 | 70 | defmt::assert_eq!(popped, Some(&mut [0u8, 0, 0, 0][..])); 71 | 72 | let peeked = unwrap!( 73 | sequential_storage::queue::peek( 74 | flash, 75 | flash_range.clone(), 76 | &mut NoCache::new(), 77 | &mut data_buffer, 78 | ) 79 | .await 80 | ); 81 | 82 | defmt::assert!(peeked.is_none()); 83 | } 84 | 85 | async fn run_map( 86 | flash: &mut impl MultiwriteNorFlash, 87 | flash_range: Range, 88 | ) { 89 | unwrap!(sequential_storage::erase_all(flash, flash_range.clone()).await); 90 | 91 | let mut data_buffer = [0; 32]; 92 | 93 | unwrap!( 94 | sequential_storage::map::store_item( 95 | flash, 96 | flash_range.clone(), 97 | &mut NoCache::new(), 98 | &mut data_buffer, 99 | &0u8, 100 | &0u8, 101 | ) 102 | .await 103 | ); 104 | 105 | unwrap!( 106 | sequential_storage::map::store_item( 107 | flash, 108 | flash_range.clone(), 109 | &mut NoCache::new(), 110 | &mut data_buffer, 111 | &1u8, 112 | &123u32, 113 | ) 114 | .await 115 | ); 116 | 117 | unwrap!( 118 | sequential_storage::map::store_item( 119 | flash, 120 | flash_range.clone(), 121 | &mut NoCache::new(), 122 | &mut data_buffer, 123 | &2u8, 124 | &0.123f32, 125 | ) 126 | .await 127 | ); 128 | 129 | let fetched = unwrap!( 130 | sequential_storage::map::fetch_item::( 131 | flash, 132 | flash_range.clone(), 133 | &mut NoCache::new(), 134 | &mut data_buffer, 135 | &3, 136 | ) 137 | .await 138 | ); 139 | 140 | defmt::assert!(fetched.is_none()); 141 | 142 | let fetched = unwrap!( 143 | sequential_storage::map::fetch_item::( 144 | flash, 145 | flash_range.clone(), 146 | &mut NoCache::new(), 147 | &mut data_buffer, 148 | &1, 149 | ) 150 | .await 151 | ); 152 | 153 | defmt::assert_eq!(fetched, Some(123)); 154 | } 155 | -------------------------------------------------------------------------------- /fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | CPUS=$(nproc --all) 6 | 7 | cargo fuzz run --sanitizer none -j$CPUS $1 8 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sequential-storage-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | libfuzzer-sys = "0.4" 12 | sequential-storage = { path = "..", features = ["_test"] } 13 | arbitrary = { version = "1.2.2", features = ["derive"] } 14 | rand = "0.8.5" 15 | rand_pcg = "0.3.1" 16 | futures = { version = "0.3.30", features = ["executor"] } 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [profile.release] 23 | debug = 1 24 | 25 | [[bin]] 26 | name = "queue" 27 | path = "fuzz_targets/queue.rs" 28 | test = false 29 | doc = false 30 | 31 | [[bin]] 32 | name = "map" 33 | path = "fuzz_targets/map.rs" 34 | test = false 35 | doc = false 36 | 37 | [lints.rust] 38 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing_repro)'] } 39 | 40 | [[bin]] 41 | name = "random-flash" 42 | path = "fuzz_targets/random-flash.rs" 43 | test = false 44 | doc = false 45 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/map.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use futures::executor::block_on; 4 | use libfuzzer_sys::arbitrary::Arbitrary; 5 | use libfuzzer_sys::fuzz_target; 6 | use rand::SeedableRng; 7 | use sequential_storage::{ 8 | cache::{KeyCacheImpl, KeyPointerCache, NoCache, PagePointerCache, PageStateCache}, 9 | mock_flash::{MockFlashBase, MockFlashError, WriteCountCheck}, 10 | Error, 11 | }; 12 | use std::{collections::HashMap, fmt::Debug, ops::Range}; 13 | 14 | const PAGES: usize = 4; 15 | const WORD_SIZE: usize = 4; 16 | const WORDS_PER_PAGE: usize = 256; 17 | 18 | fuzz_target!(|data: Input| match data.cache_type { 19 | CacheType::NoCache => fuzz(data, NoCache::new()), 20 | CacheType::PageStateCache => fuzz(data, PageStateCache::::new()), 21 | CacheType::PagePointerCache => fuzz(data, PagePointerCache::::new()), 22 | CacheType::KeyPointerCache => fuzz(data, KeyPointerCache::::new()), 23 | }); 24 | 25 | #[derive(Arbitrary, Debug, Clone)] 26 | struct Input { 27 | seed: u64, 28 | fuel: u16, 29 | ops: Vec, 30 | cache_type: CacheType, 31 | } 32 | 33 | #[derive(Arbitrary, Debug, Clone)] 34 | enum Op { 35 | Store(StoreOp), 36 | Fetch(u8), 37 | Remove(u8), 38 | RemoveAll, 39 | Iter, 40 | } 41 | 42 | #[derive(Arbitrary, Debug, Clone)] 43 | struct StoreOp { 44 | key: u8, 45 | value_len: u8, 46 | } 47 | 48 | impl StoreOp { 49 | fn into_test_item(self, rng: &mut impl rand::Rng) -> (u8, Vec) { 50 | ( 51 | self.key, 52 | (0..(self.value_len % 8) as usize) 53 | .map(|_| rng.gen()) 54 | .collect(), 55 | ) 56 | } 57 | } 58 | 59 | #[derive(Arbitrary, Debug, Clone)] 60 | enum CacheType { 61 | NoCache, 62 | PageStateCache, 63 | PagePointerCache, 64 | KeyPointerCache, 65 | } 66 | 67 | fn fuzz(ops: Input, mut cache: impl KeyCacheImpl + Debug) { 68 | let mut flash = MockFlashBase::::new( 69 | if ops 70 | .ops 71 | .iter() 72 | .any(|op| matches!(op, Op::Remove(_) | Op::RemoveAll)) 73 | { 74 | WriteCountCheck::Twice 75 | } else { 76 | WriteCountCheck::OnceOnly 77 | }, 78 | Some(ops.fuel as u32), 79 | true, 80 | ); 81 | const FLASH_RANGE: Range = 0x000..0x1000; 82 | 83 | let mut map = HashMap::new(); 84 | #[repr(align(4))] 85 | struct AlignedBuf([u8; 260]); 86 | let mut buf = AlignedBuf([0; 260]); // Max length of test item serialized, rounded up to align to flash word. 87 | 88 | let mut rng = rand_pcg::Pcg32::seed_from_u64(ops.seed); 89 | 90 | #[cfg(fuzzing_repro)] 91 | eprintln!("\n=== START ===\n"); 92 | 93 | for op in ops.ops.into_iter() { 94 | #[cfg(fuzzing_repro)] 95 | eprintln!("{}", block_on(flash.print_items())); 96 | #[cfg(fuzzing_repro)] 97 | eprintln!("{:?}", cache); 98 | #[cfg(fuzzing_repro)] 99 | eprintln!("=== OP: {op:?} ==="); 100 | 101 | match op.clone() { 102 | Op::Store(op) => { 103 | let (key, value) = op.into_test_item(&mut rng); 104 | match block_on(sequential_storage::map::store_item( 105 | &mut flash, 106 | FLASH_RANGE, 107 | &mut cache, 108 | &mut buf.0, 109 | &key, 110 | &value.as_slice(), 111 | )) { 112 | Ok(_) => { 113 | map.insert(key, value); 114 | } 115 | Err(Error::FullStorage) => {} 116 | Err(Error::Storage { 117 | value: MockFlashError::EarlyShutoff(_), 118 | backtrace: _backtrace, 119 | }) => { 120 | match block_on(sequential_storage::map::fetch_item::( 121 | &mut flash, 122 | FLASH_RANGE, 123 | &mut cache, 124 | &mut buf.0, 125 | &key, 126 | )) { 127 | Ok(Some(check_item)) if check_item == value => { 128 | #[cfg(fuzzing_repro)] 129 | eprintln!("Early shutoff when storing key: {key}, value: {value:?}! (but it still stored fully). Originated from:\n{_backtrace:#}"); 130 | // Even though we got a shutoff, it still managed to store well 131 | map.insert(key, value); 132 | } 133 | _ => { 134 | // Could not fetch the item we stored... 135 | #[cfg(fuzzing_repro)] 136 | eprintln!("Early shutoff when storing key: {key}, value: {value:?}! Originated from:\n{_backtrace:#}"); 137 | } 138 | } 139 | } 140 | Err(Error::Corrupted { 141 | backtrace: _backtrace, 142 | }) => { 143 | #[cfg(fuzzing_repro)] 144 | eprintln!("Corrupted when storing! Originated from:\n{_backtrace:#}"); 145 | panic!("Corrupted!"); 146 | } 147 | Err(e) => panic!("{e:?}"), 148 | } 149 | } 150 | Op::Fetch(key) => { 151 | match block_on(sequential_storage::map::fetch_item::( 152 | &mut flash, 153 | FLASH_RANGE, 154 | &mut cache, 155 | &mut buf.0, 156 | &key, 157 | )) { 158 | Ok(Some(fetch_result)) => { 159 | let map_value = map 160 | .get(&key) 161 | .expect(&format!("Map doesn't contain: {fetch_result:?}")); 162 | assert_eq!(map_value, &fetch_result, "Mismatching values"); 163 | } 164 | Ok(None) => { 165 | assert_eq!(None, map.get(&key)); 166 | } 167 | Err(Error::Storage { 168 | value: MockFlashError::EarlyShutoff(_), 169 | backtrace: _backtrace, 170 | }) => { 171 | #[cfg(fuzzing_repro)] 172 | eprintln!("Early shutoff when fetching! Originated from:\n{_backtrace:#}"); 173 | } 174 | Err(Error::Corrupted { 175 | backtrace: _backtrace, 176 | }) => { 177 | #[cfg(fuzzing_repro)] 178 | eprintln!("Corrupted when fetching! Originated from:\n{_backtrace:#}"); 179 | panic!("Corrupted!"); 180 | } 181 | Err(e) => panic!("{e:#?}"), 182 | } 183 | } 184 | Op::Remove(key) => { 185 | match block_on(sequential_storage::map::remove_item::( 186 | &mut flash, 187 | FLASH_RANGE, 188 | &mut cache, 189 | &mut buf.0, 190 | &key, 191 | )) { 192 | Ok(()) => { 193 | map.remove(&key); 194 | } 195 | Err(Error::Storage { 196 | value: MockFlashError::EarlyShutoff(_), 197 | backtrace: _backtrace, 198 | }) => { 199 | // Check if the item is still there. It might or it might not and either is fine 200 | match block_on(sequential_storage::map::fetch_item::( 201 | &mut flash, 202 | FLASH_RANGE, 203 | &mut cache, 204 | &mut buf.0, 205 | &key, 206 | )) { 207 | Ok(Some(_)) => { 208 | #[cfg(fuzzing_repro)] 209 | eprintln!("Early shutoff when removing item {key}! Originated from:\n{_backtrace:#}"); 210 | } 211 | _ => { 212 | // Could not fetch the item we stored... 213 | #[cfg(fuzzing_repro)] 214 | eprintln!("Early shutoff when removing item {key}! (but it still removed fully). Originated from:\n{_backtrace:#}"); 215 | // Even though we got a shutoff, it still managed to store well 216 | map.remove(&key); 217 | } 218 | } 219 | } 220 | Err(Error::Corrupted { 221 | backtrace: _backtrace, 222 | }) => { 223 | #[cfg(fuzzing_repro)] 224 | eprintln!("Corrupted when removing! Originated from:\n{_backtrace:#}"); 225 | panic!("Corrupted!"); 226 | } 227 | Err(e) => panic!("{e:?}"), 228 | } 229 | } 230 | Op::RemoveAll => { 231 | match block_on(sequential_storage::map::remove_all_items::( 232 | &mut flash, 233 | FLASH_RANGE, 234 | &mut cache, 235 | &mut buf.0, 236 | )) { 237 | Ok(()) => { 238 | map.clear(); 239 | } 240 | Err(Error::Storage { 241 | value: MockFlashError::EarlyShutoff(_), 242 | backtrace: _backtrace, 243 | }) => { 244 | for key in map.keys().copied().collect::>() { 245 | // Check if the item is still there. It might or it might not and either is fine 246 | match block_on(sequential_storage::map::fetch_item::( 247 | &mut flash, 248 | FLASH_RANGE, 249 | &mut cache, 250 | &mut buf.0, 251 | &key, 252 | )) { 253 | Ok(Some(_)) => { 254 | #[cfg(fuzzing_repro)] 255 | eprintln!("Early shutoff when removing item {key}! Originated from:\n{_backtrace:#}"); 256 | } 257 | _ => { 258 | // Could not fetch the item we stored... 259 | #[cfg(fuzzing_repro)] 260 | eprintln!("Early shutoff when removing item {key}! (but it still removed fully). Originated from:\n{_backtrace:#}"); 261 | // Even though we got a shutoff, it still managed to store well 262 | map.remove(&key); 263 | } 264 | } 265 | } 266 | } 267 | Err(Error::Corrupted { 268 | backtrace: _backtrace, 269 | }) => { 270 | #[cfg(fuzzing_repro)] 271 | eprintln!("Corrupted when removing! Originated from:\n{_backtrace:#}"); 272 | panic!("Corrupted!"); 273 | } 274 | Err(e) => panic!("{e:?}"), 275 | } 276 | } 277 | Op::Iter => { 278 | let mut iter = block_on(sequential_storage::map::fetch_all_items::( 279 | &mut flash, 280 | FLASH_RANGE, 281 | &mut cache, 282 | &mut buf.0, 283 | )) 284 | .unwrap(); 285 | 286 | let mut seen_items = HashMap::new(); 287 | 288 | loop { 289 | match block_on(iter.next::(&mut buf.0)) { 290 | Ok(None) => break, 291 | Ok(Some((key, val))) => { 292 | seen_items.insert(key, val.to_vec()); 293 | } 294 | Err(Error::Corrupted { 295 | backtrace: _backtrace, 296 | }) => { 297 | #[cfg(fuzzing_repro)] 298 | eprintln!("Corrupted when removing! Originated from:\n{_backtrace:#}"); 299 | panic!("Corrupted!"); 300 | } 301 | Err(e) => panic!("{e:?}"), 302 | } 303 | } 304 | 305 | assert_eq!(seen_items, map); 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/queue.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use futures::executor::block_on; 4 | use libfuzzer_sys::arbitrary::Arbitrary; 5 | use libfuzzer_sys::fuzz_target; 6 | use rand::{Rng, SeedableRng}; 7 | use sequential_storage::{ 8 | cache::{CacheImpl, NoCache, PagePointerCache, PageStateCache}, 9 | mock_flash::{MockFlashBase, MockFlashError, WriteCountCheck}, 10 | Error, 11 | }; 12 | use std::{collections::VecDeque, fmt::Debug, ops::Range}; 13 | const MAX_VALUE_SIZE: usize = u8::MAX as usize; 14 | 15 | const PAGES: usize = 4; 16 | const WORD_SIZE: usize = 4; 17 | const WORDS_PER_PAGE: usize = 256; 18 | 19 | fuzz_target!(|data: Input| match data.cache_type { 20 | CacheType::NoCache => fuzz(data, NoCache::new()), 21 | CacheType::PageStateCache => fuzz(data, PageStateCache::::new()), 22 | CacheType::PagePointerCache => fuzz(data, PagePointerCache::::new()), 23 | }); 24 | 25 | #[derive(Arbitrary, Debug, Clone)] 26 | struct Input { 27 | seed: u64, 28 | fuel: u16, 29 | ops: Vec, 30 | cache_type: CacheType, 31 | } 32 | 33 | #[derive(Arbitrary, Debug, Clone)] 34 | enum Op { 35 | Push(PushOp), 36 | Iterate(Vec), 37 | Peek, 38 | Pop, 39 | } 40 | 41 | #[derive(Arbitrary, Debug, Clone)] 42 | struct PushOp { 43 | value_len: u8, 44 | } 45 | 46 | #[derive(Arbitrary, Debug, Clone)] 47 | enum CacheType { 48 | NoCache, 49 | PageStateCache, 50 | PagePointerCache, 51 | } 52 | 53 | #[repr(align(4))] 54 | struct AlignedBuf([u8; MAX_VALUE_SIZE + 1]); 55 | 56 | fn fuzz(ops: Input, mut cache: impl CacheImpl + Debug) { 57 | let mut flash = MockFlashBase::::new( 58 | WriteCountCheck::Twice, 59 | Some(ops.fuel as u32), 60 | true, 61 | ); 62 | const FLASH_RANGE: Range = 0x000..0x1000; 63 | 64 | let mut order = VecDeque::new(); 65 | let mut buf = AlignedBuf([0; MAX_VALUE_SIZE + 1]); 66 | 67 | let mut rng = rand_pcg::Pcg32::seed_from_u64(ops.seed); 68 | 69 | #[cfg(fuzzing_repro)] 70 | eprintln!("\n=== START ===\n"); 71 | 72 | for mut op in ops.ops.into_iter() { 73 | #[cfg(fuzzing_repro)] 74 | eprintln!("{}", block_on(flash.print_items())); 75 | #[cfg(fuzzing_repro)] 76 | eprintln!("{:?}", cache); 77 | #[cfg(fuzzing_repro)] 78 | eprintln!("\n=== OP: {op:?} ===\n"); 79 | 80 | match &mut op { 81 | Op::Push(op) => { 82 | let val: Vec = (0..op.value_len as usize % 16).map(|_| rng.gen()).collect(); 83 | 84 | let max_fit = match block_on(sequential_storage::queue::find_max_fit( 85 | &mut flash, 86 | FLASH_RANGE, 87 | &mut cache, 88 | )) { 89 | Ok(val) => val, 90 | Err(Error::Corrupted { 91 | backtrace: _backtrace, 92 | }) => { 93 | #[cfg(fuzzing_repro)] 94 | eprintln!("Corrupted when finding max! Originated from:\n{_backtrace:#}"); 95 | panic!("Corrupted!"); 96 | } 97 | Err(e) => panic!("Error while finding max fit: {e:?}"), 98 | }; 99 | 100 | buf.0[..val.len()].copy_from_slice(&val); 101 | match block_on(sequential_storage::queue::push( 102 | &mut flash, 103 | FLASH_RANGE, 104 | &mut cache, 105 | &buf.0[..val.len()], 106 | false, 107 | )) { 108 | Ok(_) => { 109 | if let Some(max_fit) = max_fit { 110 | if val.len() > max_fit as usize { 111 | panic!("Pushing succeeded while value was bigger than max fit"); 112 | } 113 | } else { 114 | panic!("Pushing succeeded while there was no fit"); 115 | } 116 | 117 | order.push_back(val); 118 | } 119 | Err(Error::FullStorage) => { 120 | if let Some(max_fit) = max_fit { 121 | if val.len() <= max_fit as usize { 122 | panic!("Got a wrong full storage"); 123 | } 124 | } 125 | } 126 | Err(Error::Storage { 127 | value: MockFlashError::EarlyShutoff(address), 128 | backtrace: _backtrace, 129 | }) => { 130 | // We need to check if it managed to write 131 | if let Some(true) = block_on(flash.get_item_presence(address)) { 132 | #[cfg(fuzzing_repro)] 133 | eprintln!("Early shutoff when pushing {val:?}! (but it still stored fully). Originated from:\n{_backtrace:#}"); 134 | order.push_back(val); 135 | } else { 136 | #[cfg(fuzzing_repro)] 137 | eprintln!("Early shutoff when pushing {val:?}! Originated from:\n{_backtrace:#}"); 138 | } 139 | } 140 | Err(Error::Corrupted { 141 | backtrace: _backtrace, 142 | }) => { 143 | #[cfg(fuzzing_repro)] 144 | eprintln!("Corrupted when pushing! Originated from:\n{_backtrace:#}"); 145 | panic!("Corrupted!"); 146 | } 147 | Err(e) => panic!("Error pushing to queue: {e:?}"), 148 | } 149 | } 150 | Op::Pop => { 151 | match block_on(sequential_storage::queue::pop( 152 | &mut flash, 153 | FLASH_RANGE, 154 | &mut cache, 155 | &mut buf.0, 156 | )) { 157 | Ok(value) => { 158 | assert_eq!( 159 | value, 160 | order 161 | .pop_front() 162 | .as_mut() 163 | .map(|target| target.as_mut_slice()) 164 | ); 165 | } 166 | Err(Error::Storage { 167 | value: MockFlashError::EarlyShutoff(address), 168 | backtrace: _backtrace, 169 | }) => { 170 | #[cfg(fuzzing_repro)] 171 | eprintln!( 172 | "Early shutoff when popping (single)! Originated from:\n{_backtrace:#}" 173 | ); 174 | 175 | if !matches!(block_on(flash.get_item_presence(address)), Some(true)) { 176 | // The item is no longer readable here 177 | order.pop_front(); 178 | } 179 | } 180 | Err(Error::Corrupted { 181 | backtrace: _backtrace, 182 | }) => { 183 | #[cfg(fuzzing_repro)] 184 | eprintln!( 185 | "Corrupted when popping single! Originated from:\n{_backtrace:#}" 186 | ); 187 | panic!("Corrupted!"); 188 | } 189 | Err(e) => panic!("Error popping (single) from queue: {e:?}"), 190 | } 191 | } 192 | Op::Peek => { 193 | match block_on(sequential_storage::queue::peek( 194 | &mut flash, 195 | FLASH_RANGE, 196 | &mut cache, 197 | &mut buf.0, 198 | )) { 199 | Ok(value) => { 200 | assert_eq!( 201 | value.map(|b| &b[..]), 202 | order.front().as_ref().map(|target| target.as_slice()) 203 | ); 204 | } 205 | Err(Error::Corrupted { 206 | backtrace: _backtrace, 207 | }) => { 208 | #[cfg(fuzzing_repro)] 209 | eprintln!( 210 | "Corrupted when peeking single! Originated from:\n{_backtrace:#}" 211 | ); 212 | panic!("Corrupted!"); 213 | } 214 | Err(e) => panic!("Error popping (single) from queue: {e:?}"), 215 | } 216 | } 217 | Op::Iterate(pop_sequence) => { 218 | let mut iterator = match block_on(sequential_storage::queue::iter( 219 | &mut flash, 220 | FLASH_RANGE, 221 | &mut cache, 222 | )) { 223 | Ok(val) => val, 224 | Err(Error::Corrupted { 225 | backtrace: _backtrace, 226 | }) => { 227 | #[cfg(fuzzing_repro)] 228 | eprintln!( 229 | "Corrupted when creating iterator! Originated from:\n{_backtrace:#}" 230 | ); 231 | panic!("Corrupted!"); 232 | } 233 | Err(e) => panic!("Error while creating iterator: {e:?}"), 234 | }; 235 | 236 | let mut popped_items = 0; 237 | for (i, do_pop) in pop_sequence.iter().enumerate() { 238 | match block_on(iterator.next(&mut buf.0)) { 239 | Ok(Some(value)) => { 240 | assert_eq!( 241 | &*value, 242 | order.get(i as usize - popped_items).unwrap().as_slice() 243 | ); 244 | 245 | if *do_pop { 246 | let popped = block_on(value.pop()); 247 | 248 | match popped { 249 | Ok(value) => { 250 | assert_eq!( 251 | value, 252 | order 253 | .get(i as usize - popped_items) 254 | .unwrap() 255 | .as_slice() 256 | ); 257 | 258 | order.remove(i - popped_items).unwrap(); 259 | popped_items += 1; 260 | } 261 | Err(Error::Corrupted { 262 | backtrace: _backtrace, 263 | }) => { 264 | #[cfg(fuzzing_repro)] 265 | eprintln!( 266 | "Corrupted when popping interator entry! Originated from:\n{_backtrace:#}" 267 | ); 268 | panic!("Corrupted!"); 269 | } 270 | Err(Error::Storage { 271 | value: MockFlashError::EarlyShutoff(address), 272 | backtrace: _backtrace, 273 | }) => { 274 | #[cfg(fuzzing_repro)] 275 | eprintln!( 276 | "Early shutoff when popping iterator entry! Originated from:\n{_backtrace:#}" 277 | ); 278 | 279 | if !matches!( 280 | block_on(flash.get_item_presence(address)), 281 | Some(true) 282 | ) { 283 | // The item is no longer readable here 284 | order.remove(i - popped_items).unwrap(); 285 | } 286 | 287 | break; 288 | } 289 | Err(e) => panic!("Error popping iterator entry: {e:?}"), 290 | } 291 | } 292 | } 293 | Ok(None) => { 294 | assert_eq!(None, order.get(i as usize - popped_items)); 295 | } 296 | Err(Error::Corrupted { 297 | backtrace: _backtrace, 298 | }) => { 299 | #[cfg(fuzzing_repro)] 300 | eprintln!( 301 | "Corrupted when interating! Originated from:\n{_backtrace:#}" 302 | ); 303 | panic!("Corrupted!"); 304 | } 305 | Err(e) => panic!("Error iterating queue: {e:?}"), 306 | } 307 | } 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/random-flash.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | extern crate libfuzzer_sys; 4 | 5 | use futures::executor::block_on; 6 | use libfuzzer_sys::fuzz_target; 7 | use sequential_storage::mock_flash::{MockFlashBase, WriteCountCheck}; 8 | 9 | fuzz_target!(|data: &[u8]| fuzz(data)); 10 | 11 | const PAGES: usize = 4; 12 | const WORD_SIZE: usize = 4; 13 | const WORDS_PER_PAGE: usize = 256; 14 | 15 | fn fuzz(random_data: &[u8]) { 16 | let mut flash = 17 | MockFlashBase::::new(WriteCountCheck::Twice, None, false); 18 | 19 | let len = random_data.len().min(flash.as_bytes().len()); 20 | flash.as_bytes_mut()[..len].copy_from_slice(&random_data[..len]); 21 | 22 | block_on(flash.print_items()); 23 | } 24 | -------------------------------------------------------------------------------- /src/alloc_impl.rs: -------------------------------------------------------------------------------- 1 | extern crate alloc; 2 | use crate::map::{Key, SerializationError, Value}; 3 | use alloc::{string::String, vec::Vec}; 4 | 5 | // alloc::Vec 6 | 7 | impl Key for Vec { 8 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 9 | if buffer.len() < self.len() + 2 { 10 | return Err(SerializationError::BufferTooSmall); 11 | } 12 | 13 | if self.len() > u16::MAX as usize { 14 | return Err(SerializationError::InvalidData); 15 | } 16 | 17 | buffer[..2].copy_from_slice(&(self.len() as u16).to_le_bytes()); 18 | buffer[2..][..self.len()].copy_from_slice(self); 19 | 20 | Ok(self.len() + 2) 21 | } 22 | 23 | fn deserialize_from(buffer: &[u8]) -> Result<(Self, usize), SerializationError> { 24 | let total_len = Self::get_len(buffer)?; 25 | 26 | if buffer.len() < total_len { 27 | return Err(SerializationError::BufferTooSmall); 28 | } 29 | 30 | let data_len = total_len - 2; 31 | 32 | let output = Vec::from(&buffer[2..][..data_len]); 33 | 34 | Ok((output, total_len)) 35 | } 36 | 37 | fn get_len(buffer: &[u8]) -> Result { 38 | if buffer.len() < 2 { 39 | return Err(SerializationError::BufferTooSmall); 40 | } 41 | 42 | let len = u16::from_le_bytes(buffer[..2].try_into().unwrap()); 43 | 44 | Ok(len as usize + 2) 45 | } 46 | } 47 | 48 | impl<'a> Value<'a> for Vec { 49 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 50 | if buffer.len() < self.len() { 51 | return Err(SerializationError::BufferTooSmall); 52 | } 53 | 54 | buffer[..self.len()].copy_from_slice(self.as_slice()); 55 | Ok(self.len()) 56 | } 57 | 58 | fn deserialize_from(buffer: &'a [u8]) -> Result 59 | where 60 | Self: Sized, 61 | { 62 | Ok(Vec::from(buffer)) 63 | } 64 | } 65 | 66 | // alloc::String 67 | 68 | impl Key for String { 69 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 70 | if buffer.len() < self.len() + 2 { 71 | return Err(SerializationError::BufferTooSmall); 72 | } 73 | 74 | if self.len() > u16::MAX as usize { 75 | return Err(SerializationError::InvalidData); 76 | } 77 | 78 | buffer[..2].copy_from_slice(&(self.len() as u16).to_le_bytes()); 79 | buffer[2..][..self.len()].copy_from_slice(self.as_bytes()); 80 | 81 | Ok(self.len() + 2) 82 | } 83 | 84 | fn deserialize_from(buffer: &[u8]) -> Result<(Self, usize), SerializationError> { 85 | let total_len = Self::get_len(buffer)?; 86 | 87 | if buffer.len() < total_len { 88 | return Err(SerializationError::BufferTooSmall); 89 | } 90 | 91 | let data_len = total_len - 2; 92 | 93 | let output = String::from( 94 | core::str::from_utf8(&buffer[2..][..data_len]) 95 | .map_err(|_| SerializationError::InvalidFormat)?, 96 | ); 97 | 98 | Ok((output, total_len)) 99 | } 100 | 101 | fn get_len(buffer: &[u8]) -> Result { 102 | if buffer.len() < 2 { 103 | return Err(SerializationError::BufferTooSmall); 104 | } 105 | 106 | let len = u16::from_le_bytes(buffer[..2].try_into().unwrap()); 107 | 108 | Ok(len as usize + 2) 109 | } 110 | } 111 | 112 | impl<'a> Value<'a> for String { 113 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 114 | if buffer.len() < self.len() { 115 | return Err(SerializationError::BufferTooSmall); 116 | } 117 | 118 | buffer[..self.len()].copy_from_slice(self.as_bytes()); 119 | Ok(self.len()) 120 | } 121 | 122 | fn deserialize_from(buffer: &'a [u8]) -> Result 123 | where 124 | Self: Sized, 125 | { 126 | let output = String::from( 127 | core::str::from_utf8(buffer).map_err(|_| SerializationError::InvalidFormat)?, 128 | ); 129 | 130 | Ok(output) 131 | } 132 | } 133 | 134 | #[cfg(test)] 135 | mod tests { 136 | use super::*; 137 | 138 | #[test] 139 | fn key_serde_alloc_vec() { 140 | let mut buffer = [0; 128]; 141 | 142 | let val = Vec::from_iter([0xAAu8; 12]); 143 | Key::serialize_into(&val, &mut buffer).unwrap(); 144 | let new_val = as Key>::deserialize_from(&buffer).unwrap(); 145 | 146 | assert_eq!((val, 14), new_val); 147 | } 148 | 149 | #[test] 150 | fn key_serde_alloc_string() { 151 | let mut buffer = [0; 128]; 152 | 153 | let val = String::from("Hello world!"); 154 | Key::serialize_into(&val, &mut buffer).unwrap(); 155 | let new_val = ::deserialize_from(&buffer).unwrap(); 156 | 157 | assert_eq!((val, 14), new_val); 158 | } 159 | 160 | #[test] 161 | fn value_serde_alloc_vec() { 162 | let mut buffer = [0; 12]; 163 | 164 | let val = Vec::from_iter([0xAAu8; 12]); 165 | Value::serialize_into(&val, &mut buffer).unwrap(); 166 | let new_val = as Value>::deserialize_from(&buffer).unwrap(); 167 | 168 | assert_eq!(val, new_val); 169 | } 170 | 171 | #[test] 172 | fn value_serde_alloc_string() { 173 | let mut buffer = [0; 12]; 174 | 175 | let val = String::from("Hello world!"); 176 | Value::serialize_into(&val, &mut buffer).unwrap(); 177 | let new_val = ::deserialize_from(&buffer).unwrap(); 178 | 179 | assert_eq!(val, new_val); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/arrayvec_impl.rs: -------------------------------------------------------------------------------- 1 | use arrayvec::{ArrayString, ArrayVec}; 2 | 3 | use crate::map::{Key, SerializationError}; 4 | 5 | impl Key for ArrayVec { 6 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 7 | if buffer.len() < self.len() + 2 { 8 | return Err(SerializationError::BufferTooSmall); 9 | } 10 | 11 | if self.len() > u16::MAX as usize { 12 | return Err(SerializationError::InvalidData); 13 | } 14 | 15 | buffer[..2].copy_from_slice(&(self.len() as u16).to_le_bytes()); 16 | buffer[2..][..self.len()].copy_from_slice(self); 17 | 18 | Ok(self.len() + 2) 19 | } 20 | 21 | fn deserialize_from(buffer: &[u8]) -> Result<(Self, usize), SerializationError> { 22 | let total_len = Self::get_len(buffer)?; 23 | 24 | if buffer.len() < total_len { 25 | return Err(SerializationError::BufferTooSmall); 26 | } 27 | 28 | let data_len = total_len - 2; 29 | 30 | let mut output = ArrayVec::new(); 31 | output 32 | .try_extend_from_slice(&buffer[2..][..data_len]) 33 | .map_err(|_| SerializationError::InvalidFormat)?; 34 | 35 | Ok((output, total_len)) 36 | } 37 | 38 | fn get_len(buffer: &[u8]) -> Result { 39 | if buffer.len() < 2 { 40 | return Err(SerializationError::BufferTooSmall); 41 | } 42 | 43 | let len = u16::from_le_bytes(buffer[..2].try_into().unwrap()); 44 | 45 | Ok(len as usize + 2) 46 | } 47 | } 48 | 49 | impl Key for ArrayString { 50 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 51 | if buffer.len() < self.len() + 2 { 52 | return Err(SerializationError::BufferTooSmall); 53 | } 54 | 55 | if self.len() > u16::MAX as usize { 56 | return Err(SerializationError::InvalidData); 57 | } 58 | 59 | buffer[..2].copy_from_slice(&(self.len() as u16).to_le_bytes()); 60 | buffer[2..][..self.len()].copy_from_slice(self.as_bytes()); 61 | 62 | Ok(self.len() + 2) 63 | } 64 | 65 | fn deserialize_from(buffer: &[u8]) -> Result<(Self, usize), SerializationError> { 66 | let total_len = Self::get_len(buffer)?; 67 | 68 | if buffer.len() < total_len { 69 | return Err(SerializationError::BufferTooSmall); 70 | } 71 | 72 | let data_len = total_len - 2; 73 | 74 | let mut output = ArrayString::new(); 75 | output 76 | .try_push_str( 77 | core::str::from_utf8(&buffer[2..][..data_len]) 78 | .map_err(|_| SerializationError::InvalidFormat)?, 79 | ) 80 | .map_err(|_| SerializationError::InvalidFormat)?; 81 | 82 | Ok((output, total_len)) 83 | } 84 | 85 | fn get_len(buffer: &[u8]) -> Result { 86 | if buffer.len() < 2 { 87 | return Err(SerializationError::BufferTooSmall); 88 | } 89 | 90 | let len = u16::from_le_bytes(buffer[..2].try_into().unwrap()); 91 | 92 | Ok(len as usize + 2) 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use core::str::FromStr; 99 | 100 | use super::*; 101 | 102 | #[test] 103 | fn serde_arrayvec() { 104 | let mut buffer = [0; 128]; 105 | 106 | let val = ArrayVec::::from_iter([0xAA; 12]); 107 | val.serialize_into(&mut buffer).unwrap(); 108 | let new_val = ArrayVec::::deserialize_from(&buffer).unwrap(); 109 | 110 | assert_eq!((val, 14), new_val); 111 | } 112 | 113 | #[test] 114 | fn serde_arraystring() { 115 | let mut buffer = [0; 128]; 116 | 117 | let val = ArrayString::<45>::from_str("Hello world!").unwrap(); 118 | val.serialize_into(&mut buffer).unwrap(); 119 | let new_val = ArrayString::<45>::deserialize_from(&buffer).unwrap(); 120 | 121 | assert_eq!((val, 14), new_val); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/cache/key_pointers.rs: -------------------------------------------------------------------------------- 1 | use core::{fmt::Debug, num::NonZeroU32}; 2 | 3 | use crate::map::Key; 4 | 5 | pub(crate) trait KeyPointersCache { 6 | fn key_location(&self, key: &KEY) -> Option; 7 | 8 | fn notice_key_location(&mut self, key: &KEY, item_address: u32); 9 | fn notice_key_erased(&mut self, key: &KEY); 10 | 11 | fn invalidate_cache_state(&mut self); 12 | } 13 | 14 | #[derive(Debug)] 15 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 16 | pub(crate) struct CachedKeyPointers { 17 | key_pointers: [Option<(KEY, NonZeroU32)>; KEYS], 18 | } 19 | 20 | impl CachedKeyPointers { 21 | const ARRAY_REPEAT_VALUE: Option<(KEY, NonZeroU32)> = None; 22 | 23 | pub(crate) const fn new() -> Self { 24 | Self { 25 | key_pointers: [Self::ARRAY_REPEAT_VALUE; KEYS], 26 | } 27 | } 28 | 29 | fn key_index(&self, key: &KEY) -> Option { 30 | self.key_pointers 31 | .iter() 32 | .enumerate() 33 | .filter_map(|(index, val)| val.as_ref().map(|val| (index, val))) 34 | .find_map( 35 | |(index, (known_key, _))| { 36 | if key == known_key { Some(index) } else { None } 37 | }, 38 | ) 39 | } 40 | 41 | fn insert_front(&mut self, value: (KEY, NonZeroU32)) { 42 | self.key_pointers[KEYS - 1] = Some(value); 43 | move_to_front(&mut self.key_pointers, KEYS - 1); 44 | } 45 | } 46 | 47 | impl KeyPointersCache for CachedKeyPointers { 48 | fn key_location(&self, key: &KEY) -> Option { 49 | self.key_index(key) 50 | .map(|index| self.key_pointers[index].as_ref().unwrap().1.get()) 51 | } 52 | 53 | fn notice_key_location(&mut self, key: &KEY, item_address: u32) { 54 | match self.key_index(key) { 55 | Some(existing_index) => { 56 | self.key_pointers[existing_index] = 57 | Some((key.clone(), NonZeroU32::new(item_address).unwrap())); 58 | move_to_front(&mut self.key_pointers, existing_index); 59 | } 60 | None => self.insert_front((key.clone(), NonZeroU32::new(item_address).unwrap())), 61 | } 62 | } 63 | 64 | fn notice_key_erased(&mut self, key: &KEY) { 65 | if let Some(existing_index) = self.key_index(key) { 66 | self.key_pointers[existing_index] = None; 67 | move_to_back(&mut self.key_pointers, existing_index); 68 | } 69 | } 70 | 71 | fn invalidate_cache_state(&mut self) { 72 | *self = Self::new(); 73 | } 74 | } 75 | 76 | #[derive(Debug)] 77 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 78 | pub(crate) struct UncachedKeyPointers; 79 | 80 | impl KeyPointersCache for UncachedKeyPointers { 81 | fn key_location(&self, _key: &KEY) -> Option { 82 | None 83 | } 84 | 85 | fn notice_key_location(&mut self, _key: &KEY, _item_address: u32) {} 86 | 87 | fn notice_key_erased(&mut self, _key: &KEY) {} 88 | 89 | fn invalidate_cache_state(&mut self) {} 90 | } 91 | 92 | fn move_to_front(data: &mut [Option], index: usize) { 93 | assert!(index < data.len()); 94 | 95 | // Swap the item we're moving into this temporary 96 | let mut item = None; 97 | core::mem::swap(&mut item, &mut data[index]); 98 | 99 | unsafe { 100 | // Move the items until the index back one. 101 | // This overwrites the None we just put in. 102 | // This is fine because it's none and no drop has to occur 103 | let ptr = data.as_mut_ptr(); 104 | ptr.copy_to(ptr.add(1), index); 105 | 106 | // The item in front is now duplicated. 107 | // Swap back our item into the front. 108 | core::mem::swap(&mut item, &mut data[0]); 109 | // The duplicated item must not drop, so just forget it 110 | core::mem::forget(item); 111 | } 112 | } 113 | 114 | fn move_to_back(data: &mut [Option], index: usize) { 115 | assert!(index < data.len()); 116 | 117 | // Swap the item we're moving into this temporary 118 | let mut item = None; 119 | core::mem::swap(&mut item, &mut data[index]); 120 | 121 | unsafe { 122 | // Move the items until the index back one. 123 | // This overwrites the None we just put in. 124 | // This is fine because it's none and no drop has to occur 125 | let ptr = data.as_mut_ptr(); 126 | ptr.add(index + 1) 127 | .copy_to(ptr.add(index), data.len() - 1 - index); 128 | 129 | // The item in front is now duplicated. 130 | // Swap back our item into the back. 131 | core::mem::swap(&mut item, &mut data[data.len() - 1]); 132 | // The duplicated item must not drop, so just forget it 133 | core::mem::forget(item); 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::*; 140 | 141 | #[test] 142 | fn test_move_to_front() { 143 | let mut array = [ 144 | Some("0".into()), 145 | Some("1".into()), 146 | Some("2".into()), 147 | Some("3".into()), 148 | ]; 149 | move_to_front::(&mut array, 0); 150 | assert_eq!( 151 | array, 152 | [ 153 | Some("0".into()), 154 | Some("1".into()), 155 | Some("2".into()), 156 | Some("3".into()), 157 | ] 158 | ); 159 | 160 | let mut array = [ 161 | Some("0".into()), 162 | Some("1".into()), 163 | Some("2".into()), 164 | Some("3".into()), 165 | ]; 166 | move_to_front::(&mut array, 1); 167 | assert_eq!( 168 | array, 169 | [ 170 | Some("1".into()), 171 | Some("0".into()), 172 | Some("2".into()), 173 | Some("3".into()), 174 | ] 175 | ); 176 | 177 | let mut array = [ 178 | Some("0".into()), 179 | Some("1".into()), 180 | Some("2".into()), 181 | Some("3".into()), 182 | ]; 183 | move_to_front::(&mut array, 2); 184 | assert_eq!( 185 | array, 186 | [ 187 | Some("2".into()), 188 | Some("0".into()), 189 | Some("1".into()), 190 | Some("3".into()), 191 | ] 192 | ); 193 | 194 | let mut array = [ 195 | Some("0".into()), 196 | Some("1".into()), 197 | Some("2".into()), 198 | Some("3".into()), 199 | ]; 200 | move_to_front::(&mut array, 3); 201 | assert_eq!( 202 | array, 203 | [ 204 | Some("3".into()), 205 | Some("0".into()), 206 | Some("1".into()), 207 | Some("2".into()), 208 | ] 209 | ); 210 | } 211 | 212 | #[test] 213 | fn test_move_to_back() { 214 | let mut array = [ 215 | Some("0".into()), 216 | Some("1".into()), 217 | Some("2".into()), 218 | Some("3".into()), 219 | ]; 220 | move_to_back::(&mut array, 0); 221 | assert_eq!( 222 | array, 223 | [ 224 | Some("1".into()), 225 | Some("2".into()), 226 | Some("3".into()), 227 | Some("0".into()), 228 | ] 229 | ); 230 | 231 | let mut array = [ 232 | Some("0".into()), 233 | Some("1".into()), 234 | Some("2".into()), 235 | Some("3".into()), 236 | ]; 237 | move_to_back::(&mut array, 1); 238 | assert_eq!( 239 | array, 240 | [ 241 | Some("0".into()), 242 | Some("2".into()), 243 | Some("3".into()), 244 | Some("1".into()), 245 | ] 246 | ); 247 | 248 | let mut array = [ 249 | Some("0".into()), 250 | Some("1".into()), 251 | Some("2".into()), 252 | Some("3".into()), 253 | ]; 254 | move_to_back::(&mut array, 2); 255 | assert_eq!( 256 | array, 257 | [ 258 | Some("0".into()), 259 | Some("1".into()), 260 | Some("3".into()), 261 | Some("2".into()), 262 | ] 263 | ); 264 | 265 | let mut array = [ 266 | Some("0".into()), 267 | Some("1".into()), 268 | Some("2".into()), 269 | Some("3".into()), 270 | ]; 271 | move_to_back::(&mut array, 3); 272 | assert_eq!( 273 | array, 274 | [ 275 | Some("0".into()), 276 | Some("1".into()), 277 | Some("2".into()), 278 | Some("3".into()), 279 | ] 280 | ); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module implementing all things cache related 2 | 3 | use core::{fmt::Debug, ops::Range}; 4 | 5 | use embedded_storage_async::nor_flash::NorFlash; 6 | 7 | use crate::{PageState, item::ItemHeader, map::Key}; 8 | 9 | use self::{ 10 | key_pointers::{CachedKeyPointers, KeyPointersCache, UncachedKeyPointers}, 11 | page_pointers::{CachedPagePointers, UncachedPagePointers}, 12 | page_states::{CachedPageStates, UncachedPageStates}, 13 | }; 14 | 15 | pub(crate) mod key_pointers; 16 | pub(crate) mod page_pointers; 17 | pub(crate) mod page_states; 18 | mod tests; 19 | 20 | pub(crate) use page_pointers::PagePointersCache; 21 | pub(crate) use page_states::PageStatesCache; 22 | 23 | /// Trait implemented by all cache types 24 | #[allow(private_bounds)] 25 | pub trait CacheImpl: PrivateCacheImpl {} 26 | 27 | /// Trait implemented by all cache types that know about keys 28 | #[allow(private_bounds)] 29 | pub trait KeyCacheImpl: CacheImpl + PrivateKeyCacheImpl {} 30 | 31 | pub(crate) trait Invalidate { 32 | fn invalidate_cache_state(&mut self); 33 | } 34 | 35 | impl Invalidate for &mut T { 36 | fn invalidate_cache_state(&mut self) { 37 | T::invalidate_cache_state(self) 38 | } 39 | } 40 | 41 | pub(crate) trait PrivateCacheImpl: Invalidate { 42 | type PSC: PageStatesCache; 43 | type PPC: PagePointersCache; 44 | 45 | fn dirt_tracker(&mut self, f: impl FnOnce(&mut DirtTracker) -> R) -> Option; 46 | fn page_states(&mut self) -> &mut Self::PSC; 47 | fn page_pointers(&mut self) -> &mut Self::PPC; 48 | 49 | /// True if the cache might be inconsistent 50 | fn is_dirty(&mut self) -> bool { 51 | self.dirt_tracker(|d| d.is_dirty()).unwrap_or_default() 52 | } 53 | 54 | /// Mark the cache as potentially inconsistent with reality 55 | fn mark_dirty(&mut self) { 56 | self.dirt_tracker(|d| d.mark_dirty()); 57 | } 58 | 59 | /// Mark the cache as being consistent with reality 60 | fn unmark_dirty(&mut self) { 61 | self.dirt_tracker(|d| d.unmark_dirty()); 62 | } 63 | 64 | /// Get the cache state of the requested page 65 | fn get_page_state(&mut self, page_index: usize) -> Option { 66 | self.page_states().get_page_state(page_index) 67 | } 68 | 69 | /// Let the cache know a page state changed 70 | /// 71 | /// The dirty flag should be true if the page state is actually going to be changed. 72 | /// Keep it false if we're only discovering the state. 73 | fn notice_page_state(&mut self, page_index: usize, new_state: PageState, dirty: bool) { 74 | if dirty { 75 | self.mark_dirty(); 76 | } 77 | self.page_states().notice_page_state(page_index, new_state); 78 | self.page_pointers() 79 | .notice_page_state(page_index, new_state); 80 | } 81 | 82 | /// Get the cached address of the first non-erased item in the requested page. 83 | /// 84 | /// This is purely for the items that get erased from start to end. 85 | fn first_item_after_erased(&mut self, page_index: usize) -> Option { 86 | self.page_pointers().first_item_after_erased(page_index) 87 | } 88 | 89 | /// Get the cached address of the first free unwritten item in the requested page. 90 | fn first_item_after_written(&mut self, page_index: usize) -> Option { 91 | self.page_pointers().first_item_after_written(page_index) 92 | } 93 | 94 | /// Let the cache know that an item has been written to flash 95 | fn notice_item_written( 96 | &mut self, 97 | flash_range: Range, 98 | item_address: u32, 99 | item_header: &ItemHeader, 100 | ) { 101 | self.mark_dirty(); 102 | self.page_pointers() 103 | .notice_item_written::(flash_range, item_address, item_header) 104 | } 105 | 106 | /// Let the cache know that an item has been erased from flash 107 | fn notice_item_erased( 108 | &mut self, 109 | flash_range: Range, 110 | item_address: u32, 111 | item_header: &ItemHeader, 112 | ) { 113 | self.mark_dirty(); 114 | self.page_pointers() 115 | .notice_item_erased::(flash_range, item_address, item_header) 116 | } 117 | } 118 | 119 | impl PrivateCacheImpl for &mut T { 120 | type PSC = T::PSC; 121 | type PPC = T::PPC; 122 | 123 | fn dirt_tracker(&mut self, f: impl FnOnce(&mut DirtTracker) -> R) -> Option { 124 | T::dirt_tracker(self, f) 125 | } 126 | 127 | fn page_states(&mut self) -> &mut Self::PSC { 128 | T::page_states(self) 129 | } 130 | 131 | fn page_pointers(&mut self) -> &mut Self::PPC { 132 | T::page_pointers(self) 133 | } 134 | } 135 | 136 | pub(crate) trait PrivateKeyCacheImpl: PrivateCacheImpl { 137 | type KPC: KeyPointersCache; 138 | 139 | fn key_pointers(&mut self) -> &mut Self::KPC; 140 | 141 | fn key_location(&mut self, key: &KEY) -> Option { 142 | self.key_pointers().key_location(key) 143 | } 144 | 145 | fn notice_key_location(&mut self, key: &KEY, item_address: u32, dirty: bool) { 146 | if dirty { 147 | self.mark_dirty(); 148 | } 149 | self.key_pointers().notice_key_location(key, item_address) 150 | } 151 | #[allow(unused)] 152 | fn notice_key_erased(&mut self, key: &KEY) { 153 | self.mark_dirty(); 154 | self.key_pointers().notice_key_erased(key) 155 | } 156 | } 157 | 158 | impl> PrivateKeyCacheImpl for &mut T { 159 | type KPC = T::KPC; 160 | 161 | fn key_pointers(&mut self) -> &mut Self::KPC { 162 | T::key_pointers(self) 163 | } 164 | } 165 | 166 | #[derive(Debug)] 167 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 168 | pub(crate) struct DirtTracker { 169 | /// Managed from the library code. 170 | /// 171 | /// When true, the cache and/or flash has changed and things might not be fully 172 | /// consistent if there's an early return due to error. 173 | dirty: bool, 174 | } 175 | 176 | impl DirtTracker { 177 | pub const fn new() -> Self { 178 | DirtTracker { dirty: false } 179 | } 180 | 181 | /// True if the cache might be inconsistent 182 | pub fn is_dirty(&self) -> bool { 183 | self.dirty 184 | } 185 | 186 | /// Mark the cache as potentially inconsistent with reality 187 | pub fn mark_dirty(&mut self) { 188 | self.dirty = true; 189 | } 190 | 191 | /// Mark the cache as being consistent with reality 192 | pub fn unmark_dirty(&mut self) { 193 | self.dirty = false; 194 | } 195 | } 196 | 197 | /// A cache object implementing no cache. 198 | /// 199 | /// This type of cache doesn't have to be kept around and may be constructed on every api call. 200 | /// You could simply pass `&mut NoCache::new()` every time. 201 | #[derive(Debug)] 202 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 203 | pub struct NoCache { 204 | page_states: UncachedPageStates, 205 | page_pointers: UncachedPagePointers, 206 | key_pointers: UncachedKeyPointers, 207 | } 208 | 209 | impl NoCache { 210 | /// Construct a new instance 211 | pub const fn new() -> Self { 212 | Self { 213 | page_states: UncachedPageStates, 214 | page_pointers: UncachedPagePointers, 215 | key_pointers: UncachedKeyPointers, 216 | } 217 | } 218 | } 219 | 220 | impl Default for NoCache { 221 | fn default() -> Self { 222 | Self::new() 223 | } 224 | } 225 | 226 | impl PrivateCacheImpl for NoCache { 227 | type PSC = UncachedPageStates; 228 | type PPC = UncachedPagePointers; 229 | 230 | fn dirt_tracker(&mut self, _f: impl FnOnce(&mut DirtTracker) -> R) -> Option { 231 | // We have no state, so no need to track dirtyness 232 | None 233 | } 234 | 235 | fn page_states(&mut self) -> &mut Self::PSC { 236 | &mut self.page_states 237 | } 238 | 239 | fn page_pointers(&mut self) -> &mut Self::PPC { 240 | &mut self.page_pointers 241 | } 242 | } 243 | 244 | impl CacheImpl for NoCache {} 245 | impl KeyCacheImpl for NoCache {} 246 | 247 | impl Invalidate for NoCache { 248 | fn invalidate_cache_state(&mut self) {} 249 | } 250 | 251 | impl PrivateKeyCacheImpl for NoCache { 252 | type KPC = UncachedKeyPointers; 253 | 254 | fn key_pointers(&mut self) -> &mut Self::KPC { 255 | &mut self.key_pointers 256 | } 257 | } 258 | 259 | /// A cache object that keeps track of the page states. 260 | /// 261 | /// This cache has to be kept around and passed to *every* api call to the same memory region until the cache gets discarded. 262 | /// 263 | /// Valid usecase: 264 | /// `Create cache 1` -> `use 1` -> `use 1` -> `create cache 2` -> `use 2` -> `use 2` 265 | /// 266 | /// Invalid usecase: 267 | /// `Create cache 1` -> `use 1` -> `create cache 2` -> `use 2` -> `❌ use 1 ❌` 268 | /// 269 | /// Make sure the page count is correct. If the number is lower than the actual amount, the code will panic at some point. 270 | #[derive(Debug)] 271 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 272 | pub struct PageStateCache { 273 | dirt_tracker: DirtTracker, 274 | page_states: CachedPageStates, 275 | page_pointers: UncachedPagePointers, 276 | key_pointers: UncachedKeyPointers, 277 | } 278 | 279 | impl PageStateCache { 280 | /// Construct a new instance 281 | pub const fn new() -> Self { 282 | Self { 283 | dirt_tracker: DirtTracker::new(), 284 | page_states: CachedPageStates::new(), 285 | page_pointers: UncachedPagePointers, 286 | key_pointers: UncachedKeyPointers, 287 | } 288 | } 289 | } 290 | 291 | impl Default for PageStateCache { 292 | fn default() -> Self { 293 | Self::new() 294 | } 295 | } 296 | 297 | impl PrivateCacheImpl for PageStateCache { 298 | type PSC = CachedPageStates; 299 | type PPC = UncachedPagePointers; 300 | 301 | fn dirt_tracker(&mut self, f: impl FnOnce(&mut DirtTracker) -> R) -> Option { 302 | Some(f(&mut self.dirt_tracker)) 303 | } 304 | 305 | fn page_states(&mut self) -> &mut Self::PSC { 306 | &mut self.page_states 307 | } 308 | 309 | fn page_pointers(&mut self) -> &mut Self::PPC { 310 | &mut self.page_pointers 311 | } 312 | } 313 | 314 | impl CacheImpl for PageStateCache {} 315 | impl KeyCacheImpl for PageStateCache {} 316 | 317 | impl Invalidate for PageStateCache { 318 | fn invalidate_cache_state(&mut self) { 319 | self.dirt_tracker.unmark_dirty(); 320 | self.page_states.invalidate_cache_state(); 321 | self.page_pointers.invalidate_cache_state(); 322 | } 323 | } 324 | 325 | impl PrivateKeyCacheImpl for PageStateCache { 326 | type KPC = UncachedKeyPointers; 327 | 328 | fn key_pointers(&mut self) -> &mut Self::KPC { 329 | &mut self.key_pointers 330 | } 331 | } 332 | 333 | /// A cache object that keeps track of the page states and some pointers to the items in the page. 334 | /// 335 | /// This cache has to be kept around and passed to *every* api call to the same memory region until the cache gets discarded. 336 | /// 337 | /// Valid usecase: 338 | /// `Create cache 1` -> `use 1` -> `use 1` -> `create cache 2` -> `use 2` -> `use 2` 339 | /// 340 | /// Invalid usecase: 341 | /// `Create cache 1` -> `use 1` -> `create cache 2` -> `use 2` -> `❌ use 1 ❌` 342 | /// 343 | /// Make sure the page count is correct. If the number is lower than the actual amount, the code will panic at some point. 344 | #[derive(Debug)] 345 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 346 | pub struct PagePointerCache { 347 | dirt_tracker: DirtTracker, 348 | page_states: CachedPageStates, 349 | page_pointers: CachedPagePointers, 350 | key_pointers: UncachedKeyPointers, 351 | } 352 | 353 | impl PagePointerCache { 354 | /// Construct a new instance 355 | pub const fn new() -> Self { 356 | Self { 357 | dirt_tracker: DirtTracker::new(), 358 | page_states: CachedPageStates::new(), 359 | page_pointers: CachedPagePointers::new(), 360 | key_pointers: UncachedKeyPointers, 361 | } 362 | } 363 | } 364 | 365 | impl Default for PagePointerCache { 366 | fn default() -> Self { 367 | Self::new() 368 | } 369 | } 370 | 371 | impl PrivateCacheImpl for PagePointerCache { 372 | type PSC = CachedPageStates; 373 | type PPC = CachedPagePointers; 374 | 375 | fn dirt_tracker(&mut self, f: impl FnOnce(&mut DirtTracker) -> R) -> Option { 376 | Some(f(&mut self.dirt_tracker)) 377 | } 378 | 379 | fn page_states(&mut self) -> &mut Self::PSC { 380 | &mut self.page_states 381 | } 382 | 383 | fn page_pointers(&mut self) -> &mut Self::PPC { 384 | &mut self.page_pointers 385 | } 386 | } 387 | 388 | impl CacheImpl for PagePointerCache {} 389 | impl KeyCacheImpl for PagePointerCache {} 390 | 391 | impl Invalidate for PagePointerCache { 392 | fn invalidate_cache_state(&mut self) { 393 | self.dirt_tracker.unmark_dirty(); 394 | self.page_states.invalidate_cache_state(); 395 | self.page_pointers.invalidate_cache_state(); 396 | } 397 | } 398 | 399 | impl PrivateKeyCacheImpl for PagePointerCache { 400 | type KPC = UncachedKeyPointers; 401 | 402 | fn key_pointers(&mut self) -> &mut Self::KPC { 403 | &mut self.key_pointers 404 | } 405 | } 406 | 407 | /// An object that caches the location of the newest item with a given key. 408 | /// This cache also caches pages states and page pointers. 409 | /// 410 | /// This cache has to be kept around and passed to *every* api call to the same memory region until the cache gets discarded. 411 | /// 412 | /// Valid usecase: 413 | /// `Create cache 1` -> `use 1` -> `use 1` -> `create cache 2` -> `use 2` -> `use 2` 414 | /// 415 | /// Invalid usecase: 416 | /// `Create cache 1` -> `use 1` -> `create cache 2` -> `use 2` -> `❌ use 1 ❌` 417 | /// 418 | /// Make sure the page count is correct. If the number is lower than the actual amount, the code will panic at some point. 419 | /// 420 | /// The number of key slots can be lower than the total amount of possible keys used, but this will lower 421 | /// the chance of a cache hit. 422 | /// The keys are cached in a fifo and any time its location is updated in cache it's added to the front. 423 | #[derive(Debug)] 424 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 425 | pub struct KeyPointerCache { 426 | dirt_tracker: DirtTracker, 427 | page_states: CachedPageStates, 428 | page_pointers: CachedPagePointers, 429 | key_pointers: CachedKeyPointers, 430 | } 431 | 432 | impl KeyPointerCache { 433 | /// Construct a new instance 434 | pub const fn new() -> Self { 435 | Self { 436 | dirt_tracker: DirtTracker::new(), 437 | page_states: CachedPageStates::new(), 438 | page_pointers: CachedPagePointers::new(), 439 | key_pointers: CachedKeyPointers::new(), 440 | } 441 | } 442 | } 443 | 444 | impl Default 445 | for KeyPointerCache 446 | { 447 | fn default() -> Self { 448 | Self::new() 449 | } 450 | } 451 | 452 | impl PrivateCacheImpl 453 | for KeyPointerCache 454 | { 455 | type PSC = CachedPageStates; 456 | type PPC = CachedPagePointers; 457 | 458 | fn dirt_tracker(&mut self, f: impl FnOnce(&mut DirtTracker) -> R) -> Option { 459 | Some(f(&mut self.dirt_tracker)) 460 | } 461 | 462 | fn page_states(&mut self) -> &mut Self::PSC { 463 | &mut self.page_states 464 | } 465 | 466 | fn page_pointers(&mut self) -> &mut Self::PPC { 467 | &mut self.page_pointers 468 | } 469 | } 470 | 471 | impl CacheImpl 472 | for KeyPointerCache 473 | { 474 | } 475 | impl KeyCacheImpl 476 | for KeyPointerCache 477 | { 478 | } 479 | 480 | impl Invalidate 481 | for KeyPointerCache 482 | { 483 | fn invalidate_cache_state(&mut self) { 484 | self.dirt_tracker.unmark_dirty(); 485 | self.page_states.invalidate_cache_state(); 486 | self.page_pointers.invalidate_cache_state(); 487 | self.key_pointers.invalidate_cache_state(); 488 | } 489 | } 490 | 491 | impl PrivateKeyCacheImpl 492 | for KeyPointerCache 493 | { 494 | type KPC = CachedKeyPointers; 495 | 496 | fn key_pointers(&mut self) -> &mut Self::KPC { 497 | &mut self.key_pointers 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /src/cache/page_pointers.rs: -------------------------------------------------------------------------------- 1 | use core::{fmt::Debug, num::NonZeroU32, ops::Range}; 2 | 3 | use embedded_storage_async::nor_flash::NorFlash; 4 | 5 | use crate::{ 6 | NorFlashExt, PageState, calculate_page_address, calculate_page_index, item::ItemHeader, 7 | }; 8 | 9 | pub(crate) trait PagePointersCache: Debug { 10 | fn first_item_after_erased(&self, page_index: usize) -> Option; 11 | fn first_item_after_written(&self, page_index: usize) -> Option; 12 | 13 | fn notice_item_written( 14 | &mut self, 15 | flash_range: Range, 16 | item_address: u32, 17 | item_header: &ItemHeader, 18 | ); 19 | fn notice_item_erased( 20 | &mut self, 21 | flash_range: Range, 22 | item_address: u32, 23 | item_header: &ItemHeader, 24 | ); 25 | 26 | fn notice_page_state(&mut self, page_index: usize, new_state: PageState); 27 | fn invalidate_cache_state(&mut self); 28 | } 29 | 30 | // Use NoneZeroU32 because we never store 0's in here (because of the first page marker) 31 | // and so Option can make use of the niche so we save bytes 32 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 33 | pub(crate) struct CachedPagePointers { 34 | after_erased_pointers: [Option; PAGE_COUNT], 35 | after_written_pointers: [Option; PAGE_COUNT], 36 | } 37 | 38 | impl Debug for CachedPagePointers { 39 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 40 | write!(f, "{{ after_erased_pointers: [")?; 41 | for (i, val) in self.after_erased_pointers.iter().enumerate() { 42 | if i > 0 { 43 | write!(f, ", ")?; 44 | } 45 | 46 | if let Some(val) = val { 47 | write!(f, "{:?}", val.get())?; 48 | } else { 49 | write!(f, "?")?; 50 | } 51 | } 52 | write!(f, "], after_written_pointers: [")?; 53 | for (i, val) in self.after_written_pointers.iter().enumerate() { 54 | if i > 0 { 55 | write!(f, ", ")?; 56 | } 57 | 58 | if let Some(val) = val { 59 | write!(f, "{:?}", val.get())?; 60 | } else { 61 | write!(f, "?")?; 62 | } 63 | } 64 | write!(f, "] }}")?; 65 | 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl CachedPagePointers { 71 | pub const fn new() -> Self { 72 | Self { 73 | after_erased_pointers: [None; PAGE_COUNT], 74 | after_written_pointers: [None; PAGE_COUNT], 75 | } 76 | } 77 | } 78 | 79 | impl PagePointersCache for CachedPagePointers { 80 | fn first_item_after_erased(&self, page_index: usize) -> Option { 81 | self.after_erased_pointers[page_index].map(|val| val.get()) 82 | } 83 | 84 | fn first_item_after_written(&self, page_index: usize) -> Option { 85 | self.after_written_pointers[page_index].map(|val| val.get()) 86 | } 87 | 88 | fn notice_item_written( 89 | &mut self, 90 | flash_range: Range, 91 | item_address: u32, 92 | item_header: &ItemHeader, 93 | ) { 94 | let page_index = calculate_page_index::(flash_range, item_address); 95 | 96 | let next_item_address = item_header.next_item_address::(item_address); 97 | 98 | // We only care about the furthest written item, so discard if this is an earlier item 99 | if let Some(first_item_after_written) = self.first_item_after_written(page_index) { 100 | if next_item_address <= first_item_after_written { 101 | return; 102 | } 103 | } 104 | 105 | self.after_written_pointers[page_index] = NonZeroU32::new(next_item_address); 106 | } 107 | 108 | fn notice_item_erased( 109 | &mut self, 110 | flash_range: Range, 111 | item_address: u32, 112 | item_header: &ItemHeader, 113 | ) { 114 | let page_index = calculate_page_index::(flash_range.clone(), item_address); 115 | 116 | // Either the item we point to or the first item on the page 117 | let next_unerased_item = self.first_item_after_erased(page_index).unwrap_or_else(|| { 118 | calculate_page_address::(flash_range, page_index) + S::WORD_SIZE as u32 119 | }); 120 | 121 | if item_address == next_unerased_item { 122 | self.after_erased_pointers[page_index] = 123 | NonZeroU32::new(item_header.next_item_address::(item_address)); 124 | } 125 | } 126 | 127 | fn notice_page_state(&mut self, page_index: usize, new_state: PageState) { 128 | if new_state.is_open() { 129 | // This page was erased 130 | self.after_erased_pointers[page_index] = None; 131 | self.after_written_pointers[page_index] = None; 132 | } 133 | } 134 | 135 | fn invalidate_cache_state(&mut self) { 136 | self.after_erased_pointers = [None; PAGE_COUNT]; 137 | self.after_written_pointers = [None; PAGE_COUNT]; 138 | } 139 | } 140 | 141 | #[derive(Debug, Default)] 142 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 143 | pub(crate) struct UncachedPagePointers; 144 | 145 | impl PagePointersCache for UncachedPagePointers { 146 | fn first_item_after_erased(&self, _page_index: usize) -> Option { 147 | None 148 | } 149 | 150 | fn first_item_after_written(&self, _page_index: usize) -> Option { 151 | None 152 | } 153 | 154 | fn notice_item_written( 155 | &mut self, 156 | _flash_range: Range, 157 | _item_address: u32, 158 | _item_header: &ItemHeader, 159 | ) { 160 | } 161 | 162 | fn notice_item_erased( 163 | &mut self, 164 | _flash_range: Range, 165 | _item_address: u32, 166 | _item_header: &ItemHeader, 167 | ) { 168 | } 169 | 170 | fn notice_page_state(&mut self, _page_index: usize, _new_state: PageState) {} 171 | 172 | fn invalidate_cache_state(&mut self) {} 173 | } 174 | -------------------------------------------------------------------------------- /src/cache/page_states.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use crate::PageState; 4 | 5 | pub(crate) trait PageStatesCache: Debug { 6 | fn get_page_state(&self, page_index: usize) -> Option; 7 | fn notice_page_state(&mut self, page_index: usize, new_state: PageState); 8 | fn invalidate_cache_state(&mut self); 9 | } 10 | 11 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 12 | pub(crate) struct CachedPageStates { 13 | pages: [Option; PAGE_COUNT], 14 | } 15 | 16 | impl Debug for CachedPageStates { 17 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 18 | write!(f, "[")?; 19 | for (i, val) in self.pages.iter().enumerate() { 20 | if i > 0 { 21 | write!(f, ", ")?; 22 | } 23 | 24 | if let Some(val) = val { 25 | write!(f, "{val:?}")?; 26 | } else { 27 | write!(f, "?")?; 28 | } 29 | } 30 | write!(f, "]")?; 31 | 32 | Ok(()) 33 | } 34 | } 35 | 36 | impl CachedPageStates { 37 | pub const fn new() -> Self { 38 | Self { 39 | pages: [None; PAGE_COUNT], 40 | } 41 | } 42 | } 43 | 44 | impl PageStatesCache for CachedPageStates { 45 | fn get_page_state(&self, page_index: usize) -> Option { 46 | self.pages[page_index] 47 | } 48 | 49 | fn notice_page_state(&mut self, page_index: usize, new_state: PageState) { 50 | self.pages[page_index] = Some(new_state); 51 | } 52 | 53 | fn invalidate_cache_state(&mut self) { 54 | *self = Self::new(); 55 | } 56 | } 57 | 58 | #[derive(Debug, Default)] 59 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 60 | pub(crate) struct UncachedPageStates; 61 | 62 | impl PageStatesCache for UncachedPageStates { 63 | fn get_page_state(&self, _page_index: usize) -> Option { 64 | None 65 | } 66 | 67 | fn notice_page_state(&mut self, _page_index: usize, _new_state: PageState) {} 68 | 69 | fn invalidate_cache_state(&mut self) {} 70 | } 71 | -------------------------------------------------------------------------------- /src/cache/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod queue_tests { 3 | use core::ops::Range; 4 | 5 | use crate::{ 6 | AlignedBuf, 7 | cache::{CacheImpl, NoCache, PagePointerCache, PageStateCache}, 8 | mock_flash::{self, FlashStatsResult, WriteCountCheck}, 9 | queue::{peek, pop, push}, 10 | }; 11 | 12 | use futures_test::test; 13 | 14 | const NUM_PAGES: usize = 4; 15 | const LOOP_COUNT: usize = 2000; 16 | 17 | #[test] 18 | async fn no_cache() { 19 | assert_eq!( 20 | run_test(&mut NoCache::new()).await, 21 | FlashStatsResult { 22 | erases: 146, 23 | reads: 594934, 24 | writes: 6299, 25 | bytes_read: 2766058, 26 | bytes_written: 53299 27 | } 28 | ); 29 | } 30 | 31 | #[test] 32 | async fn page_state_cache() { 33 | assert_eq!( 34 | run_test(&mut PageStateCache::::new()).await, 35 | FlashStatsResult { 36 | erases: 146, 37 | reads: 308740, 38 | writes: 6299, 39 | bytes_read: 2479864, 40 | bytes_written: 53299 41 | } 42 | ); 43 | } 44 | 45 | #[test] 46 | async fn page_pointer_cache() { 47 | assert_eq!( 48 | run_test(&mut PagePointerCache::::new()).await, 49 | FlashStatsResult { 50 | erases: 146, 51 | reads: 211172, 52 | writes: 6299, 53 | bytes_read: 1699320, 54 | bytes_written: 53299 55 | } 56 | ); 57 | } 58 | 59 | async fn run_test(cache: &mut impl CacheImpl) -> FlashStatsResult { 60 | let mut flash = 61 | mock_flash::MockFlashBase::::new(WriteCountCheck::Twice, None, true); 62 | const FLASH_RANGE: Range = 0x00..0x400; 63 | let mut data_buffer = AlignedBuf([0; 1024]); 64 | 65 | let start_snapshot = flash.stats_snapshot(); 66 | 67 | for i in 0..LOOP_COUNT { 68 | println!("{i}"); 69 | let data = vec![i as u8; i % 20 + 1]; 70 | 71 | println!("PUSH"); 72 | push(&mut flash, FLASH_RANGE, cache, &data, true) 73 | .await 74 | .unwrap(); 75 | assert_eq!( 76 | peek(&mut flash, FLASH_RANGE, cache, &mut data_buffer) 77 | .await 78 | .unwrap() 79 | .unwrap(), 80 | &data, 81 | "At {i}" 82 | ); 83 | println!("POP"); 84 | assert_eq!( 85 | pop(&mut flash, FLASH_RANGE, cache, &mut data_buffer) 86 | .await 87 | .unwrap() 88 | .unwrap(), 89 | &data, 90 | "At {i}" 91 | ); 92 | println!("PEEK"); 93 | assert_eq!( 94 | peek(&mut flash, FLASH_RANGE, cache, &mut data_buffer) 95 | .await 96 | .unwrap(), 97 | None, 98 | "At {i}" 99 | ); 100 | println!("DONE"); 101 | } 102 | 103 | start_snapshot.compare_to(flash.stats_snapshot()) 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | mod map_tests { 109 | use core::ops::Range; 110 | 111 | use crate::{ 112 | AlignedBuf, 113 | cache::{KeyCacheImpl, KeyPointerCache, NoCache, PagePointerCache, PageStateCache}, 114 | map::{fetch_item, store_item}, 115 | mock_flash::{self, FlashStatsResult, WriteCountCheck}, 116 | }; 117 | 118 | use futures_test::test; 119 | 120 | const NUM_PAGES: usize = 4; 121 | 122 | #[test] 123 | async fn no_cache() { 124 | assert_eq!( 125 | run_test(&mut NoCache::new()).await, 126 | FlashStatsResult { 127 | erases: 198, 128 | reads: 233786, 129 | writes: 5201, 130 | bytes_read: 1837101, 131 | bytes_written: 50401 132 | } 133 | ); 134 | } 135 | 136 | #[test] 137 | async fn page_state_cache() { 138 | assert_eq!( 139 | run_test(&mut PageStateCache::::new()).await, 140 | FlashStatsResult { 141 | erases: 198, 142 | reads: 181162, 143 | writes: 5201, 144 | bytes_read: 1784477, 145 | bytes_written: 50401 146 | } 147 | ); 148 | } 149 | 150 | #[test] 151 | async fn page_pointer_cache() { 152 | assert_eq!( 153 | run_test(&mut PagePointerCache::::new()).await, 154 | FlashStatsResult { 155 | erases: 198, 156 | reads: 163273, 157 | writes: 5201, 158 | bytes_read: 1641365, 159 | bytes_written: 50401 160 | } 161 | ); 162 | } 163 | 164 | #[test] 165 | async fn key_pointer_cache_half() { 166 | assert_eq!( 167 | run_test(&mut KeyPointerCache::::new()).await, 168 | FlashStatsResult { 169 | erases: 198, 170 | reads: 131503, 171 | writes: 5201, 172 | bytes_read: 1299275, 173 | bytes_written: 50401 174 | } 175 | ); 176 | } 177 | 178 | #[test] 179 | async fn key_pointer_cache_full() { 180 | assert_eq!( 181 | run_test(&mut KeyPointerCache::::new()).await, 182 | FlashStatsResult { 183 | erases: 198, 184 | reads: 14510, 185 | writes: 5201, 186 | bytes_read: 150592, 187 | bytes_written: 50401 188 | } 189 | ); 190 | } 191 | 192 | async fn run_test(cache: &mut impl KeyCacheImpl) -> FlashStatsResult { 193 | let mut flash = 194 | mock_flash::MockFlashBase::::new(WriteCountCheck::Twice, None, true); 195 | const FLASH_RANGE: Range = 0x00..0x400; 196 | let mut data_buffer = AlignedBuf([0; 128]); 197 | 198 | const LENGHT_PER_KEY: [usize; 24] = [ 199 | 11, 13, 6, 13, 13, 10, 2, 3, 5, 36, 1, 65, 4, 6, 1, 15, 10, 7, 3, 15, 9, 3, 4, 5, 200 | ]; 201 | 202 | let start_snapshot = flash.stats_snapshot(); 203 | 204 | for _ in 0..100 { 205 | const WRITE_ORDER: [usize; 24] = [ 206 | 15, 0, 4, 22, 18, 11, 19, 8, 14, 23, 5, 1, 16, 10, 6, 12, 20, 17, 3, 9, 7, 13, 21, 207 | 2, 208 | ]; 209 | 210 | for i in WRITE_ORDER { 211 | store_item( 212 | &mut flash, 213 | FLASH_RANGE, 214 | cache, 215 | &mut data_buffer, 216 | &(i as u16), 217 | &vec![i as u8; LENGHT_PER_KEY[i]].as_slice(), 218 | ) 219 | .await 220 | .unwrap(); 221 | } 222 | 223 | const READ_ORDER: [usize; 24] = [ 224 | 8, 22, 21, 11, 16, 23, 13, 15, 19, 7, 6, 2, 12, 1, 17, 4, 20, 14, 10, 5, 9, 3, 18, 225 | 0, 226 | ]; 227 | 228 | for i in READ_ORDER { 229 | let item = fetch_item::( 230 | &mut flash, 231 | FLASH_RANGE, 232 | cache, 233 | &mut data_buffer, 234 | &(i as u16), 235 | ) 236 | .await 237 | .unwrap() 238 | .unwrap(); 239 | 240 | // println!("Fetched {item:?}"); 241 | 242 | assert_eq!(item, vec![i as u8; LENGHT_PER_KEY[i]]); 243 | } 244 | } 245 | 246 | start_snapshot.compare_to(flash.stats_snapshot()) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/heapless_impl.rs: -------------------------------------------------------------------------------- 1 | use core::str::FromStr; 2 | 3 | use heapless::{String, Vec}; 4 | 5 | use crate::map::{Key, SerializationError, Value}; 6 | 7 | // heapless:: Vec 8 | 9 | impl Key for Vec { 10 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 11 | if buffer.len() < self.len() + 2 { 12 | return Err(SerializationError::BufferTooSmall); 13 | } 14 | 15 | if self.len() > u16::MAX as usize { 16 | return Err(SerializationError::InvalidData); 17 | } 18 | 19 | buffer[..2].copy_from_slice(&(self.len() as u16).to_le_bytes()); 20 | buffer[2..][..self.len()].copy_from_slice(self); 21 | 22 | Ok(self.len() + 2) 23 | } 24 | 25 | fn deserialize_from(buffer: &[u8]) -> Result<(Self, usize), SerializationError> { 26 | let total_len = Self::get_len(buffer)?; 27 | 28 | if buffer.len() < total_len { 29 | return Err(SerializationError::BufferTooSmall); 30 | } 31 | 32 | let data_len = total_len - 2; 33 | 34 | let mut output = Vec::new(); 35 | output 36 | .extend_from_slice(&buffer[2..][..data_len]) 37 | .map_err(|_| SerializationError::InvalidFormat)?; 38 | 39 | Ok((output, total_len)) 40 | } 41 | 42 | fn get_len(buffer: &[u8]) -> Result { 43 | if buffer.len() < 2 { 44 | return Err(SerializationError::BufferTooSmall); 45 | } 46 | 47 | let len = u16::from_le_bytes(buffer[..2].try_into().unwrap()); 48 | 49 | Ok(len as usize + 2) 50 | } 51 | } 52 | 53 | impl<'a, const CAP: usize> Value<'a> for Vec { 54 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 55 | if buffer.len() < self.len() { 56 | return Err(SerializationError::BufferTooSmall); 57 | } 58 | 59 | buffer[..self.len()].copy_from_slice(self.as_slice()); 60 | Ok(self.len()) 61 | } 62 | 63 | fn deserialize_from(buffer: &'a [u8]) -> Result 64 | where 65 | Self: Sized, 66 | { 67 | Vec::try_from(buffer).map_err(|_| SerializationError::InvalidFormat) 68 | } 69 | } 70 | 71 | // heapless::String 72 | 73 | impl Key for String { 74 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 75 | if buffer.len() < self.len() + 2 { 76 | return Err(SerializationError::InvalidFormat); 77 | } 78 | 79 | if self.len() > u16::MAX as usize { 80 | return Err(SerializationError::InvalidData); 81 | } 82 | 83 | buffer[..2].copy_from_slice(&(self.len() as u16).to_le_bytes()); 84 | buffer[2..][..self.len()].copy_from_slice(self.as_bytes()); 85 | 86 | Ok(self.len() + 2) 87 | } 88 | 89 | fn deserialize_from(buffer: &[u8]) -> Result<(Self, usize), SerializationError> { 90 | let total_len = Self::get_len(buffer)?; 91 | 92 | if buffer.len() < total_len { 93 | return Err(SerializationError::BufferTooSmall); 94 | } 95 | 96 | let data_len = total_len - 2; 97 | 98 | let mut output = String::new(); 99 | output 100 | .push_str( 101 | core::str::from_utf8(&buffer[2..][..data_len]) 102 | .map_err(|_| SerializationError::InvalidFormat)?, 103 | ) 104 | .map_err(|_| SerializationError::InvalidFormat)?; 105 | 106 | Ok((output, total_len)) 107 | } 108 | 109 | fn get_len(buffer: &[u8]) -> Result { 110 | if buffer.len() < 2 { 111 | return Err(SerializationError::BufferTooSmall); 112 | } 113 | 114 | let len = u16::from_le_bytes(buffer[..2].try_into().unwrap()); 115 | 116 | Ok(len as usize + 2) 117 | } 118 | } 119 | 120 | impl<'a, const CAP: usize> Value<'a> for String { 121 | fn serialize_into(&self, buffer: &mut [u8]) -> Result { 122 | if buffer.len() < self.len() { 123 | return Err(SerializationError::BufferTooSmall); 124 | } 125 | 126 | buffer[..self.len()].copy_from_slice(self.as_bytes()); 127 | Ok(self.len()) 128 | } 129 | 130 | fn deserialize_from(buffer: &'a [u8]) -> Result 131 | where 132 | Self: Sized, 133 | { 134 | let output = String::from_str( 135 | core::str::from_utf8(buffer).map_err(|_| SerializationError::InvalidFormat)?, 136 | ) 137 | .map_err(|_| SerializationError::BufferTooSmall)?; 138 | 139 | Ok(output) 140 | } 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | use core::str::FromStr; 146 | 147 | use super::*; 148 | 149 | #[test] 150 | fn key_serde_heapless_vec() { 151 | let mut buffer = [0; 128]; 152 | 153 | let val = Vec::::from_iter([0xAA; 12]); 154 | Key::serialize_into(&val, &mut buffer).unwrap(); 155 | let new_val = as Key>::deserialize_from(&buffer).unwrap(); 156 | 157 | assert_eq!((val, 14), new_val); 158 | } 159 | 160 | #[test] 161 | fn key_serde_heapless_string() { 162 | let mut buffer = [0; 128]; 163 | 164 | let val = String::<45>::from_str("Hello world!").unwrap(); 165 | Key::serialize_into(&val, &mut buffer).unwrap(); 166 | let new_val = as Key>::deserialize_from(&buffer).unwrap(); 167 | 168 | assert_eq!((val, 14), new_val); 169 | } 170 | 171 | #[test] 172 | fn value_serde_heapless_vec() { 173 | let mut buffer = [0; 12]; 174 | 175 | let val = Vec::::from_iter([0xAA; 12]); 176 | Value::serialize_into(&val, &mut buffer).unwrap(); 177 | let new_val = as Value>::deserialize_from(&buffer).unwrap(); 178 | 179 | assert_eq!(val, new_val); 180 | } 181 | 182 | #[test] 183 | fn value_serde_heapless_string() { 184 | let mut buffer = [0; 12]; 185 | 186 | let val = String::<45>::from_str("Hello world!").unwrap(); 187 | Value::serialize_into(&val, &mut buffer).unwrap(); 188 | let new_val = as Value>::deserialize_from(&buffer).unwrap(); 189 | 190 | assert_eq!(val, new_val); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/item.rs: -------------------------------------------------------------------------------- 1 | //! Module that implements storing raw items in flash. 2 | //! This module is page-unaware. This means that start and end addresses should be the actual 3 | //! start and end addresses that don't include the page markers. 4 | //! 5 | //! Memory layout of item: 6 | //! ```text 7 | //! ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ 8 | //! │ : : : │ : │ : │ : : : : : │ : : : : : : : : : │ : : : : : │ 9 | //! │ CRC │ Length │ Length' │Pad to word align│ Data │Pad to word align│ 10 | //! │ : : : │ : │ : │ : : : : : │ : : : : : : : : : │ : : : : : │ 11 | //! └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴───┴──┴──┴──┴─┴──┴──┴──┴──┴──┴──┘ 12 | //! 0 1 2 3 4 5 6 7 8 8+padding 8+padding+length 8+padding+length+endpadding 13 | //! ``` 14 | //! 15 | //! An item consists of an [ItemHeader] and some data. 16 | //! The header has a length field that encodes the length of the data, a [crc16] of the length (`Length'`) 17 | //! and a crc field that encodes the checksum of the data. 18 | //! 19 | //! If the crc is 0, then the item is counted as being erased. 20 | //! The crc is calculated by [adapted_crc32] which never produces a 0 value on its own 21 | //! and has some other modifications to make corruption less likely to happen. 22 | //! 23 | 24 | use core::num::NonZeroU32; 25 | use core::ops::Range; 26 | 27 | use embedded_storage_async::nor_flash::{MultiwriteNorFlash, NorFlash}; 28 | 29 | use crate::{ 30 | AlignedBuf, Error, MAX_WORD_SIZE, NorFlashExt, PageState, cache::PrivateCacheImpl, 31 | calculate_page_address, calculate_page_end_address, calculate_page_index, get_page_state, 32 | round_down_to_alignment, round_down_to_alignment_usize, round_up_to_alignment, 33 | round_up_to_alignment_usize, 34 | }; 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct ItemHeader { 38 | /// Length of the item payload (so not including the header and not including word alignment) 39 | pub length: u16, 40 | pub crc: Option, 41 | } 42 | 43 | impl ItemHeader { 44 | const LENGTH: usize = 8; 45 | 46 | const DATA_CRC_FIELD: Range = 0..4; 47 | const LENGTH_FIELD: Range = 4..6; 48 | const LENGTH_CRC_FIELD: Range = 6..8; 49 | 50 | /// Read the header from the flash at the given address. 51 | /// 52 | /// If the item doesn't exist or doesn't fit between the address and the end address, none is returned. 53 | pub async fn read_new( 54 | flash: &mut S, 55 | address: u32, 56 | end_address: u32, 57 | ) -> Result, Error> { 58 | let mut buffer = [0; MAX_WORD_SIZE]; 59 | let header_slice = &mut buffer[..round_up_to_alignment_usize::(Self::LENGTH)]; 60 | 61 | if address + header_slice.len() as u32 > end_address { 62 | return Ok(None); 63 | } 64 | 65 | flash 66 | .read(address, header_slice) 67 | .await 68 | .map_err(|e| Error::Storage { 69 | value: e, 70 | #[cfg(feature = "_test")] 71 | backtrace: std::backtrace::Backtrace::capture(), 72 | })?; 73 | 74 | if header_slice.iter().all(|b| *b == 0xFF) { 75 | // What we read was fully erased bytes, so there's no header here 76 | return Ok(None); 77 | } 78 | 79 | let length_crc = 80 | u16::from_le_bytes(header_slice[Self::LENGTH_CRC_FIELD].try_into().unwrap()); 81 | let calculated_length_crc = crc16(&header_slice[Self::LENGTH_FIELD]); 82 | 83 | if calculated_length_crc != length_crc { 84 | return Err(Error::Corrupted { 85 | #[cfg(feature = "_test")] 86 | backtrace: std::backtrace::Backtrace::capture(), 87 | }); 88 | } 89 | 90 | Ok(Some(Self { 91 | length: u16::from_le_bytes(header_slice[Self::LENGTH_FIELD].try_into().unwrap()), 92 | crc: { 93 | match u32::from_le_bytes(header_slice[Self::DATA_CRC_FIELD].try_into().unwrap()) { 94 | 0 => None, 95 | value => Some(NonZeroU32::new(value).unwrap()), 96 | } 97 | }, 98 | })) 99 | } 100 | 101 | pub async fn read_item<'d, S: NorFlash>( 102 | self, 103 | flash: &mut S, 104 | data_buffer: &'d mut [u8], 105 | address: u32, 106 | end_address: u32, 107 | ) -> Result, Error> { 108 | match self.crc { 109 | None => Ok(MaybeItem::Erased(self, data_buffer)), 110 | Some(header_crc) => { 111 | let data_address = ItemHeader::data_address::(address); 112 | let read_len = round_up_to_alignment_usize::(self.length as usize); 113 | if data_address + read_len as u32 > end_address { 114 | return Ok(MaybeItem::Corrupted(self, data_buffer)); 115 | } 116 | if data_buffer.len() < read_len { 117 | return Err(Error::BufferTooSmall(read_len)); 118 | } 119 | 120 | flash 121 | .read(data_address, &mut data_buffer[..read_len]) 122 | .await 123 | .map_err(|e| Error::Storage { 124 | value: e, 125 | #[cfg(feature = "_test")] 126 | backtrace: std::backtrace::Backtrace::capture(), 127 | })?; 128 | 129 | let data = &data_buffer[..self.length as usize]; 130 | let data_crc = adapted_crc32(data); 131 | 132 | if data_crc == header_crc { 133 | Ok(MaybeItem::Present(Item { 134 | header: self, 135 | data_buffer, 136 | })) 137 | } else { 138 | Ok(MaybeItem::Corrupted(self, data_buffer)) 139 | } 140 | } 141 | } 142 | } 143 | 144 | async fn write(&self, flash: &mut S, address: u32) -> Result<(), Error> { 145 | let mut buffer = AlignedBuf([0xFF; MAX_WORD_SIZE]); 146 | 147 | buffer[Self::DATA_CRC_FIELD] 148 | .copy_from_slice(&self.crc.map(|crc| crc.get()).unwrap_or(0).to_le_bytes()); 149 | buffer[Self::LENGTH_FIELD].copy_from_slice(&self.length.to_le_bytes()); 150 | buffer[Self::LENGTH_CRC_FIELD] 151 | .copy_from_slice(&crc16(&self.length.to_le_bytes()).to_le_bytes()); 152 | 153 | flash 154 | .write( 155 | address, 156 | &buffer[..round_up_to_alignment_usize::(Self::LENGTH)], 157 | ) 158 | .await 159 | .map_err(|e| Error::Storage { 160 | value: e, 161 | #[cfg(feature = "_test")] 162 | backtrace: std::backtrace::Backtrace::capture(), 163 | }) 164 | } 165 | 166 | /// Erase this item by setting the crc to none and overwriting the header with it 167 | pub async fn erase_data( 168 | mut self, 169 | flash: &mut S, 170 | flash_range: Range, 171 | cache: &mut impl PrivateCacheImpl, 172 | address: u32, 173 | ) -> Result> { 174 | self.crc = None; 175 | cache.notice_item_erased::(flash_range.clone(), address, &self); 176 | self.write(flash, address).await?; 177 | Ok(self) 178 | } 179 | 180 | /// Get the address of the start of the data for this item 181 | pub const fn data_address(address: u32) -> u32 { 182 | address + round_up_to_alignment::(Self::LENGTH as u32) 183 | } 184 | 185 | /// Get the location of the next item in flash 186 | pub const fn next_item_address(&self, address: u32) -> u32 { 187 | let data_address = ItemHeader::data_address::(address); 188 | data_address + round_up_to_alignment::(self.length as u32) 189 | } 190 | 191 | /// Calculates the amount of bytes available for data. 192 | /// Essentially, it's the given amount minus the header and minus some alignment padding. 193 | pub const fn available_data_bytes(total_available: u32) -> Option { 194 | let data_start = Self::data_address::(0); 195 | let data_end = round_down_to_alignment::(total_available); 196 | 197 | data_end.checked_sub(data_start) 198 | } 199 | } 200 | 201 | pub struct Item<'d> { 202 | pub header: ItemHeader, 203 | data_buffer: &'d mut [u8], 204 | } 205 | 206 | impl<'d> Item<'d> { 207 | pub fn data(&self) -> &[u8] { 208 | &self.data_buffer[..self.header.length as usize] 209 | } 210 | 211 | pub fn data_mut(&mut self) -> &mut [u8] { 212 | &mut self.data_buffer[..self.header.length as usize] 213 | } 214 | 215 | /// Destruct the item to get back the full data buffer 216 | pub fn destruct(self) -> (ItemHeader, &'d mut [u8]) { 217 | (self.header, self.data_buffer) 218 | } 219 | 220 | pub async fn write_new( 221 | flash: &mut S, 222 | flash_range: Range, 223 | cache: &mut impl PrivateCacheImpl, 224 | address: u32, 225 | data: &'d [u8], 226 | ) -> Result> { 227 | let header = ItemHeader { 228 | length: data.len() as u16, 229 | crc: Some(adapted_crc32(data)), 230 | }; 231 | 232 | Self::write_raw(flash, flash_range, cache, &header, data, address).await?; 233 | 234 | Ok(header) 235 | } 236 | 237 | async fn write_raw( 238 | flash: &mut S, 239 | flash_range: Range, 240 | cache: &mut impl PrivateCacheImpl, 241 | header: &ItemHeader, 242 | data: &[u8], 243 | address: u32, 244 | ) -> Result<(), Error> { 245 | cache.notice_item_written::(flash_range, address, header); 246 | header.write(flash, address).await?; 247 | 248 | let (data_block, data_left) = data.split_at(round_down_to_alignment_usize::(data.len())); 249 | 250 | let data_address = ItemHeader::data_address::(address); 251 | flash 252 | .write(data_address, data_block) 253 | .await 254 | .map_err(|e| Error::Storage { 255 | value: e, 256 | #[cfg(feature = "_test")] 257 | backtrace: std::backtrace::Backtrace::capture(), 258 | })?; 259 | 260 | if !data_left.is_empty() { 261 | let mut buffer = AlignedBuf([0; MAX_WORD_SIZE]); 262 | buffer[..data_left.len()].copy_from_slice(data_left); 263 | flash 264 | .write( 265 | data_address + data_block.len() as u32, 266 | &buffer[..round_up_to_alignment_usize::(data_left.len())], 267 | ) 268 | .await 269 | .map_err(|e| Error::Storage { 270 | value: e, 271 | #[cfg(feature = "_test")] 272 | backtrace: std::backtrace::Backtrace::capture(), 273 | })?; 274 | } 275 | 276 | Ok(()) 277 | } 278 | 279 | pub async fn write( 280 | &self, 281 | flash: &mut S, 282 | flash_range: Range, 283 | cache: &mut impl PrivateCacheImpl, 284 | address: u32, 285 | ) -> Result<(), Error> { 286 | Self::write_raw( 287 | flash, 288 | flash_range, 289 | cache, 290 | &self.header, 291 | self.data(), 292 | address, 293 | ) 294 | .await 295 | } 296 | 297 | pub fn unborrow(self) -> ItemUnborrowed { 298 | ItemUnborrowed { 299 | header: self.header, 300 | data_buffer_len: self.data_buffer.len(), 301 | } 302 | } 303 | } 304 | 305 | impl core::fmt::Debug for Item<'_> { 306 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 307 | f.debug_struct("Item") 308 | .field("header", &self.header) 309 | .field( 310 | "data_buffer", 311 | &&self.data_buffer[..self.header.length as usize], 312 | ) 313 | .finish() 314 | } 315 | } 316 | 317 | /// A version of [Item] that does not borrow the data. This is to circumvent the borrowchecker in some places. 318 | pub struct ItemUnborrowed { 319 | pub header: ItemHeader, 320 | data_buffer_len: usize, 321 | } 322 | 323 | impl ItemUnborrowed { 324 | /// Reborrows the data. Watch out! Make sure the data buffer hasn't changed since unborrowing! 325 | pub fn reborrow(self, data_buffer: &mut [u8]) -> Item<'_> { 326 | Item { 327 | header: self.header, 328 | data_buffer: &mut data_buffer[..self.data_buffer_len], 329 | } 330 | } 331 | } 332 | 333 | /// Scans through the items to find the first spot that is free to store a new item. 334 | /// 335 | /// - `end_address` is exclusive. 336 | pub async fn find_next_free_item_spot( 337 | flash: &mut S, 338 | flash_range: Range, 339 | cache: &mut impl PrivateCacheImpl, 340 | start_address: u32, 341 | end_address: u32, 342 | data_length: u32, 343 | ) -> Result, Error> { 344 | let page_index = calculate_page_index::(flash_range, start_address); 345 | 346 | let free_item_address = match cache.first_item_after_written(page_index) { 347 | Some(free_item_address) => free_item_address, 348 | None => { 349 | ItemHeaderIter::new( 350 | cache 351 | .first_item_after_erased(page_index) 352 | .unwrap_or(0) 353 | .max(start_address), 354 | end_address, 355 | ) 356 | .traverse(flash, |_, _| true) 357 | .await? 358 | .1 359 | } 360 | }; 361 | 362 | if let Some(available) = ItemHeader::available_data_bytes::(end_address - free_item_address) 363 | { 364 | if available >= data_length { 365 | Ok(Some(free_item_address)) 366 | } else { 367 | Ok(None) 368 | } 369 | } else { 370 | Ok(None) 371 | } 372 | } 373 | 374 | pub enum MaybeItem<'d> { 375 | Corrupted(ItemHeader, &'d mut [u8]), 376 | Erased(ItemHeader, &'d mut [u8]), 377 | Present(Item<'d>), 378 | } 379 | 380 | impl core::fmt::Debug for MaybeItem<'_> { 381 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 382 | match self { 383 | Self::Corrupted(arg0, arg1) => f 384 | .debug_tuple("Corrupted") 385 | .field(arg0) 386 | .field(&arg1.get(..arg0.length as usize)) 387 | .finish(), 388 | Self::Erased(arg0, _) => f.debug_tuple("Erased").field(arg0).finish(), 389 | Self::Present(arg0) => f.debug_tuple("Present").field(arg0).finish(), 390 | } 391 | } 392 | } 393 | 394 | impl<'d> MaybeItem<'d> { 395 | pub fn unwrap(self) -> Result, Error> { 396 | match self { 397 | MaybeItem::Corrupted(_, _) => Err(Error::Corrupted { 398 | #[cfg(feature = "_test")] 399 | backtrace: std::backtrace::Backtrace::capture(), 400 | }), 401 | MaybeItem::Erased(_, _) => panic!("Cannot unwrap an erased item"), 402 | MaybeItem::Present(item) => Ok(item), 403 | } 404 | } 405 | } 406 | 407 | /// A crc that never returns 0xFFFF 408 | fn crc16(data: &[u8]) -> u16 { 409 | let mut crc = 0xffff; 410 | for byte in data.iter() { 411 | crc ^= *byte as u16; 412 | for _ in 0..8 { 413 | if crc & 1 == 1 { 414 | crc = (crc >> 1) ^ 0x1a2e; // CRC-16F/4.2 @ https://users.ece.cmu.edu/~koopman/crc/crc16.html 415 | } else { 416 | crc >>= 1; 417 | } 418 | } 419 | } 420 | crc ^= 0xffff; 421 | match crc { 422 | 0xFFFF => 0xFFFE, 423 | other => other, 424 | } 425 | } 426 | 427 | /// Calculate the crc32 of the data as used by the crate. 428 | fn adapted_crc32(data: &[u8]) -> NonZeroU32 { 429 | match crc32(data) { 430 | // CRC may not be 0 as that already means 'erased' 431 | 0 => NonZeroU32::new(1).unwrap(), 432 | // To aid in early shutoff/cancellation, we make sure that if the first byte of 433 | // the crc is erased, it has to be wrong already. 434 | // Also, if the first byte is written, it must not be all 0xFF. 435 | value if value.to_le_bytes()[0] == 0 || value.to_le_bytes()[0] == 0xFF => { 436 | NonZeroU32::new(value ^ 1).unwrap() 437 | } 438 | value => NonZeroU32::new(value).unwrap(), 439 | } 440 | } 441 | 442 | fn crc32(data: &[u8]) -> u32 { 443 | // We use a modified initial value because the normal 0xFFFFFFF does not pass 444 | // the `crc32_all_ones_resistant` test 445 | crc32_with_initial(data, 0xEEEEEEEE) 446 | } 447 | 448 | fn crc32_with_initial(data: &[u8], initial: u32) -> u32 { 449 | const POLY: u32 = 0x82f63b78; // Castagnoli 450 | 451 | let mut crc = initial; 452 | 453 | for byte in data { 454 | crc ^= *byte as u32; 455 | 456 | for _ in 0..8 { 457 | let lowest_bit_set = crc & 1 > 0; 458 | crc >>= 1; 459 | if lowest_bit_set { 460 | crc ^= POLY; 461 | } 462 | } 463 | } 464 | 465 | !crc 466 | } 467 | 468 | /// Checks if the page is open or closed with all items erased. 469 | /// By definition a partial-open page is not empty since it can still be written. 470 | /// 471 | /// The page state can optionally be given if it's already known. 472 | /// In that case the state will not be checked again. 473 | pub async fn is_page_empty( 474 | flash: &mut S, 475 | flash_range: Range, 476 | cache: &mut impl PrivateCacheImpl, 477 | page_index: usize, 478 | page_state: Option, 479 | ) -> Result> { 480 | let page_state = match page_state { 481 | Some(page_state) => page_state, 482 | None => get_page_state::(flash, flash_range.clone(), cache, page_index).await?, 483 | }; 484 | 485 | match page_state { 486 | PageState::Closed => { 487 | let page_data_start_address = 488 | calculate_page_address::(flash_range.clone(), page_index) + S::WORD_SIZE as u32; 489 | let page_data_end_address = 490 | calculate_page_end_address::(flash_range.clone(), page_index) 491 | - S::WORD_SIZE as u32; 492 | 493 | Ok(ItemHeaderIter::new( 494 | cache 495 | .first_item_after_erased(page_index) 496 | .unwrap_or(page_data_start_address), 497 | page_data_end_address, 498 | ) 499 | .traverse(flash, |header, _| header.crc.is_none()) 500 | .await? 501 | .0 502 | .is_none()) 503 | } 504 | PageState::PartialOpen => Ok(false), 505 | PageState::Open => Ok(true), 506 | } 507 | } 508 | 509 | pub struct ItemIter { 510 | header: ItemHeaderIter, 511 | } 512 | 513 | impl ItemIter { 514 | pub fn new(start_address: u32, end_address: u32) -> Self { 515 | Self { 516 | header: ItemHeaderIter::new(start_address, end_address), 517 | } 518 | } 519 | 520 | pub async fn next<'m, S: NorFlash>( 521 | &mut self, 522 | flash: &mut S, 523 | data_buffer: &'m mut [u8], 524 | ) -> Result, u32)>, Error> { 525 | let mut data_buffer = Some(data_buffer); 526 | while let (Some(header), address) = self.header.next(flash).await? { 527 | let buffer = data_buffer.take().unwrap(); 528 | match header 529 | .read_item(flash, buffer, address, self.header.end_address) 530 | .await? 531 | { 532 | MaybeItem::Corrupted(_, buffer) | MaybeItem::Erased(_, buffer) => { 533 | data_buffer.replace(buffer); 534 | } 535 | MaybeItem::Present(item) => { 536 | return Ok(Some((item, address))); 537 | } 538 | } 539 | } 540 | Ok(None) 541 | } 542 | } 543 | 544 | pub struct ItemHeaderIter { 545 | current_address: u32, 546 | end_address: u32, 547 | } 548 | 549 | impl ItemHeaderIter { 550 | pub fn new(start_address: u32, end_address: u32) -> Self { 551 | Self { 552 | current_address: start_address, 553 | end_address, 554 | } 555 | } 556 | 557 | /// Fetch next item 558 | pub async fn next( 559 | &mut self, 560 | flash: &mut S, 561 | ) -> Result<(Option, u32), Error> { 562 | self.traverse(flash, |_, _| false).await 563 | } 564 | 565 | /// Traverse headers until the callback returns false. If the callback returns true, 566 | /// the element is skipped and traversal continues. 567 | /// 568 | /// If the end of the headers is reached, a `None` item header is returned. 569 | pub async fn traverse( 570 | &mut self, 571 | flash: &mut S, 572 | callback: impl Fn(&ItemHeader, u32) -> bool, 573 | ) -> Result<(Option, u32), Error> { 574 | loop { 575 | match ItemHeader::read_new(flash, self.current_address, self.end_address).await { 576 | Ok(Some(header)) => { 577 | let next_address = header.next_item_address::(self.current_address); 578 | if callback(&header, self.current_address) { 579 | self.current_address = next_address; 580 | } else { 581 | let current_address = self.current_address; 582 | self.current_address = next_address; 583 | return Ok((Some(header), current_address)); 584 | } 585 | } 586 | Ok(None) => { 587 | return Ok((None, self.current_address)); 588 | } 589 | Err(Error::Corrupted { .. }) => { 590 | self.current_address = ItemHeader::data_address::(self.current_address); 591 | } 592 | Err(e) => return Err(e), 593 | } 594 | } 595 | } 596 | } 597 | 598 | #[cfg(test)] 599 | mod tests { 600 | use super::*; 601 | 602 | #[test] 603 | fn crc32_all_ones_resistant() { 604 | const DATA: [u8; 1024] = [0xFF; 1024]; 605 | 606 | // Note: This should hold for all lengths up to the max item length 607 | // We do not test that because it takes too long. 608 | // Instead we only test the first couple because those are most likely to go bad. 609 | for length in 0..DATA.len() { 610 | let crc = crc32(&DATA[..length]); 611 | 612 | // println!("Num 0xFF bytes: {length}, crc: {crc:08X}"); 613 | 614 | assert_ne!(crc, u32::MAX); 615 | } 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(any(test, doctest, feature = "std")), no_std)] 2 | #![deny(missing_docs)] 3 | #![doc = include_str!("../README.md")] 4 | 5 | // Assumptions made in this crate: 6 | // 7 | // - flash erase size is quite big, aka, this is a paged flash 8 | // - flash write size is quite small, so it writes words and not full pages 9 | 10 | use cache::PrivateCacheImpl; 11 | use core::{ 12 | fmt::Debug, 13 | ops::{Deref, DerefMut, Range}, 14 | }; 15 | use embedded_storage_async::nor_flash::NorFlash; 16 | use map::SerializationError; 17 | 18 | #[cfg(feature = "alloc")] 19 | mod alloc_impl; 20 | #[cfg(feature = "arrayvec")] 21 | mod arrayvec_impl; 22 | pub mod cache; 23 | #[cfg(feature = "heapless")] 24 | mod heapless_impl; 25 | mod item; 26 | pub mod map; 27 | pub mod queue; 28 | 29 | #[cfg(any(test, doctest, feature = "_test"))] 30 | /// An in-memory flash type that can be used for mocking. 31 | pub mod mock_flash; 32 | 33 | /// The biggest wordsize we support. 34 | /// 35 | /// Stm32 internal flash has 256-bit words, so 32 bytes. 36 | /// Many flashes have 4-byte or 1-byte words. 37 | const MAX_WORD_SIZE: usize = 32; 38 | 39 | /// Resets the flash in the entire given flash range. 40 | /// 41 | /// This is just a thin helper function as it just calls the flash's erase function. 42 | pub async fn erase_all( 43 | flash: &mut S, 44 | flash_range: Range, 45 | ) -> Result<(), Error> { 46 | flash 47 | .erase(flash_range.start, flash_range.end) 48 | .await 49 | .map_err(|e| Error::Storage { 50 | value: e, 51 | #[cfg(feature = "_test")] 52 | backtrace: std::backtrace::Backtrace::capture(), 53 | }) 54 | } 55 | 56 | /// Get the minimal overhead size per stored item for the given flash type. 57 | /// 58 | /// The associated data of each item is additionally padded to a full flash word size, but that's not part of this number. 59 | /// This means the full item length is `returned number + (data length).next_multiple_of(S::WORD_SIZE)`. 60 | pub const fn item_overhead_size() -> u32 { 61 | item::ItemHeader::data_address::(0) 62 | } 63 | 64 | // Type representing buffer aligned to 4 byte boundary. 65 | #[repr(align(4))] 66 | pub(crate) struct AlignedBuf(pub(crate) [u8; SIZE]); 67 | impl Deref for AlignedBuf { 68 | type Target = [u8]; 69 | fn deref(&self) -> &Self::Target { 70 | &self.0 71 | } 72 | } 73 | 74 | impl DerefMut for AlignedBuf { 75 | fn deref_mut(&mut self) -> &mut Self::Target { 76 | &mut self.0 77 | } 78 | } 79 | 80 | async fn try_general_repair( 81 | flash: &mut S, 82 | flash_range: Range, 83 | cache: &mut impl PrivateCacheImpl, 84 | ) -> Result<(), Error> { 85 | // Loop through the pages and get their state. If one returns the corrupted error, 86 | // the page is likely half-erased. Fix for that is to re-erase again to hopefully finish the job. 87 | for page_index in get_pages::(flash_range.clone(), 0) { 88 | if matches!( 89 | get_page_state(flash, flash_range.clone(), cache, page_index).await, 90 | Err(Error::Corrupted { .. }) 91 | ) { 92 | open_page(flash, flash_range.clone(), cache, page_index).await?; 93 | } 94 | } 95 | 96 | Ok(()) 97 | } 98 | 99 | /// Find the first page that is in the given page state. 100 | /// 101 | /// The search starts at starting_page_index (and wraps around back to 0 if required) 102 | async fn find_first_page( 103 | flash: &mut S, 104 | flash_range: Range, 105 | cache: &mut impl PrivateCacheImpl, 106 | starting_page_index: usize, 107 | page_state: PageState, 108 | ) -> Result, Error> { 109 | for page_index in get_pages::(flash_range.clone(), starting_page_index) { 110 | if page_state == get_page_state::(flash, flash_range.clone(), cache, page_index).await? { 111 | return Ok(Some(page_index)); 112 | } 113 | } 114 | 115 | Ok(None) 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 | flash_range: Range, 121 | starting_page_index: usize, 122 | ) -> impl DoubleEndedIterator { 123 | let page_count = flash_range.len() / S::ERASE_SIZE; 124 | flash_range 125 | .step_by(S::ERASE_SIZE) 126 | .enumerate() 127 | .map(move |(index, _)| (index + starting_page_index) % page_count) 128 | } 129 | 130 | /// Get the next page index (wrapping around to 0 if required) 131 | fn next_page(flash_range: Range, page_index: usize) -> usize { 132 | let page_count = flash_range.len() / S::ERASE_SIZE; 133 | (page_index + 1) % page_count 134 | } 135 | 136 | /// Get the previous page index (wrapping around to the biggest page if required) 137 | fn previous_page(flash_range: Range, page_index: usize) -> usize { 138 | let page_count = flash_range.len() / S::ERASE_SIZE; 139 | 140 | match page_index.checked_sub(1) { 141 | Some(new_page_index) => new_page_index, 142 | None => page_count - 1, 143 | } 144 | } 145 | 146 | /// Calculate the first address of the given page 147 | const fn calculate_page_address(flash_range: Range, page_index: usize) -> u32 { 148 | flash_range.start + (S::ERASE_SIZE * page_index) as u32 149 | } 150 | /// Calculate the last address (exclusive) of the given page 151 | const fn calculate_page_end_address( 152 | flash_range: Range, 153 | page_index: usize, 154 | ) -> u32 { 155 | flash_range.start + (S::ERASE_SIZE * (page_index + 1)) as u32 156 | } 157 | /// Get the page index from any address located inside that page 158 | #[allow(unused)] 159 | const fn calculate_page_index(flash_range: Range, address: u32) -> usize { 160 | (address - flash_range.start) as usize / S::ERASE_SIZE 161 | } 162 | 163 | const fn calculate_page_size() -> usize { 164 | // Page minus the two page status words 165 | S::ERASE_SIZE - S::WORD_SIZE * 2 166 | } 167 | 168 | /// The marker being used for page states 169 | const MARKER: u8 = 0; 170 | 171 | /// Get the state of the page located at the given index 172 | async fn get_page_state( 173 | flash: &mut S, 174 | flash_range: Range, 175 | cache: &mut impl PrivateCacheImpl, 176 | page_index: usize, 177 | ) -> Result> { 178 | if let Some(cached_page_state) = cache.get_page_state(page_index) { 179 | return Ok(cached_page_state); 180 | } 181 | 182 | let page_address = calculate_page_address::(flash_range, page_index); 183 | /// We only care about the data in the first byte to aid shutdown/cancellation. 184 | /// But we also don't want it to be too too definitive because we want to survive the occasional bitflip. 185 | /// So only half of the byte needs to be zero. 186 | const HALF_MARKER_BITS: u32 = 4; 187 | 188 | let mut buffer = [0; MAX_WORD_SIZE]; 189 | flash 190 | .read(page_address, &mut buffer[..S::READ_SIZE]) 191 | .await 192 | .map_err(|e| Error::Storage { 193 | value: e, 194 | #[cfg(feature = "_test")] 195 | backtrace: std::backtrace::Backtrace::capture(), 196 | })?; 197 | let start_marked = buffer[..S::READ_SIZE] 198 | .iter() 199 | .map(|marker_byte| marker_byte.count_zeros()) 200 | .sum::() 201 | >= HALF_MARKER_BITS; 202 | 203 | flash 204 | .read( 205 | page_address + (S::ERASE_SIZE - S::READ_SIZE) as u32, 206 | &mut buffer[..S::READ_SIZE], 207 | ) 208 | .await 209 | .map_err(|e| Error::Storage { 210 | value: e, 211 | #[cfg(feature = "_test")] 212 | backtrace: std::backtrace::Backtrace::capture(), 213 | })?; 214 | let end_marked = buffer[..S::READ_SIZE] 215 | .iter() 216 | .map(|marker_byte| marker_byte.count_zeros()) 217 | .sum::() 218 | >= HALF_MARKER_BITS; 219 | 220 | let discovered_state = match (start_marked, end_marked) { 221 | (true, true) => PageState::Closed, 222 | (true, false) => PageState::PartialOpen, 223 | // Probably an interrupted erase 224 | (false, true) => { 225 | return Err(Error::Corrupted { 226 | #[cfg(feature = "_test")] 227 | backtrace: std::backtrace::Backtrace::capture(), 228 | }); 229 | } 230 | (false, false) => PageState::Open, 231 | }; 232 | 233 | // Not dirty because nothing changed and nothing can be inconsistent 234 | cache.notice_page_state(page_index, discovered_state, false); 235 | 236 | Ok(discovered_state) 237 | } 238 | 239 | /// Erase the page to open it again 240 | async fn open_page( 241 | flash: &mut S, 242 | flash_range: Range, 243 | cache: &mut impl PrivateCacheImpl, 244 | page_index: usize, 245 | ) -> Result<(), Error> { 246 | cache.notice_page_state(page_index, PageState::Open, true); 247 | 248 | flash 249 | .erase( 250 | calculate_page_address::(flash_range.clone(), page_index), 251 | calculate_page_end_address::(flash_range.clone(), page_index), 252 | ) 253 | .await 254 | .map_err(|e| Error::Storage { 255 | value: e, 256 | #[cfg(feature = "_test")] 257 | backtrace: std::backtrace::Backtrace::capture(), 258 | })?; 259 | 260 | Ok(()) 261 | } 262 | 263 | /// Fully closes a page by writing both the start and end marker 264 | async fn close_page( 265 | flash: &mut S, 266 | flash_range: Range, 267 | cache: &mut impl PrivateCacheImpl, 268 | page_index: usize, 269 | ) -> Result<(), Error> { 270 | let current_state = 271 | partial_close_page::(flash, flash_range.clone(), cache, page_index).await?; 272 | 273 | if current_state != PageState::PartialOpen { 274 | return Ok(()); 275 | } 276 | 277 | cache.notice_page_state(page_index, PageState::Closed, true); 278 | 279 | let buffer = AlignedBuf([MARKER; MAX_WORD_SIZE]); 280 | // Close the end marker 281 | flash 282 | .write( 283 | calculate_page_end_address::(flash_range, page_index) - S::WORD_SIZE as u32, 284 | &buffer[..S::WORD_SIZE], 285 | ) 286 | .await 287 | .map_err(|e| Error::Storage { 288 | value: e, 289 | #[cfg(feature = "_test")] 290 | backtrace: std::backtrace::Backtrace::capture(), 291 | })?; 292 | 293 | Ok(()) 294 | } 295 | 296 | /// Partially close a page by writing the start marker 297 | async fn partial_close_page( 298 | flash: &mut S, 299 | flash_range: Range, 300 | cache: &mut impl PrivateCacheImpl, 301 | page_index: usize, 302 | ) -> Result> { 303 | let current_state = get_page_state::(flash, flash_range.clone(), cache, page_index).await?; 304 | 305 | if current_state != PageState::Open { 306 | return Ok(current_state); 307 | } 308 | 309 | let new_state = match current_state { 310 | PageState::Closed => PageState::Closed, 311 | PageState::PartialOpen => PageState::PartialOpen, 312 | PageState::Open => PageState::PartialOpen, 313 | }; 314 | 315 | cache.notice_page_state(page_index, new_state, true); 316 | 317 | let buffer = AlignedBuf([MARKER; MAX_WORD_SIZE]); 318 | // Close the start marker 319 | flash 320 | .write( 321 | calculate_page_address::(flash_range, page_index), 322 | &buffer[..S::WORD_SIZE], 323 | ) 324 | .await 325 | .map_err(|e| Error::Storage { 326 | value: e, 327 | #[cfg(feature = "_test")] 328 | backtrace: std::backtrace::Backtrace::capture(), 329 | })?; 330 | 331 | Ok(new_state) 332 | } 333 | 334 | /// The state of a page 335 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 336 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 337 | enum PageState { 338 | /// This page was fully written and has now been sealed 339 | Closed, 340 | /// This page has been written to, but may have some space left over 341 | PartialOpen, 342 | /// This page is fully erased 343 | Open, 344 | } 345 | 346 | #[allow(dead_code)] 347 | impl PageState { 348 | /// Returns `true` if the page state is [`Closed`]. 349 | /// 350 | /// [`Closed`]: PageState::Closed 351 | #[must_use] 352 | fn is_closed(&self) -> bool { 353 | matches!(self, Self::Closed) 354 | } 355 | 356 | /// Returns `true` if the page state is [`PartialOpen`]. 357 | /// 358 | /// [`PartialOpen`]: PageState::PartialOpen 359 | #[must_use] 360 | fn is_partial_open(&self) -> bool { 361 | matches!(self, Self::PartialOpen) 362 | } 363 | 364 | /// Returns `true` if the page state is [`Open`]. 365 | /// 366 | /// [`Open`]: PageState::Open 367 | #[must_use] 368 | fn is_open(&self) -> bool { 369 | matches!(self, Self::Open) 370 | } 371 | } 372 | 373 | /// The main error type 374 | #[non_exhaustive] 375 | #[derive(Debug)] 376 | #[cfg_attr(feature = "defmt-03", derive(defmt::Format))] 377 | pub enum Error { 378 | /// An error in the storage (flash) 379 | Storage { 380 | /// The error value 381 | value: S, 382 | #[cfg(feature = "_test")] 383 | /// Backtrace made at the construction of the error 384 | backtrace: std::backtrace::Backtrace, 385 | }, 386 | /// The item cannot be stored anymore because the storage is full. 387 | FullStorage, 388 | /// It's been detected that the memory is likely corrupted. 389 | /// You may want to erase the memory to recover. 390 | Corrupted { 391 | #[cfg(feature = "_test")] 392 | /// Backtrace made at the construction of the error 393 | backtrace: std::backtrace::Backtrace, 394 | }, 395 | /// A provided buffer was to big to be used 396 | BufferTooBig, 397 | /// A provided buffer was to small to be used (usize is size needed) 398 | BufferTooSmall(usize), 399 | /// A serialization error (from the key or value) 400 | SerializationError(SerializationError), 401 | /// The item does not fit in flash, ever. 402 | /// This is different from [Error::FullStorage] because this item is too big to fit even in empty flash. 403 | /// 404 | /// See the readme for more info about the constraints on item sizes. 405 | ItemTooBig, 406 | } 407 | 408 | impl From for Error { 409 | fn from(v: SerializationError) -> Self { 410 | Self::SerializationError(v) 411 | } 412 | } 413 | 414 | impl PartialEq for Error { 415 | fn eq(&self, other: &Self) -> bool { 416 | match (self, other) { 417 | (Self::Storage { value: l_value, .. }, Self::Storage { value: r_value, .. }) => { 418 | l_value == r_value 419 | } 420 | (Self::BufferTooSmall(l0), Self::BufferTooSmall(r0)) => l0 == r0, 421 | _ => core::mem::discriminant(self) == core::mem::discriminant(other), 422 | } 423 | } 424 | } 425 | 426 | impl core::fmt::Display for Error 427 | where 428 | S: core::fmt::Display, 429 | { 430 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 431 | match self { 432 | Error::Storage { value, .. } => write!(f, "Storage error: {value}"), 433 | Error::FullStorage => write!(f, "Storage is full"), 434 | Error::Corrupted { .. } => write!(f, "Storage is corrupted"), 435 | Error::BufferTooBig => write!(f, "A provided buffer was to big to be used"), 436 | Error::BufferTooSmall(needed) => write!( 437 | f, 438 | "A provided buffer was to small to be used. Needed was {needed}" 439 | ), 440 | Error::SerializationError(value) => write!(f, "Map value error: {value}"), 441 | Error::ItemTooBig => write!(f, "The item is too big to fit in the flash"), 442 | } 443 | } 444 | } 445 | 446 | #[cfg(feature = "std")] 447 | impl std::error::Error for Error where S: std::error::Error {} 448 | 449 | /// Round up the the given number to align with the wordsize of the flash. 450 | /// If the number is already aligned, it is not changed. 451 | const fn round_up_to_alignment(value: u32) -> u32 { 452 | let alignment = S::WORD_SIZE as u32; 453 | match value % alignment { 454 | 0 => value, 455 | r => value + (alignment - r), 456 | } 457 | } 458 | 459 | /// Round up the the given number to align with the wordsize of the flash. 460 | /// If the number is already aligned, it is not changed. 461 | const fn round_up_to_alignment_usize(value: usize) -> usize { 462 | round_up_to_alignment::(value as u32) as usize 463 | } 464 | 465 | /// Round down the the given number to align with the wordsize of the flash. 466 | /// If the number is already aligned, it is not changed. 467 | const fn round_down_to_alignment(value: u32) -> u32 { 468 | let alignment = S::WORD_SIZE as u32; 469 | (value / alignment) * alignment 470 | } 471 | 472 | /// Round down the the given number to align with the wordsize of the flash. 473 | /// If the number is already aligned, it is not changed. 474 | const fn round_down_to_alignment_usize(value: usize) -> usize { 475 | round_down_to_alignment::(value as u32) as usize 476 | } 477 | 478 | /// Extension trait to get the overall word size, which is the largest of the write and read word size 479 | trait NorFlashExt { 480 | /// The largest of the write and read word size 481 | const WORD_SIZE: usize; 482 | } 483 | 484 | impl NorFlashExt for S { 485 | const WORD_SIZE: usize = { 486 | assert!( 487 | Self::WRITE_SIZE % Self::READ_SIZE == 0, 488 | "Only flash with read and write sizes that are multiple of each other are supported" 489 | ); 490 | 491 | if Self::WRITE_SIZE > Self::READ_SIZE { 492 | Self::WRITE_SIZE 493 | } else { 494 | Self::READ_SIZE 495 | } 496 | }; 497 | } 498 | 499 | macro_rules! run_with_auto_repair { 500 | (function = $function:expr, repair = $repair_function:expr) => { 501 | match $function { 502 | Err(Error::Corrupted { 503 | #[cfg(feature = "_test")] 504 | backtrace: _backtrace, 505 | .. 506 | }) => { 507 | #[cfg(all(feature = "_test", fuzzing_repro))] 508 | eprintln!( 509 | "### Encountered curruption! Repairing now. Originated from:\n{_backtrace:#}" 510 | ); 511 | $repair_function; 512 | $function 513 | } 514 | val => val, 515 | } 516 | }; 517 | } 518 | 519 | pub(crate) use run_with_auto_repair; 520 | 521 | #[cfg(test)] 522 | mod tests { 523 | use super::*; 524 | use futures_test::test; 525 | 526 | type MockFlash = mock_flash::MockFlashBase<4, 4, 64>; 527 | 528 | async fn write_aligned( 529 | flash: &mut MockFlash, 530 | offset: u32, 531 | bytes: &[u8], 532 | ) -> Result<(), mock_flash::MockFlashError> { 533 | let mut buf = AlignedBuf([0; 256]); 534 | buf[..bytes.len()].copy_from_slice(bytes); 535 | flash.write(offset, &buf[..bytes.len()]).await 536 | } 537 | 538 | #[test] 539 | async fn test_find_pages() { 540 | // Page setup: 541 | // 0: closed 542 | // 1: closed 543 | // 2: partial-open 544 | // 3: open 545 | 546 | let mut flash = MockFlash::default(); 547 | // Page 0 markers 548 | write_aligned(&mut flash, 0x000, &[MARKER, 0, 0, 0]) 549 | .await 550 | .unwrap(); 551 | write_aligned(&mut flash, 0x100 - 4, &[0, 0, 0, MARKER]) 552 | .await 553 | .unwrap(); 554 | // Page 1 markers 555 | write_aligned(&mut flash, 0x100, &[MARKER, 0, 0, 0]) 556 | .await 557 | .unwrap(); 558 | write_aligned(&mut flash, 0x200 - 4, &[0, 0, 0, MARKER]) 559 | .await 560 | .unwrap(); 561 | // Page 2 markers 562 | write_aligned(&mut flash, 0x200, &[MARKER, 0, 0, 0]) 563 | .await 564 | .unwrap(); 565 | 566 | assert_eq!( 567 | find_first_page( 568 | &mut flash, 569 | 0x000..0x400, 570 | &mut cache::NoCache::new(), 571 | 0, 572 | PageState::Open 573 | ) 574 | .await 575 | .unwrap(), 576 | Some(3) 577 | ); 578 | assert_eq!( 579 | find_first_page( 580 | &mut flash, 581 | 0x000..0x400, 582 | &mut cache::NoCache::new(), 583 | 0, 584 | PageState::PartialOpen 585 | ) 586 | .await 587 | .unwrap(), 588 | Some(2) 589 | ); 590 | assert_eq!( 591 | find_first_page( 592 | &mut flash, 593 | 0x000..0x400, 594 | &mut cache::NoCache::new(), 595 | 1, 596 | PageState::PartialOpen 597 | ) 598 | .await 599 | .unwrap(), 600 | Some(2) 601 | ); 602 | assert_eq!( 603 | find_first_page( 604 | &mut flash, 605 | 0x000..0x400, 606 | &mut cache::NoCache::new(), 607 | 2, 608 | PageState::PartialOpen 609 | ) 610 | .await 611 | .unwrap(), 612 | Some(2) 613 | ); 614 | assert_eq!( 615 | find_first_page( 616 | &mut flash, 617 | 0x000..0x400, 618 | &mut cache::NoCache::new(), 619 | 3, 620 | PageState::Open 621 | ) 622 | .await 623 | .unwrap(), 624 | Some(3) 625 | ); 626 | assert_eq!( 627 | find_first_page( 628 | &mut flash, 629 | 0x000..0x200, 630 | &mut cache::NoCache::new(), 631 | 0, 632 | PageState::PartialOpen 633 | ) 634 | .await 635 | .unwrap(), 636 | None 637 | ); 638 | 639 | assert_eq!( 640 | find_first_page( 641 | &mut flash, 642 | 0x000..0x400, 643 | &mut cache::NoCache::new(), 644 | 0, 645 | PageState::Closed 646 | ) 647 | .await 648 | .unwrap(), 649 | Some(0) 650 | ); 651 | assert_eq!( 652 | find_first_page( 653 | &mut flash, 654 | 0x000..0x400, 655 | &mut cache::NoCache::new(), 656 | 1, 657 | PageState::Closed 658 | ) 659 | .await 660 | .unwrap(), 661 | Some(1) 662 | ); 663 | assert_eq!( 664 | find_first_page( 665 | &mut flash, 666 | 0x000..0x400, 667 | &mut cache::NoCache::new(), 668 | 2, 669 | PageState::Closed 670 | ) 671 | .await 672 | .unwrap(), 673 | Some(0) 674 | ); 675 | assert_eq!( 676 | find_first_page( 677 | &mut flash, 678 | 0x000..0x400, 679 | &mut cache::NoCache::new(), 680 | 3, 681 | PageState::Closed 682 | ) 683 | .await 684 | .unwrap(), 685 | Some(0) 686 | ); 687 | assert_eq!( 688 | find_first_page( 689 | &mut flash, 690 | 0x200..0x400, 691 | &mut cache::NoCache::new(), 692 | 0, 693 | PageState::Closed 694 | ) 695 | .await 696 | .unwrap(), 697 | None 698 | ); 699 | } 700 | } 701 | -------------------------------------------------------------------------------- /src/mock_flash.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Add, AddAssign, Range}; 2 | use embedded_storage_async::nor_flash::{ 3 | ErrorType, MultiwriteNorFlash, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash, 4 | }; 5 | 6 | /// State of a word in the flash. 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 | enum Writable { 9 | /// Twice 10 | T, 11 | /// Once (can only convert 1 bits to 0 12 | O, 13 | /// Never (must be cleared before being writable again) 14 | N, 15 | } 16 | 17 | use Writable::*; 18 | 19 | /// Base type for in memory flash that can be used for mocking. 20 | #[derive(Debug, Clone)] 21 | pub struct MockFlashBase { 22 | writable: Vec, 23 | data: Vec, 24 | current_stats: FlashStatsSnapshot, 25 | /// Check that all write locations are writeable. 26 | pub write_count_check: WriteCountCheck, 27 | /// A countdown to shutoff. When some and 0, an early shutoff will happen. 28 | pub bytes_until_shutoff: Option, 29 | /// When true, write buffers have to be aligned 30 | pub alignment_check: bool, 31 | } 32 | 33 | impl Default 34 | for MockFlashBase 35 | { 36 | fn default() -> Self { 37 | Self::new(WriteCountCheck::OnceOnly, None, true) 38 | } 39 | } 40 | 41 | impl 42 | MockFlashBase 43 | { 44 | const CAPACITY_WORDS: usize = PAGES * PAGE_WORDS; 45 | const CAPACITY_BYTES: usize = Self::CAPACITY_WORDS * BYTES_PER_WORD; 46 | 47 | const PAGE_BYTES: usize = PAGE_WORDS * BYTES_PER_WORD; 48 | 49 | /// The full address range of this flash 50 | pub const FULL_FLASH_RANGE: Range = 0..(PAGES * PAGE_WORDS * BYTES_PER_WORD) as u32; 51 | 52 | /// Create a new flash instance. 53 | pub fn new( 54 | write_count_check: WriteCountCheck, 55 | bytes_until_shutoff: Option, 56 | alignment_check: bool, 57 | ) -> Self { 58 | Self { 59 | writable: vec![T; Self::CAPACITY_WORDS], 60 | data: vec![u8::MAX; Self::CAPACITY_BYTES], 61 | current_stats: FlashStatsSnapshot { 62 | erases: 0, 63 | reads: 0, 64 | writes: 0, 65 | bytes_read: 0, 66 | bytes_written: 0, 67 | }, 68 | write_count_check, 69 | bytes_until_shutoff, 70 | alignment_check, 71 | } 72 | } 73 | 74 | /// Get a reference to the underlying data. 75 | pub fn as_bytes(&self) -> &[u8] { 76 | &self.data 77 | } 78 | 79 | /// Get a mutable reference to the underlying data. 80 | pub fn as_bytes_mut(&mut self) -> &mut [u8] { 81 | &mut self.data 82 | } 83 | 84 | fn validate_operation(offset: u32, length: usize) -> Result, MockFlashError> { 85 | let offset = offset as usize; 86 | if (offset % Self::READ_SIZE) != 0 { 87 | Err(MockFlashError::NotAligned) 88 | } else if offset > Self::CAPACITY_BYTES || offset + length > Self::CAPACITY_BYTES { 89 | Err(MockFlashError::OutOfBounds) 90 | } else { 91 | Ok(offset..(offset + length)) 92 | } 93 | } 94 | 95 | fn check_shutoff(&mut self, address: u32, _operation: &str) -> Result<(), MockFlashError> { 96 | if let Some(bytes_until_shutoff) = self.bytes_until_shutoff.as_mut() { 97 | if let Some(next) = bytes_until_shutoff.checked_sub(1) { 98 | *bytes_until_shutoff = next; 99 | Ok(()) 100 | } else { 101 | #[cfg(fuzzing_repro)] 102 | eprintln!("!!! Shutoff at {address} while doing '{_operation}' !!!"); 103 | self.bytes_until_shutoff = None; 104 | Err(MockFlashError::EarlyShutoff(address)) 105 | } 106 | } else { 107 | Ok(()) 108 | } 109 | } 110 | 111 | /// Get a snapshot of the performance counters 112 | pub fn stats_snapshot(&self) -> FlashStatsSnapshot { 113 | self.current_stats 114 | } 115 | 116 | #[cfg(any(test, feature = "_test"))] 117 | /// Print all items in flash to the returned string 118 | pub async fn print_items(&mut self) -> String { 119 | use crate::NorFlashExt; 120 | use crate::cache::NoCache; 121 | use std::fmt::Write; 122 | 123 | let mut buf = [0; 1024 * 16]; 124 | 125 | let mut s = String::new(); 126 | 127 | writeln!(s, "Items in flash:").unwrap(); 128 | writeln!(s, " Bytes until shutoff: {:?}", self.bytes_until_shutoff).unwrap(); 129 | 130 | for page_index in 0..PAGES { 131 | writeln!( 132 | s, 133 | " Page {page_index} ({}):", 134 | match crate::get_page_state( 135 | self, 136 | Self::FULL_FLASH_RANGE, 137 | &mut NoCache::new(), 138 | page_index 139 | ) 140 | .await 141 | { 142 | Ok(value) => format!("{value:?}"), 143 | Err(e) => format!("Error ({e:?})"), 144 | } 145 | ) 146 | .unwrap(); 147 | let page_data_start = 148 | crate::calculate_page_address::(Self::FULL_FLASH_RANGE, page_index) 149 | + Self::WORD_SIZE as u32; 150 | let page_data_end = 151 | crate::calculate_page_end_address::(Self::FULL_FLASH_RANGE, page_index) 152 | - Self::WORD_SIZE as u32; 153 | 154 | let mut it = crate::item::ItemHeaderIter::new(page_data_start, page_data_end); 155 | while let (Some(header), item_address) = it.traverse(self, |_, _| false).await.unwrap() 156 | { 157 | let next_item_address = header.next_item_address::(item_address); 158 | let maybe_item = match header 159 | .read_item(self, &mut buf, item_address, page_data_end) 160 | .await 161 | { 162 | Ok(maybe_item) => maybe_item, 163 | Err(e) => { 164 | writeln!( 165 | s, 166 | " Item COULD NOT BE READ at {item_address}..{next_item_address}" 167 | ) 168 | .unwrap(); 169 | 170 | println!("{s}"); 171 | panic!("{e:?}"); 172 | } 173 | }; 174 | 175 | writeln!( 176 | s, 177 | " Item {maybe_item:?} at {item_address}..{next_item_address}" 178 | ) 179 | .unwrap(); 180 | } 181 | } 182 | 183 | s 184 | } 185 | 186 | #[cfg(any(test, feature = "_test"))] 187 | /// Get the presence of the item at the given address. 188 | /// 189 | /// - If some, the item is there. 190 | /// - If true, the item is present and fine. 191 | /// - If false, the item is corrupt or erased. 192 | pub async fn get_item_presence(&mut self, target_item_address: u32) -> Option { 193 | use crate::NorFlashExt; 194 | 195 | if !Self::FULL_FLASH_RANGE.contains(&target_item_address) { 196 | return None; 197 | } 198 | 199 | let mut buf = [0; 1024 * 16]; 200 | 201 | let page_index = 202 | crate::calculate_page_index::(Self::FULL_FLASH_RANGE, target_item_address); 203 | 204 | let page_data_start = 205 | crate::calculate_page_address::(Self::FULL_FLASH_RANGE, page_index) 206 | + Self::WORD_SIZE as u32; 207 | let page_data_end = 208 | crate::calculate_page_end_address::(Self::FULL_FLASH_RANGE, page_index) 209 | - Self::WORD_SIZE as u32; 210 | 211 | let mut found_item = None; 212 | let mut it = crate::item::ItemHeaderIter::new(page_data_start, page_data_end); 213 | while let (Some(header), item_address) = it.traverse(self, |_, _| false).await.unwrap() { 214 | let next_item_address = header.next_item_address::(item_address); 215 | 216 | if (item_address..next_item_address).contains(&target_item_address) { 217 | let maybe_item = header 218 | .read_item(self, &mut buf, item_address, page_data_end) 219 | .await 220 | .unwrap(); 221 | 222 | match maybe_item { 223 | crate::item::MaybeItem::Corrupted(_, _) 224 | | crate::item::MaybeItem::Erased(_, _) => { 225 | found_item.replace(false); 226 | break; 227 | } 228 | crate::item::MaybeItem::Present(_) => { 229 | found_item.replace(true); 230 | break; 231 | } 232 | } 233 | } 234 | } 235 | 236 | found_item 237 | } 238 | } 239 | 240 | impl ErrorType 241 | for MockFlashBase 242 | { 243 | type Error = MockFlashError; 244 | } 245 | 246 | impl ReadNorFlash 247 | for MockFlashBase 248 | { 249 | const READ_SIZE: usize = BYTES_PER_WORD; 250 | 251 | async fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { 252 | self.current_stats.reads += 1; 253 | self.current_stats.bytes_read += bytes.len() as u64; 254 | 255 | if bytes.len() % Self::READ_SIZE != 0 { 256 | panic!("any read must be a multiple of Self::READ_SIZE bytes"); 257 | } 258 | 259 | let range = Self::validate_operation(offset, bytes.len())?; 260 | 261 | bytes.copy_from_slice(&self.as_bytes()[range]); 262 | 263 | Ok(()) 264 | } 265 | 266 | fn capacity(&self) -> usize { 267 | Self::CAPACITY_BYTES 268 | } 269 | } 270 | 271 | impl MultiwriteNorFlash 272 | for MockFlashBase 273 | { 274 | } 275 | 276 | impl NorFlash 277 | for MockFlashBase 278 | { 279 | const WRITE_SIZE: usize = BYTES_PER_WORD; 280 | 281 | const ERASE_SIZE: usize = Self::PAGE_BYTES; 282 | 283 | async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { 284 | self.current_stats.erases += 1; 285 | 286 | let from = from as usize; 287 | let to = to as usize; 288 | 289 | assert!(from <= to); 290 | 291 | if to > Self::CAPACITY_BYTES { 292 | return Err(MockFlashError::OutOfBounds); 293 | } 294 | 295 | if from % Self::PAGE_BYTES != 0 || to % Self::PAGE_BYTES != 0 { 296 | return Err(MockFlashError::NotAligned); 297 | } 298 | 299 | for index in from..to { 300 | self.check_shutoff(index as u32, "erase")?; 301 | self.as_bytes_mut()[index] = u8::MAX; 302 | 303 | if index % BYTES_PER_WORD == 0 { 304 | self.writable[index / BYTES_PER_WORD] = T; 305 | } 306 | } 307 | 308 | Ok(()) 309 | } 310 | 311 | async fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { 312 | self.current_stats.writes += 1; 313 | 314 | let range = Self::validate_operation(offset, bytes.len())?; 315 | 316 | // Check alignment. Some flash types are strict about the alignment of the input buffer. This ensures 317 | // that the mock flash is also strict to catch bugs and avoid regressions. 318 | if self.alignment_check && bytes.as_ptr() as usize % 4 != 0 { 319 | panic!("write buffer must be aligned to 4 bytes"); 320 | } 321 | 322 | if bytes.len() % Self::WRITE_SIZE != 0 { 323 | panic!("any write must be a multiple of Self::WRITE_SIZE bytes"); 324 | } 325 | 326 | for (source_word, address) in bytes 327 | .chunks_exact(BYTES_PER_WORD) 328 | .zip(range.step_by(BYTES_PER_WORD)) 329 | { 330 | for (byte_index, byte) in source_word.iter().enumerate() { 331 | self.check_shutoff((address + byte_index) as u32, "write")?; 332 | 333 | if byte_index == 0 { 334 | let word_writable = &mut self.writable[address / BYTES_PER_WORD]; 335 | *word_writable = match (*word_writable, self.write_count_check) { 336 | (v, WriteCountCheck::Disabled) => v, 337 | (Writable::T, _) => Writable::O, 338 | (Writable::O, WriteCountCheck::Twice) => Writable::N, 339 | (Writable::O, WriteCountCheck::TwiceDifferent) 340 | if source_word == &self.data[address..][..BYTES_PER_WORD] => 341 | { 342 | Writable::O 343 | } 344 | (Writable::O, WriteCountCheck::TwiceDifferent) => Writable::N, 345 | (Writable::O, WriteCountCheck::TwiceWithZero) 346 | if source_word.iter().all(|b| *b == 0) => 347 | { 348 | Writable::N 349 | } 350 | _ => return Err(MockFlashError::NotWritable(address as u32)), 351 | }; 352 | } 353 | 354 | self.current_stats.bytes_written += 1; 355 | 356 | self.as_bytes_mut()[address + byte_index] &= byte; 357 | } 358 | } 359 | 360 | Ok(()) 361 | } 362 | } 363 | 364 | /// Errors reported by mock flash. 365 | #[derive(Debug, Clone, PartialEq, Eq)] 366 | pub enum MockFlashError { 367 | /// Operation out of bounds. 368 | OutOfBounds, 369 | /// Offset or data not aligned. 370 | NotAligned, 371 | /// Location not writeable. 372 | NotWritable(u32), 373 | /// We got a shutoff 374 | EarlyShutoff(u32), 375 | } 376 | 377 | impl NorFlashError for MockFlashError { 378 | fn kind(&self) -> NorFlashErrorKind { 379 | match self { 380 | MockFlashError::OutOfBounds => NorFlashErrorKind::OutOfBounds, 381 | MockFlashError::NotAligned => NorFlashErrorKind::NotAligned, 382 | MockFlashError::NotWritable(_) => NorFlashErrorKind::Other, 383 | MockFlashError::EarlyShutoff(_) => NorFlashErrorKind::Other, 384 | } 385 | } 386 | } 387 | 388 | /// The mode the write counter works in 389 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 390 | pub enum WriteCountCheck { 391 | /// A word may only be written once 392 | OnceOnly, 393 | /// A word may be written twice, but it only counts when it actually changes 394 | TwiceDifferent, 395 | /// A word may be written twice 396 | Twice, 397 | /// A work may be written twice, but the second time has to be all zeroes. (STM32 does this) 398 | TwiceWithZero, 399 | /// No check at all 400 | Disabled, 401 | } 402 | 403 | /// A snapshot of the flash performance statistics 404 | #[derive(Debug, Clone, Copy)] 405 | pub struct FlashStatsSnapshot { 406 | erases: u64, 407 | reads: u64, 408 | writes: u64, 409 | bytes_read: u64, 410 | bytes_written: u64, 411 | } 412 | 413 | impl FlashStatsSnapshot { 414 | /// Compare the snapshot to another snapshot. 415 | /// 416 | /// The oldest snapshot goes first, so it's `old.compare_to(new)`. 417 | pub fn compare_to(&self, other: Self) -> FlashStatsResult { 418 | FlashStatsResult { 419 | erases: other 420 | .erases 421 | .checked_sub(self.erases) 422 | .expect("Order is old compare to new"), 423 | reads: other 424 | .reads 425 | .checked_sub(self.reads) 426 | .expect("Order is old compare to new"), 427 | writes: other 428 | .writes 429 | .checked_sub(self.writes) 430 | .expect("Order is old compare to new"), 431 | bytes_read: other 432 | .bytes_read 433 | .checked_sub(self.bytes_read) 434 | .expect("Order is old compare to new"), 435 | bytes_written: other 436 | .bytes_written 437 | .checked_sub(self.bytes_written) 438 | .expect("Order is old compare to new"), 439 | } 440 | } 441 | } 442 | 443 | /// The performance stats of everything between two snapshots 444 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 445 | pub struct FlashStatsResult { 446 | /// The amount of times a page has been erased 447 | pub erases: u64, 448 | /// The amount of times a read operation was started 449 | pub reads: u64, 450 | /// The amount of times a write operation was started 451 | pub writes: u64, 452 | /// The total amount of bytes that were read 453 | pub bytes_read: u64, 454 | /// The total amount of bytes that were written 455 | pub bytes_written: u64, 456 | } 457 | 458 | impl FlashStatsResult { 459 | /// Take the average of the stats 460 | pub fn take_average(&self, divider: u64) -> FlashAverageStatsResult { 461 | FlashAverageStatsResult { 462 | avg_erases: self.erases as f64 / divider as f64, 463 | avg_reads: self.reads as f64 / divider as f64, 464 | avg_writes: self.writes as f64 / divider as f64, 465 | avg_bytes_read: self.bytes_read as f64 / divider as f64, 466 | avg_bytes_written: self.bytes_written as f64 / divider as f64, 467 | } 468 | } 469 | } 470 | 471 | impl AddAssign for FlashStatsResult { 472 | fn add_assign(&mut self, rhs: Self) { 473 | *self = *self + rhs; 474 | } 475 | } 476 | 477 | impl Add for FlashStatsResult { 478 | type Output = FlashStatsResult; 479 | 480 | fn add(self, rhs: Self) -> Self::Output { 481 | Self { 482 | erases: self.erases + rhs.erases, 483 | reads: self.reads + rhs.reads, 484 | writes: self.writes + rhs.writes, 485 | bytes_read: self.bytes_read + rhs.bytes_read, 486 | bytes_written: self.bytes_written + rhs.bytes_written, 487 | } 488 | } 489 | } 490 | 491 | /// The averaged performance stats of everything between two snapshots 492 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 493 | pub struct FlashAverageStatsResult { 494 | /// The amount of times a page has been erased 495 | pub avg_erases: f64, 496 | /// The amount of times a read operation was started 497 | pub avg_reads: f64, 498 | /// The amount of times a write operation was started 499 | pub avg_writes: f64, 500 | /// The total amount of bytes that were read 501 | pub avg_bytes_read: f64, 502 | /// The total amount of bytes that were written 503 | pub avg_bytes_written: f64, 504 | } 505 | 506 | impl approx::AbsDiffEq for FlashAverageStatsResult { 507 | type Epsilon = f64; 508 | 509 | fn default_epsilon() -> Self::Epsilon { 510 | f64::EPSILON 511 | } 512 | 513 | fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { 514 | self.avg_erases.abs_diff_eq(&other.avg_erases, epsilon) 515 | && self.avg_reads.abs_diff_eq(&other.avg_reads, epsilon) 516 | && self.avg_writes.abs_diff_eq(&other.avg_writes, epsilon) 517 | && self 518 | .avg_bytes_read 519 | .abs_diff_eq(&other.avg_bytes_read, epsilon) 520 | && self 521 | .avg_bytes_written 522 | .abs_diff_eq(&other.avg_bytes_written, epsilon) 523 | } 524 | } 525 | 526 | impl approx::RelativeEq for FlashAverageStatsResult { 527 | fn default_max_relative() -> Self::Epsilon { 528 | f64::default_max_relative() 529 | } 530 | 531 | fn relative_eq( 532 | &self, 533 | other: &Self, 534 | epsilon: Self::Epsilon, 535 | max_relative: Self::Epsilon, 536 | ) -> bool { 537 | self.avg_erases 538 | .relative_eq(&other.avg_erases, epsilon, max_relative) 539 | && self 540 | .avg_reads 541 | .relative_eq(&other.avg_reads, epsilon, max_relative) 542 | && self 543 | .avg_writes 544 | .relative_eq(&other.avg_writes, epsilon, max_relative) 545 | && self 546 | .avg_bytes_read 547 | .relative_eq(&other.avg_bytes_read, epsilon, max_relative) 548 | && self 549 | .avg_bytes_written 550 | .relative_eq(&other.avg_bytes_written, epsilon, max_relative) 551 | } 552 | } 553 | --------------------------------------------------------------------------------