├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── NOTICE ├── README.md ├── examples ├── append_file.rs ├── big_dir.rs ├── create_file.rs ├── delete_file.rs ├── linux │ └── mod.rs ├── list_dir.rs ├── read_file.rs ├── readme_test.rs └── shell.rs ├── src ├── blockdevice.rs ├── fat │ ├── bpb.rs │ ├── info.rs │ ├── mod.rs │ ├── ondiskdirentry.rs │ └── volume.rs ├── filesystem │ ├── attributes.rs │ ├── cluster.rs │ ├── directory.rs │ ├── filename.rs │ ├── files.rs │ ├── handles.rs │ ├── mod.rs │ └── timestamp.rs ├── lib.rs ├── sdcard │ ├── mod.rs │ └── proto.rs ├── structure.rs └── volume_mgr.rs └── tests ├── directories.rs ├── disk.img.gz ├── open_files.rs ├── read_file.rs ├── utils └── mod.rs ├── volume.rs └── write_file.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | formatting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: dtolnay/rust-toolchain@stable 11 | with: 12 | components: rustfmt 13 | - name: Check formatting 14 | run: cargo fmt -- --check 15 | 16 | build-test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | # Always run MSRV too! 21 | rust: ["stable", "1.76"] 22 | features: ['log', 'defmt-log', '""'] 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: dtolnay/rust-toolchain@master 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | - name: Build 29 | run: cargo build --no-default-features --features ${{matrix.features}} --verbose 30 | env: 31 | DEFMT_LOG: debug 32 | - name: Run Tests 33 | run: cargo test --no-default-features --features ${{matrix.features}} --verbose 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | disk.img 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning]. 6 | 7 | ## [Unreleased] 8 | 9 | ### Changed 10 | 11 | - __Breaking Change__: `VolumeManager` now uses interior-mutability (with a `RefCell`) and so most methods are now `&self`. This also makes it easier to open multiple `File`, `Directory` or `Volume` objects at once. 12 | - __Breaking Change__: The `VolumeManager`, `File`, `Directory` and `Volume` no longer implement `Send` or `Sync. 13 | - `VolumeManager` uses an interior block cache of 512 bytes, increasing its size by about 520 bytes but hugely reducing stack space required at run-time. 14 | - __Breaking Change__: The `VolumeManager::device` method now takes a callback rather than giving you a reference to the underlying `BlockDevice` 15 | - __Breaking Change__: `Error:LockError` variant added. 16 | - __Breaking Change__: `SearchId` was renamed to `Handle` 17 | 18 | ### Added 19 | 20 | - `File` now implements the `embedded-io` `Read`, `Write` and `Seek` traits. 21 | - New `iterate_dir_lfn` method on `VolumeManager` and `Directory` - provides decoded Long File Names as `Option<&str>` 22 | 23 | ### Removed 24 | 25 | - __Breaking Change__: Removed the `reason: &str` argument from `BlockDevice` 26 | 27 | ## [Version 0.8.0] - 2024-07-12 28 | 29 | ### Changed 30 | 31 | - Fixed a bug when seeking backwards through files. 32 | - Updated to `heapless-0.8` and `embedded-hal-bus-0.2`. 33 | - No longer panics if the close fails when a `Volume` is dropped - the failure is instead ignored. 34 | 35 | ### Added 36 | 37 | - `File` now has a `flush()` method. 38 | - `File` now has a `close()` method. 39 | 40 | ### Removed 41 | 42 | - __Breaking Change__: Removed `CS` type-param on `SdCard` - now we use the `SpiDevice` chip-select (closing [#126]) 43 | - __Breaking Change__: Removed the 74 clock cycle 'init' sequence - now applications must do this 44 | 45 | ## [Version 0.7.0] - 2024-02-04 46 | 47 | ### Changed 48 | 49 | - __Breaking Change__: `Volume`, `Directory` and `File` are now smart! They hold references to the thing they were made from, and will clean themselves up when dropped. The trade-off is you can can't open multiple volumes, directories or files at the same time. 50 | - __Breaking Change__: Renamed the old types to `RawVolume`, `RawDirectory` and `RawFile` 51 | - __Breaking Change__: Renamed `Error::FileNotFound` to `Error::NotFound` 52 | - Fixed long-standing bug that caused an integer overflow when a FAT32 directory was longer than one cluster ([#74]) 53 | - You can now open directories multiple times without error 54 | - Updated to [embedded-hal] 1.0 55 | 56 | ### Added 57 | 58 | - `RawVolume`, `RawDirectory` and `RawFile` types (like the old `Volume`, `Directory` and `File` types) 59 | - New method `make_dir_in_dir` 60 | - Empty strings and `"."` convert to `ShortFileName::this_dir()` 61 | - New API `change_dir` which changes a directory to point to some child directory (or the parent) without opening a new directory. 62 | - Updated 'shell' example to support `mkdir`, `tree` and relative/absolute paths 63 | 64 | ### Removed 65 | 66 | - None 67 | 68 | [#126]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/126 69 | [#74]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/74 70 | [embedded-hal]: https://crates.io/crates/embedded-hal 71 | 72 | ## [Version 0.6.0] - 2023-10-20 73 | 74 | ### Changed 75 | 76 | - Writing to a file no longer flushes file metadata to the Directory Entry. 77 | Instead closing a file now flushes file metadata to the Directory Entry. 78 | Requires mutable access to the Volume ([#94]). 79 | - Files now have the correct length when modified, not appended ([#72]). 80 | - Calling `SdCard::get_card_type` will now perform card initialisation ([#87] and [#90]). 81 | - Removed warning about unused arguments. 82 | - Types are now documented at the top level ([#86]). 83 | - Renamed `Cluster` to `ClusterId` and stopped you adding two together 84 | 85 | [#72]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/72 86 | [#86]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/86 87 | [#87]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/87 88 | [#90]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/90 89 | [#94]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/94 90 | 91 | ### Added 92 | 93 | - New examples, `append_file`, `create_file`, `delete_file`, `list_dir`, `shell` 94 | - New test cases `tests/directories.rs`, `tests/read_file.rs` 95 | 96 | ### Removed 97 | 98 | - __Breaking Change__: `Controller` alias for `VolumeManager` removed. 99 | - __Breaking Change__: `VolumeManager::open_dir_entry` removed, as it was unsafe to the user to randomly pick a starting cluster. 100 | - Old examples `create_test`, `test_mount`, `write_test`, `delete_test` 101 | 102 | ## [Version 0.5.0] - 2023-05-20 103 | 104 | ### Changed 105 | 106 | - __Breaking Change__: Renamed `Controller` to `VolumeManager`, to better describe what it does. 107 | - __Breaking Change__: Renamed `SdMmcSpi` to `SdCard` 108 | - __Breaking Change__: `AcquireOpts` now has `use_crc` (which makes it ask for CRCs to be enabled) instead of `require_crc` (which simply allowed the enable-CRC command to fail) 109 | - __Breaking Change__: `SdCard::new` now requires an object that implements the embedded-hal `DelayUs` trait 110 | - __Breaking Change__: Renamed `card_size_bytes` to `num_bytes`, to match `num_blocks` 111 | - More robust card intialisation procedure, with added retries 112 | - Supports building with neither `defmt` nor `log` logging 113 | 114 | ### Added 115 | 116 | - Added `mark_card_as_init` method, if you know the card is initialised and want to skip the initialisation step 117 | 118 | ### Removed 119 | 120 | - __Breaking Change__: Removed `BlockSpi` type - card initialisation now handled as an internal state variable 121 | 122 | ## [Version 0.4.0] - 2023-01-18 123 | 124 | ### Changed 125 | 126 | - Optionally use [defmt] s/defmt) for logging. 127 | Controlled by `defmt-log` feature flag. 128 | - __Breaking Change__: Use SPI blocking traits instead to ease SPI peripheral sharing. 129 | See: 130 | - Added `Controller::has_open_handles` and `Controller::free` methods. 131 | - __Breaking Change__: Changed interface to enforce correct SD state at compile time. 132 | - __Breaking Change__: Added custom error type for `File` operations. 133 | - Fix `env_logger` pulling in the `std` feature in `log` in library builds. 134 | - Raise the minimum supported Rust version to 1.56.0. 135 | - Code tidy-ups and more documentation. 136 | - Add `MAX_DIRS` and `MAX_FILES` generics to `Controller` to allow an arbitrary numbers of concurrent open directories and files. 137 | - Add new constructor method `Controller::new_with_limits(block_device: D, timesource: T) -> Controller` 138 | to create a `Controller` with custom limits. 139 | 140 | ## [Version 0.3.0] - 2019-12-16 141 | 142 | ### Changed 143 | 144 | - Updated to `v2` embedded-hal traits. 145 | - Added open support for all modes. 146 | - Added write support for files. 147 | - Added `Info_Sector` tracking for FAT32. 148 | - Change directory iteration to look in all the directory's clusters. 149 | - Added `write_test` and `create_test`. 150 | - De-duplicated FAT16 and FAT32 code () 151 | 152 | ## [Version 0.2.1] - 2019-02-19 153 | 154 | ### Changed 155 | 156 | - Added `readme=README.md` to `Cargo.toml` 157 | 158 | ## [Version 0.2.0] - 2019-01-24 159 | 160 | ### Changed 161 | 162 | - Reduce delay waiting for response. Big speed improvements. 163 | 164 | ## [Version 0.1.1] - 2018-12-23 165 | 166 | ### Changed 167 | 168 | - Can read blocks from an SD Card using an `embedded_hal::SPI` device and a 169 | `embedded_hal::OutputPin` for Chip Select. 170 | - Can read partition tables and open a FAT32 or FAT16 formatted partition. 171 | - Can open and iterate the root directory of a FAT16 formatted partition. 172 | 173 | [Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ 174 | [Semantic Versioning]: http://semver.org/spec/v2.0.0.html 175 | [Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.0...develop 176 | [Version 0.8.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.0...v0.7.0 177 | [Version 0.7.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.7.0...v0.6.0 178 | [Version 0.6.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.6.0...v0.5.0 179 | [Version 0.5.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.5.0...v0.4.0 180 | [Version 0.4.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.4.0...v0.3.0 181 | [Version 0.3.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.3.0...v0.2.1 182 | [Version 0.2.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.1...v0.2.0 183 | [Version 0.2.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.0...v0.1.1 184 | [Version 0.1.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.1.1 185 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Jonathan 'theJPster' Pallant ", "Rust Embedded Community Developers"] 3 | categories = ["embedded", "no-std"] 4 | description = "A basic SD/MMC driver for Embedded Rust." 5 | edition = "2021" 6 | keywords = ["sdcard", "mmc", "embedded", "fat32"] 7 | license = "MIT OR Apache-2.0" 8 | name = "embedded-sdmmc" 9 | readme = "README.md" 10 | repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" 11 | version = "0.8.0" 12 | 13 | # Make sure to update the CI too! 14 | rust-version = "1.76" 15 | 16 | [dependencies] 17 | byteorder = {version = "1", default-features = false} 18 | defmt = {version = "0.3", optional = true} 19 | embedded-hal = "1.0.0" 20 | embedded-io = "0.6.1" 21 | heapless = "^0.8" 22 | log = {version = "0.4", default-features = false, optional = true} 23 | 24 | [dev-dependencies] 25 | chrono = "0.4" 26 | embedded-hal-bus = "0.2.0" 27 | env_logger = "0.10.0" 28 | flate2 = "1.0" 29 | hex-literal = "0.4.1" 30 | sha2 = "0.10" 31 | 32 | [features] 33 | default = ["log"] 34 | defmt-log = ["dep:defmt"] 35 | log = ["dep:log"] 36 | -------------------------------------------------------------------------------- /LICENSE-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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2024 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers 2 | Copyright (c) 2011-2018 Bill Greiman 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without 8 | limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software 11 | is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice 15 | shall be included in all copies or substantial portions 16 | of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 19 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 22 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | # Copyright Notices 2 | 3 | This is a copyright notices file, as described by the Apache-2.0 license. 4 | 5 | Copyright (c) 2018-2024 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers 6 | Copyright (c) 2011-2018 Bill Greiman 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embedded SD/MMC [![crates.io](https://img.shields.io/crates/v/embedded-sdmmc.svg)](https://crates.io/crates/embedded-sdmmc) [![Documentation](https://docs.rs/embedded-sdmmc/badge.svg)](https://docs.rs/embedded-sdmmc) 2 | 3 | This crate is intended to allow you to read/write files on a FAT formatted SD 4 | card on your Rust Embedded device, as easily as using the `SdFat` Arduino 5 | library. It is written in pure-Rust, is `#![no_std]` and does not use `alloc` 6 | or `collections` to keep the memory footprint low. In the first instance it is 7 | designed for readability and simplicity over performance. 8 | 9 | ## Using the crate 10 | 11 | You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. 12 | 13 | ```rust 14 | use embedded_sdmmc::{SdCard, VolumeManager, Mode, VolumeIdx}; 15 | // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object 16 | let sdcard = SdCard::new(sdmmc_spi, delay); 17 | // Get the card size (this also triggers card initialisation because it's not been done yet) 18 | println!("Card size is {} bytes", sdcard.num_bytes()?); 19 | // Now let's look for volumes (also known as partitions) on our block device. 20 | // To do this we need a Volume Manager. It will take ownership of the block device. 21 | let volume_mgr = VolumeManager::new(sdcard, time_source); 22 | // Try and access Volume 0 (i.e. the first partition). 23 | // The volume object holds information about the filesystem on that volume. 24 | let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; 25 | println!("Volume 0: {:?}", volume0); 26 | // Open the root directory (mutably borrows from the volume). 27 | let root_dir = volume0.open_root_dir()?; 28 | // Open a file called "MY_FILE.TXT" in the root directory 29 | // This mutably borrows the directory. 30 | let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; 31 | // Print the contents of the file, assuming it's in ISO-8859-1 encoding 32 | while !my_file.is_eof() { 33 | let mut buffer = [0u8; 32]; 34 | let num_read = my_file.read(&mut buffer)?; 35 | for b in &buffer[0..num_read] { 36 | print!("{}", *b as char); 37 | } 38 | } 39 | ``` 40 | 41 | For writing files: 42 | 43 | ```rust 44 | let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; 45 | my_other_file.write(b"Timestamp,Signal,Value\n")?; 46 | my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; 47 | my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; 48 | my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; 49 | 50 | // Don't forget to flush the file so that the directory entry is updated 51 | my_other_file.flush()?; 52 | ``` 53 | 54 | ### Open directories and files 55 | 56 | By default the `VolumeManager` will initialize with a maximum number of `4` open directories, files and volumes. This can be customized by specifying the `MAX_DIR`, `MAX_FILES` and `MAX_VOLUMES` generic consts of the `VolumeManager`: 57 | 58 | ```rust 59 | // Create a volume manager with a maximum of 6 open directories, 12 open files, and 4 volumes (or partitions) 60 | let cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); 61 | ``` 62 | 63 | ## Supported features 64 | 65 | * Open files in all supported methods from an open directory 66 | * Open an arbitrary number of directories and files 67 | * Read data from open files 68 | * Write data to open files 69 | * Close files 70 | * Delete files 71 | * Iterate root directory 72 | * Iterate sub-directories 73 | * Log over defmt or the common log interface (feature flags). 74 | 75 | ## No-std usage 76 | 77 | This repository houses no examples for no-std usage, however you can check out the following examples: 78 | 79 | * [Pi Pico](https://github.com/rp-rs/rp-hal-boards/blob/main/boards/rp-pico/examples/pico_spi_sd_card.rs) 80 | * [STM32H7XX](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/sdmmc_fat.rs) 81 | * [atsamd(pygamer)](https://github.com/atsamd-rs/atsamd/blob/master/boards/pygamer/examples/sd_card.rs) 82 | 83 | ## Todo List (PRs welcome!) 84 | 85 | * Create new dirs 86 | * Delete (empty) directories 87 | * Handle MS-DOS `/path/foo/bar.txt` style paths. 88 | 89 | ## Changelog 90 | 91 | The changelog has moved to [CHANGELOG.md](/CHANGELOG.md) 92 | 93 | ## License 94 | 95 | Licensed under either of 96 | 97 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 98 | ) 99 | 100 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 101 | 102 | at your option. 103 | 104 | Copyright notices are stored in the [NOTICE](./NOTICE) file. 105 | 106 | ## Contribution 107 | 108 | Unless you explicitly state otherwise, any contribution intentionally 109 | submitted for inclusion in the work by you, as defined in the Apache-2.0 110 | license, shall be dual licensed as above, without any additional terms or 111 | conditions. 112 | -------------------------------------------------------------------------------- /examples/append_file.rs: -------------------------------------------------------------------------------- 1 | //! Append File Example. 2 | //! 3 | //! ```bash 4 | //! $ cargo run --example append_file -- ./disk.img 5 | //! $ cargo run --example append_file -- /dev/mmcblk0 6 | //! ``` 7 | //! 8 | //! If you pass a block device it should be unmounted. There is a gzipped 9 | //! example disk image which you can gunzip and test with if you don't have a 10 | //! suitable block device. 11 | //! 12 | //! ```bash 13 | //! zcat ./tests/disk.img.gz > ./disk.img 14 | //! $ cargo run --example append_file -- ./disk.img 15 | //! ``` 16 | 17 | mod linux; 18 | use linux::*; 19 | 20 | const FILE_TO_APPEND: &str = "README.TXT"; 21 | 22 | use embedded_sdmmc::{Error, Mode, VolumeIdx}; 23 | 24 | type VolumeManager = embedded_sdmmc::VolumeManager; 25 | 26 | fn main() -> Result<(), Error> { 27 | env_logger::init(); 28 | let mut args = std::env::args().skip(1); 29 | let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); 30 | let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); 31 | let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; 32 | let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); 33 | let volume = volume_mgr.open_volume(VolumeIdx(0))?; 34 | let root_dir = volume.open_root_dir()?; 35 | println!("\nCreating file {}...", FILE_TO_APPEND); 36 | let f = root_dir.open_file_in_dir(FILE_TO_APPEND, Mode::ReadWriteAppend)?; 37 | f.write(b"\r\n\r\nThis has been added to your file.\r\n")?; 38 | Ok(()) 39 | } 40 | 41 | // **************************************************************************** 42 | // 43 | // End Of File 44 | // 45 | // **************************************************************************** 46 | -------------------------------------------------------------------------------- /examples/big_dir.rs: -------------------------------------------------------------------------------- 1 | //! Big Directory Example. 2 | //! 3 | //! Attempts to create an infinite number of files in the root directory of the 4 | //! first volume of the given block device. This is basically to see what 5 | //! happens when the root directory runs out of space. 6 | //! 7 | //! ```bash 8 | //! $ cargo run --example big_dir -- ./disk.img 9 | //! $ cargo run --example big_dir -- /dev/mmcblk0 10 | //! ``` 11 | //! 12 | //! If you pass a block device it should be unmounted. There is a gzipped 13 | //! example disk image which you can gunzip and test with if you don't have a 14 | //! suitable block device. 15 | //! 16 | //! ```bash 17 | //! zcat ./tests/disk.img.gz > ./disk.img 18 | //! $ cargo run --example big_dir -- ./disk.img 19 | //! ``` 20 | 21 | mod linux; 22 | use linux::*; 23 | 24 | use embedded_sdmmc::{Error, Mode, VolumeIdx}; 25 | 26 | type VolumeManager = embedded_sdmmc::VolumeManager; 27 | 28 | fn main() -> Result<(), Error> { 29 | env_logger::init(); 30 | let mut args = std::env::args().skip(1); 31 | let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); 32 | let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); 33 | let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; 34 | let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); 35 | let volume = volume_mgr.open_volume(VolumeIdx(0)).unwrap(); 36 | println!("Volume: {:?}", volume); 37 | let root_dir = volume.open_root_dir().unwrap(); 38 | 39 | let mut file_num = 0; 40 | loop { 41 | file_num += 1; 42 | let file_name = format!("{}.da", file_num); 43 | println!("opening file {file_name} for writing"); 44 | let file = root_dir 45 | .open_file_in_dir(file_name.as_str(), Mode::ReadWriteCreateOrTruncate) 46 | .unwrap(); 47 | let buf = b"hello world, from rust"; 48 | println!("writing to file"); 49 | file.write(&buf[..]).unwrap(); 50 | println!("closing file"); 51 | drop(file); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/create_file.rs: -------------------------------------------------------------------------------- 1 | //! Create File Example. 2 | //! 3 | //! ```bash 4 | //! $ cargo run --example create_file -- ./disk.img 5 | //! $ cargo run --example create_file -- /dev/mmcblk0 6 | //! ``` 7 | //! 8 | //! If you pass a block device it should be unmounted. There is a gzipped 9 | //! example disk image which you can gunzip and test with if you don't have a 10 | //! suitable block device. 11 | //! 12 | //! ```bash 13 | //! zcat ./tests/disk.img.gz > ./disk.img 14 | //! $ cargo run --example create_file -- ./disk.img 15 | //! ``` 16 | 17 | mod linux; 18 | use linux::*; 19 | 20 | const FILE_TO_CREATE: &str = "CREATE.TXT"; 21 | 22 | use embedded_sdmmc::{Error, Mode, VolumeIdx}; 23 | 24 | type VolumeManager = embedded_sdmmc::VolumeManager; 25 | 26 | fn main() -> Result<(), Error> { 27 | env_logger::init(); 28 | let mut args = std::env::args().skip(1); 29 | let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); 30 | let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); 31 | let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; 32 | let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); 33 | let volume = volume_mgr.open_volume(VolumeIdx(0))?; 34 | let root_dir = volume.open_root_dir()?; 35 | println!("\nCreating file {}...", FILE_TO_CREATE); 36 | // This will panic if the file already exists: use ReadWriteCreateOrAppend 37 | // or ReadWriteCreateOrTruncate instead if you want to modify an existing 38 | // file. 39 | let f = root_dir.open_file_in_dir(FILE_TO_CREATE, Mode::ReadWriteCreate)?; 40 | f.write(b"Hello, this is a new file on disk\r\n")?; 41 | Ok(()) 42 | } 43 | 44 | // **************************************************************************** 45 | // 46 | // End Of File 47 | // 48 | // **************************************************************************** 49 | -------------------------------------------------------------------------------- /examples/delete_file.rs: -------------------------------------------------------------------------------- 1 | //! Delete File Example. 2 | //! 3 | //! ```bash 4 | //! $ cargo run --example delete_file -- ./disk.img 5 | //! $ cargo run --example delete_file -- /dev/mmcblk0 6 | //! ``` 7 | //! 8 | //! NOTE: THIS EXAMPLE DELETES A FILE CALLED README.TXT. IF YOU DO NOT WANT THAT 9 | //! FILE DELETED FROM YOUR DISK IMAGE, DO NOT RUN THIS EXAMPLE. 10 | //! 11 | //! If you pass a block device it should be unmounted. There is a gzipped 12 | //! example disk image which you can gunzip and test with if you don't have a 13 | //! suitable block device. 14 | //! 15 | //! ```bash 16 | //! zcat ./tests/disk.img.gz > ./disk.img 17 | //! $ cargo run --example delete_file -- ./disk.img 18 | //! ``` 19 | 20 | mod linux; 21 | use linux::*; 22 | 23 | const FILE_TO_DELETE: &str = "README.TXT"; 24 | 25 | use embedded_sdmmc::{Error, VolumeIdx}; 26 | 27 | type VolumeManager = embedded_sdmmc::VolumeManager; 28 | 29 | fn main() -> Result<(), Error> { 30 | env_logger::init(); 31 | let mut args = std::env::args().skip(1); 32 | let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); 33 | let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); 34 | let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; 35 | let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); 36 | let volume = volume_mgr.open_volume(VolumeIdx(0))?; 37 | let root_dir = volume.open_root_dir()?; 38 | println!("Deleting file {}...", FILE_TO_DELETE); 39 | root_dir.delete_file_in_dir(FILE_TO_DELETE)?; 40 | println!("Deleted!"); 41 | Ok(()) 42 | } 43 | 44 | // **************************************************************************** 45 | // 46 | // End Of File 47 | // 48 | // **************************************************************************** 49 | -------------------------------------------------------------------------------- /examples/linux/mod.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for using embedded-sdmmc on Linux 2 | 3 | use chrono::Timelike; 4 | use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx, TimeSource, Timestamp}; 5 | use std::cell::RefCell; 6 | use std::fs::{File, OpenOptions}; 7 | use std::io::prelude::*; 8 | use std::io::SeekFrom; 9 | use std::path::Path; 10 | 11 | #[derive(Debug)] 12 | pub struct LinuxBlockDevice { 13 | file: RefCell, 14 | print_blocks: bool, 15 | } 16 | 17 | impl LinuxBlockDevice { 18 | pub fn new

(device_name: P, print_blocks: bool) -> Result 19 | where 20 | P: AsRef, 21 | { 22 | Ok(LinuxBlockDevice { 23 | file: RefCell::new( 24 | OpenOptions::new() 25 | .read(true) 26 | .write(true) 27 | .open(device_name)?, 28 | ), 29 | print_blocks, 30 | }) 31 | } 32 | } 33 | 34 | impl BlockDevice for LinuxBlockDevice { 35 | type Error = std::io::Error; 36 | 37 | fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { 38 | self.file 39 | .borrow_mut() 40 | .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; 41 | for block in blocks.iter_mut() { 42 | self.file.borrow_mut().read_exact(&mut block.contents)?; 43 | if self.print_blocks { 44 | println!("Read block {:?}: {:?}", start_block_idx, &block); 45 | } 46 | } 47 | Ok(()) 48 | } 49 | 50 | fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { 51 | self.file 52 | .borrow_mut() 53 | .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; 54 | for block in blocks.iter() { 55 | self.file.borrow_mut().write_all(&block.contents)?; 56 | if self.print_blocks { 57 | println!("Wrote: {:?}", &block); 58 | } 59 | } 60 | Ok(()) 61 | } 62 | 63 | fn num_blocks(&self) -> Result { 64 | let num_blocks = self.file.borrow().metadata().unwrap().len() / 512; 65 | Ok(BlockCount(num_blocks as u32)) 66 | } 67 | } 68 | 69 | #[derive(Debug)] 70 | pub struct Clock; 71 | 72 | impl TimeSource for Clock { 73 | fn get_timestamp(&self) -> Timestamp { 74 | use chrono::Datelike; 75 | let local: chrono::DateTime = chrono::Local::now(); 76 | Timestamp { 77 | year_since_1970: (local.year() - 1970) as u8, 78 | zero_indexed_month: local.month0() as u8, 79 | zero_indexed_day: local.day0() as u8, 80 | hours: local.hour() as u8, 81 | minutes: local.minute() as u8, 82 | seconds: local.second() as u8, 83 | } 84 | } 85 | } 86 | 87 | // **************************************************************************** 88 | // 89 | // End Of File 90 | // 91 | // **************************************************************************** 92 | -------------------------------------------------------------------------------- /examples/list_dir.rs: -------------------------------------------------------------------------------- 1 | //! Recursive Directory Listing Example. 2 | //! 3 | //! ```bash 4 | //! $ cargo run --example list_dir -- /dev/mmcblk0 5 | //! Compiling embedded-sdmmc v0.5.0 (/Users/jonathan/embedded-sdmmc-rs) 6 | //! Finished dev [unoptimized + debuginfo] target(s) in 0.20s 7 | //! Running `/Users/jonathan/embedded-sdmmc-rs/target/debug/examples/list_dir /dev/mmcblk0` 8 | //! Listing / 9 | //! README.TXT 258 2018-12-09 19:22:34 10 | //! EMPTY.DAT 0 2018-12-09 19:21:16 11 | //! TEST 0 2018-12-09 19:23:16

12 | //! 64MB.DAT 67108864 2018-12-09 19:21:38 13 | //! FSEVEN~1 0 2023-09-21 11:32:04 14 | //! Listing /TEST 15 | //! . 0 2018-12-09 19:21:02 16 | //! .. 0 2018-12-09 19:21:02 17 | //! TEST.DAT 3500 2018-12-09 19:22:12 18 | //! Listing /FSEVEN~1 19 | //! . 0 2023-09-21 11:32:22 20 | //! .. 0 2023-09-21 11:32:04 21 | //! FSEVEN~1 36 2023-09-21 11:32:04 22 | //! $ 23 | //! ``` 24 | //! 25 | //! If you pass a block device it should be unmounted. There is a gzipped 26 | //! example disk image which you can gunzip and test with if you don't have a 27 | //! suitable block device. 28 | //! 29 | //! ```bash 30 | //! zcat ./tests/disk.img.gz > ./disk.img 31 | //! $ cargo run --example list_dir -- ./disk.img 32 | //! ``` 33 | 34 | mod linux; 35 | use linux::*; 36 | 37 | use embedded_sdmmc::{ShortFileName, VolumeIdx}; 38 | 39 | type Error = embedded_sdmmc::Error; 40 | 41 | type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 4, 4>; 42 | type VolumeManager = embedded_sdmmc::VolumeManager; 43 | 44 | fn main() -> Result<(), Error> { 45 | env_logger::init(); 46 | let mut args = std::env::args().skip(1); 47 | let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); 48 | let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); 49 | 50 | let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; 51 | let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); 52 | let volume = volume_mgr.open_volume(VolumeIdx(0))?; 53 | let root_dir = volume.open_root_dir()?; 54 | list_dir(root_dir, "/")?; 55 | Ok(()) 56 | } 57 | 58 | /// Recursively print a directory listing for the open directory given. 59 | /// 60 | /// The path is for display purposes only. 61 | fn list_dir(directory: Directory<'_>, path: &str) -> Result<(), Error> { 62 | println!("Listing {}", path); 63 | let mut children = Vec::new(); 64 | directory.iterate_dir(|entry| { 65 | println!( 66 | "{:12} {:9} {} {}", 67 | entry.name, 68 | entry.size, 69 | entry.mtime, 70 | if entry.attributes.is_directory() { 71 | "" 72 | } else { 73 | "" 74 | } 75 | ); 76 | if entry.attributes.is_directory() 77 | && entry.name != ShortFileName::parent_dir() 78 | && entry.name != ShortFileName::this_dir() 79 | { 80 | children.push(entry.name.clone()); 81 | } 82 | })?; 83 | for child_name in children { 84 | let child_dir = directory.open_dir(&child_name)?; 85 | let child_path = if path == "/" { 86 | format!("/{}", child_name) 87 | } else { 88 | format!("{}/{}", path, child_name) 89 | }; 90 | list_dir(child_dir, &child_path)?; 91 | } 92 | Ok(()) 93 | } 94 | 95 | // **************************************************************************** 96 | // 97 | // End Of File 98 | // 99 | // **************************************************************************** 100 | -------------------------------------------------------------------------------- /examples/read_file.rs: -------------------------------------------------------------------------------- 1 | //! Read File Example. 2 | //! 3 | //! ```bash 4 | //! $ cargo run --example read_file -- ./disk.img 5 | //! Reading file README.TXT... 6 | //! 00000000 [54, 68, 69, 73, 20, 69, 73, 20, 61, 20, 46, 41, 54, 31, 36, 20] |This.is.a.FAT16.| 7 | //! 00000010 [70, 61, 74, 69, 74, 69, 6f, 6e, 2e, 20, 49, 74, 20, 63, 6f, 6e] |patition..It.con| 8 | //! 00000020 [74, 61, 69, 6e, 73, 20, 66, 6f, 75, 72, 20, 66, 69, 6c, 65, 73] |tains.four.files| 9 | //! 00000030 [20, 61, 6e, 64, 20, 61, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |.and.a.directory| 10 | //! 00000040 [2e, 0a, 0a, 2a, 20, 54, 68, 69, 73, 20, 66, 69, 6c, 65, 20, 28] |...*.This.file.(| 11 | //! 00000050 [52, 45, 41, 44, 4d, 45, 2e, 54, 58, 54, 29, 0a, 2a, 20, 41, 20] |README.TXT).*.A.| 12 | //! 00000060 [36, 34, 20, 4d, 69, 42, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c] |64.MiB.file.full| 13 | //! 00000070 [20, 6f, 66, 20, 7a, 65, 72, 6f, 73, 20, 28, 36, 34, 4d, 42, 2e] |.of.zeros.(64MB.| 14 | //! 00000080 [44, 41, 54, 29, 2e, 0a, 2a, 20, 41, 20, 33, 35, 30, 30, 20, 62] |DAT)..*.A.3500.b| 15 | //! 00000090 [79, 74, 65, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c, 20, 6f, 66] |yte.file.full.of| 16 | //! 000000a0 [20, 72, 61, 6e, 64, 6f, 6d, 20, 64, 61, 74, 61, 2e, 0a, 2a, 20] |.random.data..*.| 17 | //! 000000b0 [41, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79, 20, 63, 61, 6c, 6c] |A.directory.call| 18 | //! 000000c0 [65, 64, 20, 54, 45, 53, 54, 0a, 2a, 20, 41, 20, 7a, 65, 72, 6f] |ed.TEST.*.A.zero| 19 | //! 000000d0 [20, 62, 79, 74, 65, 20, 66, 69, 6c, 65, 20, 69, 6e, 20, 74, 68] |.byte.file.in.th| 20 | //! 000000e0 [65, 20, 54, 45, 53, 54, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |e.TEST.directory| 21 | //! 000000f0 [20, 63, 61, 6c, 6c, 65, 64, 20, 45, 4d, 50, 54, 59, 2e, 44, 41] |.called.EMPTY.DA| 22 | //! 00000100 [54, 0a, 0d] |T...............| 23 | //! ``` 24 | //! 25 | //! If you pass a block device it should be unmounted. There is a gzipped 26 | //! example disk image which you can gunzip and test with if you don't have a 27 | //! suitable block device. 28 | //! 29 | //! ```bash 30 | //! zcat ./tests/disk.img.gz > ./disk.img 31 | //! $ cargo run --example read_file -- ./disk.img 32 | //! ``` 33 | 34 | mod linux; 35 | use linux::*; 36 | 37 | const FILE_TO_READ: &str = "README.TXT"; 38 | 39 | use embedded_sdmmc::{Error, Mode, VolumeIdx}; 40 | 41 | type VolumeManager = embedded_sdmmc::VolumeManager; 42 | 43 | fn main() -> Result<(), Error> { 44 | env_logger::init(); 45 | let mut args = std::env::args().skip(1); 46 | let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); 47 | let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); 48 | let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; 49 | let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); 50 | let volume = volume_mgr.open_volume(VolumeIdx(0))?; 51 | let root_dir = volume.open_root_dir()?; 52 | println!("\nReading file {}...", FILE_TO_READ); 53 | let f = root_dir.open_file_in_dir(FILE_TO_READ, Mode::ReadOnly)?; 54 | // Proves we can open two files at once now (or try to - this file doesn't exist) 55 | let f2 = root_dir.open_file_in_dir("MISSING.DAT", Mode::ReadOnly); 56 | assert!(f2.is_err()); 57 | while !f.is_eof() { 58 | let mut buffer = [0u8; 16]; 59 | let offset = f.offset(); 60 | let mut len = f.read(&mut buffer)?; 61 | print!("{:08x} {:02x?}", offset, &buffer[0..len]); 62 | while len < buffer.len() { 63 | print!(" "); 64 | len += 1; 65 | } 66 | print!(" |"); 67 | for b in buffer.iter() { 68 | let ch = char::from(*b); 69 | if ch.is_ascii_graphic() { 70 | print!("{}", ch); 71 | } else { 72 | print!("."); 73 | } 74 | } 75 | println!("|"); 76 | } 77 | Ok(()) 78 | } 79 | 80 | // **************************************************************************** 81 | // 82 | // End Of File 83 | // 84 | // **************************************************************************** 85 | -------------------------------------------------------------------------------- /examples/readme_test.rs: -------------------------------------------------------------------------------- 1 | //! This is the code from the README.md file. 2 | //! 3 | //! We add enough stuff to make it compile, but it won't run because our fake 4 | //! SPI doesn't do any replies. 5 | 6 | #![allow(dead_code)] 7 | 8 | use core::cell::RefCell; 9 | 10 | use embedded_sdmmc::{Error, SdCardError, TimeSource, Timestamp}; 11 | 12 | pub struct DummyCsPin; 13 | 14 | impl embedded_hal::digital::ErrorType for DummyCsPin { 15 | type Error = core::convert::Infallible; 16 | } 17 | 18 | impl embedded_hal::digital::OutputPin for DummyCsPin { 19 | #[inline(always)] 20 | fn set_low(&mut self) -> Result<(), Self::Error> { 21 | Ok(()) 22 | } 23 | 24 | #[inline(always)] 25 | fn set_high(&mut self) -> Result<(), Self::Error> { 26 | Ok(()) 27 | } 28 | } 29 | 30 | struct FakeSpiBus(); 31 | 32 | impl embedded_hal::spi::ErrorType for FakeSpiBus { 33 | type Error = core::convert::Infallible; 34 | } 35 | 36 | impl embedded_hal::spi::SpiBus for FakeSpiBus { 37 | fn read(&mut self, _: &mut [u8]) -> Result<(), Self::Error> { 38 | Ok(()) 39 | } 40 | 41 | fn write(&mut self, _: &[u8]) -> Result<(), Self::Error> { 42 | Ok(()) 43 | } 44 | 45 | fn transfer(&mut self, _: &mut [u8], _: &[u8]) -> Result<(), Self::Error> { 46 | Ok(()) 47 | } 48 | 49 | fn transfer_in_place(&mut self, _: &mut [u8]) -> Result<(), Self::Error> { 50 | Ok(()) 51 | } 52 | 53 | fn flush(&mut self) -> Result<(), Self::Error> { 54 | Ok(()) 55 | } 56 | } 57 | 58 | struct FakeCs(); 59 | 60 | impl embedded_hal::digital::ErrorType for FakeCs { 61 | type Error = core::convert::Infallible; 62 | } 63 | 64 | impl embedded_hal::digital::OutputPin for FakeCs { 65 | fn set_low(&mut self) -> Result<(), Self::Error> { 66 | Ok(()) 67 | } 68 | 69 | fn set_high(&mut self) -> Result<(), Self::Error> { 70 | Ok(()) 71 | } 72 | } 73 | 74 | #[derive(Clone, Copy)] 75 | struct FakeDelayer(); 76 | 77 | impl embedded_hal::delay::DelayNs for FakeDelayer { 78 | fn delay_ns(&mut self, ns: u32) { 79 | std::thread::sleep(std::time::Duration::from_nanos(u64::from(ns))); 80 | } 81 | } 82 | 83 | struct FakeTimesource(); 84 | 85 | impl TimeSource for FakeTimesource { 86 | fn get_timestamp(&self) -> Timestamp { 87 | Timestamp { 88 | year_since_1970: 0, 89 | zero_indexed_month: 0, 90 | zero_indexed_day: 0, 91 | hours: 0, 92 | minutes: 0, 93 | seconds: 0, 94 | } 95 | } 96 | } 97 | 98 | #[derive(Debug, Clone)] 99 | enum MyError { 100 | Filesystem(Error), 101 | Disk(SdCardError), 102 | } 103 | 104 | impl From> for MyError { 105 | fn from(value: Error) -> MyError { 106 | MyError::Filesystem(value) 107 | } 108 | } 109 | 110 | impl From for MyError { 111 | fn from(value: SdCardError) -> MyError { 112 | MyError::Disk(value) 113 | } 114 | } 115 | 116 | fn main() -> Result<(), MyError> { 117 | // BEGIN Fake stuff that will be replaced with real peripherals 118 | let spi_bus = RefCell::new(FakeSpiBus()); 119 | let delay = FakeDelayer(); 120 | let sdmmc_spi = embedded_hal_bus::spi::RefCellDevice::new(&spi_bus, DummyCsPin, delay).unwrap(); 121 | let time_source = FakeTimesource(); 122 | // END Fake stuff that will be replaced with real peripherals 123 | 124 | use embedded_sdmmc::{Mode, SdCard, VolumeIdx, VolumeManager}; 125 | // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object 126 | let sdcard = SdCard::new(sdmmc_spi, delay); 127 | // Get the card size (this also triggers card initialisation because it's not been done yet) 128 | println!("Card size is {} bytes", sdcard.num_bytes()?); 129 | // Now let's look for volumes (also known as partitions) on our block device. 130 | // To do this we need a Volume Manager. It will take ownership of the block device. 131 | let volume_mgr = VolumeManager::new(sdcard, time_source); 132 | // Try and access Volume 0 (i.e. the first partition). 133 | // The volume object holds information about the filesystem on that volume. 134 | let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; 135 | println!("Volume 0: {:?}", volume0); 136 | // Open the root directory (mutably borrows from the volume). 137 | let root_dir = volume0.open_root_dir()?; 138 | // Open a file called "MY_FILE.TXT" in the root directory 139 | // This mutably borrows the directory. 140 | let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; 141 | // Print the contents of the file, assuming it's in ISO-8859-1 encoding 142 | while !my_file.is_eof() { 143 | let mut buffer = [0u8; 32]; 144 | let num_read = my_file.read(&mut buffer)?; 145 | for b in &buffer[0..num_read] { 146 | print!("{}", *b as char); 147 | } 148 | } 149 | 150 | Ok(()) 151 | } 152 | 153 | // **************************************************************************** 154 | // 155 | // End Of File 156 | // 157 | // **************************************************************************** 158 | -------------------------------------------------------------------------------- /src/blockdevice.rs: -------------------------------------------------------------------------------- 1 | //! Traits and types for working with Block Devices. 2 | //! 3 | //! Generic code for handling block devices, such as types for identifying 4 | //! a particular block on a block device by its index. 5 | 6 | /// A standard 512 byte block (also known as a sector). 7 | /// 8 | /// IBM PC formatted 5.25" and 3.5" floppy disks, IDE/SATA Hard Drives up to 9 | /// about 2 TiB, and almost all SD/MMC cards have 512 byte blocks. 10 | /// 11 | /// This library does not support devices with a block size other than 512 12 | /// bytes. 13 | #[derive(Clone)] 14 | pub struct Block { 15 | /// The 512 bytes in this block (or sector). 16 | pub contents: [u8; Block::LEN], 17 | } 18 | 19 | impl Block { 20 | /// All our blocks are a fixed length of 512 bytes. We do not support 21 | /// 'Advanced Format' Hard Drives with 4 KiB blocks, nor weird old 22 | /// pre-3.5-inch floppy disk formats. 23 | pub const LEN: usize = 512; 24 | 25 | /// Sometimes we want `LEN` as a `u32` and the casts don't look nice. 26 | pub const LEN_U32: u32 = 512; 27 | 28 | /// Create a new block full of zeros. 29 | pub fn new() -> Block { 30 | Block { 31 | contents: [0u8; Self::LEN], 32 | } 33 | } 34 | } 35 | 36 | impl core::ops::Deref for Block { 37 | type Target = [u8; 512]; 38 | fn deref(&self) -> &[u8; 512] { 39 | &self.contents 40 | } 41 | } 42 | 43 | impl core::ops::DerefMut for Block { 44 | fn deref_mut(&mut self) -> &mut [u8; 512] { 45 | &mut self.contents 46 | } 47 | } 48 | 49 | impl core::fmt::Debug for Block { 50 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { 51 | writeln!(fmt, "Block:")?; 52 | for line in self.contents.chunks(32) { 53 | for b in line { 54 | write!(fmt, "{:02x}", b)?; 55 | } 56 | write!(fmt, " ")?; 57 | for &b in line { 58 | if (0x20..=0x7F).contains(&b) { 59 | write!(fmt, "{}", b as char)?; 60 | } else { 61 | write!(fmt, ".")?; 62 | } 63 | } 64 | writeln!(fmt)?; 65 | } 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl Default for Block { 71 | fn default() -> Self { 72 | Self::new() 73 | } 74 | } 75 | 76 | /// A block device - a device which can read and write blocks (or 77 | /// sectors). Only supports devices which are <= 2 TiB in size. 78 | pub trait BlockDevice { 79 | /// The errors that the `BlockDevice` can return. Must be debug formattable. 80 | type Error: core::fmt::Debug; 81 | /// Read one or more blocks, starting at the given block index. 82 | fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; 83 | /// Write one or more blocks, starting at the given block index. 84 | fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; 85 | /// Determine how many blocks this device can hold. 86 | fn num_blocks(&self) -> Result; 87 | } 88 | 89 | /// A caching layer for block devices 90 | /// 91 | /// Caches a single block. 92 | #[derive(Debug)] 93 | pub struct BlockCache { 94 | block_device: D, 95 | block: [Block; 1], 96 | block_idx: Option, 97 | } 98 | 99 | impl BlockCache 100 | where 101 | D: BlockDevice, 102 | { 103 | /// Create a new block cache 104 | pub fn new(block_device: D) -> BlockCache { 105 | BlockCache { 106 | block_device, 107 | block: [Block::new()], 108 | block_idx: None, 109 | } 110 | } 111 | 112 | /// Read a block, and return a reference to it. 113 | pub fn read(&mut self, block_idx: BlockIdx) -> Result<&Block, D::Error> { 114 | if self.block_idx != Some(block_idx) { 115 | self.block_idx = None; 116 | self.block_device.read(&mut self.block, block_idx)?; 117 | self.block_idx = Some(block_idx); 118 | } 119 | Ok(&self.block[0]) 120 | } 121 | 122 | /// Read a block, and return a reference to it. 123 | pub fn read_mut(&mut self, block_idx: BlockIdx) -> Result<&mut Block, D::Error> { 124 | if self.block_idx != Some(block_idx) { 125 | self.block_idx = None; 126 | self.block_device.read(&mut self.block, block_idx)?; 127 | self.block_idx = Some(block_idx); 128 | } 129 | Ok(&mut self.block[0]) 130 | } 131 | 132 | /// Write back a block you read with [`Self::read_mut`] and then modified. 133 | pub fn write_back(&mut self) -> Result<(), D::Error> { 134 | self.block_device.write( 135 | &self.block, 136 | self.block_idx.expect("write_back with no read"), 137 | ) 138 | } 139 | 140 | /// Write back a block you read with [`Self::read_mut`] and then modified, but to two locations. 141 | /// 142 | /// This is useful for updating two File Allocation Tables. 143 | pub fn write_back_with_duplicate(&mut self, duplicate: BlockIdx) -> Result<(), D::Error> { 144 | self.block_device.write( 145 | &self.block, 146 | self.block_idx.expect("write_back with no read"), 147 | )?; 148 | self.block_device.write(&self.block, duplicate)?; 149 | Ok(()) 150 | } 151 | 152 | /// Access a blank sector 153 | pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block { 154 | self.block_idx = Some(block_idx); 155 | self.block[0].fill(0); 156 | &mut self.block[0] 157 | } 158 | 159 | /// Access the block device 160 | pub fn block_device(&mut self) -> &mut D { 161 | // invalidate the cache 162 | self.block_idx = None; 163 | // give them the block device 164 | &mut self.block_device 165 | } 166 | 167 | /// Get the block device back 168 | pub fn free(self) -> D { 169 | self.block_device 170 | } 171 | } 172 | 173 | /// The linear numeric address of a block (or sector). 174 | /// 175 | /// The first block on a disk gets `BlockIdx(0)` (which usually contains the 176 | /// Master Boot Record). 177 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 178 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 179 | pub struct BlockIdx(pub u32); 180 | 181 | impl BlockIdx { 182 | /// Convert a block index into a 64-bit byte offset from the start of the 183 | /// volume. Useful if your underlying block device actually works in 184 | /// bytes, like `open("/dev/mmcblk0")` does on Linux. 185 | pub fn into_bytes(self) -> u64 { 186 | (u64::from(self.0)) * (Block::LEN as u64) 187 | } 188 | 189 | /// Create an iterator from the current `BlockIdx` through the given 190 | /// number of blocks. 191 | pub fn range(self, num: BlockCount) -> BlockIter { 192 | BlockIter::new(self, self + BlockCount(num.0)) 193 | } 194 | } 195 | 196 | impl core::ops::Add for BlockIdx { 197 | type Output = BlockIdx; 198 | fn add(self, rhs: BlockCount) -> BlockIdx { 199 | BlockIdx(self.0 + rhs.0) 200 | } 201 | } 202 | 203 | impl core::ops::AddAssign for BlockIdx { 204 | fn add_assign(&mut self, rhs: BlockCount) { 205 | self.0 += rhs.0 206 | } 207 | } 208 | 209 | impl core::ops::Sub for BlockIdx { 210 | type Output = BlockIdx; 211 | fn sub(self, rhs: BlockCount) -> BlockIdx { 212 | BlockIdx(self.0 - rhs.0) 213 | } 214 | } 215 | 216 | impl core::ops::SubAssign for BlockIdx { 217 | fn sub_assign(&mut self, rhs: BlockCount) { 218 | self.0 -= rhs.0 219 | } 220 | } 221 | 222 | /// The a number of blocks (or sectors). 223 | /// 224 | /// Add this to a `BlockIdx` to get an actual address on disk. 225 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 226 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 227 | pub struct BlockCount(pub u32); 228 | 229 | impl core::ops::Add for BlockCount { 230 | type Output = BlockCount; 231 | fn add(self, rhs: BlockCount) -> BlockCount { 232 | BlockCount(self.0 + rhs.0) 233 | } 234 | } 235 | 236 | impl core::ops::AddAssign for BlockCount { 237 | fn add_assign(&mut self, rhs: BlockCount) { 238 | self.0 += rhs.0 239 | } 240 | } 241 | 242 | impl core::ops::Sub for BlockCount { 243 | type Output = BlockCount; 244 | fn sub(self, rhs: BlockCount) -> BlockCount { 245 | BlockCount(self.0 - rhs.0) 246 | } 247 | } 248 | 249 | impl core::ops::SubAssign for BlockCount { 250 | fn sub_assign(&mut self, rhs: BlockCount) { 251 | self.0 -= rhs.0 252 | } 253 | } 254 | 255 | impl BlockCount { 256 | /// How many blocks are required to hold this many bytes. 257 | /// 258 | /// ``` 259 | /// # use embedded_sdmmc::BlockCount; 260 | /// assert_eq!(BlockCount::from_bytes(511), BlockCount(1)); 261 | /// assert_eq!(BlockCount::from_bytes(512), BlockCount(1)); 262 | /// assert_eq!(BlockCount::from_bytes(513), BlockCount(2)); 263 | /// assert_eq!(BlockCount::from_bytes(1024), BlockCount(2)); 264 | /// assert_eq!(BlockCount::from_bytes(1025), BlockCount(3)); 265 | /// ``` 266 | pub const fn from_bytes(byte_count: u32) -> BlockCount { 267 | let mut count = byte_count / Block::LEN_U32; 268 | if (count * Block::LEN_U32) != byte_count { 269 | count += 1; 270 | } 271 | BlockCount(count) 272 | } 273 | 274 | /// Take a number of blocks and increment by the integer number of blocks 275 | /// required to get to the block that holds the byte at the given offset. 276 | pub fn offset_bytes(self, offset: u32) -> Self { 277 | BlockCount(self.0 + (offset / Block::LEN_U32)) 278 | } 279 | } 280 | 281 | /// An iterator returned from `Block::range`. 282 | pub struct BlockIter { 283 | inclusive_end: BlockIdx, 284 | current: BlockIdx, 285 | } 286 | 287 | impl BlockIter { 288 | /// Create a new `BlockIter`, from the given start block, through (and 289 | /// including) the given end block. 290 | pub const fn new(start: BlockIdx, inclusive_end: BlockIdx) -> BlockIter { 291 | BlockIter { 292 | inclusive_end, 293 | current: start, 294 | } 295 | } 296 | } 297 | 298 | impl core::iter::Iterator for BlockIter { 299 | type Item = BlockIdx; 300 | fn next(&mut self) -> Option { 301 | if self.current.0 >= self.inclusive_end.0 { 302 | None 303 | } else { 304 | let this = self.current; 305 | self.current += BlockCount(1); 306 | Some(this) 307 | } 308 | } 309 | } 310 | 311 | // **************************************************************************** 312 | // 313 | // End Of File 314 | // 315 | // **************************************************************************** 316 | -------------------------------------------------------------------------------- /src/fat/bpb.rs: -------------------------------------------------------------------------------- 1 | //! Boot Parameter Block 2 | 3 | use crate::{ 4 | blockdevice::BlockCount, 5 | fat::{FatType, OnDiskDirEntry}, 6 | }; 7 | use byteorder::{ByteOrder, LittleEndian}; 8 | 9 | /// A Boot Parameter Block. 10 | /// 11 | /// This is the first sector of a FAT formatted partition, and it describes 12 | /// various properties of the FAT filesystem. 13 | pub struct Bpb<'a> { 14 | data: &'a [u8; 512], 15 | pub(crate) fat_type: FatType, 16 | cluster_count: u32, 17 | } 18 | 19 | impl<'a> Bpb<'a> { 20 | pub(crate) const FOOTER_VALUE: u16 = 0xAA55; 21 | 22 | /// Attempt to parse a Boot Parameter Block from a 512 byte sector. 23 | pub fn create_from_bytes(data: &[u8; 512]) -> Result { 24 | let mut bpb = Bpb { 25 | data, 26 | fat_type: FatType::Fat16, 27 | cluster_count: 0, 28 | }; 29 | if bpb.footer() != Self::FOOTER_VALUE { 30 | return Err("Bad BPB footer"); 31 | } 32 | 33 | let root_dir_blocks = 34 | BlockCount::from_bytes(u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32).0; 35 | let non_data_blocks = u32::from(bpb.reserved_block_count()) 36 | + (u32::from(bpb.num_fats()) * bpb.fat_size()) 37 | + root_dir_blocks; 38 | let data_blocks = bpb.total_blocks() - non_data_blocks; 39 | bpb.cluster_count = data_blocks / u32::from(bpb.blocks_per_cluster()); 40 | if bpb.cluster_count < 4085 { 41 | return Err("FAT12 is unsupported"); 42 | } else if bpb.cluster_count < 65525 { 43 | bpb.fat_type = FatType::Fat16; 44 | } else { 45 | bpb.fat_type = FatType::Fat32; 46 | } 47 | 48 | match bpb.fat_type { 49 | FatType::Fat16 => Ok(bpb), 50 | FatType::Fat32 if bpb.fs_ver() == 0 => { 51 | // Only support FAT32 version 0.0 52 | Ok(bpb) 53 | } 54 | _ => Err("Invalid FAT format"), 55 | } 56 | } 57 | 58 | // FAT16/FAT32 59 | define_field!(bytes_per_block, u16, 11); 60 | define_field!(blocks_per_cluster, u8, 13); 61 | define_field!(reserved_block_count, u16, 14); 62 | define_field!(num_fats, u8, 16); 63 | define_field!(root_entries_count, u16, 17); 64 | define_field!(total_blocks16, u16, 19); 65 | define_field!(media, u8, 21); 66 | define_field!(fat_size16, u16, 22); 67 | define_field!(blocks_per_track, u16, 24); 68 | define_field!(num_heads, u16, 26); 69 | define_field!(hidden_blocks, u32, 28); 70 | define_field!(total_blocks32, u32, 32); 71 | define_field!(footer, u16, 510); 72 | 73 | // FAT32 only 74 | define_field!(fat_size32, u32, 36); 75 | define_field!(fs_ver, u16, 42); 76 | define_field!(first_root_dir_cluster, u32, 44); 77 | define_field!(fs_info, u16, 48); 78 | define_field!(backup_boot_block, u16, 50); 79 | 80 | /// Get the OEM name string for this volume 81 | pub fn oem_name(&self) -> &[u8] { 82 | &self.data[3..11] 83 | } 84 | 85 | // FAT16/FAT32 functions 86 | 87 | /// Get the Volume Label string for this volume 88 | pub fn volume_label(&self) -> [u8; 11] { 89 | let mut result = [0u8; 11]; 90 | match self.fat_type { 91 | FatType::Fat16 => result.copy_from_slice(&self.data[43..=53]), 92 | FatType::Fat32 => result.copy_from_slice(&self.data[71..=81]), 93 | } 94 | result 95 | } 96 | 97 | // FAT32 only functions 98 | 99 | /// On a FAT32 volume, return the free block count from the Info Block. On 100 | /// a FAT16 volume, returns None. 101 | pub fn fs_info_block(&self) -> Option { 102 | match self.fat_type { 103 | FatType::Fat16 => None, 104 | FatType::Fat32 => Some(BlockCount(u32::from(self.fs_info()))), 105 | } 106 | } 107 | 108 | // Magic functions that get the right FAT16/FAT32 result 109 | 110 | /// Get the size of the File Allocation Table in blocks. 111 | pub fn fat_size(&self) -> u32 { 112 | let result = u32::from(self.fat_size16()); 113 | if result != 0 { 114 | result 115 | } else { 116 | self.fat_size32() 117 | } 118 | } 119 | 120 | /// Get the total number of blocks in this filesystem. 121 | pub fn total_blocks(&self) -> u32 { 122 | let result = u32::from(self.total_blocks16()); 123 | if result != 0 { 124 | result 125 | } else { 126 | self.total_blocks32() 127 | } 128 | } 129 | 130 | /// Get the total number of clusters in this filesystem. 131 | pub fn total_clusters(&self) -> u32 { 132 | self.cluster_count 133 | } 134 | } 135 | 136 | // **************************************************************************** 137 | // 138 | // End Of File 139 | // 140 | // **************************************************************************** 141 | -------------------------------------------------------------------------------- /src/fat/info.rs: -------------------------------------------------------------------------------- 1 | use crate::{BlockCount, BlockIdx, ClusterId}; 2 | use byteorder::{ByteOrder, LittleEndian}; 3 | 4 | /// Indentifies the supported types of FAT format 5 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 6 | #[derive(Debug, Clone, Eq, PartialEq)] 7 | pub enum FatSpecificInfo { 8 | /// Fat16 Format 9 | Fat16(Fat16Info), 10 | /// Fat32 Format 11 | Fat32(Fat32Info), 12 | } 13 | 14 | /// FAT32 specific data 15 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 16 | #[derive(Debug, Clone, Eq, PartialEq)] 17 | pub struct Fat32Info { 18 | /// The root directory does not have a reserved area in FAT32. This is the 19 | /// cluster it starts in (nominally 2). 20 | pub(crate) first_root_dir_cluster: ClusterId, 21 | /// Block idx of the info sector 22 | pub(crate) info_location: BlockIdx, 23 | } 24 | 25 | /// FAT16 specific data 26 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 27 | #[derive(Debug, Clone, Eq, PartialEq)] 28 | pub struct Fat16Info { 29 | /// The block the root directory starts in. Relative to start of partition 30 | /// (so add `self.lba_offset` before passing to volume manager) 31 | pub(crate) first_root_dir_block: BlockCount, 32 | /// Number of entries in root directory (it's reserved and not in the FAT) 33 | pub(crate) root_entries_count: u16, 34 | } 35 | 36 | /// File System Information structure is only present on FAT32 partitions. It 37 | /// may contain a valid number of free clusters and the number of the next 38 | /// free cluster. The information contained in the structure must be 39 | /// considered as advisory only. File system driver implementations are not 40 | /// required to ensure that information within the structure is kept 41 | /// consistent. 42 | pub struct InfoSector<'a> { 43 | data: &'a [u8; 512], 44 | } 45 | 46 | impl<'a> InfoSector<'a> { 47 | const LEAD_SIG: u32 = 0x4161_5252; 48 | const STRUC_SIG: u32 = 0x6141_7272; 49 | const TRAIL_SIG: u32 = 0xAA55_0000; 50 | 51 | /// Try and create a new Info Sector from a block. 52 | pub fn create_from_bytes(data: &[u8; 512]) -> Result { 53 | let info = InfoSector { data }; 54 | if info.lead_sig() != Self::LEAD_SIG { 55 | return Err("Bad lead signature on InfoSector"); 56 | } 57 | if info.struc_sig() != Self::STRUC_SIG { 58 | return Err("Bad struc signature on InfoSector"); 59 | } 60 | if info.trail_sig() != Self::TRAIL_SIG { 61 | return Err("Bad trail signature on InfoSector"); 62 | } 63 | Ok(info) 64 | } 65 | 66 | define_field!(lead_sig, u32, 0); 67 | define_field!(struc_sig, u32, 484); 68 | define_field!(free_count, u32, 488); 69 | define_field!(next_free, u32, 492); 70 | define_field!(trail_sig, u32, 508); 71 | 72 | /// Return how many free clusters are left in this volume, if known. 73 | pub fn free_clusters_count(&self) -> Option { 74 | match self.free_count() { 75 | 0xFFFF_FFFF => None, 76 | n => Some(n), 77 | } 78 | } 79 | 80 | /// Return the number of the next free cluster, if known. 81 | pub fn next_free_cluster(&self) -> Option { 82 | match self.next_free() { 83 | // 0 and 1 are reserved clusters 84 | 0xFFFF_FFFF | 0 | 1 => None, 85 | n => Some(ClusterId(n)), 86 | } 87 | } 88 | } 89 | 90 | // **************************************************************************** 91 | // 92 | // End Of File 93 | // 94 | // **************************************************************************** 95 | -------------------------------------------------------------------------------- /src/fat/mod.rs: -------------------------------------------------------------------------------- 1 | //! FAT16/FAT32 file system implementation 2 | //! 3 | //! Implements the File Allocation Table file system. Supports FAT16 and FAT32 volumes. 4 | 5 | /// Number of entries reserved at the start of a File Allocation Table 6 | pub const RESERVED_ENTRIES: u32 = 2; 7 | 8 | /// Indentifies the supported types of FAT format 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 10 | pub enum FatType { 11 | /// FAT16 Format 12 | Fat16, 13 | /// FAT32 Format 14 | Fat32, 15 | } 16 | 17 | mod bpb; 18 | mod info; 19 | mod ondiskdirentry; 20 | mod volume; 21 | 22 | pub use bpb::Bpb; 23 | pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; 24 | pub use ondiskdirentry::OnDiskDirEntry; 25 | pub use volume::{parse_volume, FatVolume, VolumeName}; 26 | 27 | // **************************************************************************** 28 | // 29 | // Unit Tests 30 | // 31 | // **************************************************************************** 32 | 33 | #[cfg(test)] 34 | mod test { 35 | 36 | use super::*; 37 | use crate::{Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; 38 | 39 | fn parse(input: &str) -> Vec { 40 | let mut output = Vec::new(); 41 | for line in input.lines() { 42 | let line = line.trim(); 43 | if !line.is_empty() { 44 | // 32 bytes per line 45 | for index in 0..32 { 46 | let start = index * 2; 47 | let end = start + 1; 48 | let piece = &line[start..=end]; 49 | let value = u8::from_str_radix(piece, 16).unwrap(); 50 | output.push(value); 51 | } 52 | } 53 | } 54 | output 55 | } 56 | 57 | /// This is the first block of this directory listing. 58 | /// total 19880 59 | /// -rw-r--r-- 1 jonathan jonathan 10841 2016-03-01 19:56:36.000000000 +0000 bcm2708-rpi-b.dtb 60 | /// -rw-r--r-- 1 jonathan jonathan 11120 2016-03-01 19:56:34.000000000 +0000 bcm2708-rpi-b-plus.dtb 61 | /// -rw-r--r-- 1 jonathan jonathan 10871 2016-03-01 19:56:36.000000000 +0000 bcm2708-rpi-cm.dtb 62 | /// -rw-r--r-- 1 jonathan jonathan 12108 2016-03-01 19:56:36.000000000 +0000 bcm2709-rpi-2-b.dtb 63 | /// -rw-r--r-- 1 jonathan jonathan 12575 2016-03-01 19:56:36.000000000 +0000 bcm2710-rpi-3-b.dtb 64 | /// -rw-r--r-- 1 jonathan jonathan 17920 2016-03-01 19:56:38.000000000 +0000 bootcode.bin 65 | /// -rw-r--r-- 1 jonathan jonathan 136 2015-11-21 20:28:30.000000000 +0000 cmdline.txt 66 | /// -rw-r--r-- 1 jonathan jonathan 1635 2015-11-21 20:28:30.000000000 +0000 config.txt 67 | /// -rw-r--r-- 1 jonathan jonathan 18693 2016-03-01 19:56:30.000000000 +0000 COPYING.linux 68 | /// -rw-r--r-- 1 jonathan jonathan 2505 2016-03-01 19:56:38.000000000 +0000 fixup_cd.dat 69 | /// -rw-r--r-- 1 jonathan jonathan 6481 2016-03-01 19:56:38.000000000 +0000 fixup.dat 70 | /// -rw-r--r-- 1 jonathan jonathan 9722 2016-03-01 19:56:38.000000000 +0000 fixup_db.dat 71 | /// -rw-r--r-- 1 jonathan jonathan 9724 2016-03-01 19:56:38.000000000 +0000 fixup_x.dat 72 | /// -rw-r--r-- 1 jonathan jonathan 110 2015-11-21 21:32:06.000000000 +0000 issue.txt 73 | /// -rw-r--r-- 1 jonathan jonathan 4046732 2016-03-01 19:56:40.000000000 +0000 kernel7.img 74 | /// -rw-r--r-- 1 jonathan jonathan 3963140 2016-03-01 19:56:38.000000000 +0000 kernel.img 75 | /// -rw-r--r-- 1 jonathan jonathan 1494 2016-03-01 19:56:34.000000000 +0000 LICENCE.broadcom 76 | /// -rw-r--r-- 1 jonathan jonathan 18974 2015-11-21 21:32:06.000000000 +0000 LICENSE.oracle 77 | /// drwxr-xr-x 2 jonathan jonathan 8192 2016-03-01 19:56:54.000000000 +0000 overlays 78 | /// -rw-r--r-- 1 jonathan jonathan 612472 2016-03-01 19:56:40.000000000 +0000 start_cd.elf 79 | /// -rw-r--r-- 1 jonathan jonathan 4888200 2016-03-01 19:56:42.000000000 +0000 start_db.elf 80 | /// -rw-r--r-- 1 jonathan jonathan 2739672 2016-03-01 19:56:40.000000000 +0000 start.elf 81 | /// -rw-r--r-- 1 jonathan jonathan 3840328 2016-03-01 19:56:44.000000000 +0000 start_x.elf 82 | /// drwxr-xr-x 2 jonathan jonathan 8192 2015-12-05 21:55:06.000000000 +0000 'System Volume Information' 83 | #[test] 84 | fn test_dir_entries() { 85 | #[derive(Debug)] 86 | enum Expected { 87 | Lfn(bool, u8, u8, [u16; 13]), 88 | Short(DirEntry), 89 | } 90 | let raw_data = r#" 91 | 626f6f7420202020202020080000699c754775470000699c7547000000000000 boot ...i.uGuG..i.uG...... 92 | 416f007600650072006c000f00476100790073000000ffffffff0000ffffffff Ao.v.e.r.l...Ga.y.s............. 93 | 4f5645524c4159532020201000001b9f6148614800001b9f6148030000000000 OVERLAYS .....aHaH....aH...... 94 | 422d0070006c00750073000f00792e006400740062000000ffff0000ffffffff B-.p.l.u.s...y..d.t.b........... 95 | 01620063006d00320037000f0079300038002d0072007000690000002d006200 .b.c.m.2.7...y0.8.-.r.p.i...-.b. 96 | 42434d3237307e31445442200064119f614861480000119f61480900702b0000 BCM270~1DTB .d..aHaH....aH..p+.. 97 | 4143004f005000590049000f00124e0047002e006c0069006e00000075007800 AC.O.P.Y.I....N.G...l.i.n...u.x. 98 | 434f5059494e7e314c494e2000000f9f6148614800000f9f6148050005490000 COPYIN~1LIN ....aHaH....aH...I.. 99 | 4263006f006d000000ffff0f0067ffffffffffffffffffffffff0000ffffffff Bc.o.m.......g.................. 100 | 014c004900430045004e000f0067430045002e00620072006f00000061006400 .L.I.C.E.N...gC.E...b.r.o...a.d. 101 | 4c4943454e437e3142524f200000119f614861480000119f61480800d6050000 LICENC~1BRO ....aHaH....aH...... 102 | 422d0062002e00640074000f001962000000ffffffffffffffff0000ffffffff B-.b...d.t....b................. 103 | 01620063006d00320037000f0019300039002d0072007000690000002d003200 .b.c.m.2.7....0.9.-.r.p.i...-.2. 104 | 42434d3237307e34445442200064129f614861480000129f61480f004c2f0000 BCM270~4DTB .d..aHaH....aH..L/.. 105 | 422e0064007400620000000f0059ffffffffffffffffffffffff0000ffffffff B..d.t.b.....Y.................. 106 | 01620063006d00320037000f0059300038002d0072007000690000002d006200 .b.c.m.2.7...Y0.8.-.r.p.i...-.b. 107 | "#; 108 | 109 | let results = [ 110 | Expected::Short(DirEntry { 111 | name: unsafe { 112 | VolumeName::create_from_str("boot") 113 | .unwrap() 114 | .to_short_filename() 115 | }, 116 | mtime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), 117 | ctime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), 118 | attributes: Attributes::create_from_fat(Attributes::VOLUME), 119 | cluster: ClusterId(0), 120 | size: 0, 121 | entry_block: BlockIdx(0), 122 | entry_offset: 0, 123 | }), 124 | Expected::Lfn( 125 | true, 126 | 1, 127 | 0x47, 128 | [ 129 | 'o' as u16, 'v' as u16, 'e' as u16, 'r' as u16, 'l' as u16, 'a' as u16, 130 | 'y' as u16, 's' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 131 | ], 132 | ), 133 | Expected::Short(DirEntry { 134 | name: ShortFileName::create_from_str("OVERLAYS").unwrap(), 135 | mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), 136 | ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), 137 | attributes: Attributes::create_from_fat(Attributes::DIRECTORY), 138 | cluster: ClusterId(3), 139 | size: 0, 140 | entry_block: BlockIdx(0), 141 | entry_offset: 0, 142 | }), 143 | Expected::Lfn( 144 | true, 145 | 2, 146 | 0x79, 147 | [ 148 | '-' as u16, 'p' as u16, 'l' as u16, 'u' as u16, 's' as u16, '.' as u16, 149 | 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 150 | ], 151 | ), 152 | Expected::Lfn( 153 | false, 154 | 1, 155 | 0x79, 156 | [ 157 | 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, 158 | '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, 159 | 'b' as u16, 160 | ], 161 | ), 162 | Expected::Short(DirEntry { 163 | name: ShortFileName::create_from_str("BCM270~1.DTB").unwrap(), 164 | mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), 165 | ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), 166 | attributes: Attributes::create_from_fat(Attributes::ARCHIVE), 167 | cluster: ClusterId(9), 168 | size: 11120, 169 | entry_block: BlockIdx(0), 170 | entry_offset: 0, 171 | }), 172 | Expected::Lfn( 173 | true, 174 | 1, 175 | 0x12, 176 | [ 177 | 'C' as u16, 'O' as u16, 'P' as u16, 'Y' as u16, 'I' as u16, 'N' as u16, 178 | 'G' as u16, '.' as u16, 'l' as u16, 'i' as u16, 'n' as u16, 'u' as u16, 179 | 'x' as u16, 180 | ], 181 | ), 182 | Expected::Short(DirEntry { 183 | name: ShortFileName::create_from_str("COPYIN~1.LIN").unwrap(), 184 | mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), 185 | ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), 186 | attributes: Attributes::create_from_fat(Attributes::ARCHIVE), 187 | cluster: ClusterId(5), 188 | size: 18693, 189 | entry_block: BlockIdx(0), 190 | entry_offset: 0, 191 | }), 192 | Expected::Lfn( 193 | true, 194 | 2, 195 | 0x67, 196 | [ 197 | 'c' as u16, 198 | 'o' as u16, 199 | 'm' as u16, 200 | '\u{0}' as u16, 201 | 0xFFFF, 202 | 0xFFFF, 203 | 0xFFFF, 204 | 0xFFFF, 205 | 0xFFFF, 206 | 0xFFFF, 207 | 0xFFFF, 208 | 0xFFFF, 209 | 0xFFFF, 210 | ], 211 | ), 212 | Expected::Lfn( 213 | false, 214 | 1, 215 | 0x67, 216 | [ 217 | 'L' as u16, 'I' as u16, 'C' as u16, 'E' as u16, 'N' as u16, 'C' as u16, 218 | 'E' as u16, '.' as u16, 'b' as u16, 'r' as u16, 'o' as u16, 'a' as u16, 219 | 'd' as u16, 220 | ], 221 | ), 222 | Expected::Short(DirEntry { 223 | name: ShortFileName::create_from_str("LICENC~1.BRO").unwrap(), 224 | mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), 225 | ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), 226 | attributes: Attributes::create_from_fat(Attributes::ARCHIVE), 227 | cluster: ClusterId(8), 228 | size: 1494, 229 | entry_block: BlockIdx(0), 230 | entry_offset: 0, 231 | }), 232 | Expected::Lfn( 233 | true, 234 | 2, 235 | 0x19, 236 | [ 237 | '-' as u16, 'b' as u16, '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 238 | 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 239 | ], 240 | ), 241 | Expected::Lfn( 242 | false, 243 | 1, 244 | 0x19, 245 | [ 246 | 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, 247 | '9' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, 248 | '2' as u16, 249 | ], 250 | ), 251 | Expected::Short(DirEntry { 252 | name: ShortFileName::create_from_str("BCM270~4.DTB").unwrap(), 253 | mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), 254 | ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), 255 | attributes: Attributes::create_from_fat(Attributes::ARCHIVE), 256 | cluster: ClusterId(15), 257 | size: 12108, 258 | entry_block: BlockIdx(0), 259 | entry_offset: 0, 260 | }), 261 | Expected::Lfn( 262 | true, 263 | 2, 264 | 0x59, 265 | [ 266 | '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 267 | 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 268 | ], 269 | ), 270 | Expected::Lfn( 271 | false, 272 | 1, 273 | 0x59, 274 | [ 275 | 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, 276 | '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, 277 | 'b' as u16, 278 | ], 279 | ), 280 | ]; 281 | 282 | let data = parse(raw_data); 283 | for (part, expected) in data.chunks(OnDiskDirEntry::LEN).zip(results.iter()) { 284 | let on_disk_entry = OnDiskDirEntry::new(part); 285 | match expected { 286 | Expected::Lfn(start, index, csum, contents) if on_disk_entry.is_lfn() => { 287 | let (calc_start, calc_index, calc_csum, calc_contents) = 288 | on_disk_entry.lfn_contents().unwrap(); 289 | assert_eq!(*start, calc_start); 290 | assert_eq!(*index, calc_index); 291 | assert_eq!(*contents, calc_contents); 292 | assert_eq!(*csum, calc_csum); 293 | } 294 | Expected::Short(expected_entry) if !on_disk_entry.is_lfn() => { 295 | let parsed_entry = on_disk_entry.get_entry(FatType::Fat32, BlockIdx(0), 0); 296 | assert_eq!(*expected_entry, parsed_entry); 297 | } 298 | _ => { 299 | panic!( 300 | "Bad dir entry, expected:\n{:#?}\nhad\n{:#?}", 301 | expected, on_disk_entry 302 | ); 303 | } 304 | } 305 | } 306 | } 307 | 308 | #[test] 309 | fn test_bpb() { 310 | // Taken from a Raspberry Pi bootable SD-Card 311 | const BPB_EXAMPLE: [u8; 512] = hex!( 312 | "EB 3C 90 6D 6B 66 73 2E 66 61 74 00 02 10 01 00 313 | 02 00 02 00 00 F8 20 00 3F 00 FF 00 00 00 00 00 314 | 00 E0 01 00 80 01 29 BB B0 71 77 62 6F 6F 74 20 315 | 20 20 20 20 20 20 46 41 54 31 36 20 20 20 0E 1F 316 | BE 5B 7C AC 22 C0 74 0B 56 B4 0E BB 07 00 CD 10 317 | 5E EB F0 32 E4 CD 16 CD 19 EB FE 54 68 69 73 20 318 | 69 73 20 6E 6F 74 20 61 20 62 6F 6F 74 61 62 6C 319 | 65 20 64 69 73 6B 2E 20 20 50 6C 65 61 73 65 20 320 | 69 6E 73 65 72 74 20 61 20 62 6F 6F 74 61 62 6C 321 | 65 20 66 6C 6F 70 70 79 20 61 6E 64 0D 0A 70 72 322 | 65 73 73 20 61 6E 79 20 6B 65 79 20 74 6F 20 74 323 | 72 79 20 61 67 61 69 6E 20 2E 2E 2E 20 0D 0A 00 324 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 325 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 326 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 327 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 328 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 329 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 330 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 331 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 332 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 333 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 334 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 335 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 336 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 337 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 338 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 339 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 340 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 341 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 342 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 343 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" 344 | ); 345 | let bpb = Bpb::create_from_bytes(&BPB_EXAMPLE).unwrap(); 346 | assert_eq!(bpb.footer(), Bpb::FOOTER_VALUE); 347 | assert_eq!(bpb.oem_name(), b"mkfs.fat"); 348 | assert_eq!(bpb.bytes_per_block(), 512); 349 | assert_eq!(bpb.blocks_per_cluster(), 16); 350 | assert_eq!(bpb.reserved_block_count(), 1); 351 | assert_eq!(bpb.num_fats(), 2); 352 | assert_eq!(bpb.root_entries_count(), 512); 353 | assert_eq!(bpb.total_blocks16(), 0); 354 | assert_eq!(bpb.fat_size16(), 32); 355 | assert_eq!(bpb.total_blocks32(), 122_880); 356 | assert_eq!(bpb.footer(), 0xAA55); 357 | assert_eq!(bpb.volume_label(), *b"boot "); 358 | assert_eq!(bpb.fat_size(), 32); 359 | assert_eq!(bpb.total_blocks(), 122_880); 360 | assert_eq!(bpb.fat_type, FatType::Fat16); 361 | } 362 | } 363 | 364 | // **************************************************************************** 365 | // 366 | // End Of File 367 | // 368 | // **************************************************************************** 369 | -------------------------------------------------------------------------------- /src/fat/ondiskdirentry.rs: -------------------------------------------------------------------------------- 1 | //! Directory Entry as stored on-disk 2 | 3 | use crate::{fat::FatType, Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; 4 | use byteorder::{ByteOrder, LittleEndian}; 5 | 6 | /// A 32-byte directory entry as stored on-disk in a directory file. 7 | /// 8 | /// This is the same for FAT16 and FAT32 (except FAT16 doesn't use 9 | /// first_cluster_hi). 10 | pub struct OnDiskDirEntry<'a> { 11 | data: &'a [u8], 12 | } 13 | 14 | impl<'a> core::fmt::Debug for OnDiskDirEntry<'a> { 15 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 16 | write!(f, "OnDiskDirEntry<")?; 17 | write!(f, "raw_attr = {}", self.raw_attr())?; 18 | write!(f, ", create_time = {}", self.create_time())?; 19 | write!(f, ", create_date = {}", self.create_date())?; 20 | write!(f, ", last_access_data = {}", self.last_access_data())?; 21 | write!(f, ", first_cluster_hi = {}", self.first_cluster_hi())?; 22 | write!(f, ", write_time = {}", self.write_time())?; 23 | write!(f, ", write_date = {}", self.write_date())?; 24 | write!(f, ", first_cluster_lo = {}", self.first_cluster_lo())?; 25 | write!(f, ", file_size = {}", self.file_size())?; 26 | write!(f, ", is_end = {}", self.is_end())?; 27 | write!(f, ", is_valid = {}", self.is_valid())?; 28 | write!(f, ", is_lfn = {}", self.is_lfn())?; 29 | write!( 30 | f, 31 | ", first_cluster_fat32 = {:?}", 32 | self.first_cluster_fat32() 33 | )?; 34 | write!( 35 | f, 36 | ", first_cluster_fat16 = {:?}", 37 | self.first_cluster_fat16() 38 | )?; 39 | write!(f, ">")?; 40 | Ok(()) 41 | } 42 | } 43 | 44 | impl<'a> OnDiskDirEntry<'a> { 45 | pub(crate) const LEN: usize = 32; 46 | pub(crate) const LEN_U32: u32 = 32; 47 | 48 | define_field!(raw_attr, u8, 11); 49 | define_field!(create_time, u16, 14); 50 | define_field!(create_date, u16, 16); 51 | define_field!(last_access_data, u16, 18); 52 | define_field!(first_cluster_hi, u16, 20); 53 | define_field!(write_time, u16, 22); 54 | define_field!(write_date, u16, 24); 55 | define_field!(first_cluster_lo, u16, 26); 56 | define_field!(file_size, u32, 28); 57 | 58 | /// Create a new on-disk directory entry from a block of 32 bytes read 59 | /// from a directory file. 60 | pub fn new(data: &[u8]) -> OnDiskDirEntry { 61 | OnDiskDirEntry { data } 62 | } 63 | 64 | /// Is this the last entry in the directory? 65 | pub fn is_end(&self) -> bool { 66 | self.data[0] == 0x00 67 | } 68 | 69 | /// Is this a valid entry? 70 | pub fn is_valid(&self) -> bool { 71 | !self.is_end() && (self.data[0] != 0xE5) 72 | } 73 | 74 | /// Is this a Long Filename entry? 75 | pub fn is_lfn(&self) -> bool { 76 | let attributes = Attributes::create_from_fat(self.raw_attr()); 77 | attributes.is_lfn() 78 | } 79 | 80 | /// If this is an LFN, get the contents so we can re-assemble the filename. 81 | pub fn lfn_contents(&self) -> Option<(bool, u8, u8, [u16; 13])> { 82 | if self.is_lfn() { 83 | let is_start = (self.data[0] & 0x40) != 0; 84 | let sequence = self.data[0] & 0x1F; 85 | let csum = self.data[13]; 86 | let buffer = [ 87 | LittleEndian::read_u16(&self.data[1..=2]), 88 | LittleEndian::read_u16(&self.data[3..=4]), 89 | LittleEndian::read_u16(&self.data[5..=6]), 90 | LittleEndian::read_u16(&self.data[7..=8]), 91 | LittleEndian::read_u16(&self.data[9..=10]), 92 | LittleEndian::read_u16(&self.data[14..=15]), 93 | LittleEndian::read_u16(&self.data[16..=17]), 94 | LittleEndian::read_u16(&self.data[18..=19]), 95 | LittleEndian::read_u16(&self.data[20..=21]), 96 | LittleEndian::read_u16(&self.data[22..=23]), 97 | LittleEndian::read_u16(&self.data[24..=25]), 98 | LittleEndian::read_u16(&self.data[28..=29]), 99 | LittleEndian::read_u16(&self.data[30..=31]), 100 | ]; 101 | Some((is_start, sequence, csum, buffer)) 102 | } else { 103 | None 104 | } 105 | } 106 | 107 | /// Does this on-disk entry match the given filename? 108 | pub fn matches(&self, sfn: &ShortFileName) -> bool { 109 | self.data[0..11] == sfn.contents 110 | } 111 | 112 | /// Which cluster, if any, does this file start at? Assumes this is from a FAT32 volume. 113 | pub fn first_cluster_fat32(&self) -> ClusterId { 114 | let cluster_no = 115 | (u32::from(self.first_cluster_hi()) << 16) | u32::from(self.first_cluster_lo()); 116 | ClusterId(cluster_no) 117 | } 118 | 119 | /// Which cluster, if any, does this file start at? Assumes this is from a FAT16 volume. 120 | fn first_cluster_fat16(&self) -> ClusterId { 121 | let cluster_no = u32::from(self.first_cluster_lo()); 122 | ClusterId(cluster_no) 123 | } 124 | 125 | /// Convert the on-disk format into a DirEntry 126 | pub fn get_entry( 127 | &self, 128 | fat_type: FatType, 129 | entry_block: BlockIdx, 130 | entry_offset: u32, 131 | ) -> DirEntry { 132 | let attributes = Attributes::create_from_fat(self.raw_attr()); 133 | let mut result = DirEntry { 134 | name: ShortFileName { 135 | contents: [0u8; 11], 136 | }, 137 | mtime: Timestamp::from_fat(self.write_date(), self.write_time()), 138 | ctime: Timestamp::from_fat(self.create_date(), self.create_time()), 139 | attributes, 140 | cluster: { 141 | let cluster = if fat_type == FatType::Fat32 { 142 | self.first_cluster_fat32() 143 | } else { 144 | self.first_cluster_fat16() 145 | }; 146 | if cluster == ClusterId::EMPTY && attributes.is_directory() { 147 | // FAT16/FAT32 uses a cluster ID of `0` in the ".." entry to mean 'root directory' 148 | ClusterId::ROOT_DIR 149 | } else { 150 | cluster 151 | } 152 | }, 153 | size: self.file_size(), 154 | entry_block, 155 | entry_offset, 156 | }; 157 | result.name.contents.copy_from_slice(&self.data[0..11]); 158 | result 159 | } 160 | } 161 | 162 | // **************************************************************************** 163 | // 164 | // End Of File 165 | // 166 | // **************************************************************************** 167 | -------------------------------------------------------------------------------- /src/filesystem/attributes.rs: -------------------------------------------------------------------------------- 1 | /// Indicates whether a directory entry is read-only, a directory, a volume 2 | /// label, etc. 3 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 4 | #[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] 5 | pub struct Attributes(pub(crate) u8); 6 | 7 | impl Attributes { 8 | /// Indicates this file cannot be written. 9 | pub const READ_ONLY: u8 = 0x01; 10 | /// Indicates the file is hidden. 11 | pub const HIDDEN: u8 = 0x02; 12 | /// Indicates this is a system file. 13 | pub const SYSTEM: u8 = 0x04; 14 | /// Indicates this is a volume label. 15 | pub const VOLUME: u8 = 0x08; 16 | /// Indicates this is a directory. 17 | pub const DIRECTORY: u8 = 0x10; 18 | /// Indicates this file needs archiving (i.e. has been modified since last 19 | /// archived). 20 | pub const ARCHIVE: u8 = 0x20; 21 | /// This set of flags indicates the file is actually a long file name 22 | /// fragment. 23 | pub const LFN: u8 = Self::READ_ONLY | Self::HIDDEN | Self::SYSTEM | Self::VOLUME; 24 | 25 | /// Create a `Attributes` value from the `u8` stored in a FAT16/FAT32 26 | /// Directory Entry. 27 | pub(crate) fn create_from_fat(value: u8) -> Attributes { 28 | Attributes(value) 29 | } 30 | 31 | pub(crate) fn set_archive(&mut self, flag: bool) { 32 | let archive = if flag { 0x20 } else { 0x00 }; 33 | self.0 |= archive; 34 | } 35 | 36 | /// Does this file has the read-only attribute set? 37 | pub fn is_read_only(self) -> bool { 38 | (self.0 & Self::READ_ONLY) == Self::READ_ONLY 39 | } 40 | 41 | /// Does this file has the hidden attribute set? 42 | pub fn is_hidden(self) -> bool { 43 | (self.0 & Self::HIDDEN) == Self::HIDDEN 44 | } 45 | 46 | /// Does this file has the system attribute set? 47 | pub fn is_system(self) -> bool { 48 | (self.0 & Self::SYSTEM) == Self::SYSTEM 49 | } 50 | 51 | /// Does this file has the volume attribute set? 52 | pub fn is_volume(self) -> bool { 53 | (self.0 & Self::VOLUME) == Self::VOLUME 54 | } 55 | 56 | /// Does this entry point at a directory? 57 | pub fn is_directory(self) -> bool { 58 | (self.0 & Self::DIRECTORY) == Self::DIRECTORY 59 | } 60 | 61 | /// Does this need archiving? 62 | pub fn is_archive(self) -> bool { 63 | (self.0 & Self::ARCHIVE) == Self::ARCHIVE 64 | } 65 | 66 | /// Is this a long file name fragment? 67 | pub fn is_lfn(self) -> bool { 68 | (self.0 & Self::LFN) == Self::LFN 69 | } 70 | } 71 | 72 | impl core::fmt::Debug for Attributes { 73 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 74 | // Worst case is "DRHSVA" 75 | let mut output = heapless::String::<7>::new(); 76 | if self.is_lfn() { 77 | output.push_str("LFN").unwrap(); 78 | } else { 79 | if self.is_directory() { 80 | output.push_str("D").unwrap(); 81 | } else { 82 | output.push_str("F").unwrap(); 83 | } 84 | if self.is_read_only() { 85 | output.push_str("R").unwrap(); 86 | } 87 | if self.is_hidden() { 88 | output.push_str("H").unwrap(); 89 | } 90 | if self.is_system() { 91 | output.push_str("S").unwrap(); 92 | } 93 | if self.is_volume() { 94 | output.push_str("V").unwrap(); 95 | } 96 | if self.is_archive() { 97 | output.push_str("A").unwrap(); 98 | } 99 | } 100 | f.pad(&output) 101 | } 102 | } 103 | 104 | // **************************************************************************** 105 | // 106 | // End Of File 107 | // 108 | // **************************************************************************** 109 | -------------------------------------------------------------------------------- /src/filesystem/cluster.rs: -------------------------------------------------------------------------------- 1 | /// Identifies a cluster on disk. 2 | /// 3 | /// A cluster is a consecutive group of blocks. Each cluster has a a numeric ID. 4 | /// Some numeric IDs are reserved for special purposes. 5 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 6 | #[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] 7 | pub struct ClusterId(pub(crate) u32); 8 | 9 | impl ClusterId { 10 | /// Magic value indicating an invalid cluster value. 11 | pub const INVALID: ClusterId = ClusterId(0xFFFF_FFF6); 12 | /// Magic value indicating a bad cluster. 13 | pub const BAD: ClusterId = ClusterId(0xFFFF_FFF7); 14 | /// Magic value indicating a empty cluster. 15 | pub const EMPTY: ClusterId = ClusterId(0x0000_0000); 16 | /// Magic value indicating the cluster holding the root directory (which 17 | /// doesn't have a number in FAT16 as there's a reserved region). 18 | pub const ROOT_DIR: ClusterId = ClusterId(0xFFFF_FFFC); 19 | /// Magic value indicating that the cluster is allocated and is the final cluster for the file 20 | pub const END_OF_FILE: ClusterId = ClusterId(0xFFFF_FFFF); 21 | } 22 | 23 | impl core::ops::Add for ClusterId { 24 | type Output = ClusterId; 25 | fn add(self, rhs: u32) -> ClusterId { 26 | ClusterId(self.0 + rhs) 27 | } 28 | } 29 | 30 | impl core::ops::AddAssign for ClusterId { 31 | fn add_assign(&mut self, rhs: u32) { 32 | self.0 += rhs; 33 | } 34 | } 35 | 36 | impl core::fmt::Debug for ClusterId { 37 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 38 | write!(f, "ClusterId(")?; 39 | match *self { 40 | Self::INVALID => { 41 | write!(f, "{:08}", "INVALID")?; 42 | } 43 | Self::BAD => { 44 | write!(f, "{:08}", "BAD")?; 45 | } 46 | Self::EMPTY => { 47 | write!(f, "{:08}", "EMPTY")?; 48 | } 49 | Self::ROOT_DIR => { 50 | write!(f, "{:08}", "ROOT")?; 51 | } 52 | Self::END_OF_FILE => { 53 | write!(f, "{:08}", "EOF")?; 54 | } 55 | ClusterId(value) => { 56 | write!(f, "{:08x}", value)?; 57 | } 58 | } 59 | write!(f, ")")?; 60 | Ok(()) 61 | } 62 | } 63 | 64 | // **************************************************************************** 65 | // 66 | // End Of File 67 | // 68 | // **************************************************************************** 69 | -------------------------------------------------------------------------------- /src/filesystem/directory.rs: -------------------------------------------------------------------------------- 1 | use crate::blockdevice::BlockIdx; 2 | use crate::fat::{FatType, OnDiskDirEntry}; 3 | use crate::filesystem::{Attributes, ClusterId, Handle, LfnBuffer, ShortFileName, Timestamp}; 4 | use crate::{Error, RawVolume, VolumeManager}; 5 | 6 | use super::ToShortFileName; 7 | 8 | /// A directory entry, which tells you about other files and directories. 9 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 10 | #[derive(Debug, PartialEq, Eq, Clone)] 11 | pub struct DirEntry { 12 | /// The name of the file 13 | pub name: ShortFileName, 14 | /// When the file was last modified 15 | pub mtime: Timestamp, 16 | /// When the file was first created 17 | pub ctime: Timestamp, 18 | /// The file attributes (Read Only, Archive, etc) 19 | pub attributes: Attributes, 20 | /// The starting cluster of the file. The FAT tells us the following Clusters. 21 | pub cluster: ClusterId, 22 | /// The size of the file in bytes. 23 | pub size: u32, 24 | /// The disk block of this entry 25 | pub entry_block: BlockIdx, 26 | /// The offset on its block (in bytes) 27 | pub entry_offset: u32, 28 | } 29 | 30 | /// A handle for an open directory on disk. 31 | /// 32 | /// Do NOT drop this object! It doesn't hold a reference to the Volume Manager 33 | /// it was created from and if you drop it, the VolumeManager will think you 34 | /// still have the directory open, and it won't let you open the directory 35 | /// again. 36 | /// 37 | /// Instead you must pass it to [`crate::VolumeManager::close_dir`] to close it 38 | /// cleanly. 39 | /// 40 | /// If you want your directories to close themselves on drop, create your own 41 | /// `Directory` type that wraps this one and also holds a `VolumeManager` 42 | /// reference. You'll then also need to put your `VolumeManager` in some kind of 43 | /// Mutex or RefCell, and deal with the fact you can't put them both in the same 44 | /// struct any more because one refers to the other. Basically, it's complicated 45 | /// and there's a reason we did it this way. 46 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 47 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 48 | pub struct RawDirectory(pub(crate) Handle); 49 | 50 | impl RawDirectory { 51 | /// Convert a raw directory into a droppable [`Directory`] 52 | pub fn to_directory< 53 | D, 54 | T, 55 | const MAX_DIRS: usize, 56 | const MAX_FILES: usize, 57 | const MAX_VOLUMES: usize, 58 | >( 59 | self, 60 | volume_mgr: &VolumeManager, 61 | ) -> Directory 62 | where 63 | D: crate::BlockDevice, 64 | T: crate::TimeSource, 65 | { 66 | Directory::new(self, volume_mgr) 67 | } 68 | } 69 | 70 | /// A handle for an open directory on disk, which closes on drop. 71 | /// 72 | /// In contrast to a `RawDirectory`, a `Directory` holds a mutable reference to 73 | /// its parent `VolumeManager`, which restricts which operations you can perform. 74 | /// 75 | /// If you drop a value of this type, it closes the directory automatically, but 76 | /// any error that may occur will be ignored. To handle potential errors, use 77 | /// the [`Directory::close`] method. 78 | pub struct Directory< 79 | 'a, 80 | D, 81 | T, 82 | const MAX_DIRS: usize, 83 | const MAX_FILES: usize, 84 | const MAX_VOLUMES: usize, 85 | > where 86 | D: crate::BlockDevice, 87 | T: crate::TimeSource, 88 | { 89 | raw_directory: RawDirectory, 90 | volume_mgr: &'a VolumeManager, 91 | } 92 | 93 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 94 | Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 95 | where 96 | D: crate::BlockDevice, 97 | T: crate::TimeSource, 98 | { 99 | /// Create a new `Directory` from a `RawDirectory` 100 | pub fn new( 101 | raw_directory: RawDirectory, 102 | volume_mgr: &'a VolumeManager, 103 | ) -> Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { 104 | Directory { 105 | raw_directory, 106 | volume_mgr, 107 | } 108 | } 109 | 110 | /// Open a directory. 111 | /// 112 | /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. 113 | pub fn open_dir( 114 | &self, 115 | name: N, 116 | ) -> Result, Error> 117 | where 118 | N: ToShortFileName, 119 | { 120 | let d = self.volume_mgr.open_dir(self.raw_directory, name)?; 121 | Ok(d.to_directory(self.volume_mgr)) 122 | } 123 | 124 | /// Change to a directory, mutating this object. 125 | /// 126 | /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. 127 | pub fn change_dir(&mut self, name: N) -> Result<(), Error> 128 | where 129 | N: ToShortFileName, 130 | { 131 | let d = self.volume_mgr.open_dir(self.raw_directory, name)?; 132 | self.volume_mgr.close_dir(self.raw_directory).unwrap(); 133 | self.raw_directory = d; 134 | Ok(()) 135 | } 136 | 137 | /// Look in a directory for a named file. 138 | pub fn find_directory_entry(&self, name: N) -> Result> 139 | where 140 | N: ToShortFileName, 141 | { 142 | self.volume_mgr 143 | .find_directory_entry(self.raw_directory, name) 144 | } 145 | 146 | /// Call a callback function for each directory entry in a directory. 147 | /// 148 | /// Long File Names will be ignored. 149 | /// 150 | ///
151 | /// 152 | /// Do not attempt to call any methods on the VolumeManager or any of its 153 | /// handles from inside the callback. You will get a lock error because the 154 | /// object is already locked in order to do the iteration. 155 | /// 156 | ///
157 | pub fn iterate_dir(&self, func: F) -> Result<(), Error> 158 | where 159 | F: FnMut(&DirEntry), 160 | { 161 | self.volume_mgr.iterate_dir(self.raw_directory, func) 162 | } 163 | 164 | /// Call a callback function for each directory entry in a directory, and 165 | /// process Long File Names. 166 | /// 167 | /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the 168 | /// Long File Name. If you pass one that isn't large enough, any Long File 169 | /// Names that don't fit will be ignored and presented as if they only had a 170 | /// Short File Name. 171 | /// 172 | ///
173 | /// 174 | /// Do not attempt to call any methods on the VolumeManager or any of its 175 | /// handles from inside the callback. You will get a lock error because the 176 | /// object is already locked in order to do the iteration. 177 | /// 178 | ///
179 | pub fn iterate_dir_lfn( 180 | &self, 181 | lfn_buffer: &mut LfnBuffer<'_>, 182 | func: F, 183 | ) -> Result<(), Error> 184 | where 185 | F: FnMut(&DirEntry, Option<&str>), 186 | { 187 | self.volume_mgr 188 | .iterate_dir_lfn(self.raw_directory, lfn_buffer, func) 189 | } 190 | 191 | /// Open a file with the given full path. A file can only be opened once. 192 | pub fn open_file_in_dir( 193 | &self, 194 | name: N, 195 | mode: crate::Mode, 196 | ) -> Result, crate::Error> 197 | where 198 | N: super::ToShortFileName, 199 | { 200 | let f = self 201 | .volume_mgr 202 | .open_file_in_dir(self.raw_directory, name, mode)?; 203 | Ok(f.to_file(self.volume_mgr)) 204 | } 205 | 206 | /// Delete a closed file with the given filename, if it exists. 207 | pub fn delete_file_in_dir(&self, name: N) -> Result<(), Error> 208 | where 209 | N: ToShortFileName, 210 | { 211 | self.volume_mgr.delete_file_in_dir(self.raw_directory, name) 212 | } 213 | 214 | /// Make a directory inside this directory 215 | pub fn make_dir_in_dir(&self, name: N) -> Result<(), Error> 216 | where 217 | N: ToShortFileName, 218 | { 219 | self.volume_mgr.make_dir_in_dir(self.raw_directory, name) 220 | } 221 | 222 | /// Convert back to a raw directory 223 | pub fn to_raw_directory(self) -> RawDirectory { 224 | let d = self.raw_directory; 225 | core::mem::forget(self); 226 | d 227 | } 228 | 229 | /// Consume the `Directory` handle and close it. The behavior of this is similar 230 | /// to using [`core::mem::drop`] or letting the `Directory` go out of scope, 231 | /// except this lets the user handle any errors that may occur in the process, 232 | /// whereas when using drop, any errors will be discarded silently. 233 | pub fn close(self) -> Result<(), Error> { 234 | let result = self.volume_mgr.close_dir(self.raw_directory); 235 | core::mem::forget(self); 236 | result 237 | } 238 | } 239 | 240 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop 241 | for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 242 | where 243 | D: crate::BlockDevice, 244 | T: crate::TimeSource, 245 | { 246 | fn drop(&mut self) { 247 | _ = self.volume_mgr.close_dir(self.raw_directory) 248 | } 249 | } 250 | 251 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 252 | core::fmt::Debug for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 253 | where 254 | D: crate::BlockDevice, 255 | T: crate::TimeSource, 256 | { 257 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 258 | write!(f, "Directory({})", self.raw_directory.0 .0) 259 | } 260 | } 261 | 262 | #[cfg(feature = "defmt-log")] 263 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 264 | defmt::Format for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 265 | where 266 | D: crate::BlockDevice, 267 | T: crate::TimeSource, 268 | { 269 | fn format(&self, fmt: defmt::Formatter) { 270 | defmt::write!(fmt, "Directory({})", self.raw_directory.0 .0) 271 | } 272 | } 273 | 274 | /// Holds information about an open file on disk 275 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 276 | #[derive(Debug, Clone)] 277 | pub(crate) struct DirectoryInfo { 278 | /// The handle for this directory. 279 | pub(crate) raw_directory: RawDirectory, 280 | /// The handle for the volume this directory is on 281 | pub(crate) raw_volume: RawVolume, 282 | /// The starting point of the directory listing. 283 | pub(crate) cluster: ClusterId, 284 | } 285 | 286 | impl DirEntry { 287 | pub(crate) fn serialize(&self, fat_type: FatType) -> [u8; OnDiskDirEntry::LEN] { 288 | let mut data = [0u8; OnDiskDirEntry::LEN]; 289 | data[0..11].copy_from_slice(&self.name.contents); 290 | data[11] = self.attributes.0; 291 | // 12: Reserved. Must be set to zero 292 | // 13: CrtTimeTenth, not supported, set to zero 293 | data[14..18].copy_from_slice(&self.ctime.serialize_to_fat()[..]); 294 | // 0 + 18: LastAccDate, not supported, set to zero 295 | let cluster_number = self.cluster.0; 296 | let cluster_hi = if fat_type == FatType::Fat16 { 297 | [0u8; 2] 298 | } else { 299 | // Safe due to the AND operation 300 | (((cluster_number >> 16) & 0x0000_FFFF) as u16).to_le_bytes() 301 | }; 302 | data[20..22].copy_from_slice(&cluster_hi[..]); 303 | data[22..26].copy_from_slice(&self.mtime.serialize_to_fat()[..]); 304 | // Safe due to the AND operation 305 | let cluster_lo = ((cluster_number & 0x0000_FFFF) as u16).to_le_bytes(); 306 | data[26..28].copy_from_slice(&cluster_lo[..]); 307 | data[28..32].copy_from_slice(&self.size.to_le_bytes()[..]); 308 | data 309 | } 310 | 311 | pub(crate) fn new( 312 | name: ShortFileName, 313 | attributes: Attributes, 314 | cluster: ClusterId, 315 | ctime: Timestamp, 316 | entry_block: BlockIdx, 317 | entry_offset: u32, 318 | ) -> Self { 319 | Self { 320 | name, 321 | mtime: ctime, 322 | ctime, 323 | attributes, 324 | cluster, 325 | size: 0, 326 | entry_block, 327 | entry_offset, 328 | } 329 | } 330 | } 331 | 332 | // **************************************************************************** 333 | // 334 | // End Of File 335 | // 336 | // **************************************************************************** 337 | -------------------------------------------------------------------------------- /src/filesystem/filename.rs: -------------------------------------------------------------------------------- 1 | //! Filename related types 2 | 3 | use crate::fat::VolumeName; 4 | use crate::trace; 5 | 6 | /// Various filename related errors that can occur. 7 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 8 | #[derive(Debug, Clone)] 9 | pub enum FilenameError { 10 | /// Tried to create a file with an invalid character. 11 | InvalidCharacter, 12 | /// Tried to create a file with no file name. 13 | FilenameEmpty, 14 | /// Given name was too long (we are limited to 8.3). 15 | NameTooLong, 16 | /// Can't start a file with a period, or after 8 characters. 17 | MisplacedPeriod, 18 | /// Can't extract utf8 from file name 19 | Utf8Error, 20 | } 21 | 22 | /// Describes things we can convert to short 8.3 filenames 23 | pub trait ToShortFileName { 24 | /// Try and convert this value into a [`ShortFileName`]. 25 | fn to_short_filename(self) -> Result; 26 | } 27 | 28 | impl ToShortFileName for ShortFileName { 29 | fn to_short_filename(self) -> Result { 30 | Ok(self) 31 | } 32 | } 33 | 34 | impl ToShortFileName for &ShortFileName { 35 | fn to_short_filename(self) -> Result { 36 | Ok(self.clone()) 37 | } 38 | } 39 | 40 | impl ToShortFileName for &str { 41 | fn to_short_filename(self) -> Result { 42 | ShortFileName::create_from_str(self) 43 | } 44 | } 45 | 46 | /// An MS-DOS 8.3 filename. 47 | /// 48 | /// ISO-8859-1 encoding is assumed. All lower-case is converted to upper-case by 49 | /// default. 50 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 51 | #[derive(PartialEq, Eq, Clone)] 52 | pub struct ShortFileName { 53 | pub(crate) contents: [u8; Self::TOTAL_LEN], 54 | } 55 | 56 | impl ShortFileName { 57 | const BASE_LEN: usize = 8; 58 | const TOTAL_LEN: usize = 11; 59 | 60 | /// Get a short file name containing "..", which means "parent directory". 61 | pub const fn parent_dir() -> Self { 62 | Self { 63 | contents: *b".. ", 64 | } 65 | } 66 | 67 | /// Get a short file name containing ".", which means "this directory". 68 | pub const fn this_dir() -> Self { 69 | Self { 70 | contents: *b". ", 71 | } 72 | } 73 | 74 | /// Get base name (without extension) of the file. 75 | pub fn base_name(&self) -> &[u8] { 76 | Self::bytes_before_space(&self.contents[..Self::BASE_LEN]) 77 | } 78 | 79 | /// Get extension of the file (without base name). 80 | pub fn extension(&self) -> &[u8] { 81 | Self::bytes_before_space(&self.contents[Self::BASE_LEN..]) 82 | } 83 | 84 | fn bytes_before_space(bytes: &[u8]) -> &[u8] { 85 | bytes.split(|b| *b == b' ').next().unwrap_or(&[]) 86 | } 87 | 88 | /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry. 89 | /// 90 | /// The output uses ISO-8859-1 encoding. 91 | pub fn create_from_str(name: &str) -> Result { 92 | let mut sfn = ShortFileName { 93 | contents: [b' '; Self::TOTAL_LEN], 94 | }; 95 | 96 | // Special case `..`, which means "parent directory". 97 | if name == ".." { 98 | return Ok(ShortFileName::parent_dir()); 99 | } 100 | 101 | // Special case `.` (or blank), which means "this directory". 102 | if name.is_empty() || name == "." { 103 | return Ok(ShortFileName::this_dir()); 104 | } 105 | 106 | let mut idx = 0; 107 | let mut seen_dot = false; 108 | for ch in name.chars() { 109 | match ch { 110 | // Microsoft say these are the invalid characters 111 | '\u{0000}'..='\u{001F}' 112 | | '"' 113 | | '*' 114 | | '+' 115 | | ',' 116 | | '/' 117 | | ':' 118 | | ';' 119 | | '<' 120 | | '=' 121 | | '>' 122 | | '?' 123 | | '[' 124 | | '\\' 125 | | ']' 126 | | ' ' 127 | | '|' => { 128 | return Err(FilenameError::InvalidCharacter); 129 | } 130 | x if x > '\u{00FF}' => { 131 | // We only handle ISO-8859-1 which is Unicode Code Points 132 | // \U+0000 to \U+00FF. This is above that. 133 | return Err(FilenameError::InvalidCharacter); 134 | } 135 | '.' => { 136 | // Denotes the start of the file extension 137 | if (1..=Self::BASE_LEN).contains(&idx) { 138 | idx = Self::BASE_LEN; 139 | seen_dot = true; 140 | } else { 141 | return Err(FilenameError::MisplacedPeriod); 142 | } 143 | } 144 | _ => { 145 | let b = ch.to_ascii_uppercase() as u8; 146 | if seen_dot { 147 | if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) { 148 | sfn.contents[idx] = b; 149 | } else { 150 | return Err(FilenameError::NameTooLong); 151 | } 152 | } else if idx < Self::BASE_LEN { 153 | sfn.contents[idx] = b; 154 | } else { 155 | return Err(FilenameError::NameTooLong); 156 | } 157 | idx += 1; 158 | } 159 | } 160 | } 161 | if idx == 0 { 162 | return Err(FilenameError::FilenameEmpty); 163 | } 164 | Ok(sfn) 165 | } 166 | 167 | /// Convert a Short File Name to a Volume Label. 168 | /// 169 | /// # Safety 170 | /// 171 | /// Volume Labels can contain things that Short File Names cannot, so only 172 | /// do this conversion if you have the name of a directory entry with the 173 | /// 'Volume Label' attribute. 174 | pub unsafe fn to_volume_label(self) -> VolumeName { 175 | VolumeName { 176 | contents: self.contents, 177 | } 178 | } 179 | 180 | /// Get the LFN checksum for this short filename 181 | pub fn csum(&self) -> u8 { 182 | let mut result = 0u8; 183 | for b in self.contents.iter() { 184 | result = result.rotate_right(1).wrapping_add(*b); 185 | } 186 | result 187 | } 188 | } 189 | 190 | impl core::fmt::Display for ShortFileName { 191 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 192 | let mut printed = 0; 193 | for (i, &c) in self.contents.iter().enumerate() { 194 | if c != b' ' { 195 | if i == Self::BASE_LEN { 196 | write!(f, ".")?; 197 | printed += 1; 198 | } 199 | // converting a byte to a codepoint means you are assuming 200 | // ISO-8859-1 encoding, because that's how Unicode was designed. 201 | write!(f, "{}", c as char)?; 202 | printed += 1; 203 | } 204 | } 205 | if let Some(mut width) = f.width() { 206 | if width > printed { 207 | width -= printed; 208 | for _ in 0..width { 209 | write!(f, "{}", f.fill())?; 210 | } 211 | } 212 | } 213 | Ok(()) 214 | } 215 | } 216 | 217 | impl core::fmt::Debug for ShortFileName { 218 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 219 | write!(f, "ShortFileName(\"{}\")", self) 220 | } 221 | } 222 | 223 | /// Used to store a Long File Name 224 | #[derive(Debug)] 225 | pub struct LfnBuffer<'a> { 226 | /// We fill this buffer in from the back 227 | inner: &'a mut [u8], 228 | /// How many bytes are free. 229 | /// 230 | /// This is also the byte index the string starts from. 231 | free: usize, 232 | /// Did we overflow? 233 | overflow: bool, 234 | /// If a surrogate-pair is split over two directory entries, remember half of it here. 235 | unpaired_surrogate: Option, 236 | } 237 | 238 | impl<'a> LfnBuffer<'a> { 239 | /// Create a new, empty, LFN Buffer using the given mutable slice as its storage. 240 | pub fn new(storage: &'a mut [u8]) -> LfnBuffer<'a> { 241 | let len = storage.len(); 242 | LfnBuffer { 243 | inner: storage, 244 | free: len, 245 | overflow: false, 246 | unpaired_surrogate: None, 247 | } 248 | } 249 | 250 | /// Empty out this buffer 251 | pub fn clear(&mut self) { 252 | self.free = self.inner.len(); 253 | self.overflow = false; 254 | self.unpaired_surrogate = None; 255 | } 256 | 257 | /// Push the 13 UTF-16 codepoints into this string. 258 | /// 259 | /// We assume they are pushed last-chunk-first, as you would find 260 | /// them on disk. 261 | /// 262 | /// Any chunk starting with a half of a surrogate pair has that saved for the next call. 263 | /// 264 | /// ```text 265 | /// [de00, 002e, 0074, 0078, 0074, 0000, ffff, ffff, ffff, ffff, ffff, ffff, ffff] 266 | /// [0041, 0042, 0030, 0031, 0032, 0033, 0034, 0035, 0036, 0037, 0038, 0039, d83d] 267 | /// 268 | /// Would map to 269 | /// 270 | /// 0041 0042 0030 0031 0032 0033 0034 0035 0036 0037 0038 0039 1f600 002e 0074 0078 0074, or 271 | /// 272 | /// "AB0123456789😀.txt" 273 | /// ``` 274 | pub fn push(&mut self, buffer: &[u16; 13]) { 275 | // find the first null, if any 276 | let null_idx = buffer 277 | .iter() 278 | .position(|&b| b == 0x0000) 279 | .unwrap_or(buffer.len()); 280 | // take all the wide chars, up to the null (or go to the end) 281 | let buffer = &buffer[0..null_idx]; 282 | 283 | // This next part will convert the 16-bit values into chars, noting that 284 | // chars outside the Basic Multilingual Plane will require two 16-bit 285 | // values to encode (see UTF-16 Surrogate Pairs). 286 | // 287 | // We cache the decoded chars into this array so we can iterate them 288 | // backwards. It's 60 bytes, but it'll have to do. 289 | let mut char_vec: heapless::Vec = heapless::Vec::new(); 290 | // Now do the decode, including the unpaired surrogate (if any) from 291 | // last time (maybe it has a pair now!) 292 | let mut is_first = true; 293 | for ch in char::decode_utf16( 294 | buffer 295 | .iter() 296 | .cloned() 297 | .chain(self.unpaired_surrogate.take().iter().cloned()), 298 | ) { 299 | match ch { 300 | Ok(ch) => { 301 | char_vec.push(ch).expect("Vec was full!?"); 302 | } 303 | Err(e) => { 304 | // OK, so we found half a surrogate pair and nothing to go 305 | // with it. Was this the first codepoint in the chunk? 306 | if is_first { 307 | // it was - the other half is probably in the next chunk 308 | // so save this for next time 309 | trace!("LFN saved {:?}", e.unpaired_surrogate()); 310 | self.unpaired_surrogate = Some(e.unpaired_surrogate()); 311 | } else { 312 | // it wasn't - can't deal with it these mid-sequence, so 313 | // replace it 314 | trace!("LFN replaced {:?}", e.unpaired_surrogate()); 315 | char_vec.push('\u{fffd}').expect("Vec was full?!"); 316 | } 317 | } 318 | } 319 | is_first = false; 320 | } 321 | 322 | for ch in char_vec.iter().rev() { 323 | trace!("LFN push {:?}", ch); 324 | // a buffer of length 4 is enough to encode any char 325 | let mut encoded_ch = [0u8; 4]; 326 | let encoded_ch = ch.encode_utf8(&mut encoded_ch); 327 | if self.free < encoded_ch.len() { 328 | // the LFN buffer they gave us was not long enough. Note for 329 | // later, so we don't show them garbage. 330 | self.overflow = true; 331 | return; 332 | } 333 | // Store the encoded char in the buffer, working backwards. We 334 | // already checked there was enough space. 335 | for b in encoded_ch.bytes().rev() { 336 | self.free -= 1; 337 | self.inner[self.free] = b; 338 | } 339 | } 340 | } 341 | 342 | /// View this LFN buffer as a string-slice 343 | /// 344 | /// If the buffer overflowed while parsing the LFN, or if this buffer is 345 | /// empty, you get an empty string. 346 | pub fn as_str(&self) -> &str { 347 | if self.overflow { 348 | "" 349 | } else { 350 | // we always only put UTF-8 encoded data in here 351 | unsafe { core::str::from_utf8_unchecked(&self.inner[self.free..]) } 352 | } 353 | } 354 | } 355 | 356 | // **************************************************************************** 357 | // 358 | // Unit Tests 359 | // 360 | // **************************************************************************** 361 | 362 | #[cfg(test)] 363 | mod test { 364 | use super::*; 365 | 366 | #[test] 367 | fn filename_no_extension() { 368 | let sfn = ShortFileName { 369 | contents: *b"HELLO ", 370 | }; 371 | assert_eq!(format!("{}", &sfn), "HELLO"); 372 | assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap()); 373 | assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap()); 374 | assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap()); 375 | assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap()); 376 | } 377 | 378 | #[test] 379 | fn filename_extension() { 380 | let sfn = ShortFileName { 381 | contents: *b"HELLO TXT", 382 | }; 383 | assert_eq!(format!("{}", &sfn), "HELLO.TXT"); 384 | assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap()); 385 | } 386 | 387 | #[test] 388 | fn filename_get_extension() { 389 | let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap(); 390 | assert_eq!(sfn.extension(), "TXT".as_bytes()); 391 | sfn = ShortFileName::create_from_str("hello").unwrap(); 392 | assert_eq!(sfn.extension(), "".as_bytes()); 393 | sfn = ShortFileName::create_from_str("hello.a").unwrap(); 394 | assert_eq!(sfn.extension(), "A".as_bytes()); 395 | } 396 | 397 | #[test] 398 | fn filename_get_base_name() { 399 | let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap(); 400 | assert_eq!(sfn.base_name(), "HELLO".as_bytes()); 401 | sfn = ShortFileName::create_from_str("12345678").unwrap(); 402 | assert_eq!(sfn.base_name(), "12345678".as_bytes()); 403 | sfn = ShortFileName::create_from_str("1").unwrap(); 404 | assert_eq!(sfn.base_name(), "1".as_bytes()); 405 | } 406 | 407 | #[test] 408 | fn filename_fulllength() { 409 | let sfn = ShortFileName { 410 | contents: *b"12345678TXT", 411 | }; 412 | assert_eq!(format!("{}", &sfn), "12345678.TXT"); 413 | assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap()); 414 | } 415 | 416 | #[test] 417 | fn filename_short_extension() { 418 | let sfn = ShortFileName { 419 | contents: *b"12345678C ", 420 | }; 421 | assert_eq!(format!("{}", &sfn), "12345678.C"); 422 | assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap()); 423 | } 424 | 425 | #[test] 426 | fn filename_short() { 427 | let sfn = ShortFileName { 428 | contents: *b"1 C ", 429 | }; 430 | assert_eq!(format!("{}", &sfn), "1.C"); 431 | assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap()); 432 | } 433 | 434 | #[test] 435 | fn filename_empty() { 436 | assert_eq!( 437 | ShortFileName::create_from_str("").unwrap(), 438 | ShortFileName::this_dir() 439 | ); 440 | } 441 | 442 | #[test] 443 | fn filename_bad() { 444 | assert!(ShortFileName::create_from_str(" ").is_err()); 445 | assert!(ShortFileName::create_from_str("123456789").is_err()); 446 | assert!(ShortFileName::create_from_str("12345678.ABCD").is_err()); 447 | } 448 | 449 | #[test] 450 | fn checksum() { 451 | assert_eq!( 452 | 0xB3, 453 | ShortFileName::create_from_str("UNARCH~1.DAT") 454 | .unwrap() 455 | .csum() 456 | ); 457 | } 458 | 459 | #[test] 460 | fn one_piece() { 461 | let mut storage = [0u8; 64]; 462 | let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); 463 | buf.push(&[ 464 | 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 465 | 0xFFFF, 0xFFFF, 466 | ]); 467 | assert_eq!(buf.as_str(), "0123∂"); 468 | } 469 | 470 | #[test] 471 | fn two_piece() { 472 | let mut storage = [0u8; 64]; 473 | let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); 474 | buf.push(&[ 475 | 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 476 | 0xFFFF, 0xFFFF, 477 | ]); 478 | buf.push(&[ 479 | 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b, 480 | 0x004c, 0x004d, 481 | ]); 482 | assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂"); 483 | } 484 | 485 | #[test] 486 | fn two_piece_split_surrogate() { 487 | let mut storage = [0u8; 64]; 488 | let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); 489 | 490 | buf.push(&[ 491 | 0xde00, 0x002e, 0x0074, 0x0078, 0x0074, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 492 | 0xffff, 0xffff, 493 | ]); 494 | buf.push(&[ 495 | 0xd83d, 0xde00, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 496 | 0x0039, 0xd83d, 497 | ]); 498 | assert_eq!(buf.as_str(), "😀0123456789😀.txt"); 499 | } 500 | } 501 | 502 | // **************************************************************************** 503 | // 504 | // End Of File 505 | // 506 | // **************************************************************************** 507 | -------------------------------------------------------------------------------- /src/filesystem/files.rs: -------------------------------------------------------------------------------- 1 | use super::TimeSource; 2 | use crate::{ 3 | filesystem::{ClusterId, DirEntry, Handle}, 4 | BlockDevice, Error, RawVolume, VolumeManager, 5 | }; 6 | use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; 7 | 8 | /// A handle for an open file on disk. 9 | /// 10 | /// Do NOT drop this object! It doesn't hold a reference to the Volume Manager 11 | /// it was created from and cannot update the directory entry if you drop it. 12 | /// Additionally, the VolumeManager will think you still have the file open if 13 | /// you just drop it, and it won't let you open the file again. 14 | /// 15 | /// Instead you must pass it to [`crate::VolumeManager::close_file`] to close it 16 | /// cleanly. 17 | /// 18 | /// If you want your files to close themselves on drop, create your own File 19 | /// type that wraps this one and also holds a `VolumeManager` reference. You'll 20 | /// then also need to put your `VolumeManager` in some kind of Mutex or RefCell, 21 | /// and deal with the fact you can't put them both in the same struct any more 22 | /// because one refers to the other. Basically, it's complicated and there's a 23 | /// reason we did it this way. 24 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 25 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 26 | pub struct RawFile(pub(crate) Handle); 27 | 28 | impl RawFile { 29 | /// Convert a raw file into a droppable [`File`] 30 | pub fn to_file( 31 | self, 32 | volume_mgr: &VolumeManager, 33 | ) -> File 34 | where 35 | D: crate::BlockDevice, 36 | T: crate::TimeSource, 37 | { 38 | File::new(self, volume_mgr) 39 | } 40 | } 41 | 42 | /// A handle for an open file on disk, which closes on drop. 43 | /// 44 | /// In contrast to a `RawFile`, a `File` holds a mutable reference to its 45 | /// parent `VolumeManager`, which restricts which operations you can perform. 46 | /// 47 | /// If you drop a value of this type, it closes the file automatically, and but 48 | /// error that may occur will be ignored. To handle potential errors, use 49 | /// the [`File::close`] method. 50 | pub struct File<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 51 | where 52 | D: crate::BlockDevice, 53 | T: crate::TimeSource, 54 | { 55 | raw_file: RawFile, 56 | volume_mgr: &'a VolumeManager, 57 | } 58 | 59 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 60 | File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 61 | where 62 | D: crate::BlockDevice, 63 | T: crate::TimeSource, 64 | { 65 | /// Create a new `File` from a `RawFile` 66 | pub fn new( 67 | raw_file: RawFile, 68 | volume_mgr: &'a VolumeManager, 69 | ) -> File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { 70 | File { 71 | raw_file, 72 | volume_mgr, 73 | } 74 | } 75 | 76 | /// Read from the file 77 | /// 78 | /// Returns how many bytes were read, or an error. 79 | pub fn read(&self, buffer: &mut [u8]) -> Result> { 80 | self.volume_mgr.read(self.raw_file, buffer) 81 | } 82 | 83 | /// Write to the file 84 | pub fn write(&self, buffer: &[u8]) -> Result<(), crate::Error> { 85 | self.volume_mgr.write(self.raw_file, buffer) 86 | } 87 | 88 | /// Check if a file is at End Of File. 89 | pub fn is_eof(&self) -> bool { 90 | self.volume_mgr 91 | .file_eof(self.raw_file) 92 | .expect("Corrupt file ID") 93 | } 94 | 95 | /// Seek a file with an offset from the current position. 96 | pub fn seek_from_current(&self, offset: i32) -> Result<(), crate::Error> { 97 | self.volume_mgr 98 | .file_seek_from_current(self.raw_file, offset) 99 | } 100 | 101 | /// Seek a file with an offset from the start of the file. 102 | pub fn seek_from_start(&self, offset: u32) -> Result<(), crate::Error> { 103 | self.volume_mgr.file_seek_from_start(self.raw_file, offset) 104 | } 105 | 106 | /// Seek a file with an offset back from the end of the file. 107 | pub fn seek_from_end(&self, offset: u32) -> Result<(), crate::Error> { 108 | self.volume_mgr.file_seek_from_end(self.raw_file, offset) 109 | } 110 | 111 | /// Get the length of a file 112 | pub fn length(&self) -> u32 { 113 | self.volume_mgr 114 | .file_length(self.raw_file) 115 | .expect("Corrupt file ID") 116 | } 117 | 118 | /// Get the current offset of a file 119 | pub fn offset(&self) -> u32 { 120 | self.volume_mgr 121 | .file_offset(self.raw_file) 122 | .expect("Corrupt file ID") 123 | } 124 | 125 | /// Convert back to a raw file 126 | pub fn to_raw_file(self) -> RawFile { 127 | let f = self.raw_file; 128 | core::mem::forget(self); 129 | f 130 | } 131 | 132 | /// Flush any written data by updating the directory entry. 133 | pub fn flush(&self) -> Result<(), Error> { 134 | self.volume_mgr.flush_file(self.raw_file) 135 | } 136 | 137 | /// Consume the `File` handle and close it. The behavior of this is similar 138 | /// to using [`core::mem::drop`] or letting the `File` go out of scope, 139 | /// except this lets the user handle any errors that may occur in the process, 140 | /// whereas when using drop, any errors will be discarded silently. 141 | pub fn close(self) -> Result<(), Error> { 142 | let result = self.volume_mgr.close_file(self.raw_file); 143 | core::mem::forget(self); 144 | result 145 | } 146 | } 147 | 148 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop 149 | for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 150 | where 151 | D: crate::BlockDevice, 152 | T: crate::TimeSource, 153 | { 154 | fn drop(&mut self) { 155 | _ = self.volume_mgr.close_file(self.raw_file); 156 | } 157 | } 158 | 159 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 160 | core::fmt::Debug for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 161 | where 162 | D: crate::BlockDevice, 163 | T: crate::TimeSource, 164 | { 165 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 166 | write!(f, "File({})", self.raw_file.0 .0) 167 | } 168 | } 169 | 170 | impl< 171 | D: BlockDevice, 172 | T: TimeSource, 173 | const MAX_DIRS: usize, 174 | const MAX_FILES: usize, 175 | const MAX_VOLUMES: usize, 176 | > ErrorType for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 177 | { 178 | type Error = crate::Error; 179 | } 180 | 181 | impl< 182 | D: BlockDevice, 183 | T: TimeSource, 184 | const MAX_DIRS: usize, 185 | const MAX_FILES: usize, 186 | const MAX_VOLUMES: usize, 187 | > Read for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 188 | { 189 | fn read(&mut self, buf: &mut [u8]) -> Result { 190 | if buf.is_empty() { 191 | Ok(0) 192 | } else { 193 | self.read(buf) 194 | } 195 | } 196 | } 197 | 198 | impl< 199 | D: BlockDevice, 200 | T: TimeSource, 201 | const MAX_DIRS: usize, 202 | const MAX_FILES: usize, 203 | const MAX_VOLUMES: usize, 204 | > Write for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 205 | { 206 | fn write(&mut self, buf: &[u8]) -> Result { 207 | if buf.is_empty() { 208 | Ok(0) 209 | } else { 210 | self.write(buf)?; 211 | Ok(buf.len()) 212 | } 213 | } 214 | 215 | fn flush(&mut self) -> Result<(), Self::Error> { 216 | Self::flush(self) 217 | } 218 | } 219 | 220 | impl< 221 | D: BlockDevice, 222 | T: TimeSource, 223 | const MAX_DIRS: usize, 224 | const MAX_FILES: usize, 225 | const MAX_VOLUMES: usize, 226 | > Seek for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 227 | { 228 | fn seek(&mut self, pos: SeekFrom) -> Result { 229 | match pos { 230 | SeekFrom::Start(offset) => { 231 | self.seek_from_start(offset.try_into().map_err(|_| Error::InvalidOffset)?)? 232 | } 233 | SeekFrom::End(offset) => { 234 | self.seek_from_end((-offset).try_into().map_err(|_| Error::InvalidOffset)?)? 235 | } 236 | SeekFrom::Current(offset) => { 237 | self.seek_from_current(offset.try_into().map_err(|_| Error::InvalidOffset)?)? 238 | } 239 | } 240 | Ok(self.offset().into()) 241 | } 242 | } 243 | 244 | #[cfg(feature = "defmt-log")] 245 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 246 | defmt::Format for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 247 | where 248 | D: crate::BlockDevice, 249 | T: crate::TimeSource, 250 | { 251 | fn format(&self, fmt: defmt::Formatter) { 252 | defmt::write!(fmt, "File({})", self.raw_file.0 .0) 253 | } 254 | } 255 | 256 | /// Errors related to file operations 257 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 258 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 259 | pub enum FileError { 260 | /// Tried to use an invalid offset. 261 | InvalidOffset, 262 | } 263 | 264 | /// The different ways we can open a file. 265 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 266 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 267 | pub enum Mode { 268 | /// Open a file for reading, if it exists. 269 | ReadOnly, 270 | /// Open a file for appending (writing to the end of the existing file), if it exists. 271 | ReadWriteAppend, 272 | /// Open a file and remove all contents, before writing to the start of the existing file, if it exists. 273 | ReadWriteTruncate, 274 | /// Create a new empty file. Fail if it exists. 275 | ReadWriteCreate, 276 | /// Create a new empty file, or truncate an existing file. 277 | ReadWriteCreateOrTruncate, 278 | /// Create a new empty file, or append to an existing file. 279 | ReadWriteCreateOrAppend, 280 | } 281 | 282 | /// Internal metadata about an open file 283 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 284 | #[derive(Debug, Clone)] 285 | pub(crate) struct FileInfo { 286 | /// Handle for this file 287 | pub(crate) raw_file: RawFile, 288 | /// The handle for the volume this directory is on 289 | pub(crate) raw_volume: RawVolume, 290 | /// The last cluster we accessed, and how many bytes that short-cuts us. 291 | /// 292 | /// This saves us walking from the very start of the FAT chain when we move 293 | /// forward through a file. 294 | pub(crate) current_cluster: (u32, ClusterId), 295 | /// How far through the file we've read (in bytes). 296 | pub(crate) current_offset: u32, 297 | /// What mode the file was opened in 298 | pub(crate) mode: Mode, 299 | /// DirEntry of this file 300 | pub(crate) entry: DirEntry, 301 | /// Did we write to this file? 302 | pub(crate) dirty: bool, 303 | } 304 | 305 | impl FileInfo { 306 | /// Are we at the end of the file? 307 | pub fn eof(&self) -> bool { 308 | self.current_offset == self.entry.size 309 | } 310 | 311 | /// How long is the file? 312 | pub fn length(&self) -> u32 { 313 | self.entry.size 314 | } 315 | 316 | /// Seek to a new position in the file, relative to the start of the file. 317 | pub fn seek_from_start(&mut self, offset: u32) -> Result<(), FileError> { 318 | if offset > self.entry.size { 319 | return Err(FileError::InvalidOffset); 320 | } 321 | self.current_offset = offset; 322 | Ok(()) 323 | } 324 | 325 | /// Seek to a new position in the file, relative to the end of the file. 326 | pub fn seek_from_end(&mut self, offset: u32) -> Result<(), FileError> { 327 | if offset > self.entry.size { 328 | return Err(FileError::InvalidOffset); 329 | } 330 | self.current_offset = self.entry.size - offset; 331 | Ok(()) 332 | } 333 | 334 | /// Seek to a new position in the file, relative to the current position. 335 | pub fn seek_from_current(&mut self, offset: i32) -> Result<(), FileError> { 336 | let new_offset = i64::from(self.current_offset) + i64::from(offset); 337 | if new_offset < 0 || new_offset > i64::from(self.entry.size) { 338 | return Err(FileError::InvalidOffset); 339 | } 340 | self.current_offset = new_offset as u32; 341 | Ok(()) 342 | } 343 | 344 | /// Amount of file left to read. 345 | pub fn left(&self) -> u32 { 346 | self.entry.size - self.current_offset 347 | } 348 | 349 | /// Update the file's length. 350 | pub(crate) fn update_length(&mut self, new: u32) { 351 | self.entry.size = new; 352 | } 353 | } 354 | 355 | // **************************************************************************** 356 | // 357 | // End Of File 358 | // 359 | // **************************************************************************** 360 | -------------------------------------------------------------------------------- /src/filesystem/handles.rs: -------------------------------------------------------------------------------- 1 | //! Contains the Handles and the HandleGenerator. 2 | 3 | use core::num::Wrapping; 4 | 5 | #[derive(Clone, Copy, PartialEq, Eq)] 6 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 7 | /// Unique ID used to identify things in the open Volume/File/Directory lists 8 | pub struct Handle(pub(crate) u32); 9 | 10 | impl core::fmt::Debug for Handle { 11 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 12 | write!(f, "{:#08x}", self.0) 13 | } 14 | } 15 | 16 | /// A Handle Generator. 17 | /// 18 | /// This object will always return a different ID. 19 | /// 20 | /// Well, it will wrap after `2**32` IDs. But most systems won't open that many 21 | /// files, and if they do, they are unlikely to hold one file open and then 22 | /// open/close `2**32 - 1` others. 23 | #[derive(Debug)] 24 | pub struct HandleGenerator { 25 | next_id: Wrapping, 26 | } 27 | 28 | impl HandleGenerator { 29 | /// Create a new generator of Handles. 30 | pub const fn new(offset: u32) -> Self { 31 | Self { 32 | next_id: Wrapping(offset), 33 | } 34 | } 35 | 36 | /// Generate a new, unique [`Handle`]. 37 | pub fn generate(&mut self) -> Handle { 38 | let id = self.next_id; 39 | self.next_id += 1; 40 | Handle(id.0) 41 | } 42 | } 43 | 44 | // **************************************************************************** 45 | // 46 | // End Of File 47 | // 48 | // **************************************************************************** 49 | -------------------------------------------------------------------------------- /src/filesystem/mod.rs: -------------------------------------------------------------------------------- 1 | //! Generic File System structures 2 | //! 3 | //! Implements generic file system components. These should be applicable to 4 | //! most (if not all) supported filesystems. 5 | 6 | /// Maximum file size supported by this library 7 | pub const MAX_FILE_SIZE: u32 = u32::MAX; 8 | 9 | mod attributes; 10 | mod cluster; 11 | mod directory; 12 | mod filename; 13 | mod files; 14 | mod handles; 15 | mod timestamp; 16 | 17 | pub use self::attributes::Attributes; 18 | pub use self::cluster::ClusterId; 19 | pub use self::directory::{DirEntry, Directory, RawDirectory}; 20 | pub use self::filename::{FilenameError, LfnBuffer, ShortFileName, ToShortFileName}; 21 | pub use self::files::{File, FileError, Mode, RawFile}; 22 | pub use self::handles::{Handle, HandleGenerator}; 23 | pub use self::timestamp::{TimeSource, Timestamp}; 24 | 25 | pub(crate) use self::directory::DirectoryInfo; 26 | pub(crate) use self::files::FileInfo; 27 | 28 | // **************************************************************************** 29 | // 30 | // End Of File 31 | // 32 | // **************************************************************************** 33 | -------------------------------------------------------------------------------- /src/filesystem/timestamp.rs: -------------------------------------------------------------------------------- 1 | /// Things that impl this can tell you the current time. 2 | pub trait TimeSource { 3 | /// Returns the current time 4 | fn get_timestamp(&self) -> Timestamp; 5 | } 6 | 7 | /// A Gregorian Calendar date/time, in the local time zone. 8 | /// 9 | /// TODO: Consider replacing this with POSIX time as a `u32`, which would save 10 | /// two bytes at the expense of some maths. 11 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 12 | #[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] 13 | pub struct Timestamp { 14 | /// Add 1970 to this file to get the calendar year 15 | pub year_since_1970: u8, 16 | /// Add one to this value to get the calendar month 17 | pub zero_indexed_month: u8, 18 | /// Add one to this value to get the calendar day 19 | pub zero_indexed_day: u8, 20 | /// The number of hours past midnight 21 | pub hours: u8, 22 | /// The number of minutes past the hour 23 | pub minutes: u8, 24 | /// The number of seconds past the minute 25 | pub seconds: u8, 26 | } 27 | 28 | impl Timestamp { 29 | /// Create a `Timestamp` from the 16-bit FAT date and time fields. 30 | pub fn from_fat(date: u16, time: u16) -> Timestamp { 31 | let year = 1980 + (date >> 9); 32 | let month = ((date >> 5) & 0x000F) as u8; 33 | let day = (date & 0x001F) as u8; 34 | let hours = ((time >> 11) & 0x001F) as u8; 35 | let minutes = ((time >> 5) & 0x0003F) as u8; 36 | let seconds = ((time << 1) & 0x0003F) as u8; 37 | // Volume labels have a zero for month/day, so tolerate that... 38 | Timestamp { 39 | year_since_1970: (year - 1970) as u8, 40 | zero_indexed_month: if month == 0 { 0 } else { month - 1 }, 41 | zero_indexed_day: if day == 0 { 0 } else { day - 1 }, 42 | hours, 43 | minutes, 44 | seconds, 45 | } 46 | } 47 | 48 | // TODO add tests for the method 49 | /// Serialize a `Timestamp` to FAT format 50 | pub fn serialize_to_fat(self) -> [u8; 4] { 51 | let mut data = [0u8; 4]; 52 | 53 | let hours = (u16::from(self.hours) << 11) & 0xF800; 54 | let minutes = (u16::from(self.minutes) << 5) & 0x07E0; 55 | let seconds = (u16::from(self.seconds / 2)) & 0x001F; 56 | data[..2].copy_from_slice(&(hours | minutes | seconds).to_le_bytes()[..]); 57 | 58 | let year = if self.year_since_1970 < 10 { 59 | 0 60 | } else { 61 | (u16::from(self.year_since_1970 - 10) << 9) & 0xFE00 62 | }; 63 | let month = (u16::from(self.zero_indexed_month + 1) << 5) & 0x01E0; 64 | let day = u16::from(self.zero_indexed_day + 1) & 0x001F; 65 | data[2..].copy_from_slice(&(year | month | day).to_le_bytes()[..]); 66 | data 67 | } 68 | 69 | /// Create a `Timestamp` from year/month/day/hour/minute/second. 70 | /// 71 | /// Values should be given as you'd write then (i.e. 1980, 01, 01, 13, 30, 72 | /// 05) is 1980-Jan-01, 1:30:05pm. 73 | pub fn from_calendar( 74 | year: u16, 75 | month: u8, 76 | day: u8, 77 | hours: u8, 78 | minutes: u8, 79 | seconds: u8, 80 | ) -> Result { 81 | Ok(Timestamp { 82 | year_since_1970: if (1970..=(1970 + 255)).contains(&year) { 83 | (year - 1970) as u8 84 | } else { 85 | return Err("Bad year"); 86 | }, 87 | zero_indexed_month: if (1..=12).contains(&month) { 88 | month - 1 89 | } else { 90 | return Err("Bad month"); 91 | }, 92 | zero_indexed_day: if (1..=31).contains(&day) { 93 | day - 1 94 | } else { 95 | return Err("Bad day"); 96 | }, 97 | hours: if hours <= 23 { 98 | hours 99 | } else { 100 | return Err("Bad hours"); 101 | }, 102 | minutes: if minutes <= 59 { 103 | minutes 104 | } else { 105 | return Err("Bad minutes"); 106 | }, 107 | seconds: if seconds <= 59 { 108 | seconds 109 | } else { 110 | return Err("Bad seconds"); 111 | }, 112 | }) 113 | } 114 | } 115 | 116 | impl core::fmt::Debug for Timestamp { 117 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 118 | write!(f, "Timestamp({})", self) 119 | } 120 | } 121 | 122 | impl core::fmt::Display for Timestamp { 123 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 124 | write!( 125 | f, 126 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", 127 | u16::from(self.year_since_1970) + 1970, 128 | self.zero_indexed_month + 1, 129 | self.zero_indexed_day + 1, 130 | self.hours, 131 | self.minutes, 132 | self.seconds 133 | ) 134 | } 135 | } 136 | 137 | // **************************************************************************** 138 | // 139 | // End Of File 140 | // 141 | // **************************************************************************** 142 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # embedded-sdmmc 2 | //! 3 | //! > An SD/MMC Library written in Embedded Rust 4 | //! 5 | //! This crate is intended to allow you to read/write files on a FAT formatted 6 | //! SD card on your Rust Embedded device, as easily as using the `SdFat` Arduino 7 | //! library. It is written in pure-Rust, is `#![no_std]` and does not use 8 | //! `alloc` or `collections` to keep the memory footprint low. In the first 9 | //! instance it is designed for readability and simplicity over performance. 10 | //! 11 | //! ## Using the crate 12 | //! 13 | //! You will need something that implements the `BlockDevice` trait, which can 14 | //! read and write the 512-byte blocks (or sectors) from your card. If you were 15 | //! to implement this over USB Mass Storage, there's no reason this crate 16 | //! couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` 17 | //! suitable for reading SD and SDHC cards over SPI. 18 | //! 19 | //! ```rust 20 | //! use embedded_sdmmc::{Error, Mode, SdCard, SdCardError, TimeSource, VolumeIdx, VolumeManager}; 21 | //! 22 | //! fn example(spi: S, delay: D, ts: T) -> Result<(), Error> 23 | //! where 24 | //! S: embedded_hal::spi::SpiDevice, 25 | //! D: embedded_hal::delay::DelayNs, 26 | //! T: TimeSource, 27 | //! { 28 | //! let sdcard = SdCard::new(spi, delay); 29 | //! println!("Card size is {} bytes", sdcard.num_bytes()?); 30 | //! let volume_mgr = VolumeManager::new(sdcard, ts); 31 | //! let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; 32 | //! println!("Volume 0: {:?}", volume0); 33 | //! let root_dir = volume0.open_root_dir()?; 34 | //! let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; 35 | //! while !my_file.is_eof() { 36 | //! let mut buffer = [0u8; 32]; 37 | //! let num_read = my_file.read(&mut buffer)?; 38 | //! for b in &buffer[0..num_read] { 39 | //! print!("{}", *b as char); 40 | //! } 41 | //! } 42 | //! Ok(()) 43 | //! } 44 | //! ``` 45 | //! 46 | //! For writing files: 47 | //! 48 | //! ```rust 49 | //! use embedded_sdmmc::{BlockDevice, Directory, Error, Mode, TimeSource}; 50 | //! fn write_file( 51 | //! root_dir: &mut Directory, 52 | //! ) -> Result<(), Error> 53 | //! { 54 | //! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", Mode::ReadWriteCreateOrAppend)?; 55 | //! my_other_file.write(b"Timestamp,Signal,Value\n")?; 56 | //! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; 57 | //! my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; 58 | //! my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; 59 | //! // Don't forget to flush the file so that the directory entry is updated 60 | //! my_other_file.flush()?; 61 | //! Ok(()) 62 | //! } 63 | //! ``` 64 | //! 65 | //! ## Features 66 | //! 67 | //! * `log`: Enabled by default. Generates log messages using the `log` crate. 68 | //! * `defmt-log`: By turning off the default features and enabling the 69 | //! `defmt-log` feature you can configure this crate to log messages over defmt 70 | //! instead. 71 | //! 72 | //! You cannot enable both the `log` feature and the `defmt-log` feature. 73 | 74 | #![cfg_attr(not(test), no_std)] 75 | #![deny(missing_docs)] 76 | 77 | // **************************************************************************** 78 | // 79 | // Imports 80 | // 81 | // **************************************************************************** 82 | 83 | #[cfg(test)] 84 | #[macro_use] 85 | extern crate hex_literal; 86 | 87 | #[macro_use] 88 | mod structure; 89 | 90 | pub mod blockdevice; 91 | pub mod fat; 92 | pub mod filesystem; 93 | pub mod sdcard; 94 | 95 | use core::fmt::Debug; 96 | use embedded_io::ErrorKind; 97 | use filesystem::Handle; 98 | 99 | #[doc(inline)] 100 | pub use crate::blockdevice::{Block, BlockCache, BlockCount, BlockDevice, BlockIdx}; 101 | 102 | #[doc(inline)] 103 | pub use crate::fat::{FatVolume, VolumeName}; 104 | 105 | #[doc(inline)] 106 | pub use crate::filesystem::{ 107 | Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, Mode, RawDirectory, 108 | RawFile, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, 109 | }; 110 | 111 | use filesystem::DirectoryInfo; 112 | 113 | #[doc(inline)] 114 | pub use crate::sdcard::Error as SdCardError; 115 | 116 | #[doc(inline)] 117 | pub use crate::sdcard::SdCard; 118 | 119 | mod volume_mgr; 120 | #[doc(inline)] 121 | pub use volume_mgr::VolumeManager; 122 | 123 | #[cfg(all(feature = "defmt-log", feature = "log"))] 124 | compile_error!("Cannot enable both log and defmt-log"); 125 | 126 | #[cfg(feature = "log")] 127 | use log::{debug, trace, warn}; 128 | 129 | #[cfg(feature = "defmt-log")] 130 | use defmt::{debug, trace, warn}; 131 | 132 | #[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] 133 | #[macro_export] 134 | /// Like log::debug! but does nothing at all 135 | macro_rules! debug { 136 | ($($arg:tt)+) => {}; 137 | } 138 | 139 | #[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] 140 | #[macro_export] 141 | /// Like log::trace! but does nothing at all 142 | macro_rules! trace { 143 | ($($arg:tt)+) => {}; 144 | } 145 | 146 | #[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] 147 | #[macro_export] 148 | /// Like log::warn! but does nothing at all 149 | macro_rules! warn { 150 | ($($arg:tt)+) => {}; 151 | } 152 | 153 | // **************************************************************************** 154 | // 155 | // Public Types 156 | // 157 | // **************************************************************************** 158 | 159 | /// All the ways the functions in this crate can fail. 160 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 161 | #[derive(Debug, Clone)] 162 | pub enum Error 163 | where 164 | E: core::fmt::Debug, 165 | { 166 | /// The underlying block device threw an error. 167 | DeviceError(E), 168 | /// The filesystem is badly formatted (or this code is buggy). 169 | FormatError(&'static str), 170 | /// The given `VolumeIdx` was bad, 171 | NoSuchVolume, 172 | /// The given filename was bad 173 | FilenameError(FilenameError), 174 | /// Out of memory opening volumes 175 | TooManyOpenVolumes, 176 | /// Out of memory opening directories 177 | TooManyOpenDirs, 178 | /// Out of memory opening files 179 | TooManyOpenFiles, 180 | /// Bad handle given 181 | BadHandle, 182 | /// That file or directory doesn't exist 183 | NotFound, 184 | /// You can't open a file twice or delete an open file 185 | FileAlreadyOpen, 186 | /// You can't open a directory twice 187 | DirAlreadyOpen, 188 | /// You can't open a directory as a file 189 | OpenedDirAsFile, 190 | /// You can't open a file as a directory 191 | OpenedFileAsDir, 192 | /// You can't delete a directory as a file 193 | DeleteDirAsFile, 194 | /// You can't close a volume with open files or directories 195 | VolumeStillInUse, 196 | /// You can't open a volume twice 197 | VolumeAlreadyOpen, 198 | /// We can't do that yet 199 | Unsupported, 200 | /// Tried to read beyond end of file 201 | EndOfFile, 202 | /// Found a bad cluster 203 | BadCluster, 204 | /// Error while converting types 205 | ConversionError, 206 | /// The device does not have enough space for the operation 207 | NotEnoughSpace, 208 | /// Cluster was not properly allocated by the library 209 | AllocationError, 210 | /// Jumped to free space during FAT traversing 211 | UnterminatedFatChain, 212 | /// Tried to open Read-Only file with write mode 213 | ReadOnly, 214 | /// Tried to create an existing file 215 | FileAlreadyExists, 216 | /// Bad block size - only 512 byte blocks supported 217 | BadBlockSize(u16), 218 | /// Bad offset given when seeking 219 | InvalidOffset, 220 | /// Disk is full 221 | DiskFull, 222 | /// A directory with that name already exists 223 | DirAlreadyExists, 224 | /// The filesystem tried to gain a lock whilst already locked. 225 | /// 226 | /// This is either a bug in the filesystem, or you tried to access the 227 | /// filesystem API from inside a directory iterator (that isn't allowed). 228 | LockError, 229 | } 230 | 231 | impl embedded_io::Error for Error { 232 | fn kind(&self) -> ErrorKind { 233 | match self { 234 | Error::DeviceError(_) 235 | | Error::FormatError(_) 236 | | Error::FileAlreadyOpen 237 | | Error::DirAlreadyOpen 238 | | Error::VolumeStillInUse 239 | | Error::VolumeAlreadyOpen 240 | | Error::EndOfFile 241 | | Error::DiskFull 242 | | Error::NotEnoughSpace 243 | | Error::AllocationError 244 | | Error::LockError => ErrorKind::Other, 245 | Error::NoSuchVolume 246 | | Error::FilenameError(_) 247 | | Error::BadHandle 248 | | Error::InvalidOffset => ErrorKind::InvalidInput, 249 | Error::TooManyOpenVolumes | Error::TooManyOpenDirs | Error::TooManyOpenFiles => { 250 | ErrorKind::OutOfMemory 251 | } 252 | Error::NotFound => ErrorKind::NotFound, 253 | Error::OpenedDirAsFile 254 | | Error::OpenedFileAsDir 255 | | Error::DeleteDirAsFile 256 | | Error::BadCluster 257 | | Error::ConversionError 258 | | Error::UnterminatedFatChain => ErrorKind::InvalidData, 259 | Error::Unsupported | Error::BadBlockSize(_) => ErrorKind::Unsupported, 260 | Error::ReadOnly => ErrorKind::PermissionDenied, 261 | Error::FileAlreadyExists | Error::DirAlreadyExists => ErrorKind::AlreadyExists, 262 | } 263 | } 264 | } 265 | 266 | impl From for Error 267 | where 268 | E: core::fmt::Debug, 269 | { 270 | fn from(value: E) -> Error { 271 | Error::DeviceError(value) 272 | } 273 | } 274 | 275 | /// A handle to a volume. 276 | /// 277 | /// A volume is a partition with a filesystem within it. 278 | /// 279 | /// Do NOT drop this object! It doesn't hold a reference to the Volume Manager 280 | /// it was created from and the VolumeManager will think you still have the 281 | /// volume open if you just drop it, and it won't let you open the file again. 282 | /// 283 | /// Instead you must pass it to [`crate::VolumeManager::close_volume`] to close 284 | /// it cleanly. 285 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 286 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 287 | pub struct RawVolume(Handle); 288 | 289 | impl RawVolume { 290 | /// Convert a raw volume into a droppable [`Volume`] 291 | pub fn to_volume< 292 | D, 293 | T, 294 | const MAX_DIRS: usize, 295 | const MAX_FILES: usize, 296 | const MAX_VOLUMES: usize, 297 | >( 298 | self, 299 | volume_mgr: &VolumeManager, 300 | ) -> Volume 301 | where 302 | D: crate::BlockDevice, 303 | T: crate::TimeSource, 304 | { 305 | Volume::new(self, volume_mgr) 306 | } 307 | } 308 | 309 | /// A handle for an open volume on disk, which closes on drop. 310 | /// 311 | /// In contrast to a `RawVolume`, a `Volume` holds a mutable reference to its 312 | /// parent `VolumeManager`, which restricts which operations you can perform. 313 | /// 314 | /// If you drop a value of this type, it closes the volume automatically, but 315 | /// any error that may occur will be ignored. To handle potential errors, use 316 | /// the [`Volume::close`] method. 317 | pub struct Volume<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 318 | where 319 | D: crate::BlockDevice, 320 | T: crate::TimeSource, 321 | { 322 | raw_volume: RawVolume, 323 | volume_mgr: &'a VolumeManager, 324 | } 325 | 326 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 327 | Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 328 | where 329 | D: crate::BlockDevice, 330 | T: crate::TimeSource, 331 | { 332 | /// Create a new `Volume` from a `RawVolume` 333 | pub fn new( 334 | raw_volume: RawVolume, 335 | volume_mgr: &'a VolumeManager, 336 | ) -> Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { 337 | Volume { 338 | raw_volume, 339 | volume_mgr, 340 | } 341 | } 342 | 343 | /// Open the volume's root directory. 344 | /// 345 | /// You can then read the directory entries with `iterate_dir`, or you can 346 | /// use `open_file_in_dir`. 347 | pub fn open_root_dir( 348 | &self, 349 | ) -> Result, Error> { 350 | let d = self.volume_mgr.open_root_dir(self.raw_volume)?; 351 | Ok(d.to_directory(self.volume_mgr)) 352 | } 353 | 354 | /// Convert back to a raw volume 355 | pub fn to_raw_volume(self) -> RawVolume { 356 | let v = self.raw_volume; 357 | core::mem::forget(self); 358 | v 359 | } 360 | 361 | /// Consume the `Volume` handle and close it. The behavior of this is similar 362 | /// to using [`core::mem::drop`] or letting the `Volume` go out of scope, 363 | /// except this lets the user handle any errors that may occur in the process, 364 | /// whereas when using drop, any errors will be discarded silently. 365 | pub fn close(self) -> Result<(), Error> { 366 | let result = self.volume_mgr.close_volume(self.raw_volume); 367 | core::mem::forget(self); 368 | result 369 | } 370 | } 371 | 372 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop 373 | for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 374 | where 375 | D: crate::BlockDevice, 376 | T: crate::TimeSource, 377 | { 378 | fn drop(&mut self) { 379 | _ = self.volume_mgr.close_volume(self.raw_volume) 380 | } 381 | } 382 | 383 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 384 | core::fmt::Debug for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 385 | where 386 | D: crate::BlockDevice, 387 | T: crate::TimeSource, 388 | { 389 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 390 | write!(f, "Volume({})", self.raw_volume.0 .0) 391 | } 392 | } 393 | 394 | #[cfg(feature = "defmt-log")] 395 | impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> 396 | defmt::Format for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> 397 | where 398 | D: crate::BlockDevice, 399 | T: crate::TimeSource, 400 | { 401 | fn format(&self, fmt: defmt::Formatter) { 402 | defmt::write!(fmt, "Volume({})", self.raw_volume.0 .0) 403 | } 404 | } 405 | 406 | /// Internal information about a Volume 407 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 408 | #[derive(Debug, PartialEq, Eq)] 409 | pub(crate) struct VolumeInfo { 410 | /// Handle for this volume. 411 | raw_volume: RawVolume, 412 | /// Which volume (i.e. partition) we opened on the disk 413 | idx: VolumeIdx, 414 | /// What kind of volume this is 415 | volume_type: VolumeType, 416 | } 417 | 418 | /// This enum holds the data for the various different types of filesystems we 419 | /// support. 420 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 421 | #[derive(Debug, PartialEq, Eq)] 422 | pub enum VolumeType { 423 | /// FAT16/FAT32 formatted volumes. 424 | Fat(FatVolume), 425 | } 426 | 427 | /// A number which identifies a volume (or partition) on a disk. 428 | /// 429 | /// `VolumeIdx(0)` is the first primary partition on an MBR partitioned disk. 430 | #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] 431 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 432 | pub struct VolumeIdx(pub usize); 433 | 434 | /// Marker for a FAT32 partition. Sometimes also use for FAT16 formatted 435 | /// partitions. 436 | const PARTITION_ID_FAT32_LBA: u8 = 0x0C; 437 | /// Marker for a FAT16 partition with LBA. Seen on a Raspberry Pi SD card. 438 | const PARTITION_ID_FAT16_LBA: u8 = 0x0E; 439 | /// Marker for a FAT16 partition. Seen on a card formatted with the official 440 | /// SD-Card formatter. 441 | const PARTITION_ID_FAT16: u8 = 0x06; 442 | /// Marker for a FAT16 partition smaller than 32MB. Seen on the wowki simulated 443 | /// microsd card 444 | const PARTITION_ID_FAT16_SMALL: u8 = 0x04; 445 | /// Marker for a FAT32 partition. What Macosx disk utility (and also SD-Card formatter?) 446 | /// use. 447 | const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B; 448 | 449 | // **************************************************************************** 450 | // 451 | // Unit Tests 452 | // 453 | // **************************************************************************** 454 | 455 | // None 456 | 457 | // **************************************************************************** 458 | // 459 | // End Of File 460 | // 461 | // **************************************************************************** 462 | -------------------------------------------------------------------------------- /src/structure.rs: -------------------------------------------------------------------------------- 1 | //! Useful macros for parsing SD/MMC structures. 2 | 3 | macro_rules! access_field { 4 | ($self:expr, $offset:expr, $start_bit:expr, 1) => { 5 | ($self.data[$offset] & (1 << $start_bit)) != 0 6 | }; 7 | ($self:expr, $offset:expr, $start:expr, $num_bits:expr) => { 8 | ($self.data[$offset] >> $start) & (((1u16 << $num_bits) - 1) as u8) 9 | }; 10 | } 11 | 12 | macro_rules! define_field { 13 | ($name:ident, bool, $offset:expr, $bit:expr) => { 14 | /// Get the value from the $name field 15 | pub fn $name(&self) -> bool { 16 | access_field!(self, $offset, $bit, 1) 17 | } 18 | }; 19 | ($name:ident, u8, $offset:expr, $start_bit:expr, $num_bits:expr) => { 20 | /// Get the value from the $name field 21 | pub fn $name(&self) -> u8 { 22 | access_field!(self, $offset, $start_bit, $num_bits) 23 | } 24 | }; 25 | ($name:ident, $type:ty, [ $( ( $offset:expr, $start_bit:expr, $num_bits:expr ) ),+ ]) => { 26 | /// Gets the value from the $name field 27 | pub fn $name(&self) -> $type { 28 | let mut result = 0; 29 | $( 30 | result <<= $num_bits; 31 | let part = access_field!(self, $offset, $start_bit, $num_bits) as $type; 32 | result |= part; 33 | )+ 34 | result 35 | } 36 | }; 37 | 38 | ($name:ident, u8, $offset:expr) => { 39 | /// Get the value from the $name field 40 | pub fn $name(&self) -> u8 { 41 | self.data[$offset] 42 | } 43 | }; 44 | 45 | ($name:ident, u16, $offset:expr) => { 46 | /// Get the value from the $name field 47 | pub fn $name(&self) -> u16 { 48 | LittleEndian::read_u16(&self.data[$offset..$offset+2]) 49 | } 50 | }; 51 | 52 | ($name:ident, u32, $offset:expr) => { 53 | /// Get the $name field 54 | pub fn $name(&self) -> u32 { 55 | LittleEndian::read_u32(&self.data[$offset..$offset+4]) 56 | } 57 | }; 58 | } 59 | 60 | // **************************************************************************** 61 | // 62 | // End Of File 63 | // 64 | // **************************************************************************** 65 | -------------------------------------------------------------------------------- /tests/directories.rs: -------------------------------------------------------------------------------- 1 | //! Directory related tests 2 | 3 | use embedded_sdmmc::{LfnBuffer, Mode, ShortFileName}; 4 | 5 | mod utils; 6 | 7 | #[derive(Debug, Clone)] 8 | struct ExpectedDirEntry { 9 | name: String, 10 | mtime: String, 11 | ctime: String, 12 | size: u32, 13 | is_dir: bool, 14 | } 15 | 16 | impl PartialEq for ExpectedDirEntry { 17 | fn eq(&self, other: &embedded_sdmmc::DirEntry) -> bool { 18 | if other.name.to_string() != self.name { 19 | return false; 20 | } 21 | if format!("{}", other.mtime) != self.mtime { 22 | return false; 23 | } 24 | if format!("{}", other.ctime) != self.ctime { 25 | return false; 26 | } 27 | if other.size != self.size { 28 | return false; 29 | } 30 | if other.attributes.is_directory() != self.is_dir { 31 | return false; 32 | } 33 | true 34 | } 35 | } 36 | 37 | #[test] 38 | fn fat16_root_directory_listing() { 39 | let time_source = utils::make_time_source(); 40 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 41 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 42 | 43 | let fat16_volume = volume_mgr 44 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 45 | .expect("open volume 0"); 46 | let root_dir = volume_mgr 47 | .open_root_dir(fat16_volume) 48 | .expect("open root dir"); 49 | 50 | let expected = [ 51 | ( 52 | ExpectedDirEntry { 53 | name: String::from("README.TXT"), 54 | mtime: String::from("2018-12-09 19:22:34"), 55 | ctime: String::from("2018-12-09 19:22:34"), 56 | size: 258, 57 | is_dir: false, 58 | }, 59 | None, 60 | ), 61 | ( 62 | ExpectedDirEntry { 63 | name: String::from("EMPTY.DAT"), 64 | mtime: String::from("2018-12-09 19:21:16"), 65 | ctime: String::from("2018-12-09 19:21:16"), 66 | size: 0, 67 | is_dir: false, 68 | }, 69 | None, 70 | ), 71 | ( 72 | ExpectedDirEntry { 73 | name: String::from("TEST"), 74 | mtime: String::from("2018-12-09 19:23:16"), 75 | ctime: String::from("2018-12-09 19:23:16"), 76 | size: 0, 77 | is_dir: true, 78 | }, 79 | None, 80 | ), 81 | ( 82 | ExpectedDirEntry { 83 | name: String::from("64MB.DAT"), 84 | mtime: String::from("2018-12-09 19:21:38"), 85 | ctime: String::from("2018-12-09 19:21:38"), 86 | size: 64 * 1024 * 1024, 87 | is_dir: false, 88 | }, 89 | None, 90 | ), 91 | ( 92 | ExpectedDirEntry { 93 | name: String::from("FSEVEN~4"), 94 | mtime: String::from("2024-10-25 16:30:42"), 95 | ctime: String::from("2024-10-25 16:30:42"), 96 | size: 0, 97 | is_dir: true, 98 | }, 99 | Some(String::from(".fseventsd")), 100 | ), 101 | ( 102 | ExpectedDirEntry { 103 | name: String::from("P-FAT16"), 104 | mtime: String::from("2024-10-30 18:43:12"), 105 | ctime: String::from("2024-10-30 18:43:12"), 106 | size: 0, 107 | is_dir: false, 108 | }, 109 | None, 110 | ), 111 | ]; 112 | 113 | let mut listing = Vec::new(); 114 | let mut storage = [0u8; 128]; 115 | let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); 116 | 117 | volume_mgr 118 | .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { 119 | listing.push((d.clone(), opt_lfn.map(String::from))); 120 | }) 121 | .expect("iterate directory"); 122 | 123 | for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { 124 | assert_eq!( 125 | expected_entry.0, given_entry.0, 126 | "{:#?} does not match {:#?}", 127 | given_entry, expected_entry 128 | ); 129 | assert_eq!( 130 | expected_entry.1, given_entry.1, 131 | "{:#?} does not match {:#?}", 132 | given_entry, expected_entry 133 | ); 134 | } 135 | assert_eq!( 136 | expected.len(), 137 | listing.len(), 138 | "{:#?} != {:#?}", 139 | expected, 140 | listing 141 | ); 142 | } 143 | 144 | #[test] 145 | fn fat16_sub_directory_listing() { 146 | let time_source = utils::make_time_source(); 147 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 148 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 149 | 150 | let fat16_volume = volume_mgr 151 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 152 | .expect("open volume 0"); 153 | let root_dir = volume_mgr 154 | .open_root_dir(fat16_volume) 155 | .expect("open root dir"); 156 | let test_dir = volume_mgr 157 | .open_dir(root_dir, "TEST") 158 | .expect("open test dir"); 159 | 160 | let expected = [ 161 | ExpectedDirEntry { 162 | name: String::from("."), 163 | mtime: String::from("2018-12-09 19:21:02"), 164 | ctime: String::from("2018-12-09 19:21:02"), 165 | size: 0, 166 | is_dir: true, 167 | }, 168 | ExpectedDirEntry { 169 | name: String::from(".."), 170 | mtime: String::from("2018-12-09 19:21:02"), 171 | ctime: String::from("2018-12-09 19:21:02"), 172 | size: 0, 173 | is_dir: true, 174 | }, 175 | ExpectedDirEntry { 176 | name: String::from("TEST.DAT"), 177 | mtime: String::from("2018-12-09 19:22:12"), 178 | ctime: String::from("2018-12-09 19:22:12"), 179 | size: 3500, 180 | is_dir: false, 181 | }, 182 | ]; 183 | 184 | let mut listing = Vec::new(); 185 | let mut count = 0; 186 | 187 | volume_mgr 188 | .iterate_dir(test_dir, |d| { 189 | if count == 0 { 190 | assert!(d.name == ShortFileName::this_dir()); 191 | } else if count == 1 { 192 | assert!(d.name == ShortFileName::parent_dir()); 193 | } 194 | count += 1; 195 | listing.push(d.clone()); 196 | }) 197 | .expect("iterate directory"); 198 | 199 | for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { 200 | assert_eq!( 201 | expected_entry, given_entry, 202 | "{:#?} does not match {:#?}", 203 | given_entry, expected_entry 204 | ); 205 | } 206 | assert_eq!( 207 | expected.len(), 208 | listing.len(), 209 | "{:#?} != {:#?}", 210 | expected, 211 | listing 212 | ); 213 | } 214 | 215 | #[test] 216 | fn fat32_root_directory_listing() { 217 | let time_source = utils::make_time_source(); 218 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 219 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 220 | 221 | let fat32_volume = volume_mgr 222 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 223 | .expect("open volume 1"); 224 | let root_dir = volume_mgr 225 | .open_root_dir(fat32_volume) 226 | .expect("open root dir"); 227 | 228 | let expected = [ 229 | ( 230 | ExpectedDirEntry { 231 | name: String::from("64MB.DAT"), 232 | mtime: String::from("2018-12-09 19:22:56"), 233 | ctime: String::from("2018-12-09 19:22:56"), 234 | size: 64 * 1024 * 1024, 235 | is_dir: false, 236 | }, 237 | None, 238 | ), 239 | ( 240 | ExpectedDirEntry { 241 | name: String::from("EMPTY.DAT"), 242 | mtime: String::from("2018-12-09 19:22:56"), 243 | ctime: String::from("2018-12-09 19:22:56"), 244 | size: 0, 245 | is_dir: false, 246 | }, 247 | None, 248 | ), 249 | ( 250 | ExpectedDirEntry { 251 | name: String::from("README.TXT"), 252 | mtime: String::from("2023-09-21 09:48:06"), 253 | ctime: String::from("2018-12-09 19:22:56"), 254 | size: 258, 255 | is_dir: false, 256 | }, 257 | None, 258 | ), 259 | ( 260 | ExpectedDirEntry { 261 | name: String::from("TEST"), 262 | mtime: String::from("2018-12-09 19:23:20"), 263 | ctime: String::from("2018-12-09 19:23:20"), 264 | size: 0, 265 | is_dir: true, 266 | }, 267 | None, 268 | ), 269 | ( 270 | ExpectedDirEntry { 271 | name: String::from("FSEVEN~4"), 272 | mtime: String::from("2024-10-25 16:30:42"), 273 | ctime: String::from("2024-10-25 16:30:42"), 274 | size: 0, 275 | is_dir: true, 276 | }, 277 | Some(String::from(".fseventsd")), 278 | ), 279 | ( 280 | ExpectedDirEntry { 281 | name: String::from("P-FAT32"), 282 | mtime: String::from("2024-10-30 18:43:16"), 283 | ctime: String::from("2024-10-30 18:43:16"), 284 | size: 0, 285 | is_dir: false, 286 | }, 287 | None, 288 | ), 289 | ( 290 | ExpectedDirEntry { 291 | name: String::from("THISIS~9"), 292 | mtime: String::from("2024-10-25 16:30:54"), 293 | ctime: String::from("2024-10-25 16:30:50"), 294 | size: 0, 295 | is_dir: true, 296 | }, 297 | Some(String::from("This is a long file name £99")), 298 | ), 299 | ( 300 | ExpectedDirEntry { 301 | name: String::from("COPYO~13.TXT"), 302 | mtime: String::from("2024-10-25 16:31:14"), 303 | ctime: String::from("2018-12-09 19:22:56"), 304 | size: 258, 305 | is_dir: false, 306 | }, 307 | Some(String::from("Copy of Readme.txt")), 308 | ), 309 | ]; 310 | 311 | let mut listing = Vec::new(); 312 | let mut storage = [0u8; 128]; 313 | let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); 314 | 315 | volume_mgr 316 | .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { 317 | listing.push((d.clone(), opt_lfn.map(String::from))); 318 | }) 319 | .expect("iterate directory"); 320 | 321 | for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { 322 | assert_eq!( 323 | expected_entry.0, given_entry.0, 324 | "{:#?} does not match {:#?}", 325 | given_entry, expected_entry 326 | ); 327 | assert_eq!( 328 | expected_entry.1, given_entry.1, 329 | "{:#?} does not match {:#?}", 330 | given_entry, expected_entry 331 | ); 332 | } 333 | assert_eq!( 334 | expected.len(), 335 | listing.len(), 336 | "{:#?} != {:#?}", 337 | expected, 338 | listing 339 | ); 340 | } 341 | 342 | #[test] 343 | fn open_dir_twice() { 344 | let time_source = utils::make_time_source(); 345 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 346 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 347 | 348 | let fat32_volume = volume_mgr 349 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 350 | .expect("open volume 1"); 351 | 352 | let root_dir = volume_mgr 353 | .open_root_dir(fat32_volume) 354 | .expect("open root dir"); 355 | 356 | let root_dir2 = volume_mgr 357 | .open_root_dir(fat32_volume) 358 | .expect("open it again"); 359 | 360 | assert!(matches!( 361 | volume_mgr.open_dir(root_dir, "README.TXT"), 362 | Err(embedded_sdmmc::Error::OpenedFileAsDir) 363 | )); 364 | 365 | let test_dir = volume_mgr 366 | .open_dir(root_dir, "TEST") 367 | .expect("open test dir"); 368 | 369 | let test_dir2 = volume_mgr.open_dir(root_dir, "TEST").unwrap(); 370 | 371 | volume_mgr.close_dir(root_dir).expect("close root dir"); 372 | volume_mgr.close_dir(test_dir).expect("close test dir"); 373 | volume_mgr.close_dir(test_dir2).expect("close test dir"); 374 | volume_mgr.close_dir(root_dir2).expect("close test dir"); 375 | 376 | assert!(matches!( 377 | volume_mgr.close_dir(test_dir), 378 | Err(embedded_sdmmc::Error::BadHandle) 379 | )); 380 | } 381 | 382 | #[test] 383 | fn open_too_many_dirs() { 384 | let time_source = utils::make_time_source(); 385 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 386 | let volume_mgr: embedded_sdmmc::VolumeManager< 387 | utils::RamDisk>, 388 | utils::TestTimeSource, 389 | 1, 390 | 4, 391 | 2, 392 | > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); 393 | 394 | let fat32_volume = volume_mgr 395 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 396 | .expect("open volume 1"); 397 | let root_dir = volume_mgr 398 | .open_root_dir(fat32_volume) 399 | .expect("open root dir"); 400 | 401 | assert!(matches!( 402 | volume_mgr.open_dir(root_dir, "TEST"), 403 | Err(embedded_sdmmc::Error::TooManyOpenDirs) 404 | )); 405 | } 406 | 407 | #[test] 408 | fn find_dir_entry() { 409 | let time_source = utils::make_time_source(); 410 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 411 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 412 | 413 | let fat32_volume = volume_mgr 414 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 415 | .expect("open volume 1"); 416 | 417 | let root_dir = volume_mgr 418 | .open_root_dir(fat32_volume) 419 | .expect("open root dir"); 420 | 421 | let dir_entry = volume_mgr 422 | .find_directory_entry(root_dir, "README.TXT") 423 | .expect("Find directory entry"); 424 | assert!(dir_entry.attributes.is_archive()); 425 | assert!(!dir_entry.attributes.is_directory()); 426 | assert!(!dir_entry.attributes.is_hidden()); 427 | assert!(!dir_entry.attributes.is_lfn()); 428 | assert!(!dir_entry.attributes.is_system()); 429 | assert!(!dir_entry.attributes.is_volume()); 430 | 431 | assert!(matches!( 432 | volume_mgr.find_directory_entry(root_dir, "README.TXS"), 433 | Err(embedded_sdmmc::Error::NotFound) 434 | )); 435 | } 436 | 437 | #[test] 438 | fn delete_file() { 439 | let time_source = utils::make_time_source(); 440 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 441 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 442 | 443 | let fat32_volume = volume_mgr 444 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 445 | .expect("open volume 1"); 446 | 447 | let root_dir = volume_mgr 448 | .open_root_dir(fat32_volume) 449 | .expect("open root dir"); 450 | 451 | let file = volume_mgr 452 | .open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly) 453 | .unwrap(); 454 | 455 | assert!(matches!( 456 | volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), 457 | Err(embedded_sdmmc::Error::FileAlreadyOpen) 458 | )); 459 | 460 | assert!(matches!( 461 | volume_mgr.delete_file_in_dir(root_dir, "README2.TXT"), 462 | Err(embedded_sdmmc::Error::NotFound) 463 | )); 464 | 465 | volume_mgr.close_file(file).unwrap(); 466 | 467 | volume_mgr 468 | .delete_file_in_dir(root_dir, "README.TXT") 469 | .unwrap(); 470 | 471 | assert!(matches!( 472 | volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), 473 | Err(embedded_sdmmc::Error::NotFound) 474 | )); 475 | 476 | assert!(matches!( 477 | volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), 478 | Err(embedded_sdmmc::Error::NotFound) 479 | )); 480 | } 481 | 482 | #[test] 483 | fn make_directory() { 484 | let time_source = utils::make_time_source(); 485 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 486 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 487 | 488 | let fat32_volume = volume_mgr 489 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 490 | .expect("open volume 1"); 491 | 492 | let root_dir = volume_mgr 493 | .open_root_dir(fat32_volume) 494 | .expect("open root dir"); 495 | 496 | let test_dir_name = ShortFileName::create_from_str("12345678.ABC").unwrap(); 497 | let test_file_name = ShortFileName::create_from_str("ABC.TXT").unwrap(); 498 | 499 | volume_mgr 500 | .make_dir_in_dir(root_dir, &test_dir_name) 501 | .unwrap(); 502 | 503 | let new_dir = volume_mgr.open_dir(root_dir, &test_dir_name).unwrap(); 504 | 505 | let mut has_this = false; 506 | let mut has_parent = false; 507 | volume_mgr 508 | .iterate_dir(new_dir, |item| { 509 | if item.name == ShortFileName::parent_dir() { 510 | has_parent = true; 511 | assert!(item.attributes.is_directory()); 512 | assert_eq!(item.size, 0); 513 | assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); 514 | assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); 515 | } else if item.name == ShortFileName::this_dir() { 516 | has_this = true; 517 | assert!(item.attributes.is_directory()); 518 | assert_eq!(item.size, 0); 519 | assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); 520 | assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); 521 | } else { 522 | panic!("Unexpected item in new dir"); 523 | } 524 | }) 525 | .unwrap(); 526 | assert!(has_this); 527 | assert!(has_parent); 528 | 529 | let new_file = volume_mgr 530 | .open_file_in_dir( 531 | new_dir, 532 | &test_file_name, 533 | embedded_sdmmc::Mode::ReadWriteCreate, 534 | ) 535 | .expect("open new file"); 536 | volume_mgr 537 | .write(new_file, b"Hello") 538 | .expect("write to new file"); 539 | volume_mgr.close_file(new_file).expect("close new file"); 540 | 541 | let mut has_this = false; 542 | let mut has_parent = false; 543 | let mut has_new_file = false; 544 | volume_mgr 545 | .iterate_dir(new_dir, |item| { 546 | if item.name == ShortFileName::parent_dir() { 547 | has_parent = true; 548 | assert!(item.attributes.is_directory()); 549 | assert_eq!(item.size, 0); 550 | assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); 551 | assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); 552 | } else if item.name == ShortFileName::this_dir() { 553 | has_this = true; 554 | assert!(item.attributes.is_directory()); 555 | assert_eq!(item.size, 0); 556 | assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); 557 | assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); 558 | } else if item.name == test_file_name { 559 | has_new_file = true; 560 | // We wrote "Hello" to it 561 | assert_eq!(item.size, 5); 562 | assert!(!item.attributes.is_directory()); 563 | assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); 564 | assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); 565 | } else { 566 | panic!("Unexpected item in new dir"); 567 | } 568 | }) 569 | .unwrap(); 570 | assert!(has_this); 571 | assert!(has_parent); 572 | assert!(has_new_file); 573 | 574 | // Close the root dir and look again 575 | volume_mgr.close_dir(root_dir).expect("close root"); 576 | volume_mgr.close_dir(new_dir).expect("close new_dir"); 577 | let root_dir = volume_mgr 578 | .open_root_dir(fat32_volume) 579 | .expect("open root dir"); 580 | // Check we can't make it again now it exists 581 | assert!(volume_mgr 582 | .make_dir_in_dir(root_dir, &test_dir_name) 583 | .is_err()); 584 | let new_dir = volume_mgr 585 | .open_dir(root_dir, &test_dir_name) 586 | .expect("find new dir"); 587 | let new_file = volume_mgr 588 | .open_file_in_dir(new_dir, &test_file_name, embedded_sdmmc::Mode::ReadOnly) 589 | .expect("re-open new file"); 590 | volume_mgr.close_dir(root_dir).expect("close root"); 591 | volume_mgr.close_dir(new_dir).expect("close new dir"); 592 | volume_mgr.close_file(new_file).expect("close file"); 593 | } 594 | 595 | // **************************************************************************** 596 | // 597 | // End Of File 598 | // 599 | // **************************************************************************** 600 | -------------------------------------------------------------------------------- /tests/disk.img.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-embedded-community/embedded-sdmmc-rs/a17e0bc08def6b05a59994e7c59c554161deae31/tests/disk.img.gz -------------------------------------------------------------------------------- /tests/open_files.rs: -------------------------------------------------------------------------------- 1 | //! File opening related tests 2 | 3 | use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; 4 | 5 | mod utils; 6 | 7 | #[test] 8 | fn open_files() { 9 | let time_source = utils::make_time_source(); 10 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 11 | let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = 12 | VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); 13 | let volume = volume_mgr 14 | .open_raw_volume(VolumeIdx(0)) 15 | .expect("open volume"); 16 | let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); 17 | 18 | // Open with string 19 | let f = volume_mgr 20 | .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) 21 | .expect("open file"); 22 | 23 | assert!(matches!( 24 | volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), 25 | Err(Error::FileAlreadyOpen) 26 | )); 27 | 28 | volume_mgr.close_file(f).expect("close file"); 29 | 30 | // Open with SFN 31 | 32 | let dir_entry = volume_mgr 33 | .find_directory_entry(root_dir, "README.TXT") 34 | .expect("find file"); 35 | 36 | let f = volume_mgr 37 | .open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadWriteCreateOrAppend) 38 | .expect("open file with dir entry"); 39 | 40 | assert!(matches!( 41 | volume_mgr.open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadOnly), 42 | Err(Error::FileAlreadyOpen) 43 | )); 44 | 45 | // Can still spot duplicates even if name given the other way around 46 | 47 | assert!(matches!( 48 | volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), 49 | Err(Error::FileAlreadyOpen) 50 | )); 51 | 52 | let f2 = volume_mgr 53 | .open_file_in_dir(root_dir, "64MB.DAT", Mode::ReadWriteTruncate) 54 | .expect("open file"); 55 | 56 | // Hit file limit 57 | 58 | assert!(matches!( 59 | volume_mgr.open_file_in_dir(root_dir, "EMPTY.DAT", Mode::ReadOnly), 60 | Err(Error::TooManyOpenFiles) 61 | )); 62 | 63 | volume_mgr.close_file(f).expect("close file"); 64 | volume_mgr.close_file(f2).expect("close file"); 65 | 66 | // File not found 67 | 68 | assert!(matches!( 69 | volume_mgr.open_file_in_dir(root_dir, "README.TXS", Mode::ReadOnly), 70 | Err(Error::NotFound) 71 | )); 72 | 73 | // Create a new file 74 | let f3 = volume_mgr 75 | .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadWriteCreate) 76 | .expect("open file"); 77 | 78 | volume_mgr.write(f3, b"12345").expect("write to file"); 79 | volume_mgr.write(f3, b"67890").expect("write to file"); 80 | volume_mgr.close_file(f3).expect("close file"); 81 | 82 | // Open our new file 83 | let f3 = volume_mgr 84 | .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadOnly) 85 | .expect("open file"); 86 | // Should have 10 bytes in it 87 | assert_eq!(volume_mgr.file_length(f3).expect("file length"), 10); 88 | volume_mgr.close_file(f3).expect("close file"); 89 | 90 | volume_mgr.close_dir(root_dir).expect("close dir"); 91 | volume_mgr.close_volume(volume).expect("close volume"); 92 | } 93 | 94 | #[test] 95 | fn open_non_raw() { 96 | let time_source = utils::make_time_source(); 97 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 98 | let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = 99 | VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); 100 | let volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); 101 | let root_dir = volume.open_root_dir().expect("open root dir"); 102 | let f = root_dir 103 | .open_file_in_dir("README.TXT", Mode::ReadOnly) 104 | .expect("open file"); 105 | 106 | let mut buffer = [0u8; 512]; 107 | let len = f.read(&mut buffer).expect("read from file"); 108 | // See directory listing in utils.rs, to see that README.TXT is 258 bytes long 109 | assert_eq!(len, 258); 110 | assert_eq!(f.length(), 258); 111 | f.seek_from_current(0).unwrap(); 112 | assert!(f.is_eof()); 113 | assert_eq!(f.offset(), 258); 114 | assert!(matches!(f.seek_from_current(1), Err(Error::InvalidOffset))); 115 | f.seek_from_current(-258).unwrap(); 116 | assert!(!f.is_eof()); 117 | assert_eq!(f.offset(), 0); 118 | f.seek_from_current(10).unwrap(); 119 | assert!(!f.is_eof()); 120 | assert_eq!(f.offset(), 10); 121 | f.seek_from_end(0).unwrap(); 122 | assert!(f.is_eof()); 123 | assert_eq!(f.offset(), 258); 124 | assert!(matches!( 125 | f.seek_from_current(-259), 126 | Err(Error::InvalidOffset) 127 | )); 128 | f.seek_from_start(25).unwrap(); 129 | assert!(!f.is_eof()); 130 | assert_eq!(f.offset(), 25); 131 | 132 | drop(f); 133 | 134 | let Err(Error::FileAlreadyExists) = 135 | root_dir.open_file_in_dir("README.TXT", Mode::ReadWriteCreate) 136 | else { 137 | panic!("Expected to file to exist"); 138 | }; 139 | } 140 | 141 | // **************************************************************************** 142 | // 143 | // End Of File 144 | // 145 | // **************************************************************************** 146 | -------------------------------------------------------------------------------- /tests/read_file.rs: -------------------------------------------------------------------------------- 1 | //! Reading related tests 2 | 3 | use sha2::Digest; 4 | 5 | mod utils; 6 | 7 | static TEST_DAT_SHA256_SUM: &[u8] = 8 | b"\x59\xe3\x46\x8e\x3b\xef\x8b\xfe\x37\xe6\x0a\x82\x21\xa1\x89\x6e\x10\x5b\x80\xa6\x1a\x23\x63\x76\x12\xac\x8c\xd2\x4c\xa0\x4a\x75"; 9 | 10 | #[test] 11 | fn read_file_512_blocks() { 12 | let time_source = utils::make_time_source(); 13 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 14 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 15 | 16 | let fat16_volume = volume_mgr 17 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 18 | .expect("open volume 0"); 19 | let root_dir = volume_mgr 20 | .open_root_dir(fat16_volume) 21 | .expect("open root dir"); 22 | let test_dir = volume_mgr 23 | .open_dir(root_dir, "TEST") 24 | .expect("Open test dir"); 25 | 26 | let test_file = volume_mgr 27 | .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) 28 | .expect("open test file"); 29 | 30 | let mut contents = Vec::new(); 31 | 32 | let mut partial = false; 33 | while !volume_mgr.file_eof(test_file).expect("check eof") { 34 | let mut buffer = [0u8; 512]; 35 | let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); 36 | if len != buffer.len() { 37 | if partial { 38 | panic!("Two partial reads!"); 39 | } else { 40 | partial = true; 41 | } 42 | } 43 | contents.extend(&buffer[0..len]); 44 | } 45 | 46 | let mut hasher = sha2::Sha256::new(); 47 | hasher.update(contents); 48 | let hash = hasher.finalize(); 49 | assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); 50 | } 51 | 52 | #[test] 53 | fn read_file_all() { 54 | let time_source = utils::make_time_source(); 55 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 56 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 57 | 58 | let fat16_volume = volume_mgr 59 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 60 | .expect("open volume 0"); 61 | let root_dir = volume_mgr 62 | .open_root_dir(fat16_volume) 63 | .expect("open root dir"); 64 | let test_dir = volume_mgr 65 | .open_dir(root_dir, "TEST") 66 | .expect("Open test dir"); 67 | 68 | let test_file = volume_mgr 69 | .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) 70 | .expect("open test file"); 71 | 72 | let mut contents = vec![0u8; 4096]; 73 | let len = volume_mgr 74 | .read(test_file, &mut contents) 75 | .expect("read data"); 76 | if len != 3500 { 77 | panic!("Failed to read all of TEST.DAT"); 78 | } 79 | 80 | let mut hasher = sha2::Sha256::new(); 81 | hasher.update(&contents[0..3500]); 82 | let hash = hasher.finalize(); 83 | assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); 84 | } 85 | 86 | #[test] 87 | fn read_file_prime_blocks() { 88 | let time_source = utils::make_time_source(); 89 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 90 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 91 | 92 | let fat16_volume = volume_mgr 93 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 94 | .expect("open volume 0"); 95 | let root_dir = volume_mgr 96 | .open_root_dir(fat16_volume) 97 | .expect("open root dir"); 98 | let test_dir = volume_mgr 99 | .open_dir(root_dir, "TEST") 100 | .expect("Open test dir"); 101 | 102 | let test_file = volume_mgr 103 | .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) 104 | .expect("open test file"); 105 | 106 | let mut contents = Vec::new(); 107 | 108 | let mut partial = false; 109 | while !volume_mgr.file_eof(test_file).expect("check eof") { 110 | // Exercise the alignment code by reading in chunks of 53 bytes 111 | let mut buffer = [0u8; 53]; 112 | let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); 113 | if len != buffer.len() { 114 | if partial { 115 | panic!("Two partial reads!"); 116 | } else { 117 | partial = true; 118 | } 119 | } 120 | contents.extend(&buffer[0..len]); 121 | } 122 | 123 | let mut hasher = sha2::Sha256::new(); 124 | hasher.update(&contents[0..3500]); 125 | let hash = hasher.finalize(); 126 | assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); 127 | } 128 | 129 | #[test] 130 | fn read_file_backwards() { 131 | let time_source = utils::make_time_source(); 132 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 133 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 134 | 135 | let fat16_volume = volume_mgr 136 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 137 | .expect("open volume 0"); 138 | let root_dir = volume_mgr 139 | .open_root_dir(fat16_volume) 140 | .expect("open root dir"); 141 | let test_dir = volume_mgr 142 | .open_dir(root_dir, "TEST") 143 | .expect("Open test dir"); 144 | 145 | let test_file = volume_mgr 146 | .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) 147 | .expect("open test file"); 148 | 149 | let mut contents = std::collections::VecDeque::new(); 150 | 151 | const CHUNK_SIZE: u32 = 100; 152 | let length = volume_mgr.file_length(test_file).expect("file length"); 153 | let mut read = 0; 154 | 155 | // go to end 156 | volume_mgr.file_seek_from_end(test_file, 0).expect("seek"); 157 | 158 | // We're going to read the file backwards in chunks of 100 bytes. This 159 | // checks we didn't make any assumptions about only going forwards. 160 | while read < length { 161 | // go to start of next chunk 162 | volume_mgr 163 | .file_seek_from_current(test_file, -(CHUNK_SIZE as i32)) 164 | .expect("seek"); 165 | // read chunk 166 | let mut buffer = [0u8; CHUNK_SIZE as usize]; 167 | let len = volume_mgr.read(test_file, &mut buffer).expect("read"); 168 | assert_eq!(len, CHUNK_SIZE as usize); 169 | contents.push_front(buffer.to_vec()); 170 | read += CHUNK_SIZE; 171 | // go to start of chunk we just read 172 | volume_mgr 173 | .file_seek_from_current(test_file, -(CHUNK_SIZE as i32)) 174 | .expect("seek"); 175 | } 176 | 177 | assert_eq!(read, length); 178 | 179 | let flat: Vec = contents.iter().flatten().copied().collect(); 180 | 181 | let mut hasher = sha2::Sha256::new(); 182 | hasher.update(flat); 183 | let hash = hasher.finalize(); 184 | assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); 185 | } 186 | 187 | #[test] 188 | fn read_file_with_odd_seek() { 189 | let time_source = utils::make_time_source(); 190 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 191 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 192 | 193 | let volume = volume_mgr 194 | .open_volume(embedded_sdmmc::VolumeIdx(0)) 195 | .unwrap(); 196 | let root_dir = volume.open_root_dir().unwrap(); 197 | let f = root_dir 198 | .open_file_in_dir("64MB.DAT", embedded_sdmmc::Mode::ReadOnly) 199 | .unwrap(); 200 | f.seek_from_start(0x2c).unwrap(); 201 | while f.offset() < 1000000 { 202 | let mut buffer = [0u8; 2048]; 203 | f.read(&mut buffer).unwrap(); 204 | f.seek_from_current(-1024).unwrap(); 205 | } 206 | } 207 | 208 | // **************************************************************************** 209 | // 210 | // End Of File 211 | // 212 | // **************************************************************************** 213 | -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Useful library code for tests 2 | 3 | use std::io::prelude::*; 4 | 5 | use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx}; 6 | 7 | /// This file contains: 8 | /// 9 | /// ```console 10 | /// $ fdisk ./disk.img 11 | /// Disk: ./disk.img geometry: 520/32/63 [1048576 sectors] 12 | /// Signature: 0xAA55 13 | /// Starting Ending 14 | /// #: id cyl hd sec - cyl hd sec [ start - size] 15 | /// ------------------------------------------------------------------------ 16 | /// 1: 0E 0 32 33 - 16 113 33 [ 2048 - 262144] DOS FAT-16 17 | /// 2: 0C 16 113 34 - 65 69 4 [ 264192 - 784384] Win95 FAT32L 18 | /// 3: 00 0 0 0 - 0 0 0 [ 0 - 0] unused 19 | /// 4: 00 0 0 0 - 0 0 0 [ 0 - 0] unused 20 | /// $ ls -l /Volumes/P-FAT16 21 | /// total 131080 22 | /// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT 23 | /// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT 24 | /// -rwxrwxrwx@ 1 jonathan staff 258 9 Dec 2018 README.TXT 25 | /// drwxrwxrwx 1 jonathan staff 2048 9 Dec 2018 TEST 26 | /// $ ls -l /Volumes/P-FAT16/TEST 27 | /// total 8 28 | /// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT 29 | /// $ ls -l /Volumes/P-FAT32 30 | /// total 131088 31 | /// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT 32 | /// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT 33 | /// -rwxrwxrwx@ 1 jonathan staff 258 21 Sep 09:48 README.TXT 34 | /// drwxrwxrwx 1 jonathan staff 4096 9 Dec 2018 TEST 35 | /// $ ls -l /Volumes/P-FAT32/TEST 36 | /// total 8 37 | /// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT 38 | /// ``` 39 | /// 40 | /// It will unpack to a Vec that is 1048576 * 512 = 512 MiB in size. 41 | pub static DISK_SOURCE: &[u8] = include_bytes!("../disk.img.gz"); 42 | 43 | #[derive(Debug)] 44 | #[allow(dead_code)] 45 | pub enum Error { 46 | /// Failed to read the source image 47 | Io(std::io::Error), 48 | /// Failed to unzip the source image 49 | Decode(flate2::DecompressError), 50 | /// Asked for a block we don't have 51 | OutOfBounds(BlockIdx), 52 | } 53 | 54 | impl From for Error { 55 | fn from(value: std::io::Error) -> Self { 56 | Self::Io(value) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(value: flate2::DecompressError) -> Self { 62 | Self::Decode(value) 63 | } 64 | } 65 | 66 | /// Implements the block device traits for a chunk of bytes in RAM. 67 | /// 68 | /// The slice should be a multiple of `embedded_sdmmc::Block::LEN` bytes in 69 | /// length. If it isn't the trailing data is discarded. 70 | pub struct RamDisk { 71 | contents: std::cell::RefCell, 72 | } 73 | 74 | impl RamDisk { 75 | fn new(contents: T) -> RamDisk { 76 | RamDisk { 77 | contents: std::cell::RefCell::new(contents), 78 | } 79 | } 80 | } 81 | 82 | impl BlockDevice for RamDisk 83 | where 84 | T: AsMut<[u8]> + AsRef<[u8]>, 85 | { 86 | type Error = Error; 87 | 88 | fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { 89 | let borrow = self.contents.borrow(); 90 | let contents: &[u8] = borrow.as_ref(); 91 | let mut block_idx = start_block_idx; 92 | for block in blocks.iter_mut() { 93 | let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; 94 | let end_offset = start_offset + embedded_sdmmc::Block::LEN; 95 | if end_offset > contents.len() { 96 | return Err(Error::OutOfBounds(block_idx)); 97 | } 98 | block 99 | .as_mut_slice() 100 | .copy_from_slice(&contents[start_offset..end_offset]); 101 | block_idx.0 += 1; 102 | } 103 | Ok(()) 104 | } 105 | 106 | fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { 107 | let mut borrow = self.contents.borrow_mut(); 108 | let contents: &mut [u8] = borrow.as_mut(); 109 | let mut block_idx = start_block_idx; 110 | for block in blocks.iter() { 111 | let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; 112 | let end_offset = start_offset + embedded_sdmmc::Block::LEN; 113 | if end_offset > contents.len() { 114 | return Err(Error::OutOfBounds(block_idx)); 115 | } 116 | contents[start_offset..end_offset].copy_from_slice(block.as_slice()); 117 | block_idx.0 += 1; 118 | } 119 | Ok(()) 120 | } 121 | 122 | fn num_blocks(&self) -> Result { 123 | let borrow = self.contents.borrow(); 124 | let contents: &[u8] = borrow.as_ref(); 125 | let len_blocks = contents.len() / embedded_sdmmc::Block::LEN; 126 | if len_blocks > u32::MAX as usize { 127 | panic!("Test disk too large! Only 2**32 blocks allowed"); 128 | } 129 | Ok(BlockCount(len_blocks as u32)) 130 | } 131 | } 132 | 133 | /// Unpack the fixed, static, disk image. 134 | fn unpack_disk(gzip_bytes: &[u8]) -> Result, Error> { 135 | let disk_cursor = std::io::Cursor::new(gzip_bytes); 136 | let mut gz_decoder = flate2::read::GzDecoder::new(disk_cursor); 137 | let mut output = Vec::with_capacity(512 * 1024 * 1024); 138 | gz_decoder.read_to_end(&mut output)?; 139 | Ok(output) 140 | } 141 | 142 | /// Turn some gzipped bytes into a block device, 143 | pub fn make_block_device(gzip_bytes: &[u8]) -> Result>, Error> { 144 | let data = unpack_disk(gzip_bytes)?; 145 | Ok(RamDisk::new(data)) 146 | } 147 | 148 | pub struct TestTimeSource { 149 | fixed: embedded_sdmmc::Timestamp, 150 | } 151 | 152 | impl embedded_sdmmc::TimeSource for TestTimeSource { 153 | fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { 154 | self.fixed 155 | } 156 | } 157 | 158 | /// Make a time source that gives a fixed time. 159 | /// 160 | /// It always claims to be 4 April 2003, at 13:30:05. 161 | /// 162 | /// This is an interesting time, because FAT will round it down to 13:30:04 due 163 | /// to only have two-second resolution. Hey, Real Time Clocks were optional back 164 | /// in 1981. 165 | pub fn make_time_source() -> TestTimeSource { 166 | TestTimeSource { 167 | fixed: embedded_sdmmc::Timestamp { 168 | year_since_1970: 33, 169 | zero_indexed_month: 3, 170 | zero_indexed_day: 3, 171 | hours: 13, 172 | minutes: 30, 173 | seconds: 5, 174 | }, 175 | } 176 | } 177 | 178 | /// Get the test time source time, as a string. 179 | /// 180 | /// We apply the FAT 2-second rounding here. 181 | #[allow(unused)] 182 | pub fn get_time_source_string() -> &'static str { 183 | "2003-04-04 13:30:04" 184 | } 185 | 186 | // **************************************************************************** 187 | // 188 | // End Of File 189 | // 190 | // **************************************************************************** 191 | -------------------------------------------------------------------------------- /tests/volume.rs: -------------------------------------------------------------------------------- 1 | //! Volume related tests 2 | 3 | mod utils; 4 | 5 | #[test] 6 | fn open_all_volumes() { 7 | let time_source = utils::make_time_source(); 8 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 9 | let volume_mgr: embedded_sdmmc::VolumeManager< 10 | utils::RamDisk>, 11 | utils::TestTimeSource, 12 | 4, 13 | 4, 14 | 2, 15 | > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); 16 | 17 | // Open Volume 0 18 | let fat16_volume = volume_mgr 19 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 20 | .expect("open volume 0"); 21 | 22 | // Fail to Open Volume 0 again 23 | assert!(matches!( 24 | volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(0)), 25 | Err(embedded_sdmmc::Error::VolumeAlreadyOpen) 26 | )); 27 | 28 | volume_mgr.close_volume(fat16_volume).expect("close fat16"); 29 | 30 | // Open Volume 1 31 | let fat32_volume = volume_mgr 32 | .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) 33 | .expect("open volume 1"); 34 | 35 | // Fail to Volume 1 again 36 | assert!(matches!( 37 | volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(1)), 38 | Err(embedded_sdmmc::Error::VolumeAlreadyOpen) 39 | )); 40 | 41 | // Open Volume 0 again 42 | let fat16_volume = volume_mgr 43 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 44 | .expect("open volume 0"); 45 | 46 | // Open any volume - too many volumes (0 and 1 are open) 47 | assert!(matches!( 48 | volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(0)), 49 | Err(embedded_sdmmc::Error::TooManyOpenVolumes) 50 | )); 51 | 52 | volume_mgr.close_volume(fat16_volume).expect("close fat16"); 53 | volume_mgr.close_volume(fat32_volume).expect("close fat32"); 54 | 55 | // This isn't a valid volume 56 | assert!(matches!( 57 | volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(2)), 58 | Err(embedded_sdmmc::Error::FormatError(_e)) 59 | )); 60 | 61 | // This isn't a valid volume 62 | assert!(matches!( 63 | volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(3)), 64 | Err(embedded_sdmmc::Error::FormatError(_e)) 65 | )); 66 | 67 | // This isn't a valid volume 68 | assert!(matches!( 69 | volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(9)), 70 | Err(embedded_sdmmc::Error::NoSuchVolume) 71 | )); 72 | 73 | let _root_dir = volume_mgr.open_root_dir(fat32_volume).expect("Open dir"); 74 | 75 | assert!(matches!( 76 | volume_mgr.close_volume(fat32_volume), 77 | Err(embedded_sdmmc::Error::VolumeStillInUse) 78 | )); 79 | } 80 | 81 | #[test] 82 | fn close_volume_too_early() { 83 | let time_source = utils::make_time_source(); 84 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 85 | let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); 86 | 87 | let volume = volume_mgr 88 | .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) 89 | .expect("open volume 0"); 90 | let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); 91 | 92 | // Dir open 93 | assert!(matches!( 94 | volume_mgr.close_volume(volume), 95 | Err(embedded_sdmmc::Error::VolumeStillInUse) 96 | )); 97 | 98 | let _test_file = volume_mgr 99 | .open_file_in_dir(root_dir, "64MB.DAT", embedded_sdmmc::Mode::ReadOnly) 100 | .expect("open test file"); 101 | 102 | volume_mgr.close_dir(root_dir).unwrap(); 103 | 104 | // File open, not dir open 105 | assert!(matches!( 106 | volume_mgr.close_volume(volume), 107 | Err(embedded_sdmmc::Error::VolumeStillInUse) 108 | )); 109 | } 110 | 111 | // **************************************************************************** 112 | // 113 | // End Of File 114 | // 115 | // **************************************************************************** 116 | -------------------------------------------------------------------------------- /tests/write_file.rs: -------------------------------------------------------------------------------- 1 | //! File opening related tests 2 | 3 | use embedded_sdmmc::{Mode, VolumeIdx, VolumeManager}; 4 | 5 | mod utils; 6 | 7 | #[test] 8 | fn append_file() { 9 | let time_source = utils::make_time_source(); 10 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 11 | let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = 12 | VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); 13 | let volume = volume_mgr 14 | .open_raw_volume(VolumeIdx(0)) 15 | .expect("open volume"); 16 | let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); 17 | 18 | // Open with string 19 | let f = volume_mgr 20 | .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) 21 | .expect("open file"); 22 | 23 | // Should be enough to cause a few more clusters to be allocated 24 | let test_data = vec![0xCC; 1024 * 1024]; 25 | volume_mgr.write(f, &test_data).expect("file write"); 26 | 27 | let length = volume_mgr.file_length(f).expect("get length"); 28 | assert_eq!(length, 1024 * 1024); 29 | 30 | let offset = volume_mgr.file_offset(f).expect("offset"); 31 | assert_eq!(offset, 1024 * 1024); 32 | 33 | // Now wind it back 1 byte; 34 | volume_mgr.file_seek_from_current(f, -1).expect("Seeking"); 35 | 36 | let offset = volume_mgr.file_offset(f).expect("offset"); 37 | assert_eq!(offset, (1024 * 1024) - 1); 38 | 39 | // Write another megabyte, making `2 MiB - 1` 40 | volume_mgr.write(f, &test_data).expect("file write"); 41 | 42 | let length = volume_mgr.file_length(f).expect("get length"); 43 | assert_eq!(length, (1024 * 1024 * 2) - 1); 44 | 45 | volume_mgr.close_file(f).expect("close dir"); 46 | 47 | // Now check the file length again 48 | 49 | let entry = volume_mgr 50 | .find_directory_entry(root_dir, "README.TXT") 51 | .expect("Find entry"); 52 | assert_eq!(entry.size, (1024 * 1024 * 2) - 1); 53 | 54 | volume_mgr.close_dir(root_dir).expect("close dir"); 55 | volume_mgr.close_volume(volume).expect("close volume"); 56 | } 57 | 58 | #[test] 59 | fn flush_file() { 60 | let time_source = utils::make_time_source(); 61 | let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); 62 | let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = 63 | VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); 64 | let volume = volume_mgr 65 | .open_raw_volume(VolumeIdx(0)) 66 | .expect("open volume"); 67 | let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); 68 | 69 | // Open with string 70 | let f = volume_mgr 71 | .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) 72 | .expect("open file"); 73 | 74 | // Write some data to the file 75 | let test_data = vec![0xCC; 64]; 76 | volume_mgr.write(f, &test_data).expect("file write"); 77 | 78 | // Check that the file length is zero in the directory entry, as we haven't 79 | // flushed yet 80 | let entry = volume_mgr 81 | .find_directory_entry(root_dir, "README.TXT") 82 | .expect("find entry"); 83 | assert_eq!(entry.size, 0); 84 | 85 | volume_mgr.flush_file(f).expect("flush"); 86 | 87 | // Now check the file length again after flushing 88 | let entry = volume_mgr 89 | .find_directory_entry(root_dir, "README.TXT") 90 | .expect("find entry"); 91 | assert_eq!(entry.size, 64); 92 | 93 | // Flush more writes 94 | volume_mgr.write(f, &test_data).expect("file write"); 95 | volume_mgr.write(f, &test_data).expect("file write"); 96 | volume_mgr.flush_file(f).expect("flush"); 97 | 98 | // Now check the file length again, again 99 | let entry = volume_mgr 100 | .find_directory_entry(root_dir, "README.TXT") 101 | .expect("find entry"); 102 | assert_eq!(entry.size, 64 * 3); 103 | } 104 | 105 | // **************************************************************************** 106 | // 107 | // End Of File 108 | // 109 | // **************************************************************************** 110 | --------------------------------------------------------------------------------