diff --git a/.forgejo/workflows/workflow.yml b/.forgejo/workflows/workflow.yml new file mode 100644 index 0000000..e8c70a0 --- /dev/null +++ b/.forgejo/workflows/workflow.yml @@ -0,0 +1,44 @@ +name: CI +on: push +jobs: + check: + runs-on: docker + container: + image: "devkitpro/devkitppc" + steps: + - name: Install required packages + run: | + apt-get update + apt-get install -y gcc libc6-dev nodejs clang + - name: Checkout source + uses: actions/checkout@v5 + - name: Install nightly toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y + . $HOME/.cargo/env + rustup toolchain install nightly --profile minimal --component rust-src + - name: Cargo check + run: | + . $HOME/.cargo/env + cargo check + clippy: + runs-on: docker + container: + image: "devkitpro/devkitppc" + steps: + - name: Install required packages + run: | + apt-get update + apt-get install -y gcc libc6-dev nodejs clang + - name: Checkout source + uses: actions/checkout@v5 + - name: Install nightly toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y + . $HOME/.cargo/env + rustup toolchain install nightly --profile minimal --component rust-src + rustup component add clippy + - name: Cargo clippy + run: | + . $HOME/.cargo/env + cargo clippy diff --git a/.github/workflows/rust.yml b/.github/workflows/workflow.yml similarity index 99% rename from .github/workflows/rust.yml rename to .github/workflows/workflow.yml index 1efa0f0..6e0b647 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/workflow.yml @@ -55,3 +55,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy + diff --git a/examples/ios/Cargo.lock b/examples/ios/Cargo.lock index 9484399..ae20198 100644 --- a/examples/ios/Cargo.lock +++ b/examples/ios/Cargo.lock @@ -23,7 +23,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools", @@ -54,9 +54,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cexpr" @@ -99,6 +105,29 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-sdmmc" +version = "0.9.0" +dependencies = [ + "byteorder", + "embedded-hal", + "embedded-io", + "heapless", + "log", +] + [[package]] name = "errno" version = "0.3.9" @@ -115,6 +144,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "home" version = "0.5.9" @@ -387,7 +435,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", @@ -406,6 +454,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "syn" version = "1.0.109" @@ -432,6 +486,8 @@ dependencies = [ name = "template" version = "0.1.0" dependencies = [ + "bitflags 2.9.4", + "embedded-sdmmc", "ogc-rs", ] diff --git a/examples/ios/Cargo.toml b/examples/ios/Cargo.toml index b677eb6..e7b9d1b 100644 --- a/examples/ios/Cargo.toml +++ b/examples/ios/Cargo.toml @@ -9,4 +9,6 @@ dev = { panic = "abort" } release = { panic = "abort", lto = true, codegen-units = 1, strip = "symbols", opt-level = "s" } [dependencies] +bitflags = "2.9.4" +embedded-sdmmc = {path = "./embedded-sdmmc" } ogc-rs = { path = "../../" } diff --git a/examples/ios/embedded-sdmmc/.cargo-checksum.json b/examples/ios/embedded-sdmmc/.cargo-checksum.json new file mode 100644 index 0000000..5a6c91a --- /dev/null +++ b/examples/ios/embedded-sdmmc/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{".cargo_vcs_info.json":"fdc51c58c1ab93d419b5391479a1ddf7953637ff600d45d2c01ee0e9b8eff899",".github/workflows/rust.yml":"ef823091e9e8806c9e799bf62d9c00d4a58a33f3dbaf81541391df276a737266","CHANGELOG.md":"2b57b31c1fdf26061a1c9fa33a1778a92cf8546bd53cd9424ed335d5e0c7404a","Cargo.lock":"41951573de00887244b6b260a2233c6e578f95542fb59ed5ed842207378d5b90","Cargo.toml":"e30ed8c552a22ecc01669fc0f7f5faed7a41241af9455f769137337faf4898bf","Cargo.toml.orig":"dbe63ac0424e16f2b2b366e4cecca627baf8635f5a48d629fb1570520f962d0b","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"bbb6fa175b21c44b9f5860afb8bf455f1df8a18cb2940f60d4e0d12810c1d1bf","NOTICE":"895d2a6a539cb8006b2f73260dbfdf039645121672ab82c6fc06eb7337c618ed","README.md":"629253caf1349f3cca5580120a866075380eaf71bcf4b807ff8b5f7afe95686d","examples/append_file.rs":"bc218b225aa5de4d6777b3c5b21135e1e95a73683a4e9ad7c3b7b939f2e436d8","examples/big_dir.rs":"621973ce7e3fe8791c338b4310515fc59452a1a337264afc6dc00d0c6cea410b","examples/create_file.rs":"fffe091cc2d42b1773336479ae1ade9deb255c4cf31b35d23ab315e414799f3c","examples/delete_file.rs":"7fba86d44efe35ff9e0da51a9e3b83e0efce62695f8ef64952be68e161f23a7c","examples/linux/mod.rs":"05d87ba9556a8cbb1747d2e30f0652766c72e60b2de71d57af9e1e126bb81751","examples/list_dir.rs":"02fee78deb0acd9fcdbcfa5853c706f044b1eec30c338265333818e68ba09077","examples/read_file.rs":"801dec1e763582ba87fc3de26af2f0219872b44527f686e384a4f8c6e93103e7","examples/readme_test.rs":"ea342c196cc2acf3426f830611abeeb72dcd50f13951946b019fd64e0ed2decc","examples/shell.rs":"d560cfccdfcf5bbe9d8108c395b76e6c5603d01b63e809f6652ba2ae25210474","src/blockdevice.rs":"ca8d62683a0f3f6e7a9c3779ab97b93cd8dd82cadbf1ee373b14c9f79f906d8c","src/fat/bpb.rs":"eba33b298ec91491cf165285ad2dc7f6ed4d3d7142b57ded924412c7fdb74252","src/fat/info.rs":"b51732d3a7b9e5f7811d6576cd8d92f4c39ac2d6e458017e4adf48851720c81d","src/fat/mod.rs":"10f162b31ed9008d641d7ac84891838ca6868d4397abf7d34423c63ac3d6cc69","src/fat/ondiskdirentry.rs":"314891e467111087b5bb60bfe46145593348ffbebf4e0f399a9ae20db65ad5ff","src/fat/volume.rs":"750dc7984968a6002347181fa4f9c4a8dcdb9e321366f5650259463bc3d42ddd","src/filesystem/attributes.rs":"ee450054e6e72e78faab66cf8d0deb60b0763c76f2a16a01925e95241124997e","src/filesystem/cluster.rs":"598f504a3451bd2e6872a9d4e90a4f29e887eb84d6d3211409757405914ab597","src/filesystem/directory.rs":"a9c4efed2d33f5d0b17f7b99b2c534475e9d847b03fb1ee4b09e5ce237069b46","src/filesystem/filename.rs":"0496389ec26c3195366829b5668d601fd26e7e9af2cfa0435bef55b1d2d487b1","src/filesystem/files.rs":"df40efac53e52d255e9af061ebf5a4291a0e66c621c8a3f1d6b1700bb725ab82","src/filesystem/handles.rs":"c6aa8bf344ad6701cb1f9d741c6277f85b2c1ff84bb35250d8ea92d22cbedeb6","src/filesystem/mod.rs":"96a6b7a87aeb20dfda1cfe486f6a6d39ff578d95e349a107d6e193dcb8dc5605","src/filesystem/timestamp.rs":"a20dffee9a839736d4bcd6987da31037f69393cf398f40305f09842b6eb86076","src/lib.rs":"33c9133ff73e3f2054aa08bcc0c52953b1c1097c95b83b25d7cc1376665f5075","src/sdcard/mod.rs":"edfcbef8079138c5ea94e185616ce84e809dc0d1a29cc6dccfdfd599417e6c42","src/sdcard/proto.rs":"b6501505261dce7beb8e4661bfea057e970cb8a126a6fd471527b3d68f5d17aa","src/structure.rs":"4cdd9759b11cc3a4c59dcb46bbadf0cf86fab1b0a02b8fc04d4d17898c4b9f14","src/volume_mgr.rs":"53cb76773ce0b172c0a438ee010f2a292d1691b8640b9f346a9625e5ef1a3128","tests/directories.rs":"2d6dbbe4bcdfa1986417e7b8c0fdf89fb76eaee388927b2853fef5cb68caa2b6","tests/disk.img.gz":"2456473f6f3c10a0177c2a050a99ffbd509181e8738b238178908bcf6cc8e2af","tests/open_files.rs":"265b15519fa8d688deb6aab8eac768aaf8601d81d8a785e43ce7224c3432c3bd","tests/read_file.rs":"9106bd0bf46e65d53a865fce2e3421b19d20d2d3ca336e00397e89fa106aab0c","tests/utils/mod.rs":"f14cf0f49c0166ff3151a784b801e2c9fded88e33ef5e7afd2605b1621fe2f31","tests/volume.rs":"7d20a44e79142f64a568b39221f323c5de5d0511e59e30b9a8775583038b8dd7","tests/write_file.rs":"f57607729cfae12489a2127cd5393caf9d0270291c81c6d26c1a32fda371a00b"},"package":"ce3c7f9ea039eeafc4a49597b7bd5ae3a1c8e51b2803a381cb0f29ce90fe1ec6"} \ No newline at end of file diff --git a/examples/ios/embedded-sdmmc/.cargo_vcs_info.json b/examples/ios/embedded-sdmmc/.cargo_vcs_info.json new file mode 100644 index 0000000..d520715 --- /dev/null +++ b/examples/ios/embedded-sdmmc/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "a53635929839a8ddaaf7b0a4c689c17e680ace8d" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/examples/ios/embedded-sdmmc/.github/workflows/rust.yml b/examples/ios/embedded-sdmmc/.github/workflows/rust.yml new file mode 100644 index 0000000..45b6e5d --- /dev/null +++ b/examples/ios/embedded-sdmmc/.github/workflows/rust.yml @@ -0,0 +1,33 @@ +name: Rust + +on: [push, pull_request] + +jobs: + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check formatting + run: cargo fmt -- --check + + build-test: + runs-on: ubuntu-latest + strategy: + matrix: + # Always run MSRV too! + rust: ["stable", "1.76"] + features: ['log', 'defmt-log', '""'] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Build + run: cargo build --no-default-features --features ${{matrix.features}} --verbose + env: + DEFMT_LOG: debug + - name: Run Tests + run: cargo test --no-default-features --features ${{matrix.features}} --verbose diff --git a/examples/ios/embedded-sdmmc/CHANGELOG.md b/examples/ios/embedded-sdmmc/CHANGELOG.md new file mode 100644 index 0000000..8ff7a2f --- /dev/null +++ b/examples/ios/embedded-sdmmc/CHANGELOG.md @@ -0,0 +1,204 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning]. + +## [Unreleased] + +## [Version 0.9.0] - 2025-06-08 + +### Changed + +- __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. +- __Breaking Change__: The `VolumeManager`, `File`, `Directory` and `Volume` no longer implement `Send` or `Sync`. +- `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. +- __Breaking Change__: The `VolumeManager::device` method now takes a callback rather than giving you a reference to the underlying `BlockDevice` +- __Breaking Change__: `Error:LockError` variant added. +- __Breaking Change__: `SearchId` was renamed to `Handle` +- Fixed writing at block start mid-file (previously overwrote subsequent file data with zeros up to the end of the block) + +### Added + +- `File` now implements the `embedded-io` `Read`, `Write` and `Seek` traits. +- New `iterate_dir_lfn` method on `VolumeManager` and `Directory` - provides decoded Long File Names as `Option<&str>` + +### Removed + +- __Breaking Change__: Removed the `reason: &str` argument from `BlockDevice` + +## [Version 0.8.2] - 2025-06-07 + +### Changed + +* Fixed writing at block start mid-file (previously overwrote subsequent file data with zeros up to the end of the block) + +## [Version 0.8.1] - 2024-11-03 + +### Changed + +* Second FAT is now updated, if it is present +* When creating a directory `..` now points at the root directory correctly +* The info block containing the free cluster count is now updated when unmounting a FAT32 volume. + +## [Version 0.8.0] - 2024-07-12 + +### Changed + +- Fixed a bug when seeking backwards through files. +- Updated to `heapless-0.8` and `embedded-hal-bus-0.2`. +- No longer panics if the close fails when a `Volume` is dropped - the failure is instead ignored. + +### Added + +- `File` now has a `flush()` method. +- `File` now has a `close()` method. + +### Removed + +- __Breaking Change__: Removed `CS` type-param on `SdCard` - now we use the `SpiDevice` chip-select (closing [#126]) +- __Breaking Change__: Removed the 74 clock cycle 'init' sequence - now applications must do this + +## [Version 0.7.0] - 2024-02-04 + +### Changed + +- __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. +- __Breaking Change__: Renamed the old types to `RawVolume`, `RawDirectory` and `RawFile` +- __Breaking Change__: Renamed `Error::FileNotFound` to `Error::NotFound` +- Fixed long-standing bug that caused an integer overflow when a FAT32 directory was longer than one cluster ([#74]) +- You can now open directories multiple times without error +- Updated to [embedded-hal] 1.0 + +### Added + +- `RawVolume`, `RawDirectory` and `RawFile` types (like the old `Volume`, `Directory` and `File` types) +- New method `make_dir_in_dir` +- Empty strings and `"."` convert to `ShortFileName::this_dir()` +- New API `change_dir` which changes a directory to point to some child directory (or the parent) without opening a new directory. +- Updated 'shell' example to support `mkdir`, `tree` and relative/absolute paths + +### Removed + +- None + +[#126]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/126 +[#74]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/74 +[embedded-hal]: https://crates.io/crates/embedded-hal + +## [Version 0.6.0] - 2023-10-20 + +### Changed + +- Writing to a file no longer flushes file metadata to the Directory Entry. + Instead closing a file now flushes file metadata to the Directory Entry. + Requires mutable access to the Volume ([#94]). +- Files now have the correct length when modified, not appended ([#72]). +- Calling `SdCard::get_card_type` will now perform card initialisation ([#87] and [#90]). +- Removed warning about unused arguments. +- Types are now documented at the top level ([#86]). +- Renamed `Cluster` to `ClusterId` and stopped you adding two together + +[#72]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/72 +[#86]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/86 +[#87]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/87 +[#90]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/90 +[#94]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/94 + +### Added + +- New examples, `append_file`, `create_file`, `delete_file`, `list_dir`, `shell` +- New test cases `tests/directories.rs`, `tests/read_file.rs` + +### Removed + +- __Breaking Change__: `Controller` alias for `VolumeManager` removed. +- __Breaking Change__: `VolumeManager::open_dir_entry` removed, as it was unsafe to the user to randomly pick a starting cluster. +- Old examples `create_test`, `test_mount`, `write_test`, `delete_test` + +## [Version 0.5.0] - 2023-05-20 + +### Changed + +- __Breaking Change__: Renamed `Controller` to `VolumeManager`, to better describe what it does. +- __Breaking Change__: Renamed `SdMmcSpi` to `SdCard` +- __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) +- __Breaking Change__: `SdCard::new` now requires an object that implements the embedded-hal `DelayUs` trait +- __Breaking Change__: Renamed `card_size_bytes` to `num_bytes`, to match `num_blocks` +- More robust card intialisation procedure, with added retries +- Supports building with neither `defmt` nor `log` logging + +### Added + +- Added `mark_card_as_init` method, if you know the card is initialised and want to skip the initialisation step + +### Removed + +- __Breaking Change__: Removed `BlockSpi` type - card initialisation now handled as an internal state variable + +## [Version 0.4.0] - 2023-01-18 + +### Changed + +- Optionally use [defmt] s/defmt) for logging. + Controlled by `defmt-log` feature flag. +- __Breaking Change__: Use SPI blocking traits instead to ease SPI peripheral sharing. + See: +- Added `Controller::has_open_handles` and `Controller::free` methods. +- __Breaking Change__: Changed interface to enforce correct SD state at compile time. +- __Breaking Change__: Added custom error type for `File` operations. +- Fix `env_logger` pulling in the `std` feature in `log` in library builds. +- Raise the minimum supported Rust version to 1.56.0. +- Code tidy-ups and more documentation. +- Add `MAX_DIRS` and `MAX_FILES` generics to `Controller` to allow an arbitrary numbers of concurrent open directories and files. +- Add new constructor method `Controller::new_with_limits(block_device: D, timesource: T) -> Controller` + to create a `Controller` with custom limits. + +## [Version 0.3.0] - 2019-12-16 + +### Changed + +- Updated to `v2` embedded-hal traits. +- Added open support for all modes. +- Added write support for files. +- Added `Info_Sector` tracking for FAT32. +- Change directory iteration to look in all the directory's clusters. +- Added `write_test` and `create_test`. +- De-duplicated FAT16 and FAT32 code () + +## [Version 0.2.1] - 2019-02-19 + +### Changed + +- Added `readme=README.md` to `Cargo.toml` + +## [Version 0.2.0] - 2019-01-24 + +### Changed + +- Reduce delay waiting for response. Big speed improvements. + +## [Version 0.1.1] - 2018-12-23 + +### Changed + +- Can read blocks from an SD Card using an `embedded_hal::SPI` device and a + `embedded_hal::OutputPin` for Chip Select. +- Can read partition tables and open a FAT32 or FAT16 formatted partition. +- Can open and iterate the root directory of a FAT16 formatted partition. + +[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: http://semver.org/spec/v2.0.0.html +[Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.9.0...develop +[Version 0.9.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.2...v0.9.0 +[Version 0.8.2]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.1...v0.8.2 +[Version 0.8.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.0...v0.8.1 +[Version 0.8.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.7.0...v0.8.0 +[Version 0.7.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.6.0...v0.7.0 +[Version 0.6.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.5.0...v0.6.0 +[Version 0.5.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.4.0...v0.5.0 +[Version 0.4.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.3.0...v0.4.0 +[Version 0.3.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.1...v0.3.0 +[Version 0.2.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.0...v0.2.1 +[Version 0.2.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.1.1...v0.2.0 +[Version 0.1.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.1.1 diff --git a/examples/ios/embedded-sdmmc/Cargo.lock b/examples/ios/embedded-sdmmc/Cargo.lock new file mode 100644 index 0000000..0d76a75 --- /dev/null +++ b/examples/ios/embedded-sdmmc/Cargo.lock @@ -0,0 +1,745 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-bus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3980bf28e8577db59fe2bdb3df868a419469d2cecb363644eea2b6f7797669" +dependencies = [ + "critical-section", + "embedded-hal", + "portable-atomic", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-sdmmc" +version = "0.9.0" +dependencies = [ + "byteorder", + "chrono", + "defmt 0.3.100", + "embedded-hal", + "embedded-hal-bus", + "embedded-io", + "env_logger", + "flate2", + "heapless", + "hex-literal", + "log", + "sha2", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.173" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/examples/ios/embedded-sdmmc/Cargo.toml b/examples/ios/embedded-sdmmc/Cargo.toml new file mode 100644 index 0000000..4938a65 --- /dev/null +++ b/examples/ios/embedded-sdmmc/Cargo.toml @@ -0,0 +1,141 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.76" +name = "embedded-sdmmc" +version = "0.9.0" +authors = [ + "Jonathan 'theJPster' Pallant ", + "Rust Embedded Community Developers", +] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A basic SD/MMC driver for Embedded Rust." +readme = "README.md" +keywords = [ + "sdcard", + "mmc", + "embedded", + "fat32", +] +categories = [ + "embedded", + "no-std", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" + +[features] +default = ["log"] +defmt-log = ["dep:defmt"] +log = ["dep:log"] + +[lib] +name = "embedded_sdmmc" +path = "src/lib.rs" + +[[example]] +name = "append_file" +path = "examples/append_file.rs" + +[[example]] +name = "big_dir" +path = "examples/big_dir.rs" + +[[example]] +name = "create_file" +path = "examples/create_file.rs" + +[[example]] +name = "delete_file" +path = "examples/delete_file.rs" + +[[example]] +name = "list_dir" +path = "examples/list_dir.rs" + +[[example]] +name = "read_file" +path = "examples/read_file.rs" + +[[example]] +name = "readme_test" +path = "examples/readme_test.rs" + +[[example]] +name = "shell" +path = "examples/shell.rs" + +[[test]] +name = "directories" +path = "tests/directories.rs" + +[[test]] +name = "open_files" +path = "tests/open_files.rs" + +[[test]] +name = "read_file" +path = "tests/read_file.rs" + +[[test]] +name = "volume" +path = "tests/volume.rs" + +[[test]] +name = "write_file" +path = "tests/write_file.rs" + +[dependencies.byteorder] +version = "1" +default-features = false + +[dependencies.defmt] +version = "0.3" +optional = true + +[dependencies.embedded-hal] +version = "1.0.0" + +[dependencies.embedded-io] +version = "0.6.1" + +[dependencies.heapless] +version = "^0.8" + +[dependencies.log] +version = "0.4" +optional = true +default-features = false + +[dev-dependencies.chrono] +version = "0.4" + +[dev-dependencies.embedded-hal-bus] +version = "0.2.0" + +[dev-dependencies.env_logger] +version = "0.10.0" + +[dev-dependencies.flate2] +version = "1.0" + +[dev-dependencies.hex-literal] +version = "0.4.1" + +[dev-dependencies.sha2] +version = "0.10" diff --git a/examples/ios/embedded-sdmmc/Cargo.toml.orig b/examples/ios/embedded-sdmmc/Cargo.toml.orig new file mode 100644 index 0000000..11b6ff6 --- /dev/null +++ b/examples/ios/embedded-sdmmc/Cargo.toml.orig @@ -0,0 +1,35 @@ +[package] +authors = ["Jonathan 'theJPster' Pallant ", "Rust Embedded Community Developers"] +categories = ["embedded", "no-std"] +description = "A basic SD/MMC driver for Embedded Rust." +edition = "2021" +keywords = ["sdcard", "mmc", "embedded", "fat32"] +license = "MIT OR Apache-2.0" +name = "embedded-sdmmc" +readme = "README.md" +repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" +version = "0.9.0" + +# Make sure to update the CI too! +rust-version = "1.76" + +[dependencies] +byteorder = {version = "1", default-features = false} +defmt = {version = "0.3", optional = true} +embedded-hal = "1.0.0" +embedded-io = "0.6.1" +heapless = "^0.8" +log = {version = "0.4", default-features = false, optional = true} + +[dev-dependencies] +chrono = "0.4" +embedded-hal-bus = "0.2.0" +env_logger = "0.10.0" +flate2 = "1.0" +hex-literal = "0.4.1" +sha2 = "0.10" + +[features] +default = ["log"] +defmt-log = ["dep:defmt"] +log = ["dep:log"] diff --git a/examples/ios/embedded-sdmmc/LICENSE-APACHE b/examples/ios/embedded-sdmmc/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/examples/ios/embedded-sdmmc/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/examples/ios/embedded-sdmmc/LICENSE-MIT b/examples/ios/embedded-sdmmc/LICENSE-MIT new file mode 100644 index 0000000..b59c97c --- /dev/null +++ b/examples/ios/embedded-sdmmc/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright (c) 2018-2024 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers +Copyright (c) 2011-2018 Bill Greiman + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/examples/ios/embedded-sdmmc/NOTICE b/examples/ios/embedded-sdmmc/NOTICE new file mode 100644 index 0000000..5dc1a0e --- /dev/null +++ b/examples/ios/embedded-sdmmc/NOTICE @@ -0,0 +1,6 @@ +# Copyright Notices + +This is a copyright notices file, as described by the Apache-2.0 license. + +Copyright (c) 2018-2024 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers +Copyright (c) 2011-2018 Bill Greiman diff --git a/examples/ios/embedded-sdmmc/README.md b/examples/ios/embedded-sdmmc/README.md new file mode 100644 index 0000000..9bd06b1 --- /dev/null +++ b/examples/ios/embedded-sdmmc/README.md @@ -0,0 +1,111 @@ +# 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) + +This crate is intended to allow you to read/write files on a FAT formatted SD +card on your Rust Embedded device, as easily as using the `SdFat` Arduino +library. It is written in pure-Rust, is `#![no_std]` and does not use `alloc` +or `collections` to keep the memory footprint low. In the first instance it is +designed for readability and simplicity over performance. + +## Using the crate + +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. + +```rust +use embedded_sdmmc::{SdCard, VolumeManager, Mode, VolumeIdx}; +// Build an SD Card interface out of an SPI device, a chip-select pin and the delay object +let sdcard = SdCard::new(sdmmc_spi, delay); +// Get the card size (this also triggers card initialisation because it's not been done yet) +println!("Card size is {} bytes", sdcard.num_bytes()?); +// Now let's look for volumes (also known as partitions) on our block device. +// To do this we need a Volume Manager. It will take ownership of the block device. +let volume_mgr = VolumeManager::new(sdcard, time_source); +// Try and access Volume 0 (i.e. the first partition). +// The volume object holds information about the filesystem on that volume. +let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; +println!("Volume 0: {:?}", volume0); +// Open the root directory (mutably borrows from the volume). +let root_dir = volume0.open_root_dir()?; +// Open a file called "MY_FILE.TXT" in the root directory +// This mutably borrows the directory. +let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; +// Print the contents of the file, assuming it's in ISO-8859-1 encoding +while !my_file.is_eof() { + let mut buffer = [0u8; 32]; + let num_read = my_file.read(&mut buffer)?; + for b in &buffer[0..num_read] { + print!("{}", *b as char); + } +} +``` + +For writing files: + +```rust +let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; +my_other_file.write(b"Timestamp,Signal,Value\n")?; +my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; + +// Don't forget to flush the file so that the directory entry is updated +my_other_file.flush()?; +``` + +### Open directories and files + +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`: + +```rust +// Create a volume manager with a maximum of 6 open directories, 12 open files, and 4 volumes (or partitions) +let cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); +``` + +## Supported features + +* Open files in all supported methods from an open directory +* Open an arbitrary number of directories and files +* Read data from open files +* Write data to open files +* Close files +* Delete files +* Iterate root directory +* Iterate sub-directories +* Log over defmt or the common log interface (feature flags). + +## No-std usage + +This repository houses no examples for no-std usage, however you can check out the following examples: + +* [Pi Pico](https://github.com/rp-rs/rp-hal-boards/blob/main/boards/rp-pico/examples/pico_spi_sd_card.rs) +* [STM32H7XX](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/sdmmc_fat.rs) +* [atsamd(pygamer)](https://github.com/atsamd-rs/atsamd/blob/master/boards/pygamer/examples/sd_card.rs) + +## Todo List (PRs welcome!) + +* Create new dirs +* Delete (empty) directories +* Handle MS-DOS `/path/foo/bar.txt` style paths. + +## Changelog + +The changelog has moved to [CHANGELOG.md](/CHANGELOG.md) + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) + +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +Copyright notices are stored in the [NOTICE](./NOTICE) file. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/examples/ios/embedded-sdmmc/examples/append_file.rs b/examples/ios/embedded-sdmmc/examples/append_file.rs new file mode 100644 index 0000000..54b7577 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/append_file.rs @@ -0,0 +1,45 @@ +//! Append File Example. +//! +//! ```bash +//! $ cargo run --example append_file -- ./disk.img +//! $ cargo run --example append_file -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example append_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_APPEND: &str = "README.TXT"; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("\nCreating file {}...", FILE_TO_APPEND); + let f = root_dir.open_file_in_dir(FILE_TO_APPEND, Mode::ReadWriteAppend)?; + f.write(b"\r\n\r\nThis has been added to your file.\r\n")?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/big_dir.rs b/examples/ios/embedded-sdmmc/examples/big_dir.rs new file mode 100644 index 0000000..bfc7e83 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/big_dir.rs @@ -0,0 +1,53 @@ +//! Big Directory Example. +//! +//! Attempts to create an infinite number of files in the root directory of the +//! first volume of the given block device. This is basically to see what +//! happens when the root directory runs out of space. +//! +//! ```bash +//! $ cargo run --example big_dir -- ./disk.img +//! $ cargo run --example big_dir -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example big_dir -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0)).unwrap(); + println!("Volume: {:?}", volume); + let root_dir = volume.open_root_dir().unwrap(); + + let mut file_num = 0; + loop { + file_num += 1; + let file_name = format!("{}.da", file_num); + println!("opening file {file_name} for writing"); + let file = root_dir + .open_file_in_dir(file_name.as_str(), Mode::ReadWriteCreateOrTruncate) + .unwrap(); + let buf = b"hello world, from rust"; + println!("writing to file"); + file.write(&buf[..]).unwrap(); + println!("closing file"); + drop(file); + } +} diff --git a/examples/ios/embedded-sdmmc/examples/create_file.rs b/examples/ios/embedded-sdmmc/examples/create_file.rs new file mode 100644 index 0000000..7f3cfb4 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/create_file.rs @@ -0,0 +1,48 @@ +//! Create File Example. +//! +//! ```bash +//! $ cargo run --example create_file -- ./disk.img +//! $ cargo run --example create_file -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example create_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_CREATE: &str = "CREATE.TXT"; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("\nCreating file {}...", FILE_TO_CREATE); + // This will panic if the file already exists: use ReadWriteCreateOrAppend + // or ReadWriteCreateOrTruncate instead if you want to modify an existing + // file. + let f = root_dir.open_file_in_dir(FILE_TO_CREATE, Mode::ReadWriteCreate)?; + f.write(b"Hello, this is a new file on disk\r\n")?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/delete_file.rs b/examples/ios/embedded-sdmmc/examples/delete_file.rs new file mode 100644 index 0000000..3df1978 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/delete_file.rs @@ -0,0 +1,48 @@ +//! Delete File Example. +//! +//! ```bash +//! $ cargo run --example delete_file -- ./disk.img +//! $ cargo run --example delete_file -- /dev/mmcblk0 +//! ``` +//! +//! NOTE: THIS EXAMPLE DELETES A FILE CALLED README.TXT. IF YOU DO NOT WANT THAT +//! FILE DELETED FROM YOUR DISK IMAGE, DO NOT RUN THIS EXAMPLE. +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example delete_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_DELETE: &str = "README.TXT"; + +use embedded_sdmmc::{Error, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("Deleting file {}...", FILE_TO_DELETE); + root_dir.delete_file_in_dir(FILE_TO_DELETE)?; + println!("Deleted!"); + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/linux/mod.rs b/examples/ios/embedded-sdmmc/examples/linux/mod.rs new file mode 100644 index 0000000..6eefe23 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/linux/mod.rs @@ -0,0 +1,91 @@ +//! Helpers for using embedded-sdmmc on Linux + +use chrono::Timelike; +use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx, TimeSource, Timestamp}; +use std::cell::RefCell; +use std::fs::{File, OpenOptions}; +use std::io::prelude::*; +use std::io::SeekFrom; +use std::path::Path; + +#[derive(Debug)] +pub struct LinuxBlockDevice { + file: RefCell, + print_blocks: bool, +} + +impl LinuxBlockDevice { + pub fn new

(device_name: P, print_blocks: bool) -> Result + where + P: AsRef, + { + Ok(LinuxBlockDevice { + file: RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .open(device_name)?, + ), + print_blocks, + }) + } +} + +impl BlockDevice for LinuxBlockDevice { + type Error = std::io::Error; + + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + self.file + .borrow_mut() + .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; + for block in blocks.iter_mut() { + self.file.borrow_mut().read_exact(&mut block.contents)?; + if self.print_blocks { + println!("Read block {:?}: {:?}", start_block_idx, &block); + } + } + Ok(()) + } + + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + self.file + .borrow_mut() + .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; + for block in blocks.iter() { + self.file.borrow_mut().write_all(&block.contents)?; + if self.print_blocks { + println!("Wrote: {:?}", &block); + } + } + Ok(()) + } + + fn num_blocks(&self) -> Result { + let num_blocks = self.file.borrow().metadata().unwrap().len() / 512; + Ok(BlockCount(num_blocks as u32)) + } +} + +#[derive(Debug)] +pub struct Clock; + +impl TimeSource for Clock { + fn get_timestamp(&self) -> Timestamp { + use chrono::Datelike; + let local: chrono::DateTime = chrono::Local::now(); + Timestamp { + year_since_1970: (local.year() - 1970) as u8, + zero_indexed_month: local.month0() as u8, + zero_indexed_day: local.day0() as u8, + hours: local.hour() as u8, + minutes: local.minute() as u8, + seconds: local.second() as u8, + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/list_dir.rs b/examples/ios/embedded-sdmmc/examples/list_dir.rs new file mode 100644 index 0000000..e12807a --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/list_dir.rs @@ -0,0 +1,99 @@ +//! Recursive Directory Listing Example. +//! +//! ```bash +//! $ cargo run --example list_dir -- /dev/mmcblk0 +//! Compiling embedded-sdmmc v0.5.0 (/Users/jonathan/embedded-sdmmc-rs) +//! Finished dev [unoptimized + debuginfo] target(s) in 0.20s +//! Running `/Users/jonathan/embedded-sdmmc-rs/target/debug/examples/list_dir /dev/mmcblk0` +//! Listing / +//! README.TXT 258 2018-12-09 19:22:34 +//! EMPTY.DAT 0 2018-12-09 19:21:16 +//! TEST 0 2018-12-09 19:23:16

+//! 64MB.DAT 67108864 2018-12-09 19:21:38 +//! FSEVEN~1 0 2023-09-21 11:32:04 +//! Listing /TEST +//! . 0 2018-12-09 19:21:02 +//! .. 0 2018-12-09 19:21:02 +//! TEST.DAT 3500 2018-12-09 19:22:12 +//! Listing /FSEVEN~1 +//! . 0 2023-09-21 11:32:22 +//! .. 0 2023-09-21 11:32:04 +//! FSEVEN~1 36 2023-09-21 11:32:04 +//! $ +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example list_dir -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +use embedded_sdmmc::{ShortFileName, VolumeIdx}; + +type Error = embedded_sdmmc::Error; + +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 4, 4>; +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + list_dir(root_dir, "/")?; + Ok(()) +} + +/// Recursively print a directory listing for the open directory given. +/// +/// The path is for display purposes only. +fn list_dir(directory: Directory<'_>, path: &str) -> Result<(), Error> { + println!("Listing {}", path); + let mut children = Vec::new(); + directory.iterate_dir(|entry| { + println!( + "{:12} {:9} {} {}", + entry.name, + entry.size, + entry.mtime, + if entry.attributes.is_directory() { + "" + } else { + "" + } + ); + if entry.attributes.is_directory() + && entry.name != ShortFileName::parent_dir() + && entry.name != ShortFileName::this_dir() + { + children.push(entry.name.clone()); + } + })?; + for child_name in children { + let child_dir = directory.open_dir(&child_name)?; + let child_path = if path == "/" { + format!("/{}", child_name) + } else { + format!("{}/{}", path, child_name) + }; + list_dir(child_dir, &child_path)?; + } + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/read_file.rs b/examples/ios/embedded-sdmmc/examples/read_file.rs new file mode 100644 index 0000000..0800de9 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/read_file.rs @@ -0,0 +1,84 @@ +//! Read File Example. +//! +//! ```bash +//! $ cargo run --example read_file -- ./disk.img +//! Reading file README.TXT... +//! 00000000 [54, 68, 69, 73, 20, 69, 73, 20, 61, 20, 46, 41, 54, 31, 36, 20] |This.is.a.FAT16.| +//! 00000010 [70, 61, 74, 69, 74, 69, 6f, 6e, 2e, 20, 49, 74, 20, 63, 6f, 6e] |patition..It.con| +//! 00000020 [74, 61, 69, 6e, 73, 20, 66, 6f, 75, 72, 20, 66, 69, 6c, 65, 73] |tains.four.files| +//! 00000030 [20, 61, 6e, 64, 20, 61, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |.and.a.directory| +//! 00000040 [2e, 0a, 0a, 2a, 20, 54, 68, 69, 73, 20, 66, 69, 6c, 65, 20, 28] |...*.This.file.(| +//! 00000050 [52, 45, 41, 44, 4d, 45, 2e, 54, 58, 54, 29, 0a, 2a, 20, 41, 20] |README.TXT).*.A.| +//! 00000060 [36, 34, 20, 4d, 69, 42, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c] |64.MiB.file.full| +//! 00000070 [20, 6f, 66, 20, 7a, 65, 72, 6f, 73, 20, 28, 36, 34, 4d, 42, 2e] |.of.zeros.(64MB.| +//! 00000080 [44, 41, 54, 29, 2e, 0a, 2a, 20, 41, 20, 33, 35, 30, 30, 20, 62] |DAT)..*.A.3500.b| +//! 00000090 [79, 74, 65, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c, 20, 6f, 66] |yte.file.full.of| +//! 000000a0 [20, 72, 61, 6e, 64, 6f, 6d, 20, 64, 61, 74, 61, 2e, 0a, 2a, 20] |.random.data..*.| +//! 000000b0 [41, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79, 20, 63, 61, 6c, 6c] |A.directory.call| +//! 000000c0 [65, 64, 20, 54, 45, 53, 54, 0a, 2a, 20, 41, 20, 7a, 65, 72, 6f] |ed.TEST.*.A.zero| +//! 000000d0 [20, 62, 79, 74, 65, 20, 66, 69, 6c, 65, 20, 69, 6e, 20, 74, 68] |.byte.file.in.th| +//! 000000e0 [65, 20, 54, 45, 53, 54, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |e.TEST.directory| +//! 000000f0 [20, 63, 61, 6c, 6c, 65, 64, 20, 45, 4d, 50, 54, 59, 2e, 44, 41] |.called.EMPTY.DA| +//! 00000100 [54, 0a, 0d] |T...............| +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example read_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_READ: &str = "README.TXT"; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("\nReading file {}...", FILE_TO_READ); + let f = root_dir.open_file_in_dir(FILE_TO_READ, Mode::ReadOnly)?; + // Proves we can open two files at once now (or try to - this file doesn't exist) + let f2 = root_dir.open_file_in_dir("MISSING.DAT", Mode::ReadOnly); + assert!(f2.is_err()); + while !f.is_eof() { + let mut buffer = [0u8; 16]; + let offset = f.offset(); + let mut len = f.read(&mut buffer)?; + print!("{:08x} {:02x?}", offset, &buffer[0..len]); + while len < buffer.len() { + print!(" "); + len += 1; + } + print!(" |"); + for b in buffer.iter() { + let ch = char::from(*b); + if ch.is_ascii_graphic() { + print!("{}", ch); + } else { + print!("."); + } + } + println!("|"); + } + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/readme_test.rs b/examples/ios/embedded-sdmmc/examples/readme_test.rs new file mode 100644 index 0000000..0d63d80 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/readme_test.rs @@ -0,0 +1,157 @@ +//! This is the code from the README.md file. +//! +//! We add enough stuff to make it compile, but it won't run because our fake +//! SPI doesn't do any replies. + +#![allow(dead_code)] + +use core::cell::RefCell; + +use embedded_sdmmc::{Error, SdCardError, TimeSource, Timestamp}; + +pub struct DummyCsPin; + +impl embedded_hal::digital::ErrorType for DummyCsPin { + type Error = core::convert::Infallible; +} + +impl embedded_hal::digital::OutputPin for DummyCsPin { + #[inline(always)] + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + #[inline(always)] + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +struct FakeSpiBus(); + +impl embedded_hal::spi::ErrorType for FakeSpiBus { + type Error = core::convert::Infallible; +} + +impl embedded_hal::spi::SpiBus for FakeSpiBus { + fn read(&mut self, _: &mut [u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn write(&mut self, _: &[u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn transfer(&mut self, _: &mut [u8], _: &[u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn transfer_in_place(&mut self, _: &mut [u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +struct FakeCs(); + +impl embedded_hal::digital::ErrorType for FakeCs { + type Error = core::convert::Infallible; +} + +impl embedded_hal::digital::OutputPin for FakeCs { + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Clone, Copy)] +struct FakeDelayer(); + +impl embedded_hal::delay::DelayNs for FakeDelayer { + fn delay_ns(&mut self, ns: u32) { + std::thread::sleep(std::time::Duration::from_nanos(u64::from(ns))); + } +} + +struct FakeTimesource(); + +impl TimeSource for FakeTimesource { + fn get_timestamp(&self) -> Timestamp { + Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } +} + +#[derive(Debug, Clone)] +enum MyError { + Filesystem(Error), + Disk(SdCardError), +} + +impl From> for MyError { + fn from(value: Error) -> MyError { + MyError::Filesystem(value) + } +} + +impl From for MyError { + fn from(value: SdCardError) -> MyError { + MyError::Disk(value) + } +} + +fn main() -> Result<(), MyError> { + // BEGIN Fake stuff that will be replaced with real peripherals + let spi_bus = RefCell::new(FakeSpiBus()); + let delay = FakeDelayer(); + let sdmmc_spi = embedded_hal_bus::spi::RefCellDevice::new(&spi_bus, DummyCsPin, delay).unwrap(); + let time_source = FakeTimesource(); + // END Fake stuff that will be replaced with real peripherals + + use embedded_sdmmc::{Mode, SdCard, VolumeIdx, VolumeManager}; + // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object + let sdcard = SdCard::new(sdmmc_spi, delay); + // Get the card size (this also triggers card initialisation because it's not been done yet) + println!("Card size is {} bytes", sdcard.num_bytes()?); + // Now let's look for volumes (also known as partitions) on our block device. + // To do this we need a Volume Manager. It will take ownership of the block device. + let volume_mgr = VolumeManager::new(sdcard, time_source); + // Try and access Volume 0 (i.e. the first partition). + // The volume object holds information about the filesystem on that volume. + let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; + println!("Volume 0: {:?}", volume0); + // Open the root directory (mutably borrows from the volume). + let root_dir = volume0.open_root_dir()?; + // Open a file called "MY_FILE.TXT" in the root directory + // This mutably borrows the directory. + let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; + // Print the contents of the file, assuming it's in ISO-8859-1 encoding + while !my_file.is_eof() { + let mut buffer = [0u8; 32]; + let num_read = my_file.read(&mut buffer)?; + for b in &buffer[0..num_read] { + print!("{}", *b as char); + } + } + + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/shell.rs b/examples/ios/embedded-sdmmc/examples/shell.rs new file mode 100644 index 0000000..4268276 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/shell.rs @@ -0,0 +1,609 @@ +//! A simple shell demo for embedded-sdmmc +//! +//! Presents a basic command prompt which implements some basic MS-DOS style +//! shell commands. +//! +//! ```bash +//! $ cargo run --example shell -- ./disk.img +//! $ cargo run --example shell -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example shell -- ./disk.img +//! ``` +//! +//! Note that `embedded_sdmmc` itself does not care about 'paths' - only +//! accessing files and directories on on disk, relative to some previously +//! opened directory. A 'path' is an operating-system level construct, and can +//! vary greatly (see MS-DOS paths vs POSIX paths). This example, however, +//! implements an MS-DOS style Path API over the top of embedded-sdmmc. Feel +//! free to copy it if it suits your particular application. +//! +//! The four primary partitions are scanned on the given disk image on start-up. +//! Any valid FAT16 or FAT32 volumes are mounted, and given volume labels from +//! `A:` to `D:`, like MS-DOS. Also like MS-DOS, file and directory names use +//! the `8.3` format, like `FILENAME.TXT`. Long filenames are not supported. +//! +//! Unlike MS-DOS, this application uses the POSIX `/` as the directory +//! separator. +//! +//! Every volume has its own *current working directory*. The shell has one +//! *current volume* selected but it remembers the *current working directory* +//! for the unselected volumes. +//! +//! A path comprises: +//! +//! * An optional volume specifier, like `A:` +//! * If the volume specifier is not given, the current volume is used. +//! * An optional `/` to indicate this is an absolute path, not a relative path +//! * If this is a relative path, traversal starts at the Current Working +//! Directory for the volume +//! * An optional sequence of directory names, each followed by a `/` +//! * An optional final filename +//! * If this is missing, then `.` is the default (which selects the +//! containing directory) +//! +//! An *expanded path* has all optional components, and works independently of +//! whichever volume is currently selected, or the current working directory +//! within that volume. The empty path (`""`) is invalid, but commands may +//! assume that in the absence of a path argument they are to use the current +//! working directory on the current volume. +//! +//! As an example, imagine that volume `A:` is the current volume, and we have +//! these current working directories: +//! +//! * `A:` has a CWD of `/CATS` +//! * `B:` has a CWD of `/DOGS` +//! +//! The following path expansions would occur: +//! +//! | Given Path | Volume | Absolute | Directory Names | Final Filename | Expanded Path | +//! | --------------------------- | ------- | -------- | ------------------ | -------------- | ------------------------------ | +//! | `NAMES.CSV` | Current | No | `[]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `./NAMES.CSV` | Current | No | `[.]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `BACKUP.000/` | Current | No | `[BACKUP.000]` | None | `A:/CATS/BACKUP.000/.` | +//! | `BACKUP.000/NAMES.CSV` | Current | No | `[BACKUP.000]` | `NAMES.CSV` | `A:/CATS/BACKUP.000/NAMES.CSV` | +//! | `/BACKUP.000/NAMES.CSV` | Current | Yes | `[BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `../BACKUP.000/NAMES.CSV` | Current | No | `[.., BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `A:NAMES.CSV` | `A:` | No | `[]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `A:./NAMES.CSV` | `A:` | No | `[.]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `A:BACKUP.000/` | `A:` | No | `[BACKUP.000]` | None | `A:/CATS/BACKUP.000/.` | +//! | `A:BACKUP.000/NAMES.CSV` | `A:` | No | `[BACKUP.000]` | `NAMES.CSV` | `A:/CATS/BACKUP.000/NAMES.CSV` | +//! | `A:/BACKUP.000/NAMES.CSV` | `A:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `A:../BACKUP.000/NAMES.CSV` | `A:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `B:NAMES.CSV` | `B:` | No | `[]` | `NAMES.CSV` | `B:/DOGS/NAMES.CSV` | +//! | `B:./NAMES.CSV` | `B:` | No | `[.]` | `NAMES.CSV` | `B:/DOGS/NAMES.CSV` | +//! | `B:BACKUP.000/` | `B:` | No | `[BACKUP.000]` | None | `B:/DOGS/BACKUP.000/.` | +//! | `B:BACKUP.000/NAMES.CSV` | `B:` | No | `[BACKUP.000]` | `NAMES.CSV` | `B:/DOGS/BACKUP.000/NAMES.CSV` | +//! | `B:/BACKUP.000/NAMES.CSV` | `B:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | +//! | `B:../BACKUP.000/NAMES.CSV` | `B:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | + +use std::{cell::RefCell, io::prelude::*}; + +use embedded_sdmmc::{ + Error as EsError, LfnBuffer, Mode, RawDirectory, RawVolume, ShortFileName, VolumeIdx, +}; + +type VolumeManager = embedded_sdmmc::VolumeManager; +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 8, 4>; + +use crate::linux::{Clock, LinuxBlockDevice}; + +type Error = EsError; + +mod linux; + +/// Represents a path on a volume within `embedded_sdmmc`. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +struct Path(str); + +impl std::ops::Deref for Path { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Path { + /// Create a new Path from a string slice. + /// + /// The `Path` borrows the string slice. No validation is performed on the + /// path. + fn new + ?Sized>(s: &S) -> &Path { + unsafe { &*(s.as_ref() as *const str as *const Path) } + } + + /// Does this path specify a volume? + fn volume(&self) -> Option { + let mut char_iter = self.chars(); + match (char_iter.next(), char_iter.next()) { + (Some(volume), Some(':')) => Some(volume), + _ => None, + } + } + + /// Is this an absolute path? + fn is_absolute(&self) -> bool { + let tail = self.without_volume(); + tail.starts_with('/') + } + + /// Iterate through the directory components. + /// + /// This will exclude the final path component (i.e. it will not include the + /// 'basename'). + fn iterate_dirs(&self) -> impl Iterator { + let path = self.without_volume(); + let path = path.strip_prefix('/').unwrap_or(path); + if let Some((directories, _basename)) = path.rsplit_once('/') { + directories.split('/') + } else { + "".split('/') + } + } + + /// Iterate through all the components. + /// + /// This will include the final path component (i.e. it will include the + /// 'basename'). + fn iterate_components(&self) -> impl Iterator { + let path = self.without_volume(); + let path = path.strip_prefix('/').unwrap_or(path); + path.split('/') + } + + /// Get the final component of this path (the 'basename'). + fn basename(&self) -> Option<&str> { + if let Some((_, basename)) = self.rsplit_once('/') { + if basename.is_empty() { + None + } else { + Some(basename) + } + } else { + let path = self.without_volume(); + Some(path) + } + } + + /// Return this [`Path`], but without a leading volume. + fn without_volume(&self) -> &Path { + if let Some((volume, tail)) = self.split_once(':') { + // only support single char drive letters + if volume.chars().count() == 1 { + return Path::new(tail); + } + } + self + } +} + +impl PartialEq for Path { + fn eq(&self, other: &str) -> bool { + let s: &str = self; + s == other + } +} + +struct VolumeState { + directory: RawDirectory, + volume: RawVolume, + path: Vec, +} + +struct Context { + volume_mgr: VolumeManager, + volumes: RefCell<[Option; 4]>, + current_volume: usize, +} + +impl Context { + fn current_path(&self) -> Vec { + let Some(s) = &self.volumes.borrow()[self.current_volume] else { + return vec![]; + }; + s.path.clone() + } + + /// Print some help text + fn help(&self) -> Result<(), Error> { + println!("Commands:"); + println!("\thelp -> this help text"); + println!("\t: -> change volume/partition"); + println!("\tstat -> print volume manager status"); + println!("\tdir [] -> do a directory listing"); + println!("\ttree [] -> do a recursive directory listing"); + println!("\tcd .. -> go up a level"); + println!("\tcd -> change into directory "); + println!("\tcat -> print a text file"); + println!("\thexdump -> print a binary file"); + println!("\tmkdir -> create an empty directory"); + println!("\tquit -> exits the program"); + println!(); + println!("Paths can be:"); + println!(); + println!("\t* Bare names, like `FILE.DAT`"); + println!("\t* Relative, like `../SOMEDIR/FILE.DAT` or `./FILE.DAT`"); + println!("\t* Absolute, like `B:/SOMEDIR/FILE.DAT`"); + Ok(()) + } + + /// Print volume manager status + fn stat(&self) -> Result<(), Error> { + println!("Status:\n{:#?}", self.volume_mgr); + Ok(()) + } + + /// Print a directory listing + fn dir(&self, path: &Path) -> Result<(), Error> { + println!("Directory listing of {:?}", path); + let dir = self.resolve_existing_directory(path)?; + let mut storage = [0u8; 128]; + let mut lfn_buffer = LfnBuffer::new(&mut storage); + dir.iterate_dir_lfn(&mut lfn_buffer, |entry, lfn| { + if !entry.attributes.is_volume() { + print!( + "{:12} {:9} {} {} {:08X?} {:5?}", + entry.name, + entry.size, + entry.ctime, + entry.mtime, + entry.cluster, + entry.attributes, + ); + if let Some(lfn) = lfn { + println!(" {:?}", lfn); + } else { + println!(); + } + } + })?; + Ok(()) + } + + /// Print a recursive directory listing for the given path + fn tree(&self, path: &Path) -> Result<(), Error> { + println!("Directory listing of {:?}", path); + let dir = self.resolve_existing_directory(path)?; + // tree_dir will close this directory, always + Self::tree_dir(dir) + } + + /// Print a recursive directory listing for the given open directory. + /// + /// Will close the given directory. + fn tree_dir(dir: Directory) -> Result<(), Error> { + let mut children = Vec::new(); + dir.iterate_dir(|entry| { + println!( + "{:12} {:9} {} {} {:08X?} {:?}", + entry.name, entry.size, entry.ctime, entry.mtime, entry.cluster, entry.attributes + ); + if entry.attributes.is_directory() + && entry.name != ShortFileName::this_dir() + && entry.name != ShortFileName::parent_dir() + { + children.push(entry.name.clone()); + } + })?; + for child in children { + println!("Entering {}", child); + let child_dir = dir.open_dir(&child)?; + Self::tree_dir(child_dir)?; + println!("Returning from {}", child); + } + Ok(()) + } + + /// Change into `` + /// + /// * An arg of `..` goes up one level + /// * A relative arg like `../FOO` goes up a level and then into the `FOO` + /// sub-folder, starting from the current directory on the current volume + /// * An absolute path like `B:/FOO` changes the CWD on Volume 1 to path + /// `/FOO` + fn cd(&self, full_path: &Path) -> Result<(), Error> { + let volume_idx = self.resolve_volume(full_path)?; + let (mut d, fragment) = self.resolve_filename(full_path)?; + d.change_dir(fragment)?; + let Some(s) = &mut self.volumes.borrow_mut()[volume_idx] else { + return Err(Error::NoSuchVolume); + }; + self.volume_mgr + .close_dir(s.directory) + .expect("close open dir"); + s.directory = d.to_raw_directory(); + if full_path.is_absolute() { + s.path.clear(); + } + for fragment in full_path.iterate_components().filter(|s| !s.is_empty()) { + if fragment == ".." { + s.path.pop(); + } else if fragment == "." { + // do nothing + } else { + s.path.push(fragment.to_owned()); + } + } + Ok(()) + } + + /// print a text file + fn cat(&self, filename: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(filename)?; + let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?; + let mut data = Vec::new(); + while !f.is_eof() { + let mut buffer = vec![0u8; 65536]; + let n = f.read(&mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + if let Ok(s) = std::str::from_utf8(&data) { + println!("{}", s); + } else { + println!("I'm afraid that file isn't UTF-8 encoded"); + } + Ok(()) + } + + /// print a binary file + fn hexdump(&self, filename: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(filename)?; + let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?; + let mut data = Vec::new(); + while !f.is_eof() { + let mut buffer = vec![0u8; 65536]; + let n = f.read(&mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + for (idx, chunk) in data.chunks(16).enumerate() { + print!("{:08x} | ", idx * 16); + for b in chunk { + print!("{:02x} ", b); + } + for _padding in 0..(16 - chunk.len()) { + print!(" "); + } + print!("| "); + for b in chunk { + print!( + "{}", + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + ); + } + println!(); + } + Ok(()) + } + + /// create a directory + fn mkdir(&self, dir_name: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(dir_name)?; + dir.make_dir_in_dir(filename) + } + + fn process_line(&mut self, line: &str) -> Result<(), Error> { + if line == "help" { + self.help()?; + } else if line == "A:" || line == "a:" { + self.current_volume = 0; + } else if line == "B:" || line == "b:" { + self.current_volume = 1; + } else if line == "C:" || line == "c:" { + self.current_volume = 2; + } else if line == "D:" || line == "d:" { + self.current_volume = 3; + } else if line == "dir" { + self.dir(Path::new("."))?; + } else if let Some(path) = line.strip_prefix("dir ") { + self.dir(Path::new(path.trim()))?; + } else if line == "tree" { + self.tree(Path::new("."))?; + } else if let Some(path) = line.strip_prefix("tree ") { + self.tree(Path::new(path.trim()))?; + } else if line == "stat" { + self.stat()?; + } else if let Some(path) = line.strip_prefix("cd ") { + self.cd(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("cat ") { + self.cat(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("hexdump ") { + self.hexdump(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("mkdir ") { + self.mkdir(Path::new(path.trim()))?; + } else { + println!("Unknown command {line:?} - try 'help' for help"); + } + Ok(()) + } + + /// Resolves an existing directory. + /// + /// Converts a string path into a directory handle. + /// + /// * Bare names (no leading `.`, `/` or `N:/`) are mapped to the current + /// directory in the current volume. + /// * Relative names, like `../SOMEDIR` or `./SOMEDIR`, traverse + /// starting at the current volume and directory. + /// * Absolute, like `B:/SOMEDIR/OTHERDIR` start at the given volume. + fn resolve_existing_directory<'a>(&'a self, full_path: &Path) -> Result, Error> { + let (mut dir, fragment) = self.resolve_filename(full_path)?; + dir.change_dir(fragment)?; + Ok(dir) + } + + /// Either get the volume from the path, or pick the current volume. + fn resolve_volume(&self, path: &Path) -> Result { + match path.volume() { + None => Ok(self.current_volume), + Some('A' | 'a') => Ok(0), + Some('B' | 'b') => Ok(1), + Some('C' | 'c') => Ok(2), + Some('D' | 'd') => Ok(3), + Some(_) => Err(Error::NoSuchVolume), + } + } + + /// Resolves a filename. + /// + /// Converts a string path into a directory handle and a name within that + /// directory (that may or may not exist). + /// + /// * Bare names (no leading `.`, `/` or `N:/`) are mapped to the current + /// directory in the current volume. + /// * Relative names, like `../SOMEDIR/SOMEFILE` or `./SOMEDIR/SOMEFILE`, traverse + /// starting at the current volume and directory. + /// * Absolute, like `B:/SOMEDIR/SOMEFILE` start at the given volume. + fn resolve_filename<'a, 'path>( + &'a self, + full_path: &'path Path, + ) -> Result<(Directory<'a>, &'path str), Error> { + let volume_idx = self.resolve_volume(full_path)?; + let Some(s) = &self.volumes.borrow()[volume_idx] else { + return Err(Error::NoSuchVolume); + }; + let mut work_dir = if full_path.is_absolute() { + // relative to root + self.volume_mgr + .open_root_dir(s.volume)? + .to_directory(&self.volume_mgr) + } else { + // relative to CWD + self.volume_mgr + .open_dir(s.directory, ".")? + .to_directory(&self.volume_mgr) + }; + + for fragment in full_path.iterate_dirs() { + work_dir.change_dir(fragment)?; + } + Ok((work_dir, full_path.basename().unwrap_or("."))) + } + + /// Convert a volume index to a letter + fn volume_to_letter(volume: usize) -> char { + match volume { + 0 => 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + _ => panic!("Invalid volume ID"), + } + } +} + +impl Drop for Context { + fn drop(&mut self) { + for v in self.volumes.borrow_mut().iter_mut() { + if let Some(v) = v { + println!("Closing directory {:?}", v.directory); + self.volume_mgr + .close_dir(v.directory) + .expect("Closing directory"); + println!("Closing volume {:?}", v.volume); + self.volume_mgr + .close_volume(v.volume) + .expect("Closing volume"); + } + *v = None; + } + } +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + println!("Opening '{filename}'..."); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let stdin = std::io::stdin(); + + let mut ctx = Context { + volume_mgr: VolumeManager::new_with_limits(lbd, Clock, 100), + volumes: RefCell::new([None, None, None, None]), + current_volume: 0, + }; + + let mut current_volume = None; + for volume_no in 0..4 { + match ctx.volume_mgr.open_raw_volume(VolumeIdx(volume_no)) { + Ok(volume) => { + println!( + "Volume # {}: found, label: {:?}", + Context::volume_to_letter(volume_no), + ctx.volume_mgr.get_root_volume_label(volume)? + ); + match ctx.volume_mgr.open_root_dir(volume) { + Ok(root_dir) => { + ctx.volumes.borrow_mut()[volume_no] = Some(VolumeState { + directory: root_dir, + volume, + path: vec![], + }); + if current_volume.is_none() { + current_volume = Some(volume_no); + } + } + Err(e) => { + println!("Failed to open root directory: {e:?}"); + ctx.volume_mgr.close_volume(volume).expect("close volume"); + } + } + } + Err(e) => { + println!("Failed to open volume {volume_no}: {e:?}"); + } + } + } + + match current_volume { + Some(n) => { + // Default to the first valid partition + ctx.current_volume = n; + } + None => { + println!("No volumes found in file. Sorry."); + return Ok(()); + } + }; + + loop { + print!("{}:/", Context::volume_to_letter(ctx.current_volume)); + print!("{}", ctx.current_path().join("/")); + print!("> "); + std::io::stdout().flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + if line == "quit" { + break; + } else if let Err(e) = ctx.process_line(line) { + println!("Error: {:?}", e); + } + } + + println!("Bye!"); + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/blockdevice.rs b/examples/ios/embedded-sdmmc/src/blockdevice.rs new file mode 100644 index 0000000..674ae55 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/blockdevice.rs @@ -0,0 +1,315 @@ +//! Traits and types for working with Block Devices. +//! +//! Generic code for handling block devices, such as types for identifying +//! a particular block on a block device by its index. + +/// A standard 512 byte block (also known as a sector). +/// +/// IBM PC formatted 5.25" and 3.5" floppy disks, IDE/SATA Hard Drives up to +/// about 2 TiB, and almost all SD/MMC cards have 512 byte blocks. +/// +/// This library does not support devices with a block size other than 512 +/// bytes. +#[derive(Clone)] +pub struct Block { + /// The 512 bytes in this block (or sector). + pub contents: [u8; Block::LEN], +} + +impl Block { + /// All our blocks are a fixed length of 512 bytes. We do not support + /// 'Advanced Format' Hard Drives with 4 KiB blocks, nor weird old + /// pre-3.5-inch floppy disk formats. + pub const LEN: usize = 512; + + /// Sometimes we want `LEN` as a `u32` and the casts don't look nice. + pub const LEN_U32: u32 = 512; + + /// Create a new block full of zeros. + pub fn new() -> Block { + Block { + contents: [0u8; Self::LEN], + } + } +} + +impl core::ops::Deref for Block { + type Target = [u8; 512]; + fn deref(&self) -> &[u8; 512] { + &self.contents + } +} + +impl core::ops::DerefMut for Block { + fn deref_mut(&mut self) -> &mut [u8; 512] { + &mut self.contents + } +} + +impl core::fmt::Debug for Block { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { + writeln!(fmt, "Block:")?; + for line in self.contents.chunks(32) { + for b in line { + write!(fmt, "{:02x}", b)?; + } + write!(fmt, " ")?; + for &b in line { + if (0x20..=0x7F).contains(&b) { + write!(fmt, "{}", b as char)?; + } else { + write!(fmt, ".")?; + } + } + writeln!(fmt)?; + } + Ok(()) + } +} + +impl Default for Block { + fn default() -> Self { + Self::new() + } +} + +/// A block device - a device which can read and write blocks (or +/// sectors). Only supports devices which are <= 2 TiB in size. +pub trait BlockDevice { + /// The errors that the `BlockDevice` can return. Must be debug formattable. + type Error: core::fmt::Debug; + /// Read one or more blocks, starting at the given block index. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; + /// Write one or more blocks, starting at the given block index. + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result; +} + +/// A caching layer for block devices +/// +/// Caches a single block. +#[derive(Debug)] +pub struct BlockCache { + block_device: D, + block: [Block; 1], + block_idx: Option, +} + +impl BlockCache +where + D: BlockDevice, +{ + /// Create a new block cache + pub fn new(block_device: D) -> BlockCache { + BlockCache { + block_device, + block: [Block::new()], + block_idx: None, + } + } + + /// Read a block, and return a reference to it. + pub fn read(&mut self, block_idx: BlockIdx) -> Result<&Block, D::Error> { + if self.block_idx != Some(block_idx) { + self.block_idx = None; + self.block_device.read(&mut self.block, block_idx)?; + self.block_idx = Some(block_idx); + } + Ok(&self.block[0]) + } + + /// Read a block, and return a reference to it. + pub fn read_mut(&mut self, block_idx: BlockIdx) -> Result<&mut Block, D::Error> { + if self.block_idx != Some(block_idx) { + self.block_idx = None; + self.block_device.read(&mut self.block, block_idx)?; + self.block_idx = Some(block_idx); + } + Ok(&mut self.block[0]) + } + + /// Write back a block you read with [`Self::read_mut`] and then modified. + pub fn write_back(&mut self) -> Result<(), D::Error> { + self.block_device.write( + &self.block, + self.block_idx.expect("write_back with no read"), + ) + } + + /// Write back a block you read with [`Self::read_mut`] and then modified, but to two locations. + /// + /// This is useful for updating two File Allocation Tables. + pub fn write_back_with_duplicate(&mut self, duplicate: BlockIdx) -> Result<(), D::Error> { + self.block_device.write( + &self.block, + self.block_idx.expect("write_back with no read"), + )?; + self.block_device.write(&self.block, duplicate)?; + Ok(()) + } + + /// Access a blank sector + pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block { + self.block_idx = Some(block_idx); + self.block[0].fill(0); + &mut self.block[0] + } + + /// Access the block device + pub fn block_device(&mut self) -> &mut D { + // invalidate the cache + self.block_idx = None; + // give them the block device + &mut self.block_device + } + + /// Get the block device back + pub fn free(self) -> D { + self.block_device + } +} + +/// The linear numeric address of a block (or sector). +/// +/// The first block on a disk gets `BlockIdx(0)` (which usually contains the +/// Master Boot Record). +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockIdx(pub u32); + +impl BlockIdx { + /// Convert a block index into a 64-bit byte offset from the start of the + /// volume. Useful if your underlying block device actually works in + /// bytes, like `open("/dev/mmcblk0")` does on Linux. + pub fn into_bytes(self) -> u64 { + (u64::from(self.0)) * (Block::LEN as u64) + } + + /// Create an iterator from the current `BlockIdx` through the given + /// number of blocks. + pub fn range(self, num: BlockCount) -> BlockIter { + BlockIter::new(self, self + BlockCount(num.0)) + } +} + +impl core::ops::Add for BlockIdx { + type Output = BlockIdx; + fn add(self, rhs: BlockCount) -> BlockIdx { + BlockIdx(self.0 + rhs.0) + } +} + +impl core::ops::AddAssign for BlockIdx { + fn add_assign(&mut self, rhs: BlockCount) { + self.0 += rhs.0 + } +} + +impl core::ops::Sub for BlockIdx { + type Output = BlockIdx; + fn sub(self, rhs: BlockCount) -> BlockIdx { + BlockIdx(self.0 - rhs.0) + } +} + +impl core::ops::SubAssign for BlockIdx { + fn sub_assign(&mut self, rhs: BlockCount) { + self.0 -= rhs.0 + } +} + +/// The a number of blocks (or sectors). +/// +/// Add this to a `BlockIdx` to get an actual address on disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockCount(pub u32); + +impl core::ops::Add for BlockCount { + type Output = BlockCount; + fn add(self, rhs: BlockCount) -> BlockCount { + BlockCount(self.0 + rhs.0) + } +} + +impl core::ops::AddAssign for BlockCount { + fn add_assign(&mut self, rhs: BlockCount) { + self.0 += rhs.0 + } +} + +impl core::ops::Sub for BlockCount { + type Output = BlockCount; + fn sub(self, rhs: BlockCount) -> BlockCount { + BlockCount(self.0 - rhs.0) + } +} + +impl core::ops::SubAssign for BlockCount { + fn sub_assign(&mut self, rhs: BlockCount) { + self.0 -= rhs.0 + } +} + +impl BlockCount { + /// How many blocks are required to hold this many bytes. + /// + /// ``` + /// # use embedded_sdmmc::BlockCount; + /// assert_eq!(BlockCount::from_bytes(511), BlockCount(1)); + /// assert_eq!(BlockCount::from_bytes(512), BlockCount(1)); + /// assert_eq!(BlockCount::from_bytes(513), BlockCount(2)); + /// assert_eq!(BlockCount::from_bytes(1024), BlockCount(2)); + /// assert_eq!(BlockCount::from_bytes(1025), BlockCount(3)); + /// ``` + pub const fn from_bytes(byte_count: u32) -> BlockCount { + let mut count = byte_count / Block::LEN_U32; + if (count * Block::LEN_U32) != byte_count { + count += 1; + } + BlockCount(count) + } + + /// Take a number of blocks and increment by the integer number of blocks + /// required to get to the block that holds the byte at the given offset. + pub fn offset_bytes(self, offset: u32) -> Self { + BlockCount(self.0 + (offset / Block::LEN_U32)) + } +} + +/// An iterator returned from `Block::range`. +pub struct BlockIter { + inclusive_end: BlockIdx, + current: BlockIdx, +} + +impl BlockIter { + /// Create a new `BlockIter`, from the given start block, through (and + /// including) the given end block. + pub const fn new(start: BlockIdx, inclusive_end: BlockIdx) -> BlockIter { + BlockIter { + inclusive_end, + current: start, + } + } +} + +impl core::iter::Iterator for BlockIter { + type Item = BlockIdx; + fn next(&mut self) -> Option { + if self.current.0 >= self.inclusive_end.0 { + None + } else { + let this = self.current; + self.current += BlockCount(1); + Some(this) + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/bpb.rs b/examples/ios/embedded-sdmmc/src/fat/bpb.rs new file mode 100644 index 0000000..c7e83b6 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/bpb.rs @@ -0,0 +1,140 @@ +//! Boot Parameter Block + +use crate::{ + blockdevice::BlockCount, + fat::{FatType, OnDiskDirEntry}, +}; +use byteorder::{ByteOrder, LittleEndian}; + +/// A Boot Parameter Block. +/// +/// This is the first sector of a FAT formatted partition, and it describes +/// various properties of the FAT filesystem. +pub struct Bpb<'a> { + data: &'a [u8; 512], + pub(crate) fat_type: FatType, + cluster_count: u32, +} + +impl<'a> Bpb<'a> { + pub(crate) const FOOTER_VALUE: u16 = 0xAA55; + + /// Attempt to parse a Boot Parameter Block from a 512 byte sector. + pub fn create_from_bytes(data: &[u8; 512]) -> Result { + let mut bpb = Bpb { + data, + fat_type: FatType::Fat16, + cluster_count: 0, + }; + if bpb.footer() != Self::FOOTER_VALUE { + return Err("Bad BPB footer"); + } + + let root_dir_blocks = + BlockCount::from_bytes(u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32).0; + let non_data_blocks = u32::from(bpb.reserved_block_count()) + + (u32::from(bpb.num_fats()) * bpb.fat_size()) + + root_dir_blocks; + let data_blocks = bpb.total_blocks() - non_data_blocks; + bpb.cluster_count = data_blocks / u32::from(bpb.blocks_per_cluster()); + if bpb.cluster_count < 4085 { + return Err("FAT12 is unsupported"); + } else if bpb.cluster_count < 65525 { + bpb.fat_type = FatType::Fat16; + } else { + bpb.fat_type = FatType::Fat32; + } + + match bpb.fat_type { + FatType::Fat16 => Ok(bpb), + FatType::Fat32 if bpb.fs_ver() == 0 => { + // Only support FAT32 version 0.0 + Ok(bpb) + } + _ => Err("Invalid FAT format"), + } + } + + // FAT16/FAT32 + define_field!(bytes_per_block, u16, 11); + define_field!(blocks_per_cluster, u8, 13); + define_field!(reserved_block_count, u16, 14); + define_field!(num_fats, u8, 16); + define_field!(root_entries_count, u16, 17); + define_field!(total_blocks16, u16, 19); + define_field!(media, u8, 21); + define_field!(fat_size16, u16, 22); + define_field!(blocks_per_track, u16, 24); + define_field!(num_heads, u16, 26); + define_field!(hidden_blocks, u32, 28); + define_field!(total_blocks32, u32, 32); + define_field!(footer, u16, 510); + + // FAT32 only + define_field!(fat_size32, u32, 36); + define_field!(fs_ver, u16, 42); + define_field!(first_root_dir_cluster, u32, 44); + define_field!(fs_info, u16, 48); + define_field!(backup_boot_block, u16, 50); + + /// Get the OEM name string for this volume + pub fn oem_name(&self) -> &[u8] { + &self.data[3..11] + } + + // FAT16/FAT32 functions + + /// Get the Volume Label string for this volume + pub fn volume_label(&self) -> [u8; 11] { + let mut result = [0u8; 11]; + match self.fat_type { + FatType::Fat16 => result.copy_from_slice(&self.data[43..=53]), + FatType::Fat32 => result.copy_from_slice(&self.data[71..=81]), + } + result + } + + // FAT32 only functions + + /// On a FAT32 volume, return the free block count from the Info Block. On + /// a FAT16 volume, returns None. + pub fn fs_info_block(&self) -> Option { + match self.fat_type { + FatType::Fat16 => None, + FatType::Fat32 => Some(BlockCount(u32::from(self.fs_info()))), + } + } + + // Magic functions that get the right FAT16/FAT32 result + + /// Get the size of the File Allocation Table in blocks. + pub fn fat_size(&self) -> u32 { + let result = u32::from(self.fat_size16()); + if result != 0 { + result + } else { + self.fat_size32() + } + } + + /// Get the total number of blocks in this filesystem. + pub fn total_blocks(&self) -> u32 { + let result = u32::from(self.total_blocks16()); + if result != 0 { + result + } else { + self.total_blocks32() + } + } + + /// Get the total number of clusters in this filesystem. + pub fn total_clusters(&self) -> u32 { + self.cluster_count + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/info.rs b/examples/ios/embedded-sdmmc/src/fat/info.rs new file mode 100644 index 0000000..f9f8e2c --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/info.rs @@ -0,0 +1,94 @@ +use crate::{BlockCount, BlockIdx, ClusterId}; +use byteorder::{ByteOrder, LittleEndian}; + +/// Indentifies the supported types of FAT format +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum FatSpecificInfo { + /// Fat16 Format + Fat16(Fat16Info), + /// Fat32 Format + Fat32(Fat32Info), +} + +/// FAT32 specific data +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Fat32Info { + /// The root directory does not have a reserved area in FAT32. This is the + /// cluster it starts in (nominally 2). + pub(crate) first_root_dir_cluster: ClusterId, + /// Block idx of the info sector + pub(crate) info_location: BlockIdx, +} + +/// FAT16 specific data +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Fat16Info { + /// The block the root directory starts in. Relative to start of partition + /// (so add `self.lba_offset` before passing to volume manager) + pub(crate) first_root_dir_block: BlockCount, + /// Number of entries in root directory (it's reserved and not in the FAT) + pub(crate) root_entries_count: u16, +} + +/// File System Information structure is only present on FAT32 partitions. It +/// may contain a valid number of free clusters and the number of the next +/// free cluster. The information contained in the structure must be +/// considered as advisory only. File system driver implementations are not +/// required to ensure that information within the structure is kept +/// consistent. +pub struct InfoSector<'a> { + data: &'a [u8; 512], +} + +impl<'a> InfoSector<'a> { + const LEAD_SIG: u32 = 0x4161_5252; + const STRUC_SIG: u32 = 0x6141_7272; + const TRAIL_SIG: u32 = 0xAA55_0000; + + /// Try and create a new Info Sector from a block. + pub fn create_from_bytes(data: &[u8; 512]) -> Result { + let info = InfoSector { data }; + if info.lead_sig() != Self::LEAD_SIG { + return Err("Bad lead signature on InfoSector"); + } + if info.struc_sig() != Self::STRUC_SIG { + return Err("Bad struc signature on InfoSector"); + } + if info.trail_sig() != Self::TRAIL_SIG { + return Err("Bad trail signature on InfoSector"); + } + Ok(info) + } + + define_field!(lead_sig, u32, 0); + define_field!(struc_sig, u32, 484); + define_field!(free_count, u32, 488); + define_field!(next_free, u32, 492); + define_field!(trail_sig, u32, 508); + + /// Return how many free clusters are left in this volume, if known. + pub fn free_clusters_count(&self) -> Option { + match self.free_count() { + 0xFFFF_FFFF => None, + n => Some(n), + } + } + + /// Return the number of the next free cluster, if known. + pub fn next_free_cluster(&self) -> Option { + match self.next_free() { + // 0 and 1 are reserved clusters + 0xFFFF_FFFF | 0 | 1 => None, + n => Some(ClusterId(n)), + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/mod.rs b/examples/ios/embedded-sdmmc/src/fat/mod.rs new file mode 100644 index 0000000..c27a8cd --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/mod.rs @@ -0,0 +1,368 @@ +//! FAT16/FAT32 file system implementation +//! +//! Implements the File Allocation Table file system. Supports FAT16 and FAT32 volumes. + +/// Number of entries reserved at the start of a File Allocation Table +pub const RESERVED_ENTRIES: u32 = 2; + +/// Indentifies the supported types of FAT format +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FatType { + /// FAT16 Format + Fat16, + /// FAT32 Format + Fat32, +} + +mod bpb; +mod info; +mod ondiskdirentry; +mod volume; + +pub use bpb::Bpb; +pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; +pub use ondiskdirentry::OnDiskDirEntry; +pub use volume::{parse_volume, FatVolume, VolumeName}; + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod test { + + use super::*; + use crate::{Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; + + fn parse(input: &str) -> Vec { + let mut output = Vec::new(); + for line in input.lines() { + let line = line.trim(); + if !line.is_empty() { + // 32 bytes per line + for index in 0..32 { + let start = index * 2; + let end = start + 1; + let piece = &line[start..=end]; + let value = u8::from_str_radix(piece, 16).unwrap(); + output.push(value); + } + } + } + output + } + + /// This is the first block of this directory listing. + /// total 19880 + /// -rw-r--r-- 1 jonathan jonathan 10841 2016-03-01 19:56:36.000000000 +0000 bcm2708-rpi-b.dtb + /// -rw-r--r-- 1 jonathan jonathan 11120 2016-03-01 19:56:34.000000000 +0000 bcm2708-rpi-b-plus.dtb + /// -rw-r--r-- 1 jonathan jonathan 10871 2016-03-01 19:56:36.000000000 +0000 bcm2708-rpi-cm.dtb + /// -rw-r--r-- 1 jonathan jonathan 12108 2016-03-01 19:56:36.000000000 +0000 bcm2709-rpi-2-b.dtb + /// -rw-r--r-- 1 jonathan jonathan 12575 2016-03-01 19:56:36.000000000 +0000 bcm2710-rpi-3-b.dtb + /// -rw-r--r-- 1 jonathan jonathan 17920 2016-03-01 19:56:38.000000000 +0000 bootcode.bin + /// -rw-r--r-- 1 jonathan jonathan 136 2015-11-21 20:28:30.000000000 +0000 cmdline.txt + /// -rw-r--r-- 1 jonathan jonathan 1635 2015-11-21 20:28:30.000000000 +0000 config.txt + /// -rw-r--r-- 1 jonathan jonathan 18693 2016-03-01 19:56:30.000000000 +0000 COPYING.linux + /// -rw-r--r-- 1 jonathan jonathan 2505 2016-03-01 19:56:38.000000000 +0000 fixup_cd.dat + /// -rw-r--r-- 1 jonathan jonathan 6481 2016-03-01 19:56:38.000000000 +0000 fixup.dat + /// -rw-r--r-- 1 jonathan jonathan 9722 2016-03-01 19:56:38.000000000 +0000 fixup_db.dat + /// -rw-r--r-- 1 jonathan jonathan 9724 2016-03-01 19:56:38.000000000 +0000 fixup_x.dat + /// -rw-r--r-- 1 jonathan jonathan 110 2015-11-21 21:32:06.000000000 +0000 issue.txt + /// -rw-r--r-- 1 jonathan jonathan 4046732 2016-03-01 19:56:40.000000000 +0000 kernel7.img + /// -rw-r--r-- 1 jonathan jonathan 3963140 2016-03-01 19:56:38.000000000 +0000 kernel.img + /// -rw-r--r-- 1 jonathan jonathan 1494 2016-03-01 19:56:34.000000000 +0000 LICENCE.broadcom + /// -rw-r--r-- 1 jonathan jonathan 18974 2015-11-21 21:32:06.000000000 +0000 LICENSE.oracle + /// drwxr-xr-x 2 jonathan jonathan 8192 2016-03-01 19:56:54.000000000 +0000 overlays + /// -rw-r--r-- 1 jonathan jonathan 612472 2016-03-01 19:56:40.000000000 +0000 start_cd.elf + /// -rw-r--r-- 1 jonathan jonathan 4888200 2016-03-01 19:56:42.000000000 +0000 start_db.elf + /// -rw-r--r-- 1 jonathan jonathan 2739672 2016-03-01 19:56:40.000000000 +0000 start.elf + /// -rw-r--r-- 1 jonathan jonathan 3840328 2016-03-01 19:56:44.000000000 +0000 start_x.elf + /// drwxr-xr-x 2 jonathan jonathan 8192 2015-12-05 21:55:06.000000000 +0000 'System Volume Information' + #[test] + fn test_dir_entries() { + #[derive(Debug)] + enum Expected { + Lfn(bool, u8, u8, [u16; 13]), + Short(DirEntry), + } + let raw_data = r#" + 626f6f7420202020202020080000699c754775470000699c7547000000000000 boot ...i.uGuG..i.uG...... + 416f007600650072006c000f00476100790073000000ffffffff0000ffffffff Ao.v.e.r.l...Ga.y.s............. + 4f5645524c4159532020201000001b9f6148614800001b9f6148030000000000 OVERLAYS .....aHaH....aH...... + 422d0070006c00750073000f00792e006400740062000000ffff0000ffffffff B-.p.l.u.s...y..d.t.b........... + 01620063006d00320037000f0079300038002d0072007000690000002d006200 .b.c.m.2.7...y0.8.-.r.p.i...-.b. + 42434d3237307e31445442200064119f614861480000119f61480900702b0000 BCM270~1DTB .d..aHaH....aH..p+.. + 4143004f005000590049000f00124e0047002e006c0069006e00000075007800 AC.O.P.Y.I....N.G...l.i.n...u.x. + 434f5059494e7e314c494e2000000f9f6148614800000f9f6148050005490000 COPYIN~1LIN ....aHaH....aH...I.. + 4263006f006d000000ffff0f0067ffffffffffffffffffffffff0000ffffffff Bc.o.m.......g.................. + 014c004900430045004e000f0067430045002e00620072006f00000061006400 .L.I.C.E.N...gC.E...b.r.o...a.d. + 4c4943454e437e3142524f200000119f614861480000119f61480800d6050000 LICENC~1BRO ....aHaH....aH...... + 422d0062002e00640074000f001962000000ffffffffffffffff0000ffffffff B-.b...d.t....b................. + 01620063006d00320037000f0019300039002d0072007000690000002d003200 .b.c.m.2.7....0.9.-.r.p.i...-.2. + 42434d3237307e34445442200064129f614861480000129f61480f004c2f0000 BCM270~4DTB .d..aHaH....aH..L/.. + 422e0064007400620000000f0059ffffffffffffffffffffffff0000ffffffff B..d.t.b.....Y.................. + 01620063006d00320037000f0059300038002d0072007000690000002d006200 .b.c.m.2.7...Y0.8.-.r.p.i...-.b. + "#; + + let results = [ + Expected::Short(DirEntry { + name: unsafe { + VolumeName::create_from_str("boot") + .unwrap() + .to_short_filename() + }, + mtime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), + ctime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), + attributes: Attributes::create_from_fat(Attributes::VOLUME), + cluster: ClusterId(0), + size: 0, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 1, + 0x47, + [ + 'o' as u16, 'v' as u16, 'e' as u16, 'r' as u16, 'l' as u16, 'a' as u16, + 'y' as u16, 's' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("OVERLAYS").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), + attributes: Attributes::create_from_fat(Attributes::DIRECTORY), + cluster: ClusterId(3), + size: 0, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x79, + [ + '-' as u16, 'p' as u16, 'l' as u16, 'u' as u16, 's' as u16, '.' as u16, + 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x79, + [ + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + 'b' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("BCM270~1.DTB").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(9), + size: 11120, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 1, + 0x12, + [ + 'C' as u16, 'O' as u16, 'P' as u16, 'Y' as u16, 'I' as u16, 'N' as u16, + 'G' as u16, '.' as u16, 'l' as u16, 'i' as u16, 'n' as u16, 'u' as u16, + 'x' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("COPYIN~1.LIN").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(5), + size: 18693, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x67, + [ + 'c' as u16, + 'o' as u16, + 'm' as u16, + '\u{0}' as u16, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x67, + [ + 'L' as u16, 'I' as u16, 'C' as u16, 'E' as u16, 'N' as u16, 'C' as u16, + 'E' as u16, '.' as u16, 'b' as u16, 'r' as u16, 'o' as u16, 'a' as u16, + 'd' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("LICENC~1.BRO").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(8), + size: 1494, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x19, + [ + '-' as u16, 'b' as u16, '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x19, + [ + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '9' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + '2' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("BCM270~4.DTB").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(15), + size: 12108, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x59, + [ + '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x59, + [ + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + 'b' as u16, + ], + ), + ]; + + let data = parse(raw_data); + for (part, expected) in data.chunks(OnDiskDirEntry::LEN).zip(results.iter()) { + let on_disk_entry = OnDiskDirEntry::new(part); + match expected { + Expected::Lfn(start, index, csum, contents) if on_disk_entry.is_lfn() => { + let (calc_start, calc_index, calc_csum, calc_contents) = + on_disk_entry.lfn_contents().unwrap(); + assert_eq!(*start, calc_start); + assert_eq!(*index, calc_index); + assert_eq!(*contents, calc_contents); + assert_eq!(*csum, calc_csum); + } + Expected::Short(expected_entry) if !on_disk_entry.is_lfn() => { + let parsed_entry = on_disk_entry.get_entry(FatType::Fat32, BlockIdx(0), 0); + assert_eq!(*expected_entry, parsed_entry); + } + _ => { + panic!( + "Bad dir entry, expected:\n{:#?}\nhad\n{:#?}", + expected, on_disk_entry + ); + } + } + } + } + + #[test] + fn test_bpb() { + // Taken from a Raspberry Pi bootable SD-Card + const BPB_EXAMPLE: [u8; 512] = hex!( + "EB 3C 90 6D 6B 66 73 2E 66 61 74 00 02 10 01 00 + 02 00 02 00 00 F8 20 00 3F 00 FF 00 00 00 00 00 + 00 E0 01 00 80 01 29 BB B0 71 77 62 6F 6F 74 20 + 20 20 20 20 20 20 46 41 54 31 36 20 20 20 0E 1F + BE 5B 7C AC 22 C0 74 0B 56 B4 0E BB 07 00 CD 10 + 5E EB F0 32 E4 CD 16 CD 19 EB FE 54 68 69 73 20 + 69 73 20 6E 6F 74 20 61 20 62 6F 6F 74 61 62 6C + 65 20 64 69 73 6B 2E 20 20 50 6C 65 61 73 65 20 + 69 6E 73 65 72 74 20 61 20 62 6F 6F 74 61 62 6C + 65 20 66 6C 6F 70 70 79 20 61 6E 64 0D 0A 70 72 + 65 73 73 20 61 6E 79 20 6B 65 79 20 74 6F 20 74 + 72 79 20 61 67 61 69 6E 20 2E 2E 2E 20 0D 0A 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" + ); + let bpb = Bpb::create_from_bytes(&BPB_EXAMPLE).unwrap(); + assert_eq!(bpb.footer(), Bpb::FOOTER_VALUE); + assert_eq!(bpb.oem_name(), b"mkfs.fat"); + assert_eq!(bpb.bytes_per_block(), 512); + assert_eq!(bpb.blocks_per_cluster(), 16); + assert_eq!(bpb.reserved_block_count(), 1); + assert_eq!(bpb.num_fats(), 2); + assert_eq!(bpb.root_entries_count(), 512); + assert_eq!(bpb.total_blocks16(), 0); + assert_eq!(bpb.fat_size16(), 32); + assert_eq!(bpb.total_blocks32(), 122_880); + assert_eq!(bpb.footer(), 0xAA55); + assert_eq!(bpb.volume_label(), *b"boot "); + assert_eq!(bpb.fat_size(), 32); + assert_eq!(bpb.total_blocks(), 122_880); + assert_eq!(bpb.fat_type, FatType::Fat16); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs b/examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs new file mode 100644 index 0000000..83707e4 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs @@ -0,0 +1,166 @@ +//! Directory Entry as stored on-disk + +use crate::{fat::FatType, Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; +use byteorder::{ByteOrder, LittleEndian}; + +/// A 32-byte directory entry as stored on-disk in a directory file. +/// +/// This is the same for FAT16 and FAT32 (except FAT16 doesn't use +/// first_cluster_hi). +pub struct OnDiskDirEntry<'a> { + data: &'a [u8], +} + +impl<'a> core::fmt::Debug for OnDiskDirEntry<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "OnDiskDirEntry<")?; + write!(f, "raw_attr = {}", self.raw_attr())?; + write!(f, ", create_time = {}", self.create_time())?; + write!(f, ", create_date = {}", self.create_date())?; + write!(f, ", last_access_data = {}", self.last_access_data())?; + write!(f, ", first_cluster_hi = {}", self.first_cluster_hi())?; + write!(f, ", write_time = {}", self.write_time())?; + write!(f, ", write_date = {}", self.write_date())?; + write!(f, ", first_cluster_lo = {}", self.first_cluster_lo())?; + write!(f, ", file_size = {}", self.file_size())?; + write!(f, ", is_end = {}", self.is_end())?; + write!(f, ", is_valid = {}", self.is_valid())?; + write!(f, ", is_lfn = {}", self.is_lfn())?; + write!( + f, + ", first_cluster_fat32 = {:?}", + self.first_cluster_fat32() + )?; + write!( + f, + ", first_cluster_fat16 = {:?}", + self.first_cluster_fat16() + )?; + write!(f, ">")?; + Ok(()) + } +} + +impl<'a> OnDiskDirEntry<'a> { + pub(crate) const LEN: usize = 32; + pub(crate) const LEN_U32: u32 = 32; + + define_field!(raw_attr, u8, 11); + define_field!(create_time, u16, 14); + define_field!(create_date, u16, 16); + define_field!(last_access_data, u16, 18); + define_field!(first_cluster_hi, u16, 20); + define_field!(write_time, u16, 22); + define_field!(write_date, u16, 24); + define_field!(first_cluster_lo, u16, 26); + define_field!(file_size, u32, 28); + + /// Create a new on-disk directory entry from a block of 32 bytes read + /// from a directory file. + pub fn new(data: &[u8]) -> OnDiskDirEntry { + OnDiskDirEntry { data } + } + + /// Is this the last entry in the directory? + pub fn is_end(&self) -> bool { + self.data[0] == 0x00 + } + + /// Is this a valid entry? + pub fn is_valid(&self) -> bool { + !self.is_end() && (self.data[0] != 0xE5) + } + + /// Is this a Long Filename entry? + pub fn is_lfn(&self) -> bool { + let attributes = Attributes::create_from_fat(self.raw_attr()); + attributes.is_lfn() + } + + /// If this is an LFN, get the contents so we can re-assemble the filename. + pub fn lfn_contents(&self) -> Option<(bool, u8, u8, [u16; 13])> { + if self.is_lfn() { + let is_start = (self.data[0] & 0x40) != 0; + let sequence = self.data[0] & 0x1F; + let csum = self.data[13]; + let buffer = [ + LittleEndian::read_u16(&self.data[1..=2]), + LittleEndian::read_u16(&self.data[3..=4]), + LittleEndian::read_u16(&self.data[5..=6]), + LittleEndian::read_u16(&self.data[7..=8]), + LittleEndian::read_u16(&self.data[9..=10]), + LittleEndian::read_u16(&self.data[14..=15]), + LittleEndian::read_u16(&self.data[16..=17]), + LittleEndian::read_u16(&self.data[18..=19]), + LittleEndian::read_u16(&self.data[20..=21]), + LittleEndian::read_u16(&self.data[22..=23]), + LittleEndian::read_u16(&self.data[24..=25]), + LittleEndian::read_u16(&self.data[28..=29]), + LittleEndian::read_u16(&self.data[30..=31]), + ]; + Some((is_start, sequence, csum, buffer)) + } else { + None + } + } + + /// Does this on-disk entry match the given filename? + pub fn matches(&self, sfn: &ShortFileName) -> bool { + self.data[0..11] == sfn.contents + } + + /// Which cluster, if any, does this file start at? Assumes this is from a FAT32 volume. + pub fn first_cluster_fat32(&self) -> ClusterId { + let cluster_no = + (u32::from(self.first_cluster_hi()) << 16) | u32::from(self.first_cluster_lo()); + ClusterId(cluster_no) + } + + /// Which cluster, if any, does this file start at? Assumes this is from a FAT16 volume. + fn first_cluster_fat16(&self) -> ClusterId { + let cluster_no = u32::from(self.first_cluster_lo()); + ClusterId(cluster_no) + } + + /// Convert the on-disk format into a DirEntry + pub fn get_entry( + &self, + fat_type: FatType, + entry_block: BlockIdx, + entry_offset: u32, + ) -> DirEntry { + let attributes = Attributes::create_from_fat(self.raw_attr()); + let mut result = DirEntry { + name: ShortFileName { + contents: [0u8; 11], + }, + mtime: Timestamp::from_fat(self.write_date(), self.write_time()), + ctime: Timestamp::from_fat(self.create_date(), self.create_time()), + attributes, + cluster: { + let cluster = if fat_type == FatType::Fat32 { + self.first_cluster_fat32() + } else { + self.first_cluster_fat16() + }; + if cluster == ClusterId::EMPTY && attributes.is_directory() { + // FAT16/FAT32 uses a cluster ID of `0` in the ".." entry to mean 'root directory' + ClusterId::ROOT_DIR + } else { + cluster + } + }, + size: self.file_size(), + entry_block, + entry_offset, + }; + result.name.contents.copy_from_slice(&self.data[0..11]); + result + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/volume.rs b/examples/ios/embedded-sdmmc/src/fat/volume.rs new file mode 100644 index 0000000..4344e75 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/volume.rs @@ -0,0 +1,1447 @@ +//! FAT-specific volume support. + +use crate::{ + debug, + fat::{ + Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, + RESERVED_ENTRIES, + }, + filesystem::FilenameError, + trace, warn, Attributes, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, ClusterId, + DirEntry, DirectoryInfo, Error, LfnBuffer, ShortFileName, TimeSource, VolumeType, +}; +use byteorder::{ByteOrder, LittleEndian}; +use core::convert::TryFrom; + +/// An MS-DOS 11 character volume label. +/// +/// ISO-8859-1 encoding is assumed. Trailing spaces are trimmed. Reserved +/// characters are not allowed. There is no file extension, unlike with a +/// filename. +/// +/// Volume labels can be found in the BIOS Parameter Block, and in a root +/// directory entry with the 'Volume Label' bit set. Both places should have the +/// same contents, but they can get out of sync. +/// +/// MS-DOS FDISK would show you the one in the BPB, but DIR would show you the +/// one in the root directory. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(PartialEq, Eq, Clone)] +pub struct VolumeName { + pub(crate) contents: [u8; Self::TOTAL_LEN], +} + +impl VolumeName { + const TOTAL_LEN: usize = 11; + + /// Get name + pub fn name(&self) -> &[u8] { + let mut bytes = &self.contents[..]; + while let [rest @ .., last] = bytes { + if last.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes + } + + /// Create a new MS-DOS volume label. + pub fn create_from_str(name: &str) -> Result { + let mut sfn = VolumeName { + contents: [b' '; Self::TOTAL_LEN], + }; + + let mut idx = 0; + for ch in name.chars() { + match ch { + // Microsoft say these are the invalid characters + '\u{0000}'..='\u{001F}' + | '"' + | '*' + | '+' + | ',' + | '/' + | ':' + | ';' + | '<' + | '=' + | '>' + | '?' + | '[' + | '\\' + | ']' + | '.' + | '|' => { + return Err(FilenameError::InvalidCharacter); + } + x if x > '\u{00FF}' => { + // We only handle ISO-8859-1 which is Unicode Code Points + // \U+0000 to \U+00FF. This is above that. + return Err(FilenameError::InvalidCharacter); + } + _ => { + let b = ch as u8; + if idx < Self::TOTAL_LEN { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + idx += 1; + } + } + } + if idx == 0 { + return Err(FilenameError::FilenameEmpty); + } + Ok(sfn) + } + + /// Convert to a Short File Name + /// + /// # Safety + /// + /// Volume Labels can contain things that Short File Names cannot, so only + /// do this conversion if you are creating the name of a directory entry + /// with the 'Volume Label' attribute. + pub unsafe fn to_short_filename(self) -> ShortFileName { + ShortFileName { + contents: self.contents, + } + } +} + +impl core::fmt::Display for VolumeName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let mut printed = 0; + for &c in self.name().iter() { + // converting a byte to a codepoint means you are assuming + // ISO-8859-1 encoding, because that's how Unicode was designed. + write!(f, "{}", c as char)?; + printed += 1; + } + if let Some(mut width) = f.width() { + if width > printed { + width -= printed; + for _ in 0..width { + write!(f, "{}", f.fill())?; + } + } + } + Ok(()) + } +} + +impl core::fmt::Debug for VolumeName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "VolumeName(\"{}\")", self) + } +} + +/// Identifies a FAT16 or FAT32 Volume on the disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq)] +pub struct FatVolume { + /// The block number of the start of the partition. All other BlockIdx values are relative to this. + pub(crate) lba_start: BlockIdx, + /// The number of blocks in this volume + pub(crate) num_blocks: BlockCount, + /// The name of this volume + pub(crate) name: VolumeName, + /// Number of 512 byte blocks (or Blocks) in a cluster + pub(crate) blocks_per_cluster: u8, + /// The block the data starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) first_data_block: BlockCount, + /// The block the FAT starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) fat_start: BlockCount, + /// The block the second FAT starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) second_fat_start: Option, + /// Expected number of free clusters + pub(crate) free_clusters_count: Option, + /// Number of the next expected free cluster + pub(crate) next_free_cluster: Option, + /// Total number of clusters + pub(crate) cluster_count: u32, + /// Type of FAT + pub(crate) fat_specific_info: FatSpecificInfo, +} + +impl FatVolume { + /// Write a new entry in the FAT + pub fn update_info_sector( + &mut self, + block_cache: &mut BlockCache, + ) -> Result<(), Error> + where + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_) => { + // FAT16 volumes don't have an info sector + } + FatSpecificInfo::Fat32(fat32_info) => { + if self.free_clusters_count.is_none() && self.next_free_cluster.is_none() { + return Ok(()); + } + trace!("Reading info sector"); + let block = block_cache + .read_mut(fat32_info.info_location) + .map_err(Error::DeviceError)?; + if let Some(count) = self.free_clusters_count { + block[488..492].copy_from_slice(&count.to_le_bytes()); + } + if let Some(next_free_cluster) = self.next_free_cluster { + block[492..496].copy_from_slice(&next_free_cluster.0.to_le_bytes()); + } + trace!("Writing info sector"); + block_cache.write_back()?; + } + } + Ok(()) + } + + /// Get the type of FAT this volume is + pub(crate) fn get_fat_type(&self) -> FatType { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_) => FatType::Fat16, + FatSpecificInfo::Fat32(_) => FatType::Fat32, + } + } + + /// Write a new entry in the FAT + fn update_fat( + &mut self, + block_cache: &mut BlockCache, + cluster: ClusterId, + new_value: ClusterId, + ) -> Result<(), Error> + where + D: BlockDevice, + { + let mut second_fat_block_num = None; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_fat16_info) => { + let fat_offset = cluster.0 * 2; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + if let Some(second_fat_start) = self.second_fat_start { + second_fat_block_num = + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); + } + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Reading FAT for update"); + let block = block_cache + .read_mut(this_fat_block_num) + .map_err(Error::DeviceError)?; + // See + let entry = match new_value { + ClusterId::INVALID => 0xFFF6, + ClusterId::BAD => 0xFFF7, + ClusterId::EMPTY => 0x0000, + ClusterId::END_OF_FILE => 0xFFFF, + _ => new_value.0 as u16, + }; + LittleEndian::write_u16( + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 1], + entry, + ); + } + FatSpecificInfo::Fat32(_fat32_info) => { + // FAT32 => 4 bytes per entry + let fat_offset = cluster.0 * 4; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + if let Some(second_fat_start) = self.second_fat_start { + second_fat_block_num = + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); + } + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Reading FAT for update"); + let block = block_cache + .read_mut(this_fat_block_num) + .map_err(Error::DeviceError)?; + let entry = match new_value { + ClusterId::INVALID => 0x0FFF_FFF6, + ClusterId::BAD => 0x0FFF_FFF7, + ClusterId::EMPTY => 0x0000_0000, + _ => new_value.0, + }; + let existing = + LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]); + let new = (existing & 0xF000_0000) | (entry & 0x0FFF_FFFF); + LittleEndian::write_u32( + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 3], + new, + ); + } + } + trace!("Updating FAT"); + if let Some(duplicate) = second_fat_block_num { + block_cache.write_back_with_duplicate(duplicate)?; + } else { + block_cache.write_back()?; + } + Ok(()) + } + + /// Look in the FAT to see which cluster comes next. + pub(crate) fn next_cluster( + &self, + block_cache: &mut BlockCache, + cluster: ClusterId, + ) -> Result> + where + D: BlockDevice, + { + if cluster.0 > (u32::MAX / 4) { + panic!("next_cluster called on invalid cluster {:x?}", cluster); + } + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_fat16_info) => { + let fat_offset = cluster.0 * 2; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Walking FAT"); + let block = block_cache.read(this_fat_block_num)?; + let fat_entry = + LittleEndian::read_u16(&block[this_fat_ent_offset..=this_fat_ent_offset + 1]); + match fat_entry { + 0xFFF7 => { + // Bad cluster + Err(Error::BadCluster) + } + 0xFFF8..=0xFFFF => { + // There is no next cluster + Err(Error::EndOfFile) + } + f => { + // Seems legit + Ok(ClusterId(u32::from(f))) + } + } + } + FatSpecificInfo::Fat32(_fat32_info) => { + let fat_offset = cluster.0 * 4; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Walking FAT"); + let block = block_cache.read(this_fat_block_num)?; + let fat_entry = + LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) + & 0x0FFF_FFFF; + match fat_entry { + 0x0000_0000 => { + // Jumped to free space + Err(Error::UnterminatedFatChain) + } + 0x0FFF_FFF7 => { + // Bad cluster + Err(Error::BadCluster) + } + 0x0000_0001 | 0x0FFF_FFF8..=0x0FFF_FFFF => { + // There is no next cluster + Err(Error::EndOfFile) + } + f => { + // Seems legit + Ok(ClusterId(f)) + } + } + } + } + } + + /// Number of bytes in a cluster. + pub(crate) fn bytes_per_cluster(&self) -> u32 { + u32::from(self.blocks_per_cluster) * Block::LEN_U32 + } + + /// Converts a cluster number (or `Cluster`) to a block number (or + /// `BlockIdx`). Gives an absolute `BlockIdx` you can pass to the + /// volume manager. + pub(crate) fn cluster_to_block(&self, cluster: ClusterId) -> BlockIdx { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + let block_num = match cluster { + ClusterId::ROOT_DIR => fat16_info.first_root_dir_block, + ClusterId(c) => { + // FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector; + let first_block_of_cluster = + BlockCount((c - 2) * u32::from(self.blocks_per_cluster)); + self.first_data_block + first_block_of_cluster + } + }; + self.lba_start + block_num + } + FatSpecificInfo::Fat32(fat32_info) => { + let cluster_num = match cluster { + ClusterId::ROOT_DIR => fat32_info.first_root_dir_cluster.0, + c => c.0, + }; + // FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector; + let first_block_of_cluster = + BlockCount((cluster_num - 2) * u32::from(self.blocks_per_cluster)); + self.lba_start + self.first_data_block + first_block_of_cluster + } + } + } + + /// Finds a empty entry space and writes the new entry to it, allocates a new cluster if it's + /// needed + pub(crate) fn write_new_directory_entry( + &mut self, + block_cache: &mut BlockCache, + time_source: &T, + dir_cluster: ClusterId, + name: ShortFileName, + attributes: Attributes, + ) -> Result> + where + D: BlockDevice, + T: TimeSource, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_cluster); + let mut first_dir_block_num = match dir_cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_cluster), + }; + let dir_size = match dir_cluster { + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + // Walk the directory + while let Some(cluster) = current_cluster { + for block_idx in first_dir_block_num.range(dir_size) { + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in + block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + // 0x00 or 0xE5 represents a free entry + if !dir_entry.is_valid() { + let ctime = time_source.get_timestamp(); + let entry = DirEntry::new( + name, + attributes, + ClusterId::EMPTY, + ctime, + block_idx, + (i * OnDiskDirEntry::LEN) as u32, + ); + dir_entry_bytes + .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); + trace!("Updating directory"); + block_cache.write_back()?; + return Ok(entry); + } + } + } + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + Err(Error::EndOfFile) => { + let c = self.alloc_cluster(block_cache, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + Err(Error::NotEnoughSpace) + } + FatSpecificInfo::Fat32(fat32_info) => { + // All directories on FAT32 have a cluster chain but the root + // dir starts in a specified cluster. + let mut current_cluster = match dir_cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_cluster), + }; + let mut first_dir_block_num = self.cluster_to_block(dir_cluster); + + let dir_size = BlockCount(u32::from(self.blocks_per_cluster)); + // Walk the cluster chain until we run out of clusters + while let Some(cluster) = current_cluster { + // Loop through the blocks in the cluster + for block_idx in first_dir_block_num.range(dir_size) { + // Read a block of directory entries + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)?; + // Are any entries in the block we just loaded blank? If so + // we can use them. + for (i, dir_entry_bytes) in + block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + // 0x00 or 0xE5 represents a free entry + if !dir_entry.is_valid() { + let ctime = time_source.get_timestamp(); + let entry = DirEntry::new( + name, + attributes, + ClusterId(0), + ctime, + block_idx, + (i * OnDiskDirEntry::LEN) as u32, + ); + dir_entry_bytes + .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); + trace!("Updating directory"); + block_cache.write_back()?; + return Ok(entry); + } + } + } + // Well none of the blocks in that cluster had any space in + // them, let's fetch another one. + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + Err(Error::EndOfFile) => { + let c = self.alloc_cluster(block_cache, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; + } + // We ran out of clusters in the chain, and apparently we weren't + // able to make the chain longer, so the disk must be full. + Err(Error::NotEnoughSpace) + } + } + } + + /// Calls callback `func` with every valid entry in the given directory. + /// Useful for performing directory listings. + pub(crate) fn iterate_dir( + &self, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry), + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, _| func(de)) + } + FatSpecificInfo::Fat32(fat32_info) => { + self.iterate_fat32(dir_info, fat32_info, block_cache, |de, _| func(de)) + } + } + } + + /// Calls callback `func` with every valid entry in the given directory, + /// including the Long File Name. + /// + /// Useful for performing directory listings. + pub(crate) fn iterate_dir_lfn( + &self, + block_cache: &mut BlockCache, + lfn_buffer: &mut LfnBuffer<'_>, + dir_info: &DirectoryInfo, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + D: BlockDevice, + { + #[derive(Clone, Copy)] + enum SeqState { + Waiting, + Remaining { csum: u8, next: u8 }, + Complete { csum: u8 }, + } + + impl SeqState { + fn update( + self, + lfn_buffer: &mut LfnBuffer<'_>, + start: bool, + sequence: u8, + csum: u8, + buffer: [u16; 13], + ) -> Self { + #[cfg(feature = "log")] + debug!("LFN Contents {start} {sequence} {csum:02x} {buffer:04x?}"); + #[cfg(feature = "defmt-log")] + debug!( + "LFN Contents {=bool} {=u8} {=u8:02x} {=[?; 13]:#04x}", + start, sequence, csum, buffer + ); + match (start, sequence, self) { + (true, 0x01, _) => { + lfn_buffer.clear(); + lfn_buffer.push(&buffer); + SeqState::Complete { csum } + } + (true, sequence, _) if sequence >= 0x02 && sequence < 0x14 => { + lfn_buffer.clear(); + lfn_buffer.push(&buffer); + SeqState::Remaining { + csum, + next: sequence - 1, + } + } + (false, 0x01, SeqState::Remaining { csum, next }) if next == sequence => { + lfn_buffer.push(&buffer); + SeqState::Complete { csum } + } + (false, sequence, SeqState::Remaining { csum, next }) + if sequence >= 0x01 && sequence < 0x13 && next == sequence => + { + lfn_buffer.push(&buffer); + SeqState::Remaining { + csum, + next: sequence - 1, + } + } + _ => { + // this seems wrong + lfn_buffer.clear(); + SeqState::Waiting + } + } + } + } + + let mut seq_state = SeqState::Waiting; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, odde| { + if let Some((start, this_seqno, csum, buffer)) = odde.lfn_contents() { + seq_state = seq_state.update(lfn_buffer, start, this_seqno, csum, buffer); + } else if let SeqState::Complete { csum } = seq_state { + if csum == de.name.csum() { + // Checksum is good, and all the pieces are there + func(de, Some(lfn_buffer.as_str())) + } else { + // Checksum was bad + func(de, None) + } + } else { + func(de, None) + } + }) + } + FatSpecificInfo::Fat32(fat32_info) => { + self.iterate_fat32(dir_info, fat32_info, block_cache, |de, odde| { + if let Some((start, this_seqno, csum, buffer)) = odde.lfn_contents() { + seq_state = seq_state.update(lfn_buffer, start, this_seqno, csum, buffer); + } else if let SeqState::Complete { csum } = seq_state { + if csum == de.name.csum() { + // Checksum is good, and all the pieces are there + func(de, Some(lfn_buffer.as_str())) + } else { + // Checksum was bad + func(de, None) + } + } else { + func(de, None) + } + }) + } + } + } + + fn iterate_fat16( + &self, + dir_info: &DirectoryInfo, + fat16_info: &Fat16Info, + block_cache: &mut BlockCache, + mut func: F, + ) -> Result<(), Error> + where + F: for<'odde> FnMut(&DirEntry, &OnDiskDirEntry<'odde>), + D: BlockDevice, + { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_info.cluster), + }; + let dir_size = match dir_info.cluster { + ClusterId::ROOT_DIR => { + let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + while let Some(cluster) = current_cluster { + for block_idx in first_dir_block_num.range(dir_size) { + trace!("Reading FAT"); + let block = block_cache.read(block_idx)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + return Ok(()); + } else if dir_entry.is_valid() { + // Safe, since Block::LEN always fits on a u32 + let start = (i * OnDiskDirEntry::LEN) as u32; + let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); + func(&entry, &dir_entry); + } + } + } + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + Ok(()) + } + + fn iterate_fat32( + &self, + dir_info: &DirectoryInfo, + fat32_info: &Fat32Info, + block_cache: &mut BlockCache, + mut func: F, + ) -> Result<(), Error> + where + F: for<'odde> FnMut(&DirEntry, &OnDiskDirEntry<'odde>), + D: BlockDevice, + { + // All directories on FAT32 have a cluster chain but the root + // dir starts in a specified cluster. + let mut current_cluster = match dir_info.cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_info.cluster), + }; + while let Some(cluster) = current_cluster { + let start_block_idx = self.cluster_to_block(cluster); + for block_idx in start_block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { + trace!("Reading FAT"); + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + return Ok(()); + } else if dir_entry.is_valid() { + // Safe, since Block::LEN always fits on a u32 + let start = (i * OnDiskDirEntry::LEN) as u32; + let entry = dir_entry.get_entry(FatType::Fat32, block_idx, start); + func(&entry, &dir_entry); + } + } + } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + }; + } + Ok(()) + } + + /// Get an entry from the given directory + pub(crate) fn find_directory_entry( + &self, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + match_name: &ShortFileName, + ) -> Result> + where + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_info.cluster), + }; + let dir_size = match dir_info.cluster { + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + while let Some(cluster) = current_cluster { + for block in first_dir_block_num.range(dir_size) { + match self.find_entry_in_block( + block_cache, + FatType::Fat16, + match_name, + block, + ) { + Err(Error::NotFound) => continue, + x => return x, + } + } + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + Err(Error::NotFound) + } + FatSpecificInfo::Fat32(fat32_info) => { + let mut current_cluster = match dir_info.cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_info.cluster), + }; + while let Some(cluster) = current_cluster { + let block_idx = self.cluster_to_block(cluster); + for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { + match self.find_entry_in_block( + block_cache, + FatType::Fat32, + match_name, + block, + ) { + Err(Error::NotFound) => continue, + x => return x, + } + } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } + } + Err(Error::NotFound) + } + } + } + + /// Finds an entry in a given block of directory entries. + fn find_entry_in_block( + &self, + block_cache: &mut BlockCache, + fat_type: FatType, + match_name: &ShortFileName, + block_idx: BlockIdx, + ) -> Result> + where + D: BlockDevice, + { + trace!("Reading directory"); + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + break; + } else if dir_entry.matches(match_name) { + // Found it + // Block::LEN always fits on a u32 + let start = (i * OnDiskDirEntry::LEN) as u32; + return Ok(dir_entry.get_entry(fat_type, block_idx, start)); + } + } + Err(Error::NotFound) + } + + /// Delete an entry from the given directory + pub(crate) fn delete_directory_entry( + &self, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + match_name: &ShortFileName, + ) -> Result<(), Error> + where + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_info.cluster), + }; + let dir_size = match dir_info.cluster { + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + // Walk the directory + while let Some(cluster) = current_cluster { + // Scan the cluster / root dir a block at a time + for block_idx in first_dir_block_num.range(dir_size) { + match self.delete_entry_in_block(block_cache, match_name, block_idx) { + Err(Error::NotFound) => { + // Carry on + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } + } + } + // if it's not the root dir, find the next cluster so we can keep looking + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + // Ok, give up + } + FatSpecificInfo::Fat32(fat32_info) => { + // Root directories on FAT32 start at a specified cluster, but + // they can have any length. + let mut current_cluster = match dir_info.cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_info.cluster), + }; + // Walk the directory + while let Some(cluster) = current_cluster { + // Scan the cluster a block at a time + let start_block_idx = self.cluster_to_block(cluster); + for block_idx in + start_block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) + { + match self.delete_entry_in_block(block_cache, match_name, block_idx) { + Err(Error::NotFound) => { + // Carry on + continue; + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } + } + } + // Find the next cluster + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } + } + // Ok, give up + } + } + // If we get here we never found the right entry in any of the + // blocks that made up the directory + Err(Error::NotFound) + } + + /// Deletes a directory entry from a block of directory entries. + /// + /// Entries are marked as deleted by setting the first byte of the file name + /// to a special value. + fn delete_entry_in_block( + &self, + block_cache: &mut BlockCache, + match_name: &ShortFileName, + block_idx: BlockIdx, + ) -> Result<(), Error> + where + D: BlockDevice, + { + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + break; + } else if dir_entry.matches(match_name) { + let start = i * OnDiskDirEntry::LEN; + // set first byte to the 'unused' marker + block[start] = 0xE5; + trace!("Updating directory"); + return block_cache.write_back().map_err(Error::DeviceError); + } + } + Err(Error::NotFound) + } + + /// Finds the next free cluster after the start_cluster and before end_cluster + pub(crate) fn find_next_free_cluster( + &self, + block_cache: &mut BlockCache, + start_cluster: ClusterId, + end_cluster: ClusterId, + ) -> Result> + where + D: BlockDevice, + { + let mut current_cluster = start_cluster; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_fat16_info) => { + while current_cluster.0 < end_cluster.0 { + trace!( + "current_cluster={:?}, end_cluster={:?}", + current_cluster, + end_cluster + ); + let fat_offset = current_cluster.0 * 2; + trace!("fat_offset = {:?}", fat_offset); + let this_fat_block_num = + self.lba_start + self.fat_start.offset_bytes(fat_offset); + trace!("this_fat_block_num = {:?}", this_fat_block_num); + let mut this_fat_ent_offset = usize::try_from(fat_offset % Block::LEN_U32) + .map_err(|_| Error::ConversionError)?; + trace!("Reading block {:?}", this_fat_block_num); + let block = block_cache + .read(this_fat_block_num) + .map_err(Error::DeviceError)?; + while this_fat_ent_offset <= Block::LEN - 2 { + let fat_entry = LittleEndian::read_u16( + &block[this_fat_ent_offset..=this_fat_ent_offset + 1], + ); + if fat_entry == 0 { + return Ok(current_cluster); + } + this_fat_ent_offset += 2; + current_cluster += 1; + } + } + } + FatSpecificInfo::Fat32(_fat32_info) => { + while current_cluster.0 < end_cluster.0 { + trace!( + "current_cluster={:?}, end_cluster={:?}", + current_cluster, + end_cluster + ); + let fat_offset = current_cluster.0 * 4; + trace!("fat_offset = {:?}", fat_offset); + let this_fat_block_num = + self.lba_start + self.fat_start.offset_bytes(fat_offset); + trace!("this_fat_block_num = {:?}", this_fat_block_num); + let mut this_fat_ent_offset = usize::try_from(fat_offset % Block::LEN_U32) + .map_err(|_| Error::ConversionError)?; + trace!("Reading block {:?}", this_fat_block_num); + let block = block_cache + .read(this_fat_block_num) + .map_err(Error::DeviceError)?; + while this_fat_ent_offset <= Block::LEN - 4 { + let fat_entry = LittleEndian::read_u32( + &block[this_fat_ent_offset..=this_fat_ent_offset + 3], + ) & 0x0FFF_FFFF; + if fat_entry == 0 { + return Ok(current_cluster); + } + this_fat_ent_offset += 4; + current_cluster += 1; + } + } + } + } + warn!("Out of space..."); + Err(Error::NotEnoughSpace) + } + + /// Tries to allocate a cluster + pub(crate) fn alloc_cluster( + &mut self, + block_cache: &mut BlockCache, + prev_cluster: Option, + zero: bool, + ) -> Result> + where + D: BlockDevice, + { + debug!("Allocating new cluster, prev_cluster={:?}", prev_cluster); + let end_cluster = ClusterId(self.cluster_count + RESERVED_ENTRIES); + let start_cluster = match self.next_free_cluster { + Some(cluster) if cluster.0 < end_cluster.0 => cluster, + _ => ClusterId(RESERVED_ENTRIES), + }; + trace!( + "Finding next free between {:?}..={:?}", + start_cluster, + end_cluster + ); + let new_cluster = match self.find_next_free_cluster(block_cache, start_cluster, end_cluster) + { + Ok(cluster) => cluster, + Err(_) if start_cluster.0 > RESERVED_ENTRIES => { + debug!( + "Retrying, finding next free between {:?}..={:?}", + ClusterId(RESERVED_ENTRIES), + end_cluster + ); + self.find_next_free_cluster(block_cache, ClusterId(RESERVED_ENTRIES), end_cluster)? + } + Err(e) => return Err(e), + }; + // This new cluster is the end of the file's chain + self.update_fat(block_cache, new_cluster, ClusterId::END_OF_FILE)?; + // If there's something before this new one, update the FAT to point it at us + if let Some(cluster) = prev_cluster { + trace!( + "Updating old cluster {:?} to {:?} in FAT", + cluster, + new_cluster + ); + self.update_fat(block_cache, cluster, new_cluster)?; + } + trace!( + "Finding next free between {:?}..={:?}", + new_cluster, + end_cluster + ); + self.next_free_cluster = + match self.find_next_free_cluster(block_cache, new_cluster, end_cluster) { + Ok(cluster) => Some(cluster), + Err(_) if new_cluster.0 > RESERVED_ENTRIES => { + match self.find_next_free_cluster( + block_cache, + ClusterId(RESERVED_ENTRIES), + end_cluster, + ) { + Ok(cluster) => Some(cluster), + Err(e) => return Err(e), + } + } + Err(e) => return Err(e), + }; + debug!("Next free cluster is {:?}", self.next_free_cluster); + // Record that we've allocated a cluster + if let Some(ref mut number_free_cluster) = self.free_clusters_count { + *number_free_cluster -= 1; + }; + if zero { + let start_block_idx = self.cluster_to_block(new_cluster); + let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); + for block_idx in start_block_idx.range(num_blocks) { + trace!("Zeroing cluster {:?}", block_idx); + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; + } + } + debug!("All done, returning {:?}", new_cluster); + Ok(new_cluster) + } + + /// Marks the input cluster as an EOF and all the subsequent clusters in the chain as free + pub(crate) fn truncate_cluster_chain( + &mut self, + block_cache: &mut BlockCache, + cluster: ClusterId, + ) -> Result<(), Error> + where + D: BlockDevice, + { + if cluster.0 < RESERVED_ENTRIES { + // file doesn't have any valid cluster allocated, there is nothing to do + return Ok(()); + } + let mut next = { + match self.next_cluster(block_cache, cluster) { + Ok(n) => n, + Err(Error::EndOfFile) => return Ok(()), + Err(e) => return Err(e), + } + }; + if let Some(ref mut next_free_cluster) = self.next_free_cluster { + if next_free_cluster.0 > next.0 { + *next_free_cluster = next; + } + } else { + self.next_free_cluster = Some(next); + } + self.update_fat(block_cache, cluster, ClusterId::END_OF_FILE)?; + loop { + match self.next_cluster(block_cache, next) { + Ok(n) => { + self.update_fat(block_cache, next, ClusterId::EMPTY)?; + next = n; + } + Err(Error::EndOfFile) => { + self.update_fat(block_cache, next, ClusterId::EMPTY)?; + break; + } + Err(e) => return Err(e), + } + if let Some(ref mut number_free_cluster) = self.free_clusters_count { + *number_free_cluster += 1; + }; + } + Ok(()) + } + + /// Writes a Directory Entry to the disk + pub(crate) fn write_entry_to_disk( + &self, + block_cache: &mut BlockCache, + entry: &DirEntry, + ) -> Result<(), Error> + where + D: BlockDevice, + { + let fat_type = match self.fat_specific_info { + FatSpecificInfo::Fat16(_) => FatType::Fat16, + FatSpecificInfo::Fat32(_) => FatType::Fat32, + }; + trace!("Reading directory for update"); + let block = block_cache + .read_mut(entry.entry_block) + .map_err(Error::DeviceError)?; + + let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; + block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); + + trace!("Updating directory"); + block_cache.write_back().map_err(Error::DeviceError)?; + Ok(()) + } + + /// Create a new directory. + /// + /// 1) Creates the directory entry in the parent + /// 2) Allocates a new cluster to hold the new directory + /// 3) Writes out the `.` and `..` entries in the new directory + pub(crate) fn make_dir( + &mut self, + block_cache: &mut BlockCache, + time_source: &T, + parent: ClusterId, + sfn: ShortFileName, + att: Attributes, + ) -> Result<(), Error> + where + D: BlockDevice, + T: TimeSource, + { + let mut new_dir_entry_in_parent = + self.write_new_directory_entry(block_cache, time_source, parent, sfn, att)?; + if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { + new_dir_entry_in_parent.cluster = self.alloc_cluster(block_cache, None, false)?; + // update the parent dir with the cluster of the new dir + self.write_entry_to_disk(block_cache, &new_dir_entry_in_parent)?; + } + let new_dir_start_block = self.cluster_to_block(new_dir_entry_in_parent.cluster); + debug!("Made new dir entry {:?}", new_dir_entry_in_parent); + let now = time_source.get_timestamp(); + let fat_type = self.get_fat_type(); + // A blank block + let block = block_cache.blank_mut(new_dir_start_block); + // make the "." entry + let dot_entry_in_child = DirEntry { + name: crate::ShortFileName::this_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at ourselves + cluster: new_dir_entry_in_parent.cluster, + size: 0, + entry_block: new_dir_start_block, + entry_offset: 0, + }; + debug!("New dir has {:?}", dot_entry_in_child); + let mut offset = 0; + block[offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); + offset += OnDiskDirEntry::LEN; + // make the ".." entry + let dot_dot_entry_in_child = DirEntry { + name: crate::ShortFileName::parent_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at our parent + cluster: if parent == ClusterId::ROOT_DIR { + // indicate parent is root using Cluster(0) + ClusterId::EMPTY + } else { + parent + }, + size: 0, + entry_block: new_dir_start_block, + entry_offset: OnDiskDirEntry::LEN_U32, + }; + debug!("New dir has {:?}", dot_dot_entry_in_child); + block[offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); + + block_cache.write_back()?; + + for block_idx in new_dir_start_block + .range(BlockCount(u32::from(self.blocks_per_cluster))) + .skip(1) + { + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; + } + + Ok(()) + } +} + +/// Load the boot parameter block from the start of the given partition and +/// determine if the partition contains a valid FAT16 or FAT32 file system. +pub fn parse_volume( + block_cache: &mut BlockCache, + lba_start: BlockIdx, + num_blocks: BlockCount, +) -> Result> +where + D: BlockDevice, + D::Error: core::fmt::Debug, +{ + trace!("Reading BPB"); + let block = block_cache.read(lba_start).map_err(Error::DeviceError)?; + let bpb = Bpb::create_from_bytes(&block.contents).map_err(Error::FormatError)?; + let fat_start = BlockCount(u32::from(bpb.reserved_block_count())); + let second_fat_start = if bpb.num_fats() == 2 { + Some(fat_start + BlockCount(bpb.fat_size())) + } else { + None + }; + match bpb.fat_type { + FatType::Fat16 => { + if bpb.bytes_per_block() as usize != Block::LEN { + return Err(Error::BadBlockSize(bpb.bytes_per_block())); + } + // FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz) + RootDirSectors; + let root_dir_blocks = ((u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32) + + (Block::LEN_U32 - 1)) + / Block::LEN_U32; + let first_root_dir_block = + fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); + let first_data_block = first_root_dir_block + BlockCount(root_dir_blocks); + let volume = FatVolume { + lba_start, + num_blocks, + name: VolumeName { + contents: bpb.volume_label(), + }, + blocks_per_cluster: bpb.blocks_per_cluster(), + first_data_block, + fat_start, + second_fat_start, + free_clusters_count: None, + next_free_cluster: None, + cluster_count: bpb.total_clusters(), + fat_specific_info: FatSpecificInfo::Fat16(Fat16Info { + root_entries_count: bpb.root_entries_count(), + first_root_dir_block, + }), + }; + Ok(VolumeType::Fat(volume)) + } + FatType::Fat32 => { + // FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz); + let first_data_block = + fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); + // Safe to unwrap since this is a Fat32 Type + let info_location = bpb.fs_info_block().unwrap(); + let mut volume = FatVolume { + lba_start, + num_blocks, + name: VolumeName { + contents: bpb.volume_label(), + }, + blocks_per_cluster: bpb.blocks_per_cluster(), + first_data_block, + fat_start, + second_fat_start, + free_clusters_count: None, + next_free_cluster: None, + cluster_count: bpb.total_clusters(), + fat_specific_info: FatSpecificInfo::Fat32(Fat32Info { + info_location: lba_start + info_location, + first_root_dir_cluster: ClusterId(bpb.first_root_dir_cluster()), + }), + }; + + // Now we don't need the BPB, update the volume with data from the info sector + trace!("Reading info block"); + let info_block = block_cache + .read(lba_start + info_location) + .map_err(Error::DeviceError)?; + let info_sector = + InfoSector::create_from_bytes(info_block).map_err(Error::FormatError)?; + volume.free_clusters_count = info_sector.free_clusters_count(); + volume.next_free_cluster = info_sector.next_free_cluster(); + + Ok(VolumeType::Fat(volume)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn volume_name() { + let sfn = VolumeName { + contents: *b"Hello \xA399 ", + }; + assert_eq!(sfn, VolumeName::create_from_str("Hello £99").unwrap()) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/attributes.rs b/examples/ios/embedded-sdmmc/src/filesystem/attributes.rs new file mode 100644 index 0000000..e22dcd1 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/attributes.rs @@ -0,0 +1,108 @@ +/// Indicates whether a directory entry is read-only, a directory, a volume +/// label, etc. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct Attributes(pub(crate) u8); + +impl Attributes { + /// Indicates this file cannot be written. + pub const READ_ONLY: u8 = 0x01; + /// Indicates the file is hidden. + pub const HIDDEN: u8 = 0x02; + /// Indicates this is a system file. + pub const SYSTEM: u8 = 0x04; + /// Indicates this is a volume label. + pub const VOLUME: u8 = 0x08; + /// Indicates this is a directory. + pub const DIRECTORY: u8 = 0x10; + /// Indicates this file needs archiving (i.e. has been modified since last + /// archived). + pub const ARCHIVE: u8 = 0x20; + /// This set of flags indicates the file is actually a long file name + /// fragment. + pub const LFN: u8 = Self::READ_ONLY | Self::HIDDEN | Self::SYSTEM | Self::VOLUME; + + /// Create a `Attributes` value from the `u8` stored in a FAT16/FAT32 + /// Directory Entry. + pub(crate) fn create_from_fat(value: u8) -> Attributes { + Attributes(value) + } + + pub(crate) fn set_archive(&mut self, flag: bool) { + let archive = if flag { 0x20 } else { 0x00 }; + self.0 |= archive; + } + + /// Does this file has the read-only attribute set? + pub fn is_read_only(self) -> bool { + (self.0 & Self::READ_ONLY) == Self::READ_ONLY + } + + /// Does this file has the hidden attribute set? + pub fn is_hidden(self) -> bool { + (self.0 & Self::HIDDEN) == Self::HIDDEN + } + + /// Does this file has the system attribute set? + pub fn is_system(self) -> bool { + (self.0 & Self::SYSTEM) == Self::SYSTEM + } + + /// Does this file has the volume attribute set? + pub fn is_volume(self) -> bool { + (self.0 & Self::VOLUME) == Self::VOLUME + } + + /// Does this entry point at a directory? + pub fn is_directory(self) -> bool { + (self.0 & Self::DIRECTORY) == Self::DIRECTORY + } + + /// Does this need archiving? + pub fn is_archive(self) -> bool { + (self.0 & Self::ARCHIVE) == Self::ARCHIVE + } + + /// Is this a long file name fragment? + pub fn is_lfn(self) -> bool { + (self.0 & Self::LFN) == Self::LFN + } +} + +impl core::fmt::Debug for Attributes { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + // Worst case is "DRHSVA" + let mut output = heapless::String::<7>::new(); + if self.is_lfn() { + output.push_str("LFN").unwrap(); + } else { + if self.is_directory() { + output.push_str("D").unwrap(); + } else { + output.push_str("F").unwrap(); + } + if self.is_read_only() { + output.push_str("R").unwrap(); + } + if self.is_hidden() { + output.push_str("H").unwrap(); + } + if self.is_system() { + output.push_str("S").unwrap(); + } + if self.is_volume() { + output.push_str("V").unwrap(); + } + if self.is_archive() { + output.push_str("A").unwrap(); + } + } + f.pad(&output) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/cluster.rs b/examples/ios/embedded-sdmmc/src/filesystem/cluster.rs new file mode 100644 index 0000000..14f1126 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/cluster.rs @@ -0,0 +1,68 @@ +/// Identifies a cluster on disk. +/// +/// A cluster is a consecutive group of blocks. Each cluster has a a numeric ID. +/// Some numeric IDs are reserved for special purposes. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct ClusterId(pub(crate) u32); + +impl ClusterId { + /// Magic value indicating an invalid cluster value. + pub const INVALID: ClusterId = ClusterId(0xFFFF_FFF6); + /// Magic value indicating a bad cluster. + pub const BAD: ClusterId = ClusterId(0xFFFF_FFF7); + /// Magic value indicating a empty cluster. + pub const EMPTY: ClusterId = ClusterId(0x0000_0000); + /// Magic value indicating the cluster holding the root directory (which + /// doesn't have a number in FAT16 as there's a reserved region). + pub const ROOT_DIR: ClusterId = ClusterId(0xFFFF_FFFC); + /// Magic value indicating that the cluster is allocated and is the final cluster for the file + pub const END_OF_FILE: ClusterId = ClusterId(0xFFFF_FFFF); +} + +impl core::ops::Add for ClusterId { + type Output = ClusterId; + fn add(self, rhs: u32) -> ClusterId { + ClusterId(self.0 + rhs) + } +} + +impl core::ops::AddAssign for ClusterId { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + +impl core::fmt::Debug for ClusterId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "ClusterId(")?; + match *self { + Self::INVALID => { + write!(f, "{:08}", "INVALID")?; + } + Self::BAD => { + write!(f, "{:08}", "BAD")?; + } + Self::EMPTY => { + write!(f, "{:08}", "EMPTY")?; + } + Self::ROOT_DIR => { + write!(f, "{:08}", "ROOT")?; + } + Self::END_OF_FILE => { + write!(f, "{:08}", "EOF")?; + } + ClusterId(value) => { + write!(f, "{:08x}", value)?; + } + } + write!(f, ")")?; + Ok(()) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/directory.rs b/examples/ios/embedded-sdmmc/src/filesystem/directory.rs new file mode 100644 index 0000000..527807b --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/directory.rs @@ -0,0 +1,336 @@ +use crate::blockdevice::BlockIdx; +use crate::fat::{FatType, OnDiskDirEntry}; +use crate::filesystem::{Attributes, ClusterId, Handle, LfnBuffer, ShortFileName, Timestamp}; +use crate::{Error, RawVolume, VolumeManager}; + +use super::ToShortFileName; + +/// A directory entry, which tells you about other files and directories. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DirEntry { + /// The name of the file + pub name: ShortFileName, + /// When the file was last modified + pub mtime: Timestamp, + /// When the file was first created + pub ctime: Timestamp, + /// The file attributes (Read Only, Archive, etc) + pub attributes: Attributes, + /// The starting cluster of the file. The FAT tells us the following Clusters. + pub cluster: ClusterId, + /// The size of the file in bytes. + pub size: u32, + /// The disk block of this entry + pub entry_block: BlockIdx, + /// The offset on its block (in bytes) + pub entry_offset: u32, +} + +/// A handle for an open directory on disk. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and if you drop it, the VolumeManager will think you +/// still have the directory open, and it won't let you open the directory +/// again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_dir`] to close it +/// cleanly. +/// +/// If you want your directories to close themselves on drop, create your own +/// `Directory` type that wraps this one and also holds a `VolumeManager` +/// reference. You'll then also need to put your `VolumeManager` in some kind of +/// Mutex or RefCell, and deal with the fact you can't put them both in the same +/// struct any more because one refers to the other. Basically, it's complicated +/// and there's a reason we did it this way. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RawDirectory(pub(crate) Handle); + +impl RawDirectory { + /// Convert a raw directory into a droppable [`Directory`] + pub fn to_directory< + D, + T, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + >( + self, + volume_mgr: &VolumeManager, + ) -> Directory + where + D: crate::BlockDevice, + T: crate::TimeSource, + { + Directory::new(self, volume_mgr) + } +} + +/// A handle for an open directory on disk, which closes on drop. +/// +/// In contrast to a `RawDirectory`, a `Directory` holds a mutable reference to +/// its parent `VolumeManager`, which restricts which operations you can perform. +/// +/// If you drop a value of this type, it closes the directory automatically, but +/// any error that may occur will be ignored. To handle potential errors, use +/// the [`Directory::close`] method. +pub struct Directory< + 'a, + D, + T, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, +> where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + raw_directory: RawDirectory, + volume_mgr: &'a VolumeManager, +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + /// Create a new `Directory` from a `RawDirectory` + pub fn new( + raw_directory: RawDirectory, + volume_mgr: &'a VolumeManager, + ) -> Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { + Directory { + raw_directory, + volume_mgr, + } + } + + /// Open a directory. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + pub fn open_dir( + &self, + name: N, + ) -> Result, Error> + where + N: ToShortFileName, + { + let d = self.volume_mgr.open_dir(self.raw_directory, name)?; + Ok(d.to_directory(self.volume_mgr)) + } + + /// Change to a directory, mutating this object. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + pub fn change_dir(&mut self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + let d = self.volume_mgr.open_dir(self.raw_directory, name)?; + self.volume_mgr.close_dir(self.raw_directory).unwrap(); + self.raw_directory = d; + Ok(()) + } + + /// Look in a directory for a named file. + pub fn find_directory_entry(&self, name: N) -> Result> + where + N: ToShortFileName, + { + self.volume_mgr + .find_directory_entry(self.raw_directory, name) + } + + /// Call a callback function for each directory entry in a directory. + /// + /// Long File Names will be ignored. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir(&self, func: F) -> Result<(), Error> + where + F: FnMut(&DirEntry), + { + self.volume_mgr.iterate_dir(self.raw_directory, func) + } + + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. + /// + /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the + /// Long File Name. If you pass one that isn't large enough, any Long File + /// Names that don't fit will be ignored and presented as if they only had a + /// Short File Name. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir_lfn( + &self, + lfn_buffer: &mut LfnBuffer<'_>, + func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + { + self.volume_mgr + .iterate_dir_lfn(self.raw_directory, lfn_buffer, func) + } + + /// Open a file with the given full path. A file can only be opened once. + pub fn open_file_in_dir( + &self, + name: N, + mode: crate::Mode, + ) -> Result, crate::Error> + where + N: super::ToShortFileName, + { + let f = self + .volume_mgr + .open_file_in_dir(self.raw_directory, name, mode)?; + Ok(f.to_file(self.volume_mgr)) + } + + /// Delete a closed file with the given filename, if it exists. + pub fn delete_file_in_dir(&self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + self.volume_mgr.delete_file_in_dir(self.raw_directory, name) + } + + /// Make a directory inside this directory + pub fn make_dir_in_dir(&self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + self.volume_mgr.make_dir_in_dir(self.raw_directory, name) + } + + /// Convert back to a raw directory + pub fn to_raw_directory(self) -> RawDirectory { + let d = self.raw_directory; + core::mem::forget(self); + d + } + + /// Consume the `Directory` handle and close it. The behavior of this is similar + /// to using [`core::mem::drop`] or letting the `Directory` go out of scope, + /// except this lets the user handle any errors that may occur in the process, + /// whereas when using drop, any errors will be discarded silently. + pub fn close(self) -> Result<(), Error> { + let result = self.volume_mgr.close_dir(self.raw_directory); + core::mem::forget(self); + result + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop + for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn drop(&mut self) { + _ = self.volume_mgr.close_dir(self.raw_directory) + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + core::fmt::Debug for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Directory({})", self.raw_directory.0 .0) + } +} + +#[cfg(feature = "defmt-log")] +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + defmt::Format for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "Directory({})", self.raw_directory.0 .0) + } +} + +/// Holds information about an open file on disk +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub(crate) struct DirectoryInfo { + /// The handle for this directory. + pub(crate) raw_directory: RawDirectory, + /// The handle for the volume this directory is on + pub(crate) raw_volume: RawVolume, + /// The starting point of the directory listing. + pub(crate) cluster: ClusterId, +} + +impl DirEntry { + pub(crate) fn serialize(&self, fat_type: FatType) -> [u8; OnDiskDirEntry::LEN] { + let mut data = [0u8; OnDiskDirEntry::LEN]; + data[0..11].copy_from_slice(&self.name.contents); + data[11] = self.attributes.0; + // 12: Reserved. Must be set to zero + // 13: CrtTimeTenth, not supported, set to zero + data[14..18].copy_from_slice(&self.ctime.serialize_to_fat()[..]); + // 0 + 18: LastAccDate, not supported, set to zero + let cluster_number = self.cluster.0; + let cluster_hi = if fat_type == FatType::Fat16 { + [0u8; 2] + } else { + // Safe due to the AND operation + (((cluster_number >> 16) & 0x0000_FFFF) as u16).to_le_bytes() + }; + data[20..22].copy_from_slice(&cluster_hi[..]); + data[22..26].copy_from_slice(&self.mtime.serialize_to_fat()[..]); + // Safe due to the AND operation + let cluster_lo = ((cluster_number & 0x0000_FFFF) as u16).to_le_bytes(); + data[26..28].copy_from_slice(&cluster_lo[..]); + data[28..32].copy_from_slice(&self.size.to_le_bytes()[..]); + data + } + + pub(crate) fn new( + name: ShortFileName, + attributes: Attributes, + cluster: ClusterId, + ctime: Timestamp, + entry_block: BlockIdx, + entry_offset: u32, + ) -> Self { + Self { + name, + mtime: ctime, + ctime, + attributes, + cluster, + size: 0, + entry_block, + entry_offset, + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/filename.rs b/examples/ios/embedded-sdmmc/src/filesystem/filename.rs new file mode 100644 index 0000000..31d2854 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/filename.rs @@ -0,0 +1,506 @@ +//! Filename related types + +use crate::fat::VolumeName; +use crate::trace; + +/// Various filename related errors that can occur. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub enum FilenameError { + /// Tried to create a file with an invalid character. + InvalidCharacter, + /// Tried to create a file with no file name. + FilenameEmpty, + /// Given name was too long (we are limited to 8.3). + NameTooLong, + /// Can't start a file with a period, or after 8 characters. + MisplacedPeriod, + /// Can't extract utf8 from file name + Utf8Error, +} + +/// Describes things we can convert to short 8.3 filenames +pub trait ToShortFileName { + /// Try and convert this value into a [`ShortFileName`]. + fn to_short_filename(self) -> Result; +} + +impl ToShortFileName for ShortFileName { + fn to_short_filename(self) -> Result { + Ok(self) + } +} + +impl ToShortFileName for &ShortFileName { + fn to_short_filename(self) -> Result { + Ok(self.clone()) + } +} + +impl ToShortFileName for &str { + fn to_short_filename(self) -> Result { + ShortFileName::create_from_str(self) + } +} + +/// An MS-DOS 8.3 filename. +/// +/// ISO-8859-1 encoding is assumed. All lower-case is converted to upper-case by +/// default. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(PartialEq, Eq, Clone)] +pub struct ShortFileName { + pub(crate) contents: [u8; Self::TOTAL_LEN], +} + +impl ShortFileName { + const BASE_LEN: usize = 8; + const TOTAL_LEN: usize = 11; + + /// Get a short file name containing "..", which means "parent directory". + pub const fn parent_dir() -> Self { + Self { + contents: *b".. ", + } + } + + /// Get a short file name containing ".", which means "this directory". + pub const fn this_dir() -> Self { + Self { + contents: *b". ", + } + } + + /// Get base name (without extension) of the file. + pub fn base_name(&self) -> &[u8] { + Self::bytes_before_space(&self.contents[..Self::BASE_LEN]) + } + + /// Get extension of the file (without base name). + pub fn extension(&self) -> &[u8] { + Self::bytes_before_space(&self.contents[Self::BASE_LEN..]) + } + + fn bytes_before_space(bytes: &[u8]) -> &[u8] { + bytes.split(|b| *b == b' ').next().unwrap_or(&[]) + } + + /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry. + /// + /// The output uses ISO-8859-1 encoding. + pub fn create_from_str(name: &str) -> Result { + let mut sfn = ShortFileName { + contents: [b' '; Self::TOTAL_LEN], + }; + + // Special case `..`, which means "parent directory". + if name == ".." { + return Ok(ShortFileName::parent_dir()); + } + + // Special case `.` (or blank), which means "this directory". + if name.is_empty() || name == "." { + return Ok(ShortFileName::this_dir()); + } + + let mut idx = 0; + let mut seen_dot = false; + for ch in name.chars() { + match ch { + // Microsoft say these are the invalid characters + '\u{0000}'..='\u{001F}' + | '"' + | '*' + | '+' + | ',' + | '/' + | ':' + | ';' + | '<' + | '=' + | '>' + | '?' + | '[' + | '\\' + | ']' + | ' ' + | '|' => { + return Err(FilenameError::InvalidCharacter); + } + x if x > '\u{00FF}' => { + // We only handle ISO-8859-1 which is Unicode Code Points + // \U+0000 to \U+00FF. This is above that. + return Err(FilenameError::InvalidCharacter); + } + '.' => { + // Denotes the start of the file extension + if (1..=Self::BASE_LEN).contains(&idx) { + idx = Self::BASE_LEN; + seen_dot = true; + } else { + return Err(FilenameError::MisplacedPeriod); + } + } + _ => { + let b = ch.to_ascii_uppercase() as u8; + if seen_dot { + if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + } else if idx < Self::BASE_LEN { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + idx += 1; + } + } + } + if idx == 0 { + return Err(FilenameError::FilenameEmpty); + } + Ok(sfn) + } + + /// Convert a Short File Name to a Volume Label. + /// + /// # Safety + /// + /// Volume Labels can contain things that Short File Names cannot, so only + /// do this conversion if you have the name of a directory entry with the + /// 'Volume Label' attribute. + pub unsafe fn to_volume_label(self) -> VolumeName { + VolumeName { + contents: self.contents, + } + } + + /// Get the LFN checksum for this short filename + pub fn csum(&self) -> u8 { + let mut result = 0u8; + for b in self.contents.iter() { + result = result.rotate_right(1).wrapping_add(*b); + } + result + } +} + +impl core::fmt::Display for ShortFileName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let mut printed = 0; + for (i, &c) in self.contents.iter().enumerate() { + if c != b' ' { + if i == Self::BASE_LEN { + write!(f, ".")?; + printed += 1; + } + // converting a byte to a codepoint means you are assuming + // ISO-8859-1 encoding, because that's how Unicode was designed. + write!(f, "{}", c as char)?; + printed += 1; + } + } + if let Some(mut width) = f.width() { + if width > printed { + width -= printed; + for _ in 0..width { + write!(f, "{}", f.fill())?; + } + } + } + Ok(()) + } +} + +impl core::fmt::Debug for ShortFileName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "ShortFileName(\"{}\")", self) + } +} + +/// Used to store a Long File Name +#[derive(Debug)] +pub struct LfnBuffer<'a> { + /// We fill this buffer in from the back + inner: &'a mut [u8], + /// How many bytes are free. + /// + /// This is also the byte index the string starts from. + free: usize, + /// Did we overflow? + overflow: bool, + /// If a surrogate-pair is split over two directory entries, remember half of it here. + unpaired_surrogate: Option, +} + +impl<'a> LfnBuffer<'a> { + /// Create a new, empty, LFN Buffer using the given mutable slice as its storage. + pub fn new(storage: &'a mut [u8]) -> LfnBuffer<'a> { + let len = storage.len(); + LfnBuffer { + inner: storage, + free: len, + overflow: false, + unpaired_surrogate: None, + } + } + + /// Empty out this buffer + pub fn clear(&mut self) { + self.free = self.inner.len(); + self.overflow = false; + self.unpaired_surrogate = None; + } + + /// Push the 13 UTF-16 codepoints into this string. + /// + /// We assume they are pushed last-chunk-first, as you would find + /// them on disk. + /// + /// Any chunk starting with a half of a surrogate pair has that saved for the next call. + /// + /// ```text + /// [de00, 002e, 0074, 0078, 0074, 0000, ffff, ffff, ffff, ffff, ffff, ffff, ffff] + /// [0041, 0042, 0030, 0031, 0032, 0033, 0034, 0035, 0036, 0037, 0038, 0039, d83d] + /// + /// Would map to + /// + /// 0041 0042 0030 0031 0032 0033 0034 0035 0036 0037 0038 0039 1f600 002e 0074 0078 0074, or + /// + /// "AB0123456789😀.txt" + /// ``` + pub fn push(&mut self, buffer: &[u16; 13]) { + // find the first null, if any + let null_idx = buffer + .iter() + .position(|&b| b == 0x0000) + .unwrap_or(buffer.len()); + // take all the wide chars, up to the null (or go to the end) + let buffer = &buffer[0..null_idx]; + + // This next part will convert the 16-bit values into chars, noting that + // chars outside the Basic Multilingual Plane will require two 16-bit + // values to encode (see UTF-16 Surrogate Pairs). + // + // We cache the decoded chars into this array so we can iterate them + // backwards. It's 60 bytes, but it'll have to do. + let mut char_vec: heapless::Vec = heapless::Vec::new(); + // Now do the decode, including the unpaired surrogate (if any) from + // last time (maybe it has a pair now!) + let mut is_first = true; + for ch in char::decode_utf16( + buffer + .iter() + .cloned() + .chain(self.unpaired_surrogate.take().iter().cloned()), + ) { + match ch { + Ok(ch) => { + char_vec.push(ch).expect("Vec was full!?"); + } + Err(e) => { + // OK, so we found half a surrogate pair and nothing to go + // with it. Was this the first codepoint in the chunk? + if is_first { + // it was - the other half is probably in the next chunk + // so save this for next time + trace!("LFN saved {:?}", e.unpaired_surrogate()); + self.unpaired_surrogate = Some(e.unpaired_surrogate()); + } else { + // it wasn't - can't deal with it these mid-sequence, so + // replace it + trace!("LFN replaced {:?}", e.unpaired_surrogate()); + char_vec.push('\u{fffd}').expect("Vec was full?!"); + } + } + } + is_first = false; + } + + for ch in char_vec.iter().rev() { + trace!("LFN push {:?}", ch); + // a buffer of length 4 is enough to encode any char + let mut encoded_ch = [0u8; 4]; + let encoded_ch = ch.encode_utf8(&mut encoded_ch); + if self.free < encoded_ch.len() { + // the LFN buffer they gave us was not long enough. Note for + // later, so we don't show them garbage. + self.overflow = true; + return; + } + // Store the encoded char in the buffer, working backwards. We + // already checked there was enough space. + for b in encoded_ch.bytes().rev() { + self.free -= 1; + self.inner[self.free] = b; + } + } + } + + /// View this LFN buffer as a string-slice + /// + /// If the buffer overflowed while parsing the LFN, or if this buffer is + /// empty, you get an empty string. + pub fn as_str(&self) -> &str { + if self.overflow { + "" + } else { + // we always only put UTF-8 encoded data in here + unsafe { core::str::from_utf8_unchecked(&self.inner[self.free..]) } + } + } +} + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn filename_no_extension() { + let sfn = ShortFileName { + contents: *b"HELLO ", + }; + assert_eq!(format!("{}", &sfn), "HELLO"); + assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap()); + assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap()); + assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap()); + assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap()); + } + + #[test] + fn filename_extension() { + let sfn = ShortFileName { + contents: *b"HELLO TXT", + }; + assert_eq!(format!("{}", &sfn), "HELLO.TXT"); + assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap()); + } + + #[test] + fn filename_get_extension() { + let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap(); + assert_eq!(sfn.extension(), "TXT".as_bytes()); + sfn = ShortFileName::create_from_str("hello").unwrap(); + assert_eq!(sfn.extension(), "".as_bytes()); + sfn = ShortFileName::create_from_str("hello.a").unwrap(); + assert_eq!(sfn.extension(), "A".as_bytes()); + } + + #[test] + fn filename_get_base_name() { + let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap(); + assert_eq!(sfn.base_name(), "HELLO".as_bytes()); + sfn = ShortFileName::create_from_str("12345678").unwrap(); + assert_eq!(sfn.base_name(), "12345678".as_bytes()); + sfn = ShortFileName::create_from_str("1").unwrap(); + assert_eq!(sfn.base_name(), "1".as_bytes()); + } + + #[test] + fn filename_fulllength() { + let sfn = ShortFileName { + contents: *b"12345678TXT", + }; + assert_eq!(format!("{}", &sfn), "12345678.TXT"); + assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap()); + } + + #[test] + fn filename_short_extension() { + let sfn = ShortFileName { + contents: *b"12345678C ", + }; + assert_eq!(format!("{}", &sfn), "12345678.C"); + assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap()); + } + + #[test] + fn filename_short() { + let sfn = ShortFileName { + contents: *b"1 C ", + }; + assert_eq!(format!("{}", &sfn), "1.C"); + assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap()); + } + + #[test] + fn filename_empty() { + assert_eq!( + ShortFileName::create_from_str("").unwrap(), + ShortFileName::this_dir() + ); + } + + #[test] + fn filename_bad() { + assert!(ShortFileName::create_from_str(" ").is_err()); + assert!(ShortFileName::create_from_str("123456789").is_err()); + assert!(ShortFileName::create_from_str("12345678.ABCD").is_err()); + } + + #[test] + fn checksum() { + assert_eq!( + 0xB3, + ShortFileName::create_from_str("UNARCH~1.DAT") + .unwrap() + .csum() + ); + } + + #[test] + fn one_piece() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + buf.push(&[ + 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, + ]); + assert_eq!(buf.as_str(), "0123∂"); + } + + #[test] + fn two_piece() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + buf.push(&[ + 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, + ]); + buf.push(&[ + 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b, + 0x004c, 0x004d, + ]); + assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂"); + } + + #[test] + fn two_piece_split_surrogate() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + + buf.push(&[ + 0xde00, 0x002e, 0x0074, 0x0078, 0x0074, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, + 0xffff, 0xffff, + ]); + buf.push(&[ + 0xd83d, 0xde00, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, + 0x0039, 0xd83d, + ]); + assert_eq!(buf.as_str(), "😀0123456789😀.txt"); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/files.rs b/examples/ios/embedded-sdmmc/src/filesystem/files.rs new file mode 100644 index 0000000..870d85d --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/files.rs @@ -0,0 +1,359 @@ +use super::TimeSource; +use crate::{ + filesystem::{ClusterId, DirEntry, Handle}, + BlockDevice, Error, RawVolume, VolumeManager, +}; +use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; + +/// A handle for an open file on disk. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and cannot update the directory entry if you drop it. +/// Additionally, the VolumeManager will think you still have the file open if +/// you just drop it, and it won't let you open the file again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_file`] to close it +/// cleanly. +/// +/// If you want your files to close themselves on drop, create your own File +/// type that wraps this one and also holds a `VolumeManager` reference. You'll +/// then also need to put your `VolumeManager` in some kind of Mutex or RefCell, +/// and deal with the fact you can't put them both in the same struct any more +/// because one refers to the other. Basically, it's complicated and there's a +/// reason we did it this way. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RawFile(pub(crate) Handle); + +impl RawFile { + /// Convert a raw file into a droppable [`File`] + pub fn to_file( + self, + volume_mgr: &VolumeManager, + ) -> File + where + D: crate::BlockDevice, + T: crate::TimeSource, + { + File::new(self, volume_mgr) + } +} + +/// A handle for an open file on disk, which closes on drop. +/// +/// In contrast to a `RawFile`, a `File` holds a mutable reference to its +/// parent `VolumeManager`, which restricts which operations you can perform. +/// +/// If you drop a value of this type, it closes the file automatically, and but +/// error that may occur will be ignored. To handle potential errors, use +/// the [`File::close`] method. +pub struct File<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + raw_file: RawFile, + volume_mgr: &'a VolumeManager, +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + /// Create a new `File` from a `RawFile` + pub fn new( + raw_file: RawFile, + volume_mgr: &'a VolumeManager, + ) -> File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { + File { + raw_file, + volume_mgr, + } + } + + /// Read from the file + /// + /// Returns how many bytes were read, or an error. + pub fn read(&self, buffer: &mut [u8]) -> Result> { + self.volume_mgr.read(self.raw_file, buffer) + } + + /// Write to the file + pub fn write(&self, buffer: &[u8]) -> Result<(), crate::Error> { + self.volume_mgr.write(self.raw_file, buffer) + } + + /// Check if a file is at End Of File. + pub fn is_eof(&self) -> bool { + self.volume_mgr + .file_eof(self.raw_file) + .expect("Corrupt file ID") + } + + /// Seek a file with an offset from the current position. + pub fn seek_from_current(&self, offset: i32) -> Result<(), crate::Error> { + self.volume_mgr + .file_seek_from_current(self.raw_file, offset) + } + + /// Seek a file with an offset from the start of the file. + pub fn seek_from_start(&self, offset: u32) -> Result<(), crate::Error> { + self.volume_mgr.file_seek_from_start(self.raw_file, offset) + } + + /// Seek a file with an offset back from the end of the file. + pub fn seek_from_end(&self, offset: u32) -> Result<(), crate::Error> { + self.volume_mgr.file_seek_from_end(self.raw_file, offset) + } + + /// Get the length of a file + pub fn length(&self) -> u32 { + self.volume_mgr + .file_length(self.raw_file) + .expect("Corrupt file ID") + } + + /// Get the current offset of a file + pub fn offset(&self) -> u32 { + self.volume_mgr + .file_offset(self.raw_file) + .expect("Corrupt file ID") + } + + /// Convert back to a raw file + pub fn to_raw_file(self) -> RawFile { + let f = self.raw_file; + core::mem::forget(self); + f + } + + /// Flush any written data by updating the directory entry. + pub fn flush(&self) -> Result<(), Error> { + self.volume_mgr.flush_file(self.raw_file) + } + + /// Consume the `File` handle and close it. The behavior of this is similar + /// to using [`core::mem::drop`] or letting the `File` go out of scope, + /// except this lets the user handle any errors that may occur in the process, + /// whereas when using drop, any errors will be discarded silently. + pub fn close(self) -> Result<(), Error> { + let result = self.volume_mgr.close_file(self.raw_file); + core::mem::forget(self); + result + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop + for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn drop(&mut self) { + _ = self.volume_mgr.close_file(self.raw_file); + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + core::fmt::Debug for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "File({})", self.raw_file.0 .0) + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > ErrorType for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + type Error = crate::Error; +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Read for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + Ok(0) + } else { + self.read(buf) + } + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Write for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn write(&mut self, buf: &[u8]) -> Result { + if buf.is_empty() { + Ok(0) + } else { + self.write(buf)?; + Ok(buf.len()) + } + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Self::flush(self) + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Seek for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + match pos { + SeekFrom::Start(offset) => { + self.seek_from_start(offset.try_into().map_err(|_| Error::InvalidOffset)?)? + } + SeekFrom::End(offset) => { + self.seek_from_end((-offset).try_into().map_err(|_| Error::InvalidOffset)?)? + } + SeekFrom::Current(offset) => { + self.seek_from_current(offset.try_into().map_err(|_| Error::InvalidOffset)?)? + } + } + Ok(self.offset().into()) + } +} + +#[cfg(feature = "defmt-log")] +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + defmt::Format for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "File({})", self.raw_file.0 .0) + } +} + +/// Errors related to file operations +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileError { + /// Tried to use an invalid offset. + InvalidOffset, +} + +/// The different ways we can open a file. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Mode { + /// Open a file for reading, if it exists. + ReadOnly, + /// Open a file for appending (writing to the end of the existing file), if it exists. + ReadWriteAppend, + /// Open a file and remove all contents, before writing to the start of the existing file, if it exists. + ReadWriteTruncate, + /// Create a new empty file. Fail if it exists. + ReadWriteCreate, + /// Create a new empty file, or truncate an existing file. + ReadWriteCreateOrTruncate, + /// Create a new empty file, or append to an existing file. + ReadWriteCreateOrAppend, +} + +/// Internal metadata about an open file +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub(crate) struct FileInfo { + /// Handle for this file + pub(crate) raw_file: RawFile, + /// The handle for the volume this directory is on + pub(crate) raw_volume: RawVolume, + /// The last cluster we accessed, and how many bytes that short-cuts us. + /// + /// This saves us walking from the very start of the FAT chain when we move + /// forward through a file. + pub(crate) current_cluster: (u32, ClusterId), + /// How far through the file we've read (in bytes). + pub(crate) current_offset: u32, + /// What mode the file was opened in + pub(crate) mode: Mode, + /// DirEntry of this file + pub(crate) entry: DirEntry, + /// Did we write to this file? + pub(crate) dirty: bool, +} + +impl FileInfo { + /// Are we at the end of the file? + pub fn eof(&self) -> bool { + self.current_offset == self.entry.size + } + + /// How long is the file? + pub fn length(&self) -> u32 { + self.entry.size + } + + /// Seek to a new position in the file, relative to the start of the file. + pub fn seek_from_start(&mut self, offset: u32) -> Result<(), FileError> { + if offset > self.entry.size { + return Err(FileError::InvalidOffset); + } + self.current_offset = offset; + Ok(()) + } + + /// Seek to a new position in the file, relative to the end of the file. + pub fn seek_from_end(&mut self, offset: u32) -> Result<(), FileError> { + if offset > self.entry.size { + return Err(FileError::InvalidOffset); + } + self.current_offset = self.entry.size - offset; + Ok(()) + } + + /// Seek to a new position in the file, relative to the current position. + pub fn seek_from_current(&mut self, offset: i32) -> Result<(), FileError> { + let new_offset = i64::from(self.current_offset) + i64::from(offset); + if new_offset < 0 || new_offset > i64::from(self.entry.size) { + return Err(FileError::InvalidOffset); + } + self.current_offset = new_offset as u32; + Ok(()) + } + + /// Amount of file left to read. + pub fn left(&self) -> u32 { + self.entry.size - self.current_offset + } + + /// Update the file's length. + pub(crate) fn update_length(&mut self, new: u32) { + self.entry.size = new; + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/handles.rs b/examples/ios/embedded-sdmmc/src/filesystem/handles.rs new file mode 100644 index 0000000..dd37903 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/handles.rs @@ -0,0 +1,48 @@ +//! Contains the Handles and the HandleGenerator. + +use core::num::Wrapping; + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +/// Unique ID used to identify things in the open Volume/File/Directory lists +pub struct Handle(pub(crate) u32); + +impl core::fmt::Debug for Handle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#08x}", self.0) + } +} + +/// A Handle Generator. +/// +/// This object will always return a different ID. +/// +/// Well, it will wrap after `2**32` IDs. But most systems won't open that many +/// files, and if they do, they are unlikely to hold one file open and then +/// open/close `2**32 - 1` others. +#[derive(Debug)] +pub struct HandleGenerator { + next_id: Wrapping, +} + +impl HandleGenerator { + /// Create a new generator of Handles. + pub const fn new(offset: u32) -> Self { + Self { + next_id: Wrapping(offset), + } + } + + /// Generate a new, unique [`Handle`]. + pub fn generate(&mut self) -> Handle { + let id = self.next_id; + self.next_id += 1; + Handle(id.0) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/mod.rs b/examples/ios/embedded-sdmmc/src/filesystem/mod.rs new file mode 100644 index 0000000..668ac86 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/mod.rs @@ -0,0 +1,32 @@ +//! Generic File System structures +//! +//! Implements generic file system components. These should be applicable to +//! most (if not all) supported filesystems. + +/// Maximum file size supported by this library +pub const MAX_FILE_SIZE: u32 = u32::MAX; + +mod attributes; +mod cluster; +mod directory; +mod filename; +mod files; +mod handles; +mod timestamp; + +pub use self::attributes::Attributes; +pub use self::cluster::ClusterId; +pub use self::directory::{DirEntry, Directory, RawDirectory}; +pub use self::filename::{FilenameError, LfnBuffer, ShortFileName, ToShortFileName}; +pub use self::files::{File, FileError, Mode, RawFile}; +pub use self::handles::{Handle, HandleGenerator}; +pub use self::timestamp::{TimeSource, Timestamp}; + +pub(crate) use self::directory::DirectoryInfo; +pub(crate) use self::files::FileInfo; + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs b/examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs new file mode 100644 index 0000000..ff2c0be --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs @@ -0,0 +1,141 @@ +/// Things that impl this can tell you the current time. +pub trait TimeSource { + /// Returns the current time + fn get_timestamp(&self) -> Timestamp; +} + +/// A Gregorian Calendar date/time, in the local time zone. +/// +/// TODO: Consider replacing this with POSIX time as a `u32`, which would save +/// two bytes at the expense of some maths. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct Timestamp { + /// Add 1970 to this file to get the calendar year + pub year_since_1970: u8, + /// Add one to this value to get the calendar month + pub zero_indexed_month: u8, + /// Add one to this value to get the calendar day + pub zero_indexed_day: u8, + /// The number of hours past midnight + pub hours: u8, + /// The number of minutes past the hour + pub minutes: u8, + /// The number of seconds past the minute + pub seconds: u8, +} + +impl Timestamp { + /// Create a `Timestamp` from the 16-bit FAT date and time fields. + pub fn from_fat(date: u16, time: u16) -> Timestamp { + let year = 1980 + (date >> 9); + let month = ((date >> 5) & 0x000F) as u8; + let day = (date & 0x001F) as u8; + let hours = ((time >> 11) & 0x001F) as u8; + let minutes = ((time >> 5) & 0x0003F) as u8; + let seconds = ((time << 1) & 0x0003F) as u8; + // Volume labels have a zero for month/day, so tolerate that... + Timestamp { + year_since_1970: (year - 1970) as u8, + zero_indexed_month: if month == 0 { 0 } else { month - 1 }, + zero_indexed_day: if day == 0 { 0 } else { day - 1 }, + hours, + minutes, + seconds, + } + } + + // TODO add tests for the method + /// Serialize a `Timestamp` to FAT format + pub fn serialize_to_fat(self) -> [u8; 4] { + let mut data = [0u8; 4]; + + let hours = (u16::from(self.hours) << 11) & 0xF800; + let minutes = (u16::from(self.minutes) << 5) & 0x07E0; + let seconds = (u16::from(self.seconds / 2)) & 0x001F; + data[..2].copy_from_slice(&(hours | minutes | seconds).to_le_bytes()[..]); + + let year = if self.year_since_1970 < 10 { + 0 + } else { + (u16::from(self.year_since_1970 - 10) << 9) & 0xFE00 + }; + let month = (u16::from(self.zero_indexed_month + 1) << 5) & 0x01E0; + let day = u16::from(self.zero_indexed_day + 1) & 0x001F; + data[2..].copy_from_slice(&(year | month | day).to_le_bytes()[..]); + data + } + + /// Create a `Timestamp` from year/month/day/hour/minute/second. + /// + /// Values should be given as you'd write then (i.e. 1980, 01, 01, 13, 30, + /// 05) is 1980-Jan-01, 1:30:05pm. + pub fn from_calendar( + year: u16, + month: u8, + day: u8, + hours: u8, + minutes: u8, + seconds: u8, + ) -> Result { + Ok(Timestamp { + year_since_1970: if (1970..=(1970 + 255)).contains(&year) { + (year - 1970) as u8 + } else { + return Err("Bad year"); + }, + zero_indexed_month: if (1..=12).contains(&month) { + month - 1 + } else { + return Err("Bad month"); + }, + zero_indexed_day: if (1..=31).contains(&day) { + day - 1 + } else { + return Err("Bad day"); + }, + hours: if hours <= 23 { + hours + } else { + return Err("Bad hours"); + }, + minutes: if minutes <= 59 { + minutes + } else { + return Err("Bad minutes"); + }, + seconds: if seconds <= 59 { + seconds + } else { + return Err("Bad seconds"); + }, + }) + } +} + +impl core::fmt::Debug for Timestamp { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "Timestamp({})", self) + } +} + +impl core::fmt::Display for Timestamp { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "{}-{:02}-{:02} {:02}:{:02}:{:02}", + u16::from(self.year_since_1970) + 1970, + self.zero_indexed_month + 1, + self.zero_indexed_day + 1, + self.hours, + self.minutes, + self.seconds + ) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/lib.rs b/examples/ios/embedded-sdmmc/src/lib.rs new file mode 100644 index 0000000..c6af4e9 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/lib.rs @@ -0,0 +1,461 @@ +//! # embedded-sdmmc +//! +//! > An SD/MMC Library written in Embedded Rust +//! +//! This crate is intended to allow you to read/write files on a FAT formatted +//! SD card on your Rust Embedded device, as easily as using the `SdFat` Arduino +//! library. It is written in pure-Rust, is `#![no_std]` and does not use +//! `alloc` or `collections` to keep the memory footprint low. In the first +//! instance it is designed for readability and simplicity over performance. +//! +//! ## Using the crate +//! +//! 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. +//! +//! ```rust +//! use embedded_sdmmc::{Error, Mode, SdCard, SdCardError, TimeSource, VolumeIdx, VolumeManager}; +//! +//! fn example(spi: S, delay: D, ts: T) -> Result<(), Error> +//! where +//! S: embedded_hal::spi::SpiDevice, +//! D: embedded_hal::delay::DelayNs, +//! T: TimeSource, +//! { +//! let sdcard = SdCard::new(spi, delay); +//! println!("Card size is {} bytes", sdcard.num_bytes()?); +//! let volume_mgr = VolumeManager::new(sdcard, ts); +//! let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; +//! println!("Volume 0: {:?}", volume0); +//! let root_dir = volume0.open_root_dir()?; +//! let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; +//! while !my_file.is_eof() { +//! let mut buffer = [0u8; 32]; +//! let num_read = my_file.read(&mut buffer)?; +//! for b in &buffer[0..num_read] { +//! print!("{}", *b as char); +//! } +//! } +//! Ok(()) +//! } +//! ``` +//! +//! For writing files: +//! +//! ```rust +//! use embedded_sdmmc::{BlockDevice, Directory, Error, Mode, TimeSource}; +//! fn write_file( +//! root_dir: &mut Directory, +//! ) -> Result<(), Error> +//! { +//! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", Mode::ReadWriteCreateOrAppend)?; +//! my_other_file.write(b"Timestamp,Signal,Value\n")?; +//! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +//! my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +//! my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; +//! // Don't forget to flush the file so that the directory entry is updated +//! my_other_file.flush()?; +//! Ok(()) +//! } +//! ``` +//! +//! ## Features +//! +//! * `log`: Enabled by default. Generates log messages using the `log` crate. +//! * `defmt-log`: By turning off the default features and enabling the +//! `defmt-log` feature you can configure this crate to log messages over defmt +//! instead. +//! +//! You cannot enable both the `log` feature and the `defmt-log` feature. + +#![cfg_attr(not(test), no_std)] +#![deny(missing_docs)] + +// **************************************************************************** +// +// Imports +// +// **************************************************************************** + +#[cfg(test)] +#[macro_use] +extern crate hex_literal; + +#[macro_use] +mod structure; + +pub mod blockdevice; +pub mod fat; +pub mod filesystem; +pub mod sdcard; + +use core::fmt::Debug; +use embedded_io::ErrorKind; +use filesystem::Handle; + +#[doc(inline)] +pub use crate::blockdevice::{Block, BlockCache, BlockCount, BlockDevice, BlockIdx}; + +#[doc(inline)] +pub use crate::fat::{FatVolume, VolumeName}; + +#[doc(inline)] +pub use crate::filesystem::{ + Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, Mode, RawDirectory, + RawFile, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, +}; + +use filesystem::DirectoryInfo; + +#[doc(inline)] +pub use crate::sdcard::Error as SdCardError; + +#[doc(inline)] +pub use crate::sdcard::SdCard; + +mod volume_mgr; +#[doc(inline)] +pub use volume_mgr::VolumeManager; + +#[cfg(all(feature = "defmt-log", feature = "log"))] +compile_error!("Cannot enable both log and defmt-log"); + +#[cfg(feature = "log")] +use log::{debug, trace, warn}; + +#[cfg(feature = "defmt-log")] +use defmt::{debug, trace, warn}; + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::debug! but does nothing at all +macro_rules! debug { + ($($arg:tt)+) => {}; +} + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::trace! but does nothing at all +macro_rules! trace { + ($($arg:tt)+) => {}; +} + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::warn! but does nothing at all +macro_rules! warn { + ($($arg:tt)+) => {}; +} + +// **************************************************************************** +// +// Public Types +// +// **************************************************************************** + +/// All the ways the functions in this crate can fail. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub enum Error +where + E: core::fmt::Debug, +{ + /// The underlying block device threw an error. + DeviceError(E), + /// The filesystem is badly formatted (or this code is buggy). + FormatError(&'static str), + /// The given `VolumeIdx` was bad, + NoSuchVolume, + /// The given filename was bad + FilenameError(FilenameError), + /// Out of memory opening volumes + TooManyOpenVolumes, + /// Out of memory opening directories + TooManyOpenDirs, + /// Out of memory opening files + TooManyOpenFiles, + /// Bad handle given + BadHandle, + /// That file or directory doesn't exist + NotFound, + /// You can't open a file twice or delete an open file + FileAlreadyOpen, + /// You can't open a directory twice + DirAlreadyOpen, + /// You can't open a directory as a file + OpenedDirAsFile, + /// You can't open a file as a directory + OpenedFileAsDir, + /// You can't delete a directory as a file + DeleteDirAsFile, + /// You can't close a volume with open files or directories + VolumeStillInUse, + /// You can't open a volume twice + VolumeAlreadyOpen, + /// We can't do that yet + Unsupported, + /// Tried to read beyond end of file + EndOfFile, + /// Found a bad cluster + BadCluster, + /// Error while converting types + ConversionError, + /// The device does not have enough space for the operation + NotEnoughSpace, + /// Cluster was not properly allocated by the library + AllocationError, + /// Jumped to free space during FAT traversing + UnterminatedFatChain, + /// Tried to open Read-Only file with write mode + ReadOnly, + /// Tried to create an existing file + FileAlreadyExists, + /// Bad block size - only 512 byte blocks supported + BadBlockSize(u16), + /// Bad offset given when seeking + InvalidOffset, + /// Disk is full + DiskFull, + /// A directory with that name already exists + DirAlreadyExists, + /// The filesystem tried to gain a lock whilst already locked. + /// + /// This is either a bug in the filesystem, or you tried to access the + /// filesystem API from inside a directory iterator (that isn't allowed). + LockError, +} + +impl embedded_io::Error for Error { + fn kind(&self) -> ErrorKind { + match self { + Error::DeviceError(_) + | Error::FormatError(_) + | Error::FileAlreadyOpen + | Error::DirAlreadyOpen + | Error::VolumeStillInUse + | Error::VolumeAlreadyOpen + | Error::EndOfFile + | Error::DiskFull + | Error::NotEnoughSpace + | Error::AllocationError + | Error::LockError => ErrorKind::Other, + Error::NoSuchVolume + | Error::FilenameError(_) + | Error::BadHandle + | Error::InvalidOffset => ErrorKind::InvalidInput, + Error::TooManyOpenVolumes | Error::TooManyOpenDirs | Error::TooManyOpenFiles => { + ErrorKind::OutOfMemory + } + Error::NotFound => ErrorKind::NotFound, + Error::OpenedDirAsFile + | Error::OpenedFileAsDir + | Error::DeleteDirAsFile + | Error::BadCluster + | Error::ConversionError + | Error::UnterminatedFatChain => ErrorKind::InvalidData, + Error::Unsupported | Error::BadBlockSize(_) => ErrorKind::Unsupported, + Error::ReadOnly => ErrorKind::PermissionDenied, + Error::FileAlreadyExists | Error::DirAlreadyExists => ErrorKind::AlreadyExists, + } + } +} + +impl From for Error +where + E: core::fmt::Debug, +{ + fn from(value: E) -> Error { + Error::DeviceError(value) + } +} + +/// A handle to a volume. +/// +/// A volume is a partition with a filesystem within it. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and the VolumeManager will think you still have the +/// volume open if you just drop it, and it won't let you open the file again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_volume`] to close +/// it cleanly. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RawVolume(Handle); + +impl RawVolume { + /// Convert a raw volume into a droppable [`Volume`] + pub fn to_volume< + D, + T, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + >( + self, + volume_mgr: &VolumeManager, + ) -> Volume + where + D: crate::BlockDevice, + T: crate::TimeSource, + { + Volume::new(self, volume_mgr) + } +} + +/// A handle for an open volume on disk, which closes on drop. +/// +/// In contrast to a `RawVolume`, a `Volume` holds a mutable reference to its +/// parent `VolumeManager`, which restricts which operations you can perform. +/// +/// If you drop a value of this type, it closes the volume automatically, but +/// any error that may occur will be ignored. To handle potential errors, use +/// the [`Volume::close`] method. +pub struct Volume<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + raw_volume: RawVolume, + volume_mgr: &'a VolumeManager, +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + /// Create a new `Volume` from a `RawVolume` + pub fn new( + raw_volume: RawVolume, + volume_mgr: &'a VolumeManager, + ) -> Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { + Volume { + raw_volume, + volume_mgr, + } + } + + /// Open the volume's root directory. + /// + /// You can then read the directory entries with `iterate_dir`, or you can + /// use `open_file_in_dir`. + pub fn open_root_dir( + &self, + ) -> Result, Error> { + let d = self.volume_mgr.open_root_dir(self.raw_volume)?; + Ok(d.to_directory(self.volume_mgr)) + } + + /// Convert back to a raw volume + pub fn to_raw_volume(self) -> RawVolume { + let v = self.raw_volume; + core::mem::forget(self); + v + } + + /// Consume the `Volume` handle and close it. The behavior of this is similar + /// to using [`core::mem::drop`] or letting the `Volume` go out of scope, + /// except this lets the user handle any errors that may occur in the process, + /// whereas when using drop, any errors will be discarded silently. + pub fn close(self) -> Result<(), Error> { + let result = self.volume_mgr.close_volume(self.raw_volume); + core::mem::forget(self); + result + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop + for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn drop(&mut self) { + _ = self.volume_mgr.close_volume(self.raw_volume) + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + core::fmt::Debug for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Volume({})", self.raw_volume.0 .0) + } +} + +#[cfg(feature = "defmt-log")] +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + defmt::Format for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "Volume({})", self.raw_volume.0 .0) + } +} + +/// Internal information about a Volume +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct VolumeInfo { + /// Handle for this volume. + raw_volume: RawVolume, + /// Which volume (i.e. partition) we opened on the disk + idx: VolumeIdx, + /// What kind of volume this is + volume_type: VolumeType, +} + +/// This enum holds the data for the various different types of filesystems we +/// support. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq)] +pub enum VolumeType { + /// FAT16/FAT32 formatted volumes. + Fat(FatVolume), +} + +/// A number which identifies a volume (or partition) on a disk. +/// +/// `VolumeIdx(0)` is the first primary partition on an MBR partitioned disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct VolumeIdx(pub usize); + +/// Marker for a FAT32 partition. Sometimes also use for FAT16 formatted +/// partitions. +const PARTITION_ID_FAT32_LBA: u8 = 0x0C; +/// Marker for a FAT16 partition with LBA. Seen on a Raspberry Pi SD card. +const PARTITION_ID_FAT16_LBA: u8 = 0x0E; +/// Marker for a FAT16 partition. Seen on a card formatted with the official +/// SD-Card formatter. +const PARTITION_ID_FAT16: u8 = 0x06; +/// Marker for a FAT16 partition smaller than 32MB. Seen on the wowki simulated +/// microsd card +const PARTITION_ID_FAT16_SMALL: u8 = 0x04; +/// Marker for a FAT32 partition. What Macosx disk utility (and also SD-Card formatter?) +/// use. +const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B; + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +// None + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/sdcard/mod.rs b/examples/ios/embedded-sdmmc/src/sdcard/mod.rs new file mode 100644 index 0000000..553791f --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/sdcard/mod.rs @@ -0,0 +1,720 @@ +//! Implements the BlockDevice trait for an SD/MMC Protocol over SPI. +//! +//! This is currently optimised for readability and debugability, not +//! performance. + +pub mod proto; + +use crate::{trace, Block, BlockCount, BlockDevice, BlockIdx}; +use core::cell::RefCell; +use proto::*; + +// **************************************************************************** +// Imports +// **************************************************************************** + +use crate::{debug, warn}; + +// **************************************************************************** +// Types and Implementations +// **************************************************************************** + +/// Driver for an SD Card on an SPI bus. +/// +/// Built from an [`SpiDevice`] implementation and a Chip Select pin. +/// +/// Before talking to the SD Card, the caller needs to send 74 clocks cycles on +/// the SPI Clock line, at 400 kHz, with no chip-select asserted (or at least, +/// not the chip-select of the SD Card). +/// +/// This kind of breaks the embedded-hal model, so how to do this is left to +/// the caller. You could drive the SpiBus directly, or use an SpiDevice with +/// a dummy chip-select pin. Or you could try just not doing the 74 clocks and +/// see if your card works anyway - some do, some don't. +/// +/// All the APIs take `&self` - mutability is handled using an inner `RefCell`. +/// +/// [`SpiDevice`]: embedded_hal::spi::SpiDevice +pub struct SdCard +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + inner: RefCell>, +} + +impl SdCard +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + /// Create a new SD/MMC Card driver using a raw SPI interface. + /// + /// The card will not be initialised at this time. Initialisation is + /// deferred until a method is called on the object. + /// + /// Uses the default options. + pub fn new(spi: SPI, delayer: DELAYER) -> SdCard { + Self::new_with_options(spi, delayer, AcquireOpts::default()) + } + + /// Construct a new SD/MMC Card driver, using a raw SPI interface and the given options. + /// + /// See the docs of the [`SdCard`] struct for more information about + /// how to construct the needed `SPI` and `CS` types. + /// + /// The card will not be initialised at this time. Initialisation is + /// deferred until a method is called on the object. + pub fn new_with_options( + spi: SPI, + delayer: DELAYER, + options: AcquireOpts, + ) -> SdCard { + SdCard { + inner: RefCell::new(SdCardInner { + spi, + delayer, + card_type: None, + options, + }), + } + } + + /// Get a temporary borrow on the underlying SPI device. + /// + /// The given closure will be called exactly once, and will be passed a + /// mutable reference to the underlying SPI object. + /// + /// Useful if you need to re-clock the SPI, but does not perform card + /// initialisation. + pub fn spi(&self, func: F) -> T + where + F: FnOnce(&mut SPI) -> T, + { + let mut inner = self.inner.borrow_mut(); + func(&mut inner.spi) + } + + /// Return the usable size of this SD card in bytes. + /// + /// This will trigger card (re-)initialisation. + pub fn num_bytes(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.num_bytes() + } + + /// Can this card erase single blocks? + /// + /// This will trigger card (re-)initialisation. + pub fn erase_single_block_enabled(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.erase_single_block_enabled() + } + + /// Mark the card as requiring a reset. + /// + /// The next operation will assume the card has been freshly inserted. + pub fn mark_card_uninit(&self) { + let mut inner = self.inner.borrow_mut(); + inner.card_type = None; + } + + /// Get the card type. + /// + /// This will trigger card (re-)initialisation. + pub fn get_card_type(&self) -> Option { + let mut inner = self.inner.borrow_mut(); + inner.check_init().ok()?; + inner.card_type + } + + /// Tell the driver the card has been initialised. + /// + /// This is here in case you were previously using the SD Card, and then a + /// previous instance of this object got destroyed but you know for certain + /// the SD Card remained powered up and initialised, and you'd just like to + /// read/write to/from the card again without going through the + /// initialisation sequence again. + /// + /// # Safety + /// + /// Only do this if the SD Card has actually been initialised. That is, if + /// you have been through the card initialisation sequence as specified in + /// the SD Card Specification by sending each appropriate command in turn, + /// either manually or using another variable of this [`SdCard`]. The card + /// must also be of the indicated type. Failure to uphold this will cause + /// data corruption. + pub unsafe fn mark_card_as_init(&self, card_type: CardType) { + let mut inner = self.inner.borrow_mut(); + inner.card_type = Some(card_type); + } +} + +impl BlockDevice for SdCard +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + type Error = Error; + + /// Read one or more blocks, starting at the given block index. + /// + /// This will trigger card (re-)initialisation. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut inner = self.inner.borrow_mut(); + debug!("Read {} blocks @ {}", blocks.len(), start_block_idx.0,); + inner.check_init()?; + inner.read(blocks, start_block_idx) + } + + /// Write one or more blocks, starting at the given block index. + /// + /// This will trigger card (re-)initialisation. + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut inner = self.inner.borrow_mut(); + debug!("Writing {} blocks @ {}", blocks.len(), start_block_idx.0); + inner.check_init()?; + inner.write(blocks, start_block_idx) + } + + /// Determine how many blocks this device can hold. + /// + /// This will trigger card (re-)initialisation. + fn num_blocks(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.num_blocks() + } +} + +/// Inner details for the SD Card driver. +/// +/// All the APIs required `&mut self`. +struct SdCardInner +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + spi: SPI, + delayer: DELAYER, + card_type: Option, + options: AcquireOpts, +} + +impl SdCardInner +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + /// Read one or more blocks, starting at the given block index. + fn read(&mut self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Error> { + let start_idx = match self.card_type { + Some(CardType::SD1 | CardType::SD2) => start_block_idx.0 * 512, + Some(CardType::SDHC) => start_block_idx.0, + None => return Err(Error::CardNotFound), + }; + + if blocks.len() == 1 { + // Start a single-block read + self.card_command(CMD17, start_idx)?; + self.read_data(&mut blocks[0].contents)?; + } else { + // Start a multi-block read + self.card_command(CMD18, start_idx)?; + for block in blocks.iter_mut() { + self.read_data(&mut block.contents)?; + } + // Stop the read + self.card_command(CMD12, 0)?; + } + Ok(()) + } + + /// Write one or more blocks, starting at the given block index. + fn write(&mut self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Error> { + let start_idx = match self.card_type { + Some(CardType::SD1 | CardType::SD2) => start_block_idx.0 * 512, + Some(CardType::SDHC) => start_block_idx.0, + None => return Err(Error::CardNotFound), + }; + if blocks.len() == 1 { + // Start a single-block write + self.card_command(CMD24, start_idx)?; + self.write_data(DATA_START_BLOCK, &blocks[0].contents)?; + self.wait_not_busy(Delay::new_write())?; + if self.card_command(CMD13, 0)? != 0x00 { + return Err(Error::WriteError); + } + if self.read_byte()? != 0x00 { + return Err(Error::WriteError); + } + } else { + // > It is recommended using this command preceding CMD25, some of the cards will be faster for Multiple + // > Write Blocks operation. Note that the host should send ACMD23 just before WRITE command if the host + // > wants to use the pre-erased feature + self.card_acmd(ACMD23, blocks.len() as u32)?; + // wait for card to be ready before sending the next command + self.wait_not_busy(Delay::new_write())?; + + // Start a multi-block write + self.card_command(CMD25, start_idx)?; + for block in blocks.iter() { + self.wait_not_busy(Delay::new_write())?; + self.write_data(WRITE_MULTIPLE_TOKEN, &block.contents)?; + } + // Stop the write + self.wait_not_busy(Delay::new_write())?; + self.write_byte(STOP_TRAN_TOKEN)?; + } + Ok(()) + } + + /// Determine how many blocks this device can hold. + fn num_blocks(&mut self) -> Result { + let csd = self.read_csd()?; + debug!("CSD: {:?}", csd); + let num_blocks = match csd { + Csd::V1(ref contents) => contents.card_capacity_blocks(), + Csd::V2(ref contents) => contents.card_capacity_blocks(), + }; + Ok(BlockCount(num_blocks)) + } + + /// Return the usable size of this SD card in bytes. + fn num_bytes(&mut self) -> Result { + let csd = self.read_csd()?; + debug!("CSD: {:?}", csd); + match csd { + Csd::V1(ref contents) => Ok(contents.card_capacity_bytes()), + Csd::V2(ref contents) => Ok(contents.card_capacity_bytes()), + } + } + + /// Can this card erase single blocks? + pub fn erase_single_block_enabled(&mut self) -> Result { + let csd = self.read_csd()?; + match csd { + Csd::V1(ref contents) => Ok(contents.erase_single_block_enabled()), + Csd::V2(ref contents) => Ok(contents.erase_single_block_enabled()), + } + } + + /// Read the 'card specific data' block. + fn read_csd(&mut self) -> Result { + match self.card_type { + Some(CardType::SD1) => { + let mut csd = CsdV1::new(); + if self.card_command(CMD9, 0)? != 0 { + return Err(Error::RegisterReadError); + } + self.read_data(&mut csd.data)?; + Ok(Csd::V1(csd)) + } + Some(CardType::SD2 | CardType::SDHC) => { + let mut csd = CsdV2::new(); + if self.card_command(CMD9, 0)? != 0 { + return Err(Error::RegisterReadError); + } + self.read_data(&mut csd.data)?; + Ok(Csd::V2(csd)) + } + None => Err(Error::CardNotFound), + } + } + + /// Read an arbitrary number of bytes from the card using the SD Card + /// protocol and an optional CRC. Always fills the given buffer, so make + /// sure it's the right size. + fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + // Get first non-FF byte. + let mut delay = Delay::new_read(); + let status = loop { + let s = self.read_byte()?; + if s != 0xFF { + break s; + } + delay.delay(&mut self.delayer, Error::TimeoutReadBuffer)?; + }; + if status != DATA_START_BLOCK { + return Err(Error::ReadError); + } + + buffer.fill(0xFF); + self.transfer_bytes(buffer)?; + + // These two bytes are always sent. They are either a valid CRC, or + // junk, depending on whether CRC mode was enabled. + let mut crc_bytes = [0xFF; 2]; + self.transfer_bytes(&mut crc_bytes)?; + if self.options.use_crc { + let crc = u16::from_be_bytes(crc_bytes); + let calc_crc = crc16(buffer); + if crc != calc_crc { + return Err(Error::CrcError(crc, calc_crc)); + } + } + + Ok(()) + } + + /// Write an arbitrary number of bytes to the card using the SD protocol and + /// an optional CRC. + fn write_data(&mut self, token: u8, buffer: &[u8]) -> Result<(), Error> { + self.write_byte(token)?; + self.write_bytes(buffer)?; + let crc_bytes = if self.options.use_crc { + crc16(buffer).to_be_bytes() + } else { + [0xFF, 0xFF] + }; + // These two bytes are always sent. They are either a valid CRC, or + // junk, depending on whether CRC mode was enabled. + self.write_bytes(&crc_bytes)?; + + let status = self.read_byte()?; + if (status & DATA_RES_MASK) != DATA_RES_ACCEPTED { + Err(Error::WriteError) + } else { + Ok(()) + } + } + + /// Check the card is initialised. + fn check_init(&mut self) -> Result<(), Error> { + if self.card_type.is_none() { + // If we don't know what the card type is, try and initialise the + // card. This will tell us what type of card it is. + self.acquire() + } else { + Ok(()) + } + } + + /// Initializes the card into a known state (or at least tries to). + fn acquire(&mut self) -> Result<(), Error> { + debug!("acquiring card with opts: {:?}", self.options); + let f = |s: &mut Self| { + // Assume it hasn't worked + let mut card_type; + trace!("Reset card.."); + // Enter SPI mode. + let mut delay = Delay::new(s.options.acquire_retries); + for _attempts in 1.. { + trace!("Enter SPI mode, attempt: {}..", _attempts); + match s.card_command(CMD0, 0) { + Err(Error::TimeoutCommand(0)) => { + // Try again? + warn!("Timed out, trying again.."); + // Try flushing the card as done here: https://github.com/greiman/SdFat/blob/master/src/SdCard/SdSpiCard.cpp#L170, + // https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/65#issuecomment-1270709448 + for _ in 0..0xFF { + s.write_byte(0xFF)?; + } + } + Err(e) => { + return Err(e); + } + Ok(R1_IDLE_STATE) => { + break; + } + Ok(_r) => { + // Try again + warn!("Got response: {:x}, trying again..", _r); + } + } + + delay.delay(&mut s.delayer, Error::CardNotFound)?; + } + // Enable CRC + debug!("Enable CRC: {}", s.options.use_crc); + // "The SPI interface is initialized in the CRC OFF mode in default" + // -- SD Part 1 Physical Layer Specification v9.00, Section 7.2.2 Bus Transfer Protection + if s.options.use_crc && s.card_command(CMD59, 1)? != R1_IDLE_STATE { + return Err(Error::CantEnableCRC); + } + // Check card version + let mut delay = Delay::new_command(); + let arg = loop { + if s.card_command(CMD8, 0x1AA)? == (R1_ILLEGAL_COMMAND | R1_IDLE_STATE) { + card_type = CardType::SD1; + break 0; + } + let mut buffer = [0xFF; 4]; + s.transfer_bytes(&mut buffer)?; + let status = buffer[3]; + if status == 0xAA { + card_type = CardType::SD2; + break 0x4000_0000; + } + delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD8))?; + }; + + let mut delay = Delay::new_command(); + while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { + delay.delay(&mut s.delayer, Error::TimeoutACommand(ACMD41))?; + } + + if card_type == CardType::SD2 { + if s.card_command(CMD58, 0)? != 0 { + return Err(Error::Cmd58Error); + } + let mut buffer = [0xFF; 4]; + s.transfer_bytes(&mut buffer)?; + if (buffer[0] & 0xC0) == 0xC0 { + card_type = CardType::SDHC; + } + // Ignore the other three bytes + } + debug!("Card version: {:?}", card_type); + s.card_type = Some(card_type); + Ok(()) + }; + let result = f(self); + let _ = self.read_byte(); + result + } + + /// Perform an application-specific command. + fn card_acmd(&mut self, command: u8, arg: u32) -> Result { + self.card_command(CMD55, 0)?; + self.card_command(command, arg) + } + + /// Perform a command. + fn card_command(&mut self, command: u8, arg: u32) -> Result { + if command != CMD0 && command != CMD12 { + self.wait_not_busy(Delay::new_command())?; + } + + let mut buf = [ + 0x40 | command, + (arg >> 24) as u8, + (arg >> 16) as u8, + (arg >> 8) as u8, + arg as u8, + 0, + ]; + buf[5] = crc7(&buf[0..5]); + + self.write_bytes(&buf)?; + + // skip stuff byte for stop read + if command == CMD12 { + let _result = self.read_byte()?; + } + + let mut delay = Delay::new_command(); + loop { + let result = self.read_byte()?; + if (result & 0x80) == ERROR_OK { + return Ok(result); + } + delay.delay(&mut self.delayer, Error::TimeoutCommand(command))?; + } + } + + /// Receive a byte from the SPI bus by clocking out an 0xFF byte. + fn read_byte(&mut self) -> Result { + self.transfer_byte(0xFF) + } + + /// Send a byte over the SPI bus and ignore what comes back. + fn write_byte(&mut self, out: u8) -> Result<(), Error> { + let _ = self.transfer_byte(out)?; + Ok(()) + } + + /// Send one byte and receive one byte over the SPI bus. + fn transfer_byte(&mut self, out: u8) -> Result { + let mut read_buf = [0u8; 1]; + self.spi + .transfer(&mut read_buf, &[out]) + .map_err(|_| Error::Transport)?; + Ok(read_buf[0]) + } + + /// Send multiple bytes and ignore what comes back over the SPI bus. + fn write_bytes(&mut self, out: &[u8]) -> Result<(), Error> { + self.spi.write(out).map_err(|_e| Error::Transport)?; + Ok(()) + } + + /// Send multiple bytes and replace them with what comes back over the SPI bus. + fn transfer_bytes(&mut self, in_out: &mut [u8]) -> Result<(), Error> { + self.spi + .transfer_in_place(in_out) + .map_err(|_e| Error::Transport)?; + Ok(()) + } + + /// Spin until the card returns 0xFF, or we spin too many times and + /// timeout. + fn wait_not_busy(&mut self, mut delay: Delay) -> Result<(), Error> { + loop { + let s = self.read_byte()?; + if s == 0xFF { + break; + } + delay.delay(&mut self.delayer, Error::TimeoutWaitNotBusy)?; + } + Ok(()) + } +} + +/// Options for acquiring the card. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug)] +pub struct AcquireOpts { + /// Set to true to enable CRC checking on reading/writing blocks of data. + /// + /// Set to false to disable the CRC. Some cards don't support CRC correctly + /// and this option may be useful in that instance. + /// + /// On by default because without it you might get silent data corruption on + /// your card. + pub use_crc: bool, + + /// Sets the number of times we will retry to acquire the card before giving up and returning + /// `Err(Error::CardNotFound)`. By default, card acquisition will be retried 50 times. + pub acquire_retries: u32, +} + +impl Default for AcquireOpts { + fn default() -> Self { + AcquireOpts { + use_crc: true, + acquire_retries: 50, + } + } +} + +/// The possible errors this crate can generate. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone)] +pub enum Error { + /// We got an error from the SPI peripheral + Transport, + /// We failed to enable CRC checking on the SD card + CantEnableCRC, + /// We didn't get a response when reading data from the card + TimeoutReadBuffer, + /// We didn't get a response when waiting for the card to not be busy + TimeoutWaitNotBusy, + /// We didn't get a response when executing this command + TimeoutCommand(u8), + /// We didn't get a response when executing this application-specific command + TimeoutACommand(u8), + /// We got a bad response from Command 58 + Cmd58Error, + /// We failed to read the Card Specific Data register + RegisterReadError, + /// We got a CRC mismatch (card gave us, we calculated) + CrcError(u16, u16), + /// Error reading from the card + ReadError, + /// Error writing to the card + WriteError, + /// Can't perform this operation with the card in this state + BadState, + /// Couldn't find the card + CardNotFound, + /// Couldn't set a GPIO pin + GpioError, +} + +/// The different types of card we support. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CardType { + /// An standard-capacity SD Card supporting v1.x of the standard. + /// + /// Uses byte-addressing internally, so limited to 2GiB in size. + SD1, + /// An standard-capacity SD Card supporting v2.x of the standard. + /// + /// Uses byte-addressing internally, so limited to 2GiB in size. + SD2, + /// An high-capacity 'SDHC' Card. + /// + /// Uses block-addressing internally to support capacities above 2GiB. + SDHC, +} + +/// This an object you can use to busy-wait with a timeout. +/// +/// Will let you call `delay` up to `max_retries` times before `delay` returns +/// an error. +struct Delay { + retries_left: u32, +} + +impl Delay { + /// The default number of retries for a read operation. + /// + /// At ~10us each this is ~100ms. + /// + /// See `Part1_Physical_Layer_Simplified_Specification_Ver9.00-1.pdf` Section 4.6.2.1 + pub const DEFAULT_READ_RETRIES: u32 = 10_000; + + /// The default number of retries for a write operation. + /// + /// At ~10us each this is ~500ms. + /// + /// See `Part1_Physical_Layer_Simplified_Specification_Ver9.00-1.pdf` Section 4.6.2.2 + pub const DEFAULT_WRITE_RETRIES: u32 = 50_000; + + /// The default number of retries for a control command. + /// + /// At ~10us each this is ~100ms. + /// + /// No value is given in the specification, so we pick the same as the read timeout. + pub const DEFAULT_COMMAND_RETRIES: u32 = 10_000; + + /// Create a new Delay object with the given maximum number of retries. + fn new(max_retries: u32) -> Delay { + Delay { + retries_left: max_retries, + } + } + + /// Create a new Delay object with the maximum number of retries for a read operation. + fn new_read() -> Delay { + Delay::new(Self::DEFAULT_READ_RETRIES) + } + + /// Create a new Delay object with the maximum number of retries for a write operation. + fn new_write() -> Delay { + Delay::new(Self::DEFAULT_WRITE_RETRIES) + } + + /// Create a new Delay object with the maximum number of retries for a command operation. + fn new_command() -> Delay { + Delay::new(Self::DEFAULT_COMMAND_RETRIES) + } + + /// Wait for a while. + /// + /// Checks the retry counter first, and if we hit the max retry limit, the + /// value `err` is returned. Otherwise we wait for 10us and then return + /// `Ok(())`. + fn delay(&mut self, delayer: &mut T, err: Error) -> Result<(), Error> + where + T: embedded_hal::delay::DelayNs, + { + if self.retries_left == 0 { + Err(err) + } else { + delayer.delay_us(10); + self.retries_left -= 1; + Ok(()) + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/sdcard/proto.rs b/examples/ios/embedded-sdmmc/src/sdcard/proto.rs new file mode 100644 index 0000000..68ae248 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/sdcard/proto.rs @@ -0,0 +1,739 @@ +//! Constants from the SD Specifications +//! +//! Based on SdFat, under the following terms: +//! +//! > Copyright (c) 2011-2018 Bill Greiman +//! > This file is part of the SdFat library for SD memory cards. +//! > +//! > MIT License +//! > +//! > Permission is hereby granted, free of charge, to any person obtaining a +//! > copy of this software and associated documentation files (the "Software"), +//! > to deal in the Software without restriction, including without limitation +//! > the rights to use, copy, modify, merge, publish, distribute, sublicense, +//! > and/or sell copies of the Software, and to permit persons to whom the +//! > Software is furnished to do so, subject to the following conditions: +//! > +//! > The above copyright notice and this permission notice shall be included +//! > in all copies or substantial portions of the Software. +//! > +//! > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +//! > OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//! > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//! > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +//! > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +//! > DEALINGS IN THE SOFTWARE. + +//============================================================================== + +// Possible errors the SD card can return + +/// Card indicates last operation was a success +pub const ERROR_OK: u8 = 0x00; + +//============================================================================== + +// SD Card Commands + +/// GO_IDLE_STATE - init card in spi mode if CS low +pub const CMD0: u8 = 0x00; +/// SEND_IF_COND - verify SD Memory Card interface operating condition.*/ +pub const CMD8: u8 = 0x08; +/// SEND_CSD - read the Card Specific Data (CSD register) +pub const CMD9: u8 = 0x09; +/// STOP_TRANSMISSION - end multiple block read sequence +pub const CMD12: u8 = 0x0C; +/// SEND_STATUS - read the card status register +pub const CMD13: u8 = 0x0D; +/// READ_SINGLE_BLOCK - read a single data block from the card +pub const CMD17: u8 = 0x11; +/// READ_MULTIPLE_BLOCK - read a multiple data blocks from the card +pub const CMD18: u8 = 0x12; +/// WRITE_BLOCK - write a single data block to the card +pub const CMD24: u8 = 0x18; +/// WRITE_MULTIPLE_BLOCK - write blocks of data until a STOP_TRANSMISSION +pub const CMD25: u8 = 0x19; +/// APP_CMD - escape for application specific command +pub const CMD55: u8 = 0x37; +/// READ_OCR - read the OCR register of a card +pub const CMD58: u8 = 0x3A; +/// CRC_ON_OFF - enable or disable CRC checking +pub const CMD59: u8 = 0x3B; +/// Pre-erased before writing +/// +/// > It is recommended using this command preceding CMD25, some of the cards will be faster for Multiple +/// > Write Blocks operation. Note that the host should send ACMD23 just before WRITE command if the host +/// > wants to use the pre-erased feature +pub const ACMD23: u8 = 0x17; +/// SD_SEND_OP_COMD - Sends host capacity support information and activates +/// the card's initialization process +pub const ACMD41: u8 = 0x29; + +//============================================================================== + +/// status for card in the ready state +pub const R1_READY_STATE: u8 = 0x00; + +/// status for card in the idle state +pub const R1_IDLE_STATE: u8 = 0x01; + +/// status bit for illegal command +pub const R1_ILLEGAL_COMMAND: u8 = 0x04; + +/// start data token for read or write single block*/ +pub const DATA_START_BLOCK: u8 = 0xFE; + +/// stop token for write multiple blocks*/ +pub const STOP_TRAN_TOKEN: u8 = 0xFD; + +/// start data token for write multiple blocks*/ +pub const WRITE_MULTIPLE_TOKEN: u8 = 0xFC; + +/// mask for data response tokens after a write block operation +pub const DATA_RES_MASK: u8 = 0x1F; + +/// write data accepted token +pub const DATA_RES_ACCEPTED: u8 = 0x05; + +/// Card Specific Data, version 1 +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +pub struct CsdV1 { + /// The 16-bytes of data in this Card Specific Data block + pub data: [u8; 16], +} + +/// Card Specific Data, version 2 +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +pub struct CsdV2 { + /// The 16-bytes of data in this Card Specific Data block + pub data: [u8; 16], +} + +/// Card Specific Data +#[derive(Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +pub enum Csd { + /// A version 1 CSD + V1(CsdV1), + /// A version 2 CSD + V2(CsdV2), +} + +impl CsdV1 { + /// Create a new, empty, CSD + pub fn new() -> CsdV1 { + CsdV1::default() + } + + define_field!(csd_ver, u8, 0, 6, 2); + define_field!(data_read_access_time1, u8, 1, 0, 8); + define_field!(data_read_access_time2, u8, 2, 0, 8); + define_field!(max_data_transfer_rate, u8, 3, 0, 8); + define_field!(card_command_classes, u16, [(4, 0, 8), (5, 4, 4)]); + define_field!(read_block_length, u8, 5, 0, 4); + define_field!(read_partial_blocks, bool, 6, 7); + define_field!(write_block_misalignment, bool, 6, 6); + define_field!(read_block_misalignment, bool, 6, 5); + define_field!(dsr_implemented, bool, 6, 4); + define_field!(device_size, u32, [(6, 0, 2), (7, 0, 8), (8, 6, 2)]); + define_field!(max_read_current_vdd_max, u8, 8, 0, 3); + define_field!(max_read_current_vdd_min, u8, 8, 3, 3); + define_field!(max_write_current_vdd_max, u8, 9, 2, 3); + define_field!(max_write_current_vdd_min, u8, 9, 5, 3); + define_field!(device_size_multiplier, u8, [(9, 0, 2), (10, 7, 1)]); + define_field!(erase_single_block_enabled, bool, 10, 6); + define_field!(erase_sector_size, u8, [(10, 0, 6), (11, 7, 1)]); + define_field!(write_protect_group_size, u8, 11, 0, 7); + define_field!(write_protect_group_enable, bool, 12, 7); + define_field!(write_speed_factor, u8, 12, 2, 3); + define_field!(max_write_data_length, u8, [(12, 0, 2), (13, 6, 2)]); + define_field!(write_partial_blocks, bool, 13, 5); + define_field!(file_format, u8, 14, 2, 2); + define_field!(temporary_write_protection, bool, 14, 4); + define_field!(permanent_write_protection, bool, 14, 5); + define_field!(copy_flag_set, bool, 14, 6); + define_field!(file_format_group_set, bool, 14, 7); + define_field!(crc, u8, 15, 0, 8); + + /// Returns the card capacity in bytes + pub fn card_capacity_bytes(&self) -> u64 { + let multiplier = self.device_size_multiplier() + self.read_block_length() + 2; + (u64::from(self.device_size()) + 1) << multiplier + } + + /// Returns the card capacity in 512-byte blocks + pub fn card_capacity_blocks(&self) -> u32 { + let multiplier = self.device_size_multiplier() + self.read_block_length() - 7; + (self.device_size() + 1) << multiplier + } +} + +impl CsdV2 { + /// Create a new, empty, CSD + pub fn new() -> CsdV2 { + CsdV2::default() + } + + define_field!(csd_ver, u8, 0, 6, 2); + define_field!(data_read_access_time1, u8, 1, 0, 8); + define_field!(data_read_access_time2, u8, 2, 0, 8); + define_field!(max_data_transfer_rate, u8, 3, 0, 8); + define_field!(card_command_classes, u16, [(4, 0, 8), (5, 4, 4)]); + define_field!(read_block_length, u8, 5, 0, 4); + define_field!(read_partial_blocks, bool, 6, 7); + define_field!(write_block_misalignment, bool, 6, 6); + define_field!(read_block_misalignment, bool, 6, 5); + define_field!(dsr_implemented, bool, 6, 4); + define_field!(device_size, u32, [(7, 0, 6), (8, 0, 8), (9, 0, 8)]); + define_field!(erase_single_block_enabled, bool, 10, 6); + define_field!(erase_sector_size, u8, [(10, 0, 6), (11, 7, 1)]); + define_field!(write_protect_group_size, u8, 11, 0, 7); + define_field!(write_protect_group_enable, bool, 12, 7); + define_field!(write_speed_factor, u8, 12, 2, 3); + define_field!(max_write_data_length, u8, [(12, 0, 2), (13, 6, 2)]); + define_field!(write_partial_blocks, bool, 13, 5); + define_field!(file_format, u8, 14, 2, 2); + define_field!(temporary_write_protection, bool, 14, 4); + define_field!(permanent_write_protection, bool, 14, 5); + define_field!(copy_flag_set, bool, 14, 6); + define_field!(file_format_group_set, bool, 14, 7); + define_field!(crc, u8, 15, 0, 8); + + /// Returns the card capacity in bytes + pub fn card_capacity_bytes(&self) -> u64 { + (u64::from(self.device_size()) + 1) * 512 * 1024 + } + + /// Returns the card capacity in 512-byte blocks + pub fn card_capacity_blocks(&self) -> u32 { + (self.device_size() + 1) * 1024 + } +} + +/// Perform the 7-bit CRC used on the SD card +pub fn crc7(data: &[u8]) -> u8 { + let mut crc = 0u8; + for mut d in data.iter().cloned() { + for _bit in 0..8 { + crc <<= 1; + if ((d & 0x80) ^ (crc & 0x80)) != 0 { + crc ^= 0x09; + } + d <<= 1; + } + } + (crc << 1) | 1 +} + +/// Perform the X25 CRC calculation, as used for data blocks. +pub fn crc16(data: &[u8]) -> u16 { + let mut crc = 0u16; + for &byte in data { + crc = ((crc >> 8) & 0xFF) | (crc << 8); + crc ^= u16::from(byte); + crc ^= (crc & 0xFF) >> 4; + crc ^= crc << 12; + crc ^= (crc & 0xFF) << 5; + } + crc +} + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_crc7() { + const DATA: [u8; 15] = hex!("00 26 00 32 5F 59 83 C8 AD DB CF FF D2 40 40"); + assert_eq!(crc7(&DATA), 0xA5); + } + + #[test] + fn test_crc16() { + // An actual CSD read from an SD card + const DATA: [u8; 16] = hex!("00 26 00 32 5F 5A 83 AE FE FB CF FF 92 80 40 DF"); + assert_eq!(crc16(&DATA), 0x9fc5); + } + + #[test] + fn test_csdv1b() { + const EXAMPLE: CsdV1 = CsdV1 { + data: hex!("00 26 00 32 5F 59 83 C8 AD DB CF FF D2 40 40 A5"), + }; + + // CSD Structure: describes version of CSD structure + // 0b00 [Interpreted: Version 1.0] + assert_eq!(EXAMPLE.csd_ver(), 0x00); + + // Data Read Access Time 1: defines Asynchronous part of the read + // access time 0x26 [Interpreted: 1.5 x 1ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x26); + + // Data Read Access Time 2: worst case clock dependent factor for data + // access time 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: 0x5f5 [Interpreted: Class 0: Yes. Class 1: + // No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: + // Yes. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class + // 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5f5); + + // Max Read Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x09); + + // Partial Blocks for Read Allowed: + // 0b1 [Interpreted: Yes] + assert!(EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on + // card 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)*device size multiplier*max read data block + // length) bytes 0xf22 [Decimal: 3874] + assert_eq!(EXAMPLE.device_size(), 3874); + + // Max Read Current @ VDD Min: + // 0x5 [Interpreted: 35mA] + assert_eq!(EXAMPLE.max_read_current_vdd_min(), 5); + + // Max Read Current @ VDD Max: + // 0x5 [Interpreted: 80mA] + assert_eq!(EXAMPLE.max_read_current_vdd_max(), 5); + + // Max Write Current @ VDD Min: + // 0x6 [Interpreted: 60mA] + assert_eq!(EXAMPLE.max_write_current_vdd_min(), 6); + + // Max Write Current @ VDD Max:: + // 0x6 [Interpreted: 200mA] + assert_eq!(EXAMPLE.max_write_current_vdd_max(), 6); + + // Device Size Multiplier: + // 0x7 [Interpreted: x512] + assert_eq!(EXAMPLE.device_size_multiplier(), 7); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x1f [Interpreted: 32 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x1F); + + // Write Protect Group Size: + // 0x7f [Interpreted: 128 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x7f); + + // Write Protect Group Enable: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x4 [Interpreted: x16] + assert_eq!(EXAMPLE.write_speed_factor(), 0x4); + + // Max Write Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0x9); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b1 [Interpreted: Non-Original] + assert!(EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0xa5 + assert_eq!(EXAMPLE.crc(), 0xa5); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 1_015_808_000); + assert_eq!(EXAMPLE.card_capacity_blocks(), 1_984_000); + } + + #[test] + fn test_csdv1() { + const EXAMPLE: CsdV1 = CsdV1 { + data: hex!("00 7F 00 32 5B 5A 83 AF 7F FF CF 80 16 80 00 6F"), + }; + // CSD Structure: describes version of CSD structure + // 0b00 [Interpreted: Version 1.0] + assert_eq!(EXAMPLE.csd_ver(), 0x00); + + // Data Read Access Time 1: defines Asynchronous part of the read access time + // 0x7f [Interpreted: 8.0 x 10ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x7F); + + // Data Read Access Time 2: worst case clock dependent factor for data access time + // 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: + // 0x5b5 [Interpreted: Class 0: Yes. Class 1: No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: No. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5b5); + + // Max Read Data Block Length: + // 0xa [Interpreted: 1024 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x0a); + + // Partial Blocks for Read Allowed: + // 0b1 [Interpreted: Yes] + assert!(EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on card + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)*device size multiplier*max read data block + // length) bytes 0xebd [Decimal: 3773] + assert_eq!(EXAMPLE.device_size(), 3773); + + // Max Read Current @ VDD Min: + // 0x7 [Interpreted: 100mA] + assert_eq!(EXAMPLE.max_read_current_vdd_min(), 7); + + // Max Read Current @ VDD Max: + // 0x7 [Interpreted: 200mA] + assert_eq!(EXAMPLE.max_read_current_vdd_max(), 7); + + // Max Write Current @ VDD Min: + // 0x7 [Interpreted: 100mA] + assert_eq!(EXAMPLE.max_write_current_vdd_min(), 7); + + // Max Write Current @ VDD Max:: + // 0x7 [Interpreted: 200mA] + assert_eq!(EXAMPLE.max_write_current_vdd_max(), 7); + + // Device Size Multiplier: + // 0x7 [Interpreted: x512] + assert_eq!(EXAMPLE.device_size_multiplier(), 7); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x1f [Interpreted: 32 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x1F); + + // Write Protect Group Size: + // 0x00 [Interpreted: 1 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x00); + + // Write Protect Group Enable: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x5 [Interpreted: x32] + assert_eq!(EXAMPLE.write_speed_factor(), 0x5); + + // Max Write Data Block Length: + // 0xa [Interpreted: 1024 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0xa); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b0 [Interpreted: Original] + assert!(!EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0x6f + assert_eq!(EXAMPLE.crc(), 0x6F); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 1_978_662_912); + assert_eq!(EXAMPLE.card_capacity_blocks(), 3_864_576); + } + + #[test] + fn test_csdv2() { + const EXAMPLE: CsdV2 = CsdV2 { + data: hex!("40 0E 00 32 5B 59 00 00 1D 69 7F 80 0A 40 00 8B"), + }; + // CSD Structure: describes version of CSD structure + // 0b01 [Interpreted: Version 2.0 SDHC] + assert_eq!(EXAMPLE.csd_ver(), 0x01); + + // Data Read Access Time 1: defines Asynchronous part of the read access time + // 0x0e [Interpreted: 1.0 x 1ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x0E); + + // Data Read Access Time 2: worst case clock dependent factor for data access time + // 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: + // 0x5b5 [Interpreted: Class 0: Yes. Class 1: No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: No. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5b5); + + // Max Read Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x09); + + // Partial Blocks for Read Allowed: + // 0b0 [Interpreted: Yes] + assert!(!EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on card + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)* 512kbytes + // 0x001d69 [Decimal: 7529] + assert_eq!(EXAMPLE.device_size(), 7529); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x7f [Interpreted: 128 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x7F); + + // Write Protect Group Size: + // 0x00 [Interpreted: 1 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x00); + + // Write Protect Group Enable: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x2 [Interpreted: x4] + assert_eq!(EXAMPLE.write_speed_factor(), 0x2); + + // Max Write Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0x9); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b0 [Interpreted: Original] + assert!(!EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0x8b + assert_eq!(EXAMPLE.crc(), 0x8b); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 3_947_888_640); + assert_eq!(EXAMPLE.card_capacity_blocks(), 7_710_720); + } + + #[test] + fn test_csdv2b() { + const EXAMPLE: CsdV2 = CsdV2 { + data: hex!("40 0E 00 32 5B 59 00 00 3A 91 7F 80 0A 40 00 05"), + }; + // CSD Structure: describes version of CSD structure + // 0b01 [Interpreted: Version 2.0 SDHC] + assert_eq!(EXAMPLE.csd_ver(), 0x01); + + // Data Read Access Time 1: defines Asynchronous part of the read access time + // 0x0e [Interpreted: 1.0 x 1ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x0E); + + // Data Read Access Time 2: worst case clock dependent factor for data access time + // 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: + // 0x5b5 [Interpreted: Class 0: Yes. Class 1: No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: No. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5b5); + + // Max Read Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x09); + + // Partial Blocks for Read Allowed: + // 0b0 [Interpreted: Yes] + assert!(!EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on card + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)* 512kbytes + // 0x003a91 [Decimal: 7529] + assert_eq!(EXAMPLE.device_size(), 14993); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x7f [Interpreted: 128 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x7F); + + // Write Protect Group Size: + // 0x00 [Interpreted: 1 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x00); + + // Write Protect Group Enable: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x2 [Interpreted: x4] + assert_eq!(EXAMPLE.write_speed_factor(), 0x2); + + // Max Write Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0x9); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b0 [Interpreted: Original] + assert!(!EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0x05 + assert_eq!(EXAMPLE.crc(), 0x05); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 7_861_174_272); + assert_eq!(EXAMPLE.card_capacity_blocks(), 15_353_856); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/structure.rs b/examples/ios/embedded-sdmmc/src/structure.rs new file mode 100644 index 0000000..fb0e553 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/structure.rs @@ -0,0 +1,64 @@ +//! Useful macros for parsing SD/MMC structures. + +macro_rules! access_field { + ($self:expr, $offset:expr, $start_bit:expr, 1) => { + ($self.data[$offset] & (1 << $start_bit)) != 0 + }; + ($self:expr, $offset:expr, $start:expr, $num_bits:expr) => { + ($self.data[$offset] >> $start) & (((1u16 << $num_bits) - 1) as u8) + }; +} + +macro_rules! define_field { + ($name:ident, bool, $offset:expr, $bit:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> bool { + access_field!(self, $offset, $bit, 1) + } + }; + ($name:ident, u8, $offset:expr, $start_bit:expr, $num_bits:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> u8 { + access_field!(self, $offset, $start_bit, $num_bits) + } + }; + ($name:ident, $type:ty, [ $( ( $offset:expr, $start_bit:expr, $num_bits:expr ) ),+ ]) => { + /// Gets the value from the $name field + pub fn $name(&self) -> $type { + let mut result = 0; + $( + result <<= $num_bits; + let part = access_field!(self, $offset, $start_bit, $num_bits) as $type; + result |= part; + )+ + result + } + }; + + ($name:ident, u8, $offset:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> u8 { + self.data[$offset] + } + }; + + ($name:ident, u16, $offset:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> u16 { + LittleEndian::read_u16(&self.data[$offset..$offset+2]) + } + }; + + ($name:ident, u32, $offset:expr) => { + /// Get the $name field + pub fn $name(&self) -> u32 { + LittleEndian::read_u32(&self.data[$offset..$offset+4]) + } + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/volume_mgr.rs b/examples/ios/embedded-sdmmc/src/volume_mgr.rs new file mode 100644 index 0000000..3a9ca13 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/volume_mgr.rs @@ -0,0 +1,1550 @@ +//! The Volume Manager implementation. +//! +//! The volume manager handles partitions and open files on a block device. + +use core::cell::RefCell; +use core::convert::TryFrom; +use core::ops::DerefMut; + +use byteorder::{ByteOrder, LittleEndian}; +use heapless::Vec; + +use crate::{ + debug, fat, + filesystem::{ + Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, LfnBuffer, Mode, + RawDirectory, RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, + }, + trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, + Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, + PARTITION_ID_FAT16_SMALL, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, +}; + +/// Wraps a block device and gives access to the FAT-formatted volumes within +/// it. +/// +/// Tracks which files and directories are open, to prevent you from deleting +/// a file or directory you currently have open. +#[derive(Debug)] +pub struct VolumeManager< + D, + T, + const MAX_DIRS: usize = 4, + const MAX_FILES: usize = 4, + const MAX_VOLUMES: usize = 1, +> where + D: BlockDevice, + T: TimeSource, + ::Error: core::fmt::Debug, +{ + time_source: T, + data: RefCell>, +} + +impl VolumeManager +where + D: BlockDevice, + T: TimeSource, + ::Error: core::fmt::Debug, +{ + /// Create a new Volume Manager using a generic `BlockDevice`. From this + /// object we can open volumes (partitions) and with those we can open + /// files. + /// + /// This creates a `VolumeManager` with default values + /// MAX_DIRS = 4, MAX_FILES = 4, MAX_VOLUMES = 1. Call `VolumeManager::new_with_limits(block_device, time_source)` + /// if you need different limits. + pub fn new(block_device: D, time_source: T) -> VolumeManager { + // Pick a random starting point for the IDs that's not zero, because + // zero doesn't stand out in the logs. + Self::new_with_limits(block_device, time_source, 5000) + } +} + +impl + VolumeManager +where + D: BlockDevice, + T: TimeSource, + ::Error: core::fmt::Debug, +{ + /// Create a new Volume Manager using a generic `BlockDevice`. From this + /// object we can open volumes (partitions) and with those we can open + /// files. + /// + /// You can also give an offset for all the IDs this volume manager + /// generates, which might help you find the IDs in your logs when + /// debugging. + pub fn new_with_limits( + block_device: D, + time_source: T, + id_offset: u32, + ) -> VolumeManager { + debug!("Creating new embedded-sdmmc::VolumeManager"); + VolumeManager { + time_source, + data: RefCell::new(VolumeManagerData { + block_cache: BlockCache::new(block_device), + id_generator: HandleGenerator::new(id_offset), + open_volumes: Vec::new(), + open_dirs: Vec::new(), + open_files: Vec::new(), + }), + } + } + + /// Temporarily get access to the underlying block device. + pub fn device(&self, f: F) -> T + where + F: FnOnce(&mut D) -> T, + { + let mut data = self.data.borrow_mut(); + let result = f(data.block_cache.block_device()); + result + } + + /// Get a volume (or partition) based on entries in the Master Boot Record. + /// + /// We do not support GUID Partition Table disks. Nor do we support any + /// concept of drive letters - that is for a higher layer to handle. + pub fn open_volume( + &self, + volume_idx: VolumeIdx, + ) -> Result, Error> { + let v = self.open_raw_volume(volume_idx)?; + Ok(v.to_volume(self)) + } + + /// Try to open a special volume, IE no MBR is availble + pub unsafe fn open_special( + &self, + part_type: u8, + lba_start: BlockIdx, + num_blocks: BlockCount, + ) -> Result, Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + let raw_volume = match part_type { + PARTITION_ID_FAT32_CHS_LBA + | PARTITION_ID_FAT32_LBA + | PARTITION_ID_FAT16_LBA + | PARTITION_ID_FAT16 + | PARTITION_ID_FAT16_SMALL => { + let volume = fat::parse_volume(&mut data.block_cache, lba_start, num_blocks)?; + let id = RawVolume(data.id_generator.generate()); + let info = VolumeInfo { + raw_volume: id, + idx: VolumeIdx(0), + volume_type: volume, + }; + // We already checked for space + data.open_volumes.push(info).unwrap(); + Ok(id) + } + _ => Err(Error::FormatError("Partition type not supported")), + }; + Ok(raw_volume?.to_volume(self)) + } + + /// Get a volume (or partition) based on entries in the Master Boot Record. + /// + /// We do not support GUID Partition Table disks. Nor do we support any + /// concept of drive letters - that is for a higher layer to handle. + /// + /// This function gives you a `RawVolume` and you must close the volume by + /// calling `VolumeManager::close_volume`. + pub fn open_raw_volume(&self, volume_idx: VolumeIdx) -> Result> { + const PARTITION1_START: usize = 446; + const PARTITION2_START: usize = PARTITION1_START + PARTITION_INFO_LENGTH; + const PARTITION3_START: usize = PARTITION2_START + PARTITION_INFO_LENGTH; + const PARTITION4_START: usize = PARTITION3_START + PARTITION_INFO_LENGTH; + const FOOTER_START: usize = 510; + const FOOTER_VALUE: u16 = 0xAA55; + const PARTITION_INFO_LENGTH: usize = 16; + const PARTITION_INFO_STATUS_INDEX: usize = 0; + const PARTITION_INFO_TYPE_INDEX: usize = 4; + const PARTITION_INFO_LBA_START_INDEX: usize = 8; + const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; + + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + if data.open_volumes.is_full() { + return Err(Error::TooManyOpenVolumes); + } + + for v in data.open_volumes.iter() { + if v.idx == volume_idx { + return Err(Error::VolumeAlreadyOpen); + } + } + + let (part_type, lba_start, num_blocks) = { + trace!("Reading partition table"); + let block = data + .block_cache + .read(BlockIdx(0)) + .map_err(Error::DeviceError)?; + // We only support Master Boot Record (MBR) partitioned cards, not + // GUID Partition Table (GPT) + if LittleEndian::read_u16(&block[FOOTER_START..FOOTER_START + 2]) != FOOTER_VALUE { + return Err(Error::FormatError("Invalid MBR signature")); + } + let partition = match volume_idx { + VolumeIdx(0) => { + &block[PARTITION1_START..(PARTITION1_START + PARTITION_INFO_LENGTH)] + } + VolumeIdx(1) => { + &block[PARTITION2_START..(PARTITION2_START + PARTITION_INFO_LENGTH)] + } + VolumeIdx(2) => { + &block[PARTITION3_START..(PARTITION3_START + PARTITION_INFO_LENGTH)] + } + VolumeIdx(3) => { + &block[PARTITION4_START..(PARTITION4_START + PARTITION_INFO_LENGTH)] + } + _ => { + return Err(Error::NoSuchVolume); + } + }; + // Only 0x80 and 0x00 are valid (bootable, and non-bootable) + if (partition[PARTITION_INFO_STATUS_INDEX] & 0x7F) != 0x00 { + return Err(Error::FormatError("Invalid partition status")); + } + let lba_start = LittleEndian::read_u32( + &partition[PARTITION_INFO_LBA_START_INDEX..(PARTITION_INFO_LBA_START_INDEX + 4)], + ); + let num_blocks = LittleEndian::read_u32( + &partition[PARTITION_INFO_NUM_BLOCKS_INDEX..(PARTITION_INFO_NUM_BLOCKS_INDEX + 4)], + ); + ( + partition[PARTITION_INFO_TYPE_INDEX], + BlockIdx(lba_start), + BlockCount(num_blocks), + ) + }; + match part_type { + PARTITION_ID_FAT32_CHS_LBA + | PARTITION_ID_FAT32_LBA + | PARTITION_ID_FAT16_LBA + | PARTITION_ID_FAT16 + | PARTITION_ID_FAT16_SMALL => { + let volume = fat::parse_volume(&mut data.block_cache, lba_start, num_blocks)?; + let id = RawVolume(data.id_generator.generate()); + let info = VolumeInfo { + raw_volume: id, + idx: volume_idx, + volume_type: volume, + }; + // We already checked for space + data.open_volumes.push(info).unwrap(); + Ok(id) + } + _ => Err(Error::FormatError("Partition type not supported")), + } + } + + /// Open the volume's root directory. + /// + /// You can then read the directory entries with `iterate_dir`, or you can + /// use `open_file_in_dir`. + pub fn open_root_dir(&self, volume: RawVolume) -> Result> { + debug!("Opening root on {:?}", volume); + + // Opening a root directory twice is OK + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + let directory_id = RawDirectory(data.id_generator.generate()); + let dir_info = DirectoryInfo { + raw_volume: volume, + cluster: ClusterId::ROOT_DIR, + raw_directory: directory_id, + }; + + data.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + debug!("Opened root on {:?}, got {:?}", volume, directory_id); + + Ok(directory_id) + } + + /// Open a directory. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + /// + /// Passing "." as the name results in opening the `parent_dir` a second time. + pub fn open_dir( + &self, + parent_dir: RawDirectory, + name: N, + ) -> Result> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + if data.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); + } + + // Find dir by ID + let parent_dir_idx = data.get_dir_by_id(parent_dir)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[parent_dir_idx].raw_volume)?; + let short_file_name = name.to_short_filename().map_err(Error::FilenameError)?; + + // Open the directory + + // Should we short-cut? (root dir doesn't have ".") + if short_file_name == ShortFileName::this_dir() { + let directory_id = RawDirectory(data.id_generator.generate()); + let dir_info = DirectoryInfo { + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, + cluster: data.open_dirs[parent_dir_idx].cluster, + }; + + data.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + return Ok(directory_id); + } + + // ok we'll actually look for the directory then + + let dir_entry = match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[parent_dir_idx], + &short_file_name, + )?, + }; + + debug!("Found dir entry: {:?}", dir_entry); + + if !dir_entry.attributes.is_directory() { + return Err(Error::OpenedFileAsDir); + } + + // We don't check if the directory is already open - directories hold + // no cached state and so opening a directory twice is allowable. + + // Remember this open directory. + let directory_id = RawDirectory(data.id_generator.generate()); + let dir_info = DirectoryInfo { + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, + cluster: dir_entry.cluster, + }; + + data.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + Ok(directory_id) + } + + /// Close a directory. You cannot perform operations on an open directory + /// and so must close it if you want to do something with it. + pub fn close_dir(&self, directory: RawDirectory) -> Result<(), Error> { + debug!("Closing {:?}", directory); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + for (idx, info) in data.open_dirs.iter().enumerate() { + if directory == info.raw_directory { + data.open_dirs.swap_remove(idx); + return Ok(()); + } + } + Err(Error::BadHandle) + } + + /// Close a volume + /// + /// You can't close it if there are any files or directories open on it. + pub fn close_volume(&self, volume: RawVolume) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + for f in data.open_files.iter() { + if f.raw_volume == volume { + return Err(Error::VolumeStillInUse); + } + } + + for d in data.open_dirs.iter() { + if d.raw_volume == volume { + return Err(Error::VolumeStillInUse); + } + } + + let volume_idx = data.get_volume_by_id(volume)?; + + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.update_info_sector(&mut data.block_cache)?; + } + } + + data.open_volumes.swap_remove(volume_idx); + + Ok(()) + } + + /// Look in a directory for a named file. + pub fn find_directory_entry( + &self, + directory: RawDirectory, + name: N, + ) -> Result> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ) + } + } + } + + /// Call a callback function for each directory entry in a directory. + /// + /// Long File Names will be ignored. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir( + &self, + directory: RawDirectory, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry), + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.iterate_dir( + &mut data.block_cache, + &data.open_dirs[directory_idx], + |de| { + // Hide all the LFN directory entries + if !de.attributes.is_lfn() { + func(de); + } + }, + ) + } + } + } + + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. + /// + /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the + /// Long File Name. If you pass one that isn't large enough, any Long File + /// Names that don't fit will be ignored and presented as if they only had a + /// Short File Name. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir_lfn( + &self, + directory: RawDirectory, + lfn_buffer: &mut LfnBuffer<'_>, + func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + // This API doesn't care about the on-disk directory entry, so we discard it + fat.iterate_dir_lfn( + &mut data.block_cache, + lfn_buffer, + &data.open_dirs[directory_idx], + func, + ) + } + } + } + + /// Open a file with the given full path. A file can only be opened once. + pub fn open_file_in_dir( + &self, + directory: RawDirectory, + name: N, + mode: Mode, + ) -> Result> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + // This check is load-bearing - we do an unchecked push later. + if data.open_files.is_full() { + return Err(Error::TooManyOpenFiles); + } + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_id = data.open_dirs[directory_idx].raw_volume; + let volume_idx = data.get_volume_by_id(volume_id)?; + let volume_info = &data.open_volumes[volume_idx]; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + let dir_entry = match &volume_info.volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ), + }; + + let dir_entry = match dir_entry { + Ok(entry) => { + // we are opening an existing file + Some(entry) + } + Err(_) + if (mode == Mode::ReadWriteCreate) + | (mode == Mode::ReadWriteCreateOrTruncate) + | (mode == Mode::ReadWriteCreateOrAppend) => + { + // We are opening a non-existant file, but that's OK because they + // asked us to create it + None + } + _ => { + // We are opening a non-existant file, and that's not OK. + return Err(Error::NotFound); + } + }; + + // Check if it's open already + if let Some(dir_entry) = &dir_entry { + if data.file_is_open(volume_info.raw_volume, dir_entry) { + return Err(Error::FileAlreadyOpen); + } + } + + let mode = solve_mode_variant(mode, dir_entry.is_some()); + + match mode { + Mode::ReadWriteCreate => { + if dir_entry.is_some() { + return Err(Error::FileAlreadyExists); + } + let cluster = data.open_dirs[directory_idx].cluster; + let att = Attributes::create_from_fat(0); + let volume_idx = data.get_volume_by_id(volume_id)?; + let entry = match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.write_new_directory_entry( + &mut data.block_cache, + &self.time_source, + cluster, + sfn, + att, + )?, + }; + + let file_id = RawFile(data.id_generator.generate()); + + let file = FileInfo { + raw_file: file_id, + raw_volume: volume_id, + current_cluster: (0, entry.cluster), + current_offset: 0, + mode, + entry, + dirty: false, + }; + + // Remember this open file - can't be full as we checked already + unsafe { + data.open_files.push_unchecked(file); + } + + Ok(file_id) + } + _ => { + // Safe to unwrap, since we actually have an entry if we got here + let dir_entry = dir_entry.unwrap(); + + if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly { + return Err(Error::ReadOnly); + } + + if dir_entry.attributes.is_directory() { + return Err(Error::OpenedDirAsFile); + } + + // Check it's not already open + if data.file_is_open(volume_id, &dir_entry) { + return Err(Error::FileAlreadyOpen); + } + + let mode = solve_mode_variant(mode, true); + let raw_file = RawFile(data.id_generator.generate()); + + let file = match mode { + Mode::ReadOnly => FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }, + Mode::ReadWriteAppend => { + let mut file = FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }; + // seek_from_end with 0 can't fail + file.seek_from_end(0).ok(); + file + } + Mode::ReadWriteTruncate => { + let mut file = FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.truncate_cluster_chain( + &mut data.block_cache, + file.entry.cluster, + )?, + }; + file.update_length(0); + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + file.entry.mtime = self.time_source.get_timestamp(); + fat.write_entry_to_disk(&mut data.block_cache, &file.entry)?; + } + }; + + file + } + _ => return Err(Error::Unsupported), + }; + + // Remember this open file - can't be full as we checked already + unsafe { + data.open_files.push_unchecked(file); + } + + Ok(raw_file) + } + } + } + + /// Delete a closed file with the given filename, if it exists. + pub fn delete_file_in_dir( + &self, + directory: RawDirectory, + name: N, + ) -> Result<(), Error> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let dir_idx = data.get_dir_by_id(directory)?; + let dir_info = &data.open_dirs[dir_idx]; + let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + let dir_entry = match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry(&mut data.block_cache, dir_info, &sfn), + }?; + + if dir_entry.attributes.is_directory() { + return Err(Error::DeleteDirAsFile); + } + + if data.file_is_open(dir_info.raw_volume, &dir_entry) { + return Err(Error::FileAlreadyOpen); + } + + let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.delete_directory_entry(&mut data.block_cache, dir_info, &sfn)? + } + } + + Ok(()) + } + + /// Get the volume label + /// + /// Will look in the BPB for a volume label, and if nothing is found, will + /// search the root directory for a volume label. + pub fn get_root_volume_label( + &self, + raw_volume: RawVolume, + ) -> Result, Error> { + debug!("Reading volume label for {:?}", raw_volume); + // prefer the one in the BPB - it's easier to get + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let volume_idx = data.get_volume_by_id(raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + if !fat.name.name().is_empty() { + debug!( + "Got volume label {:?} for {:?} from BPB", + fat.name, raw_volume + ); + return Ok(Some(fat.name.clone())); + } + } + } + drop(data); + + // Nothing in the BPB, let's do it the slow way + let root_dir = self.open_root_dir(raw_volume)?.to_directory(self); + let mut maybe_volume_name = None; + root_dir.iterate_dir(|de| { + if maybe_volume_name.is_none() + && de.attributes == Attributes::create_from_fat(Attributes::VOLUME) + { + maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) + } + })?; + + debug!( + "Got volume label {:?} for {:?} from root", + maybe_volume_name, raw_volume + ); + + Ok(maybe_volume_name) + } + + /// Read from an open file. + pub fn read(&self, file: RawFile, buffer: &mut [u8]) -> Result> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let file_idx = data.get_file_by_id(file)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + + // Calculate which file block the current offset lies within + // While there is more to read, read the block and copy in to the buffer. + // If we need to find the next cluster, walk the FAT. + let mut space = buffer.len(); + let mut read = 0; + while space > 0 && !data.open_files[file_idx].eof() { + let mut current_cluster = data.open_files[file_idx].current_cluster; + let (block_idx, block_offset, block_avail) = data.find_data_on_disk( + volume_idx, + &mut current_cluster, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, + )?; + data.open_files[file_idx].current_cluster = current_cluster; + trace!("Reading file ID {:?}", file); + let block = data + .block_cache + .read(block_idx) + .map_err(Error::DeviceError)?; + let to_copy = block_avail + .min(space) + .min(data.open_files[file_idx].left() as usize); + assert!(to_copy != 0); + buffer[read..read + to_copy] + .copy_from_slice(&block[block_offset..block_offset + to_copy]); + read += to_copy; + space -= to_copy; + data.open_files[file_idx] + .seek_from_current(to_copy as i32) + .unwrap(); + } + Ok(read) + } + + /// Write to a open file. + pub fn write(&self, file: RawFile, buffer: &[u8]) -> Result<(), Error> { + #[cfg(feature = "defmt-log")] + debug!("write(file={:?}, buffer={:x}", file, buffer); + + #[cfg(feature = "log")] + debug!("write(file={:?}, buffer={:x?}", file, buffer); + + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + // Clone this so we can touch our other structures. Need to ensure we + // write it back at the end. + let file_idx = data.get_file_by_id(file)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + + if data.open_files[file_idx].mode == Mode::ReadOnly { + return Err(Error::ReadOnly); + } + + data.open_files[file_idx].dirty = true; + + if data.open_files[file_idx].entry.cluster.0 < fat::RESERVED_ENTRIES { + // file doesn't have a valid allocated cluster (possible zero-length file), allocate one + data.open_files[file_idx].entry.cluster = + match data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(ref mut fat) => { + fat.alloc_cluster(&mut data.block_cache, None, false)? + } + }; + debug!( + "Alloc first cluster {:?}", + data.open_files[file_idx].entry.cluster + ); + } + + // Clone this so we can touch our other structures. + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + + if (data.open_files[file_idx].current_cluster.1) < data.open_files[file_idx].entry.cluster { + debug!("Rewinding to start"); + data.open_files[file_idx].current_cluster = + (0, data.open_files[file_idx].entry.cluster); + } + let bytes_until_max = + usize::try_from(MAX_FILE_SIZE - data.open_files[file_idx].current_offset) + .map_err(|_| Error::ConversionError)?; + let bytes_to_write = core::cmp::min(buffer.len(), bytes_until_max); + let mut written = 0; + + while written < bytes_to_write { + let mut current_cluster = data.open_files[file_idx].current_cluster; + debug!( + "Have written bytes {}/{}, finding cluster {:?}", + written, bytes_to_write, current_cluster + ); + let current_offset = data.open_files[file_idx].current_offset; + let (block_idx, block_offset, block_avail) = match data.find_data_on_disk( + volume_idx, + &mut current_cluster, + data.open_files[file_idx].entry.cluster, + current_offset, + ) { + Ok(vars) => { + debug!( + "Found block_idx={:?}, block_offset={:?}, block_avail={}", + vars.0, vars.1, vars.2 + ); + vars + } + Err(Error::EndOfFile) => { + debug!("Extending file"); + match data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(ref mut fat) => { + if fat + .alloc_cluster( + &mut data.block_cache, + Some(current_cluster.1), + false, + ) + .is_err() + { + return Err(Error::DiskFull); + } + debug!("Allocated new FAT cluster, finding offsets..."); + let new_offset = data + .find_data_on_disk( + volume_idx, + &mut current_cluster, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, + ) + .map_err(|_| Error::AllocationError)?; + debug!("New offset {:?}", new_offset); + new_offset + } + } + } + Err(e) => return Err(e), + }; + let to_copy = core::cmp::min(block_avail, bytes_to_write - written); + let block = if (block_offset == 0) && (to_copy == block_avail) { + // we're replacing the whole Block, so the previous contents + // are irrelevant + data.block_cache.blank_mut(block_idx) + } else { + debug!("Reading for partial block write"); + data.block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)? + }; + block[block_offset..block_offset + to_copy] + .copy_from_slice(&buffer[written..written + to_copy]); + debug!("Writing block {:?}", block_idx); + data.block_cache.write_back()?; + written += to_copy; + data.open_files[file_idx].current_cluster = current_cluster; + + let to_copy = to_copy as u32; + let new_offset = data.open_files[file_idx].current_offset + to_copy; + if new_offset > data.open_files[file_idx].entry.size { + // We made it longer + data.open_files[file_idx].update_length(new_offset); + } + data.open_files[file_idx] + .seek_from_start(new_offset) + .unwrap(); + // Entry update deferred to file close, for performance. + } + data.open_files[file_idx].entry.attributes.set_archive(true); + data.open_files[file_idx].entry.mtime = self.time_source.get_timestamp(); + Ok(()) + } + + /// Close a file with the given raw file handle. + pub fn close_file(&self, file: RawFile) -> Result<(), Error> { + let flush_result = self.flush_file(file); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files.swap_remove(file_idx); + flush_result + } + + /// Flush (update the entry) for a file with the given raw file handle. + pub fn flush_file(&self, file: RawFile) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let file_id = data.get_file_by_id(file)?; + + if data.open_files[file_id].dirty { + let volume_idx = data.get_volume_by_id(data.open_files[file_id].raw_volume)?; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + debug!("Updating FAT info sector"); + fat.update_info_sector(&mut data.block_cache)?; + debug!("Updating dir entry {:?}", data.open_files[file_id].entry); + if data.open_files[file_id].entry.size != 0 { + // If you have a length, you must have a cluster + assert!(data.open_files[file_id].entry.cluster.0 != 0); + } + fat.write_entry_to_disk( + &mut data.block_cache, + &data.open_files[file_id].entry, + )?; + } + }; + } + Ok(()) + } + + /// Check if any files or folders are open. + pub fn has_open_handles(&self) -> bool { + let data = self.data.borrow(); + !(data.open_dirs.is_empty() || data.open_files.is_empty()) + } + + /// Consume self and return BlockDevice and TimeSource + pub fn free(self) -> (D, T) { + let data = self.data.into_inner(); + (data.block_cache.free(), self.time_source) + } + + /// Check if a file is at End Of File. + pub fn file_eof(&self, file: RawFile) -> Result> { + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].eof()) + } + + /// Seek a file with an offset from the start of the file. + pub fn file_seek_from_start(&self, file: RawFile, offset: u32) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] + .seek_from_start(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Seek a file with an offset from the current position. + pub fn file_seek_from_current( + &self, + file: RawFile, + offset: i32, + ) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] + .seek_from_current(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Seek a file with an offset back from the end of the file. + pub fn file_seek_from_end(&self, file: RawFile, offset: u32) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] + .seek_from_end(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Get the length of a file + pub fn file_length(&self, file: RawFile) -> Result> { + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].length()) + } + + /// Get the current offset of a file + pub fn file_offset(&self, file: RawFile) -> Result> { + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].current_offset) + } + + /// Create a directory in a given directory. + pub fn make_dir_in_dir( + &self, + directory: RawDirectory, + name: N, + ) -> Result<(), Error> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + // This check is load-bearing - we do an unchecked push later. + if data.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); + } + + let parent_directory_idx = data.get_dir_by_id(directory)?; + let parent_directory_info = &data.open_dirs[parent_directory_idx]; + let volume_id = data.open_dirs[parent_directory_idx].raw_volume; + let volume_idx = data.get_volume_by_id(volume_id)?; + let volume_info = &data.open_volumes[volume_idx]; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + debug!("Creating directory '{}'", sfn); + debug!( + "Parent dir is in cluster {:?}", + parent_directory_info.cluster + ); + + // Does an entry exist with this name? + let maybe_dir_entry = match &volume_info.volume_type { + VolumeType::Fat(fat) => { + fat.find_directory_entry(&mut data.block_cache, parent_directory_info, &sfn) + } + }; + + match maybe_dir_entry { + Ok(entry) if entry.attributes.is_directory() => { + return Err(Error::DirAlreadyExists); + } + Ok(_entry) => { + return Err(Error::FileAlreadyExists); + } + Err(Error::NotFound) => { + // perfect, let's make it + } + Err(e) => { + // Some other error - tell them about it + return Err(e); + } + }; + + let att = Attributes::create_from_fat(Attributes::DIRECTORY); + + // Need mutable access for this + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + debug!("Making dir entry"); + fat.make_dir( + &mut data.block_cache, + &self.time_source, + parent_directory_info.cluster, + sfn, + att, + )?; + } + }; + + Ok(()) + } +} + +/// The mutable data the VolumeManager needs to hold +/// +/// Kept separate so its easier to wrap it in a RefCell +#[derive(Debug)] + +struct VolumeManagerData< + D, + const MAX_DIRS: usize = 4, + const MAX_FILES: usize = 4, + const MAX_VOLUMES: usize = 1, +> where + D: BlockDevice, +{ + id_generator: HandleGenerator, + block_cache: BlockCache, + open_volumes: Vec, + open_dirs: Vec, + open_files: Vec, +} + +impl + VolumeManagerData +where + D: BlockDevice, +{ + /// Check if a file is open + /// + /// Returns `true` if it's open, `false`, otherwise. + fn file_is_open(&self, raw_volume: RawVolume, dir_entry: &DirEntry) -> bool { + for f in self.open_files.iter() { + if f.raw_volume == raw_volume + && f.entry.entry_block == dir_entry.entry_block + && f.entry.entry_offset == dir_entry.entry_offset + { + return true; + } + } + false + } + + fn get_volume_by_id(&self, raw_volume: RawVolume) -> Result> + where + E: core::fmt::Debug, + { + for (idx, v) in self.open_volumes.iter().enumerate() { + if v.raw_volume == raw_volume { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + fn get_dir_by_id(&self, raw_directory: RawDirectory) -> Result> + where + E: core::fmt::Debug, + { + for (idx, d) in self.open_dirs.iter().enumerate() { + if d.raw_directory == raw_directory { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + fn get_file_by_id(&self, raw_file: RawFile) -> Result> + where + E: core::fmt::Debug, + { + for (idx, f) in self.open_files.iter().enumerate() { + if f.raw_file == raw_file { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + /// This function turns `desired_offset` into an appropriate block to be + /// read. It either calculates this based on the start of the file, or + /// from the given start point - whichever is better. + /// + /// Returns: + /// + /// * the index for the block on the disk that contains the data we want, + /// * the byte offset into that block for the data we want, and + /// * how many bytes remain in that block. + fn find_data_on_disk( + &mut self, + volume_idx: usize, + start: &mut (u32, ClusterId), + file_start: ClusterId, + desired_offset: u32, + ) -> Result<(BlockIdx, usize, usize), Error> + where + D: BlockDevice, + { + let bytes_per_cluster = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.bytes_per_cluster(), + }; + // do we need to be before our start point? + if desired_offset < start.0 { + // user wants to go backwards - start from the beginning of the file + // because the FAT is only a singly-linked list. + start.0 = 0; + start.1 = file_start; + } + // How many clusters forward do we need to go? + let offset_from_cluster = desired_offset - start.0; + // walk through the FAT chain + let num_clusters = offset_from_cluster / bytes_per_cluster; + for _ in 0..num_clusters { + start.1 = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.next_cluster(&mut self.block_cache, start.1)?, + }; + start.0 += bytes_per_cluster; + } + // How many blocks in are we now? + let offset_from_cluster = desired_offset - start.0; + assert!(offset_from_cluster < bytes_per_cluster); + let num_blocks = BlockCount(offset_from_cluster / Block::LEN_U32); + let block_idx = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.cluster_to_block(start.1), + } + num_blocks; + let block_offset = (desired_offset % Block::LEN_U32) as usize; + let available = Block::LEN - block_offset; + Ok((block_idx, block_offset, available)) + } +} + +/// Transform mode variants (ReadWriteCreate_Or_Append) to simple modes ReadWriteAppend or +/// ReadWriteCreate +fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { + let mut mode = mode; + if mode == Mode::ReadWriteCreateOrAppend { + if dir_entry_is_some { + mode = Mode::ReadWriteAppend; + } else { + mode = Mode::ReadWriteCreate; + } + } else if mode == Mode::ReadWriteCreateOrTruncate { + if dir_entry_is_some { + mode = Mode::ReadWriteTruncate; + } else { + mode = Mode::ReadWriteCreate; + } + } + mode +} + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod tests { + use super::*; + use crate::filesystem::Handle; + use crate::Timestamp; + + struct DummyBlockDevice; + + struct Clock; + + #[derive(Debug)] + enum Error { + Unknown, + } + + impl TimeSource for Clock { + fn get_timestamp(&self) -> Timestamp { + // TODO: Return actual time + Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } + } + + impl BlockDevice for DummyBlockDevice { + type Error = Error; + + /// Read one or more blocks, starting at the given block index. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + // Actual blocks taken from an SD card, except I've changed the start and length of partition 0. + static BLOCKS: [Block; 3] = [ + Block { + contents: [ + 0xfa, 0xb8, 0x00, 0x10, 0x8e, 0xd0, 0xbc, 0x00, 0xb0, 0xb8, 0x00, 0x00, + 0x8e, 0xd8, 0x8e, 0xc0, // 0x000 + 0xfb, 0xbe, 0x00, 0x7c, 0xbf, 0x00, 0x06, 0xb9, 0x00, 0x02, 0xf3, 0xa4, + 0xea, 0x21, 0x06, 0x00, // 0x010 + 0x00, 0xbe, 0xbe, 0x07, 0x38, 0x04, 0x75, 0x0b, 0x83, 0xc6, 0x10, 0x81, + 0xfe, 0xfe, 0x07, 0x75, // 0x020 + 0xf3, 0xeb, 0x16, 0xb4, 0x02, 0xb0, 0x01, 0xbb, 0x00, 0x7c, 0xb2, 0x80, + 0x8a, 0x74, 0x01, 0x8b, // 0x030 + 0x4c, 0x02, 0xcd, 0x13, 0xea, 0x00, 0x7c, 0x00, 0x00, 0xeb, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x040 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x050 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x060 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x080 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x090 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0B0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0F0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xca, 0xde, 0x06, + 0x00, 0x00, 0x00, 0x04, // 0x1B0 + 0x01, 0x04, 0x0c, 0xfe, 0xc2, 0xff, 0x01, 0x00, 0x00, 0x00, 0x33, 0x22, + 0x11, 0x00, 0x00, 0x00, // 0x1C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x55, 0xaa, // 0x1F0 + ], + }, + Block { + contents: [ + 0xeb, 0x58, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, + 0x02, 0x08, 0x20, 0x00, // 0x000 + 0x02, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x10, 0x00, 0x04, 0x00, + 0x00, 0x08, 0x00, 0x00, // 0x010 + 0x00, 0x20, 0x76, 0x00, 0x80, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, // 0x020 + 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x030 + 0x80, 0x01, 0x29, 0x0b, 0xa8, 0x89, 0x27, 0x50, 0x69, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x20, // 0x040 + 0x20, 0x20, 0x46, 0x41, 0x54, 0x33, 0x32, 0x20, 0x20, 0x20, 0x0e, 0x1f, + 0xbe, 0x77, 0x7c, 0xac, // 0x050 + 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, + 0x5e, 0xeb, 0xf0, 0x32, // 0x060 + 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, + 0x69, 0x73, 0x20, 0x6e, // 0x070 + 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x20, 0x64, 0x69, // 0x080 + 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, + 0x69, 0x6e, 0x73, 0x65, // 0x090 + 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x20, 0x66, 0x6c, // 0x0A0 + 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x20, // 0x0B0 + 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, + 0x72, 0x79, 0x20, 0x61, // 0x0C0 + 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0F0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1B0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x55, 0xaa, // 0x1F0 + ], + }, + Block { + contents: hex!( + "52 52 61 41 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 72 72 41 61 FF FF FF FF FF FF FF FF + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" + ), + }, + ]; + println!( + "Reading block {} to {}", + start_block_idx.0, + start_block_idx.0 as usize + blocks.len() + ); + for (idx, block) in blocks.iter_mut().enumerate() { + let block_idx = start_block_idx.0 as usize + idx; + if block_idx < BLOCKS.len() { + *block = BLOCKS[block_idx].clone(); + } else { + return Err(Error::Unknown); + } + } + Ok(()) + } + + /// Write one or more blocks, starting at the given block index. + fn write(&self, _blocks: &[Block], _start_block_idx: BlockIdx) -> Result<(), Self::Error> { + unimplemented!(); + } + + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result { + Ok(BlockCount(2)) + } + } + + #[test] + fn partition0() { + let c: VolumeManager = + VolumeManager::new_with_limits(DummyBlockDevice, Clock, 0xAA00_0000); + + let v = c.open_raw_volume(VolumeIdx(0)).unwrap(); + let expected_id = RawVolume(Handle(0xAA00_0000)); + assert_eq!(v, expected_id); + assert_eq!( + &c.data.borrow().open_volumes[0], + &VolumeInfo { + raw_volume: expected_id, + idx: VolumeIdx(0), + volume_type: VolumeType::Fat(crate::FatVolume { + lba_start: BlockIdx(1), + num_blocks: BlockCount(0x0011_2233), + blocks_per_cluster: 8, + first_data_block: BlockCount(15136), + fat_start: BlockCount(32), + second_fat_start: Some(BlockCount(32 + 0x0000_1D80)), + name: fat::VolumeName::create_from_str("Pictures").unwrap(), + free_clusters_count: None, + next_free_cluster: None, + cluster_count: 965_788, + fat_specific_info: fat::FatSpecificInfo::Fat32(fat::Fat32Info { + first_root_dir_cluster: ClusterId(2), + info_location: BlockIdx(1) + BlockCount(1), + }) + }) + } + ); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/directories.rs b/examples/ios/embedded-sdmmc/tests/directories.rs new file mode 100644 index 0000000..e10a901 --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/directories.rs @@ -0,0 +1,599 @@ +//! Directory related tests + +use embedded_sdmmc::{LfnBuffer, Mode, ShortFileName}; + +mod utils; + +#[derive(Debug, Clone)] +struct ExpectedDirEntry { + name: String, + mtime: String, + ctime: String, + size: u32, + is_dir: bool, +} + +impl PartialEq for ExpectedDirEntry { + fn eq(&self, other: &embedded_sdmmc::DirEntry) -> bool { + if other.name.to_string() != self.name { + return false; + } + if format!("{}", other.mtime) != self.mtime { + return false; + } + if format!("{}", other.ctime) != self.ctime { + return false; + } + if other.size != self.size { + return false; + } + if other.attributes.is_directory() != self.is_dir { + return false; + } + true + } +} + +#[test] +fn fat16_root_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + + let expected = [ + ( + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2018-12-09 19:22:34"), + ctime: String::from("2018-12-09 19:22:34"), + size: 258, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("EMPTY.DAT"), + mtime: String::from("2018-12-09 19:21:16"), + ctime: String::from("2018-12-09 19:21:16"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("TEST"), + mtime: String::from("2018-12-09 19:23:16"), + ctime: String::from("2018-12-09 19:23:16"), + size: 0, + is_dir: true, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:21:38"), + ctime: String::from("2018-12-09 19:21:38"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("FSEVEN~4"), + mtime: String::from("2024-10-25 16:30:42"), + ctime: String::from("2024-10-25 16:30:42"), + size: 0, + is_dir: true, + }, + Some(String::from(".fseventsd")), + ), + ( + ExpectedDirEntry { + name: String::from("P-FAT16"), + mtime: String::from("2024-10-30 18:43:12"), + ctime: String::from("2024-10-30 18:43:12"), + size: 0, + is_dir: false, + }, + None, + ), + ]; + + let mut listing = Vec::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); + + volume_mgr + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); + }) + .expect("iterate directory"); + + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry.0, given_entry.0, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + assert_eq!( + expected_entry.1, given_entry.1, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); +} + +#[test] +fn fat16_sub_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("open test dir"); + + let expected = [ + ExpectedDirEntry { + name: String::from("."), + mtime: String::from("2018-12-09 19:21:02"), + ctime: String::from("2018-12-09 19:21:02"), + size: 0, + is_dir: true, + }, + ExpectedDirEntry { + name: String::from(".."), + mtime: String::from("2018-12-09 19:21:02"), + ctime: String::from("2018-12-09 19:21:02"), + size: 0, + is_dir: true, + }, + ExpectedDirEntry { + name: String::from("TEST.DAT"), + mtime: String::from("2018-12-09 19:22:12"), + ctime: String::from("2018-12-09 19:22:12"), + size: 3500, + is_dir: false, + }, + ]; + + let mut listing = Vec::new(); + let mut count = 0; + + volume_mgr + .iterate_dir(test_dir, |d| { + if count == 0 { + assert!(d.name == ShortFileName::this_dir()); + } else if count == 1 { + assert!(d.name == ShortFileName::parent_dir()); + } + count += 1; + listing.push(d.clone()); + }) + .expect("iterate directory"); + + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry, given_entry, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); +} + +#[test] +fn fat32_root_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let expected = [ + ( + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("EMPTY.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2023-09-21 09:48:06"), + ctime: String::from("2018-12-09 19:22:56"), + size: 258, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("TEST"), + mtime: String::from("2018-12-09 19:23:20"), + ctime: String::from("2018-12-09 19:23:20"), + size: 0, + is_dir: true, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("FSEVEN~4"), + mtime: String::from("2024-10-25 16:30:42"), + ctime: String::from("2024-10-25 16:30:42"), + size: 0, + is_dir: true, + }, + Some(String::from(".fseventsd")), + ), + ( + ExpectedDirEntry { + name: String::from("P-FAT32"), + mtime: String::from("2024-10-30 18:43:16"), + ctime: String::from("2024-10-30 18:43:16"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("THISIS~9"), + mtime: String::from("2024-10-25 16:30:54"), + ctime: String::from("2024-10-25 16:30:50"), + size: 0, + is_dir: true, + }, + Some(String::from("This is a long file name £99")), + ), + ( + ExpectedDirEntry { + name: String::from("COPYO~13.TXT"), + mtime: String::from("2024-10-25 16:31:14"), + ctime: String::from("2018-12-09 19:22:56"), + size: 258, + is_dir: false, + }, + Some(String::from("Copy of Readme.txt")), + ), + ]; + + let mut listing = Vec::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); + + volume_mgr + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); + }) + .expect("iterate directory"); + + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry.0, given_entry.0, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + assert_eq!( + expected_entry.1, given_entry.1, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); +} + +#[test] +fn open_dir_twice() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let root_dir2 = volume_mgr + .open_root_dir(fat32_volume) + .expect("open it again"); + + assert!(matches!( + volume_mgr.open_dir(root_dir, "README.TXT"), + Err(embedded_sdmmc::Error::OpenedFileAsDir) + )); + + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("open test dir"); + + let test_dir2 = volume_mgr.open_dir(root_dir, "TEST").unwrap(); + + volume_mgr.close_dir(root_dir).expect("close root dir"); + volume_mgr.close_dir(test_dir).expect("close test dir"); + volume_mgr.close_dir(test_dir2).expect("close test dir"); + volume_mgr.close_dir(root_dir2).expect("close test dir"); + + assert!(matches!( + volume_mgr.close_dir(test_dir), + Err(embedded_sdmmc::Error::BadHandle) + )); +} + +#[test] +fn open_too_many_dirs() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: embedded_sdmmc::VolumeManager< + utils::RamDisk>, + utils::TestTimeSource, + 1, + 4, + 2, + > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + assert!(matches!( + volume_mgr.open_dir(root_dir, "TEST"), + Err(embedded_sdmmc::Error::TooManyOpenDirs) + )); +} + +#[test] +fn find_dir_entry() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let dir_entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("Find directory entry"); + assert!(dir_entry.attributes.is_archive()); + assert!(!dir_entry.attributes.is_directory()); + assert!(!dir_entry.attributes.is_hidden()); + assert!(!dir_entry.attributes.is_lfn()); + assert!(!dir_entry.attributes.is_system()); + assert!(!dir_entry.attributes.is_volume()); + + assert!(matches!( + volume_mgr.find_directory_entry(root_dir, "README.TXS"), + Err(embedded_sdmmc::Error::NotFound) + )); +} + +#[test] +fn delete_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let file = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly) + .unwrap(); + + assert!(matches!( + volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), + Err(embedded_sdmmc::Error::FileAlreadyOpen) + )); + + assert!(matches!( + volume_mgr.delete_file_in_dir(root_dir, "README2.TXT"), + Err(embedded_sdmmc::Error::NotFound) + )); + + volume_mgr.close_file(file).unwrap(); + + volume_mgr + .delete_file_in_dir(root_dir, "README.TXT") + .unwrap(); + + assert!(matches!( + volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), + Err(embedded_sdmmc::Error::NotFound) + )); + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), + Err(embedded_sdmmc::Error::NotFound) + )); +} + +#[test] +fn make_directory() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let test_dir_name = ShortFileName::create_from_str("12345678.ABC").unwrap(); + let test_file_name = ShortFileName::create_from_str("ABC.TXT").unwrap(); + + volume_mgr + .make_dir_in_dir(root_dir, &test_dir_name) + .unwrap(); + + let new_dir = volume_mgr.open_dir(root_dir, &test_dir_name).unwrap(); + + let mut has_this = false; + let mut has_parent = false; + volume_mgr + .iterate_dir(new_dir, |item| { + if item.name == ShortFileName::parent_dir() { + has_parent = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == ShortFileName::this_dir() { + has_this = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else { + panic!("Unexpected item in new dir"); + } + }) + .unwrap(); + assert!(has_this); + assert!(has_parent); + + let new_file = volume_mgr + .open_file_in_dir( + new_dir, + &test_file_name, + embedded_sdmmc::Mode::ReadWriteCreate, + ) + .expect("open new file"); + volume_mgr + .write(new_file, b"Hello") + .expect("write to new file"); + volume_mgr.close_file(new_file).expect("close new file"); + + let mut has_this = false; + let mut has_parent = false; + let mut has_new_file = false; + volume_mgr + .iterate_dir(new_dir, |item| { + if item.name == ShortFileName::parent_dir() { + has_parent = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == ShortFileName::this_dir() { + has_this = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == test_file_name { + has_new_file = true; + // We wrote "Hello" to it + assert_eq!(item.size, 5); + assert!(!item.attributes.is_directory()); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else { + panic!("Unexpected item in new dir"); + } + }) + .unwrap(); + assert!(has_this); + assert!(has_parent); + assert!(has_new_file); + + // Close the root dir and look again + volume_mgr.close_dir(root_dir).expect("close root"); + volume_mgr.close_dir(new_dir).expect("close new_dir"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + // Check we can't make it again now it exists + assert!(volume_mgr + .make_dir_in_dir(root_dir, &test_dir_name) + .is_err()); + let new_dir = volume_mgr + .open_dir(root_dir, &test_dir_name) + .expect("find new dir"); + let new_file = volume_mgr + .open_file_in_dir(new_dir, &test_file_name, embedded_sdmmc::Mode::ReadOnly) + .expect("re-open new file"); + volume_mgr.close_dir(root_dir).expect("close root"); + volume_mgr.close_dir(new_dir).expect("close new dir"); + volume_mgr.close_file(new_file).expect("close file"); +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/disk.img.gz b/examples/ios/embedded-sdmmc/tests/disk.img.gz new file mode 100644 index 0000000..507ec94 Binary files /dev/null and b/examples/ios/embedded-sdmmc/tests/disk.img.gz differ diff --git a/examples/ios/embedded-sdmmc/tests/open_files.rs b/examples/ios/embedded-sdmmc/tests/open_files.rs new file mode 100644 index 0000000..6b927bc --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/open_files.rs @@ -0,0 +1,145 @@ +//! File opening related tests + +use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; + +mod utils; + +#[test] +fn open_files() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), + Err(Error::FileAlreadyOpen) + )); + + volume_mgr.close_file(f).expect("close file"); + + // Open with SFN + + let dir_entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find file"); + + let f = volume_mgr + .open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadWriteCreateOrAppend) + .expect("open file with dir entry"); + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadOnly), + Err(Error::FileAlreadyOpen) + )); + + // Can still spot duplicates even if name given the other way around + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), + Err(Error::FileAlreadyOpen) + )); + + let f2 = volume_mgr + .open_file_in_dir(root_dir, "64MB.DAT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Hit file limit + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "EMPTY.DAT", Mode::ReadOnly), + Err(Error::TooManyOpenFiles) + )); + + volume_mgr.close_file(f).expect("close file"); + volume_mgr.close_file(f2).expect("close file"); + + // File not found + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXS", Mode::ReadOnly), + Err(Error::NotFound) + )); + + // Create a new file + let f3 = volume_mgr + .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadWriteCreate) + .expect("open file"); + + volume_mgr.write(f3, b"12345").expect("write to file"); + volume_mgr.write(f3, b"67890").expect("write to file"); + volume_mgr.close_file(f3).expect("close file"); + + // Open our new file + let f3 = volume_mgr + .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadOnly) + .expect("open file"); + // Should have 10 bytes in it + assert_eq!(volume_mgr.file_length(f3).expect("file length"), 10); + volume_mgr.close_file(f3).expect("close file"); + + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} + +#[test] +fn open_non_raw() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); + let root_dir = volume.open_root_dir().expect("open root dir"); + let f = root_dir + .open_file_in_dir("README.TXT", Mode::ReadOnly) + .expect("open file"); + + let mut buffer = [0u8; 512]; + let len = f.read(&mut buffer).expect("read from file"); + // See directory listing in utils.rs, to see that README.TXT is 258 bytes long + assert_eq!(len, 258); + assert_eq!(f.length(), 258); + f.seek_from_current(0).unwrap(); + assert!(f.is_eof()); + assert_eq!(f.offset(), 258); + assert!(matches!(f.seek_from_current(1), Err(Error::InvalidOffset))); + f.seek_from_current(-258).unwrap(); + assert!(!f.is_eof()); + assert_eq!(f.offset(), 0); + f.seek_from_current(10).unwrap(); + assert!(!f.is_eof()); + assert_eq!(f.offset(), 10); + f.seek_from_end(0).unwrap(); + assert!(f.is_eof()); + assert_eq!(f.offset(), 258); + assert!(matches!( + f.seek_from_current(-259), + Err(Error::InvalidOffset) + )); + f.seek_from_start(25).unwrap(); + assert!(!f.is_eof()); + assert_eq!(f.offset(), 25); + + drop(f); + + let Err(Error::FileAlreadyExists) = + root_dir.open_file_in_dir("README.TXT", Mode::ReadWriteCreate) + else { + panic!("Expected to file to exist"); + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/read_file.rs b/examples/ios/embedded-sdmmc/tests/read_file.rs new file mode 100644 index 0000000..904964f --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/read_file.rs @@ -0,0 +1,212 @@ +//! Reading related tests + +use sha2::Digest; + +mod utils; + +static TEST_DAT_SHA256_SUM: &[u8] = + 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"; + +#[test] +fn read_file_512_blocks() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = Vec::new(); + + let mut partial = false; + while !volume_mgr.file_eof(test_file).expect("check eof") { + let mut buffer = [0u8; 512]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); + if len != buffer.len() { + if partial { + panic!("Two partial reads!"); + } else { + partial = true; + } + } + contents.extend(&buffer[0..len]); + } + + let mut hasher = sha2::Sha256::new(); + hasher.update(contents); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_all() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = vec![0u8; 4096]; + let len = volume_mgr + .read(test_file, &mut contents) + .expect("read data"); + if len != 3500 { + panic!("Failed to read all of TEST.DAT"); + } + + let mut hasher = sha2::Sha256::new(); + hasher.update(&contents[0..3500]); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_prime_blocks() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = Vec::new(); + + let mut partial = false; + while !volume_mgr.file_eof(test_file).expect("check eof") { + // Exercise the alignment code by reading in chunks of 53 bytes + let mut buffer = [0u8; 53]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); + if len != buffer.len() { + if partial { + panic!("Two partial reads!"); + } else { + partial = true; + } + } + contents.extend(&buffer[0..len]); + } + + let mut hasher = sha2::Sha256::new(); + hasher.update(&contents[0..3500]); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_backwards() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = std::collections::VecDeque::new(); + + const CHUNK_SIZE: u32 = 100; + let length = volume_mgr.file_length(test_file).expect("file length"); + let mut read = 0; + + // go to end + volume_mgr.file_seek_from_end(test_file, 0).expect("seek"); + + // We're going to read the file backwards in chunks of 100 bytes. This + // checks we didn't make any assumptions about only going forwards. + while read < length { + // go to start of next chunk + volume_mgr + .file_seek_from_current(test_file, -(CHUNK_SIZE as i32)) + .expect("seek"); + // read chunk + let mut buffer = [0u8; CHUNK_SIZE as usize]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read"); + assert_eq!(len, CHUNK_SIZE as usize); + contents.push_front(buffer.to_vec()); + read += CHUNK_SIZE; + // go to start of chunk we just read + volume_mgr + .file_seek_from_current(test_file, -(CHUNK_SIZE as i32)) + .expect("seek"); + } + + assert_eq!(read, length); + + let flat: Vec = contents.iter().flatten().copied().collect(); + + let mut hasher = sha2::Sha256::new(); + hasher.update(flat); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_with_odd_seek() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .unwrap(); + let root_dir = volume.open_root_dir().unwrap(); + let f = root_dir + .open_file_in_dir("64MB.DAT", embedded_sdmmc::Mode::ReadOnly) + .unwrap(); + f.seek_from_start(0x2c).unwrap(); + while f.offset() < 1000000 { + let mut buffer = [0u8; 2048]; + f.read(&mut buffer).unwrap(); + f.seek_from_current(-1024).unwrap(); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/utils/mod.rs b/examples/ios/embedded-sdmmc/tests/utils/mod.rs new file mode 100644 index 0000000..3f89a0e --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/utils/mod.rs @@ -0,0 +1,190 @@ +//! Useful library code for tests + +use std::io::prelude::*; + +use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx}; + +/// This file contains: +/// +/// ```console +/// $ fdisk ./disk.img +/// Disk: ./disk.img geometry: 520/32/63 [1048576 sectors] +/// Signature: 0xAA55 +/// Starting Ending +/// #: id cyl hd sec - cyl hd sec [ start - size] +/// ------------------------------------------------------------------------ +/// 1: 0E 0 32 33 - 16 113 33 [ 2048 - 262144] DOS FAT-16 +/// 2: 0C 16 113 34 - 65 69 4 [ 264192 - 784384] Win95 FAT32L +/// 3: 00 0 0 0 - 0 0 0 [ 0 - 0] unused +/// 4: 00 0 0 0 - 0 0 0 [ 0 - 0] unused +/// $ ls -l /Volumes/P-FAT16 +/// total 131080 +/// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT +/// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT +/// -rwxrwxrwx@ 1 jonathan staff 258 9 Dec 2018 README.TXT +/// drwxrwxrwx 1 jonathan staff 2048 9 Dec 2018 TEST +/// $ ls -l /Volumes/P-FAT16/TEST +/// total 8 +/// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT +/// $ ls -l /Volumes/P-FAT32 +/// total 131088 +/// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT +/// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT +/// -rwxrwxrwx@ 1 jonathan staff 258 21 Sep 09:48 README.TXT +/// drwxrwxrwx 1 jonathan staff 4096 9 Dec 2018 TEST +/// $ ls -l /Volumes/P-FAT32/TEST +/// total 8 +/// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT +/// ``` +/// +/// It will unpack to a Vec that is 1048576 * 512 = 512 MiB in size. +pub static DISK_SOURCE: &[u8] = include_bytes!("../disk.img.gz"); + +#[derive(Debug)] +#[allow(dead_code)] +pub enum Error { + /// Failed to read the source image + Io(std::io::Error), + /// Failed to unzip the source image + Decode(flate2::DecompressError), + /// Asked for a block we don't have + OutOfBounds(BlockIdx), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: flate2::DecompressError) -> Self { + Self::Decode(value) + } +} + +/// Implements the block device traits for a chunk of bytes in RAM. +/// +/// The slice should be a multiple of `embedded_sdmmc::Block::LEN` bytes in +/// length. If it isn't the trailing data is discarded. +pub struct RamDisk { + contents: std::cell::RefCell, +} + +impl RamDisk { + fn new(contents: T) -> RamDisk { + RamDisk { + contents: std::cell::RefCell::new(contents), + } + } +} + +impl BlockDevice for RamDisk +where + T: AsMut<[u8]> + AsRef<[u8]>, +{ + type Error = Error; + + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let borrow = self.contents.borrow(); + let contents: &[u8] = borrow.as_ref(); + let mut block_idx = start_block_idx; + for block in blocks.iter_mut() { + let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; + let end_offset = start_offset + embedded_sdmmc::Block::LEN; + if end_offset > contents.len() { + return Err(Error::OutOfBounds(block_idx)); + } + block + .as_mut_slice() + .copy_from_slice(&contents[start_offset..end_offset]); + block_idx.0 += 1; + } + Ok(()) + } + + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut borrow = self.contents.borrow_mut(); + let contents: &mut [u8] = borrow.as_mut(); + let mut block_idx = start_block_idx; + for block in blocks.iter() { + let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; + let end_offset = start_offset + embedded_sdmmc::Block::LEN; + if end_offset > contents.len() { + return Err(Error::OutOfBounds(block_idx)); + } + contents[start_offset..end_offset].copy_from_slice(block.as_slice()); + block_idx.0 += 1; + } + Ok(()) + } + + fn num_blocks(&self) -> Result { + let borrow = self.contents.borrow(); + let contents: &[u8] = borrow.as_ref(); + let len_blocks = contents.len() / embedded_sdmmc::Block::LEN; + if len_blocks > u32::MAX as usize { + panic!("Test disk too large! Only 2**32 blocks allowed"); + } + Ok(BlockCount(len_blocks as u32)) + } +} + +/// Unpack the fixed, static, disk image. +fn unpack_disk(gzip_bytes: &[u8]) -> Result, Error> { + let disk_cursor = std::io::Cursor::new(gzip_bytes); + let mut gz_decoder = flate2::read::GzDecoder::new(disk_cursor); + let mut output = Vec::with_capacity(512 * 1024 * 1024); + gz_decoder.read_to_end(&mut output)?; + Ok(output) +} + +/// Turn some gzipped bytes into a block device, +pub fn make_block_device(gzip_bytes: &[u8]) -> Result>, Error> { + let data = unpack_disk(gzip_bytes)?; + Ok(RamDisk::new(data)) +} + +pub struct TestTimeSource { + fixed: embedded_sdmmc::Timestamp, +} + +impl embedded_sdmmc::TimeSource for TestTimeSource { + fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { + self.fixed + } +} + +/// Make a time source that gives a fixed time. +/// +/// It always claims to be 4 April 2003, at 13:30:05. +/// +/// This is an interesting time, because FAT will round it down to 13:30:04 due +/// to only have two-second resolution. Hey, Real Time Clocks were optional back +/// in 1981. +pub fn make_time_source() -> TestTimeSource { + TestTimeSource { + fixed: embedded_sdmmc::Timestamp { + year_since_1970: 33, + zero_indexed_month: 3, + zero_indexed_day: 3, + hours: 13, + minutes: 30, + seconds: 5, + }, + } +} + +/// Get the test time source time, as a string. +/// +/// We apply the FAT 2-second rounding here. +#[allow(unused)] +pub fn get_time_source_string() -> &'static str { + "2003-04-04 13:30:04" +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/volume.rs b/examples/ios/embedded-sdmmc/tests/volume.rs new file mode 100644 index 0000000..c2ea49b --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/volume.rs @@ -0,0 +1,115 @@ +//! Volume related tests + +mod utils; + +#[test] +fn open_all_volumes() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: embedded_sdmmc::VolumeManager< + utils::RamDisk>, + utils::TestTimeSource, + 4, + 4, + 2, + > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); + + // Open Volume 0 + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + + // Fail to Open Volume 0 again + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(0)), + Err(embedded_sdmmc::Error::VolumeAlreadyOpen) + )); + + volume_mgr.close_volume(fat16_volume).expect("close fat16"); + + // Open Volume 1 + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + // Fail to Volume 1 again + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(1)), + Err(embedded_sdmmc::Error::VolumeAlreadyOpen) + )); + + // Open Volume 0 again + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + + // Open any volume - too many volumes (0 and 1 are open) + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(0)), + Err(embedded_sdmmc::Error::TooManyOpenVolumes) + )); + + volume_mgr.close_volume(fat16_volume).expect("close fat16"); + volume_mgr.close_volume(fat32_volume).expect("close fat32"); + + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(2)), + Err(embedded_sdmmc::Error::FormatError(_e)) + )); + + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(3)), + Err(embedded_sdmmc::Error::FormatError(_e)) + )); + + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(9)), + Err(embedded_sdmmc::Error::NoSuchVolume) + )); + + let _root_dir = volume_mgr.open_root_dir(fat32_volume).expect("Open dir"); + + assert!(matches!( + volume_mgr.close_volume(fat32_volume), + Err(embedded_sdmmc::Error::VolumeStillInUse) + )); +} + +#[test] +fn close_volume_too_early() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Dir open + assert!(matches!( + volume_mgr.close_volume(volume), + Err(embedded_sdmmc::Error::VolumeStillInUse) + )); + + let _test_file = volume_mgr + .open_file_in_dir(root_dir, "64MB.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + volume_mgr.close_dir(root_dir).unwrap(); + + // File open, not dir open + assert!(matches!( + volume_mgr.close_volume(volume), + Err(embedded_sdmmc::Error::VolumeStillInUse) + )); +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/write_file.rs b/examples/ios/embedded-sdmmc/tests/write_file.rs new file mode 100644 index 0000000..9a5e4ab --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/write_file.rs @@ -0,0 +1,168 @@ +//! File opening related tests + +use embedded_sdmmc::{Mode, VolumeIdx, VolumeManager}; + +mod utils; + +#[test] +fn append_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Should be enough to cause a few more clusters to be allocated + let test_data = vec![0xCC; 1024 * 1024]; + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, 1024 * 1024); + + let offset = volume_mgr.file_offset(f).expect("offset"); + assert_eq!(offset, 1024 * 1024); + + // Now wind it back 1 byte; + volume_mgr.file_seek_from_current(f, -1).expect("Seeking"); + + let offset = volume_mgr.file_offset(f).expect("offset"); + assert_eq!(offset, (1024 * 1024) - 1); + + // Write another megabyte, making `2 MiB - 1` + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, (1024 * 1024 * 2) - 1); + + volume_mgr.close_file(f).expect("close dir"); + + // Now check the file length again + + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("Find entry"); + assert_eq!(entry.size, (1024 * 1024 * 2) - 1); + + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} + +#[test] +fn flush_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Write some data to the file + let test_data = vec![0xCC; 64]; + volume_mgr.write(f, &test_data).expect("file write"); + + // Check that the file length is zero in the directory entry, as we haven't + // flushed yet + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find entry"); + assert_eq!(entry.size, 0); + + volume_mgr.flush_file(f).expect("flush"); + + // Now check the file length again after flushing + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find entry"); + assert_eq!(entry.size, 64); + + // Flush more writes + volume_mgr.write(f, &test_data).expect("file write"); + volume_mgr.write(f, &test_data).expect("file write"); + volume_mgr.flush_file(f).expect("flush"); + + // Now check the file length again, again + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find entry"); + assert_eq!(entry.size, 64 * 3); +} + +#[test] +fn random_access_write_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + let test_data = vec![0xCC; 1024]; + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, 1024); + + for seek_offset in [100, 0] { + let mut expected_buffer = [0u8; 4]; + + // fetch some data at offset seek_offset + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("Seeking"); + volume_mgr.read(f, &mut expected_buffer).expect("read file"); + + // modify first byte + expected_buffer[0] ^= 0xff; + + // write only first byte, expecting the rest to not change + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("Seeking"); + volume_mgr + .write(f, &expected_buffer[0..1]) + .expect("file write"); + volume_mgr.flush_file(f).expect("file flush"); + + // read and verify + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("file seek"); + let mut read_buffer = [0xffu8, 0xff, 0xff, 0xff]; + volume_mgr.read(f, &mut read_buffer).expect("file read"); + assert_eq!( + read_buffer, expected_buffer, + "mismatch seek+write at offset {seek_offset} from start" + ); + } + + volume_mgr.close_file(f).expect("close file"); + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/src/main.rs b/examples/ios/src/main.rs index 394579b..06dc6d0 100644 --- a/examples/ios/src/main.rs +++ b/examples/ios/src/main.rs @@ -3,10 +3,61 @@ use alloc::vec; use ogc_rs::{ - ios::{self, Mode, SeekMode}, + ios::{ + self, + sdio::{Device, Request}, + Mode, SeekMode, + }, print, println, }; extern crate alloc; +use embedded_sdmmc::{ + blockdevice::{Block, BlockCount, BlockIdx}, + BlockDevice, TimeSource, Timestamp, VolumeIdx, VolumeManager, +}; + +pub struct SdCardDevice(Device); +pub struct DummyTimesource; +impl BlockDevice for SdCardDevice { + type Error = (); + + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + if blocks.len() == 1 { + let mut block = [0u8; 512]; + let res = self + .0 + .read_sectors(core::slice::from_mut(&mut block), start_block_idx.0 as _) + .map_err(|_| ()); + + blocks[0].contents = block; + res + } else { + todo!("read is not currently implemented") + } + } + + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + if blocks.len() == 1 { + let block = blocks[0].contents; + let res = self + .0 + .write_sectors(core::slice::from_ref(&block), start_block_idx.0 as _) + .map_err(|_| ()); + res + } else { + todo!("write is not currently implemented") + } + } + + fn num_blocks(&self) -> Result { + todo!("num_blocks is not currently implemented") + } +} +impl TimeSource for DummyTimesource { + fn get_timestamp(&self) -> Timestamp { + todo!() + } +} #[no_mangle] extern "C" fn main() { @@ -26,7 +77,508 @@ extern "C" fn main() { let _ = ios::close(fd); } } + + let (is_sdhc, mut device) = try_init_sd(); + + // let mut block = [0u8; 512]; + // + // //read_sectors(blocks: &mut [[0u8; 512]], offset: usize]) -> Result<(), ios::Error>; + // device + // .read_sectors(core::slice::from_mut(&mut block), 0) + // .unwrap(); + // + // let bpb = BPB::from_bytes(block[0x00B..].try_into().unwrap()); + // + // let mut info_block = [0u8; 512]; + // device + // .read_sectors( + // core::slice::from_mut(&mut info_block), + // bpb.fs_info_sector as usize, + // ) + // .unwrap(); + // + // let info = FSInfo::from_bytes(&info_block); + // + // println!("{:?}", bpb); + // println!("{:?}", info); + // + let volmgr = VolumeManager::new(SdCardDevice(device), DummyTimesource); + let volume = unsafe { + volmgr + .open_special(0x0C, BlockIdx(0), BlockCount(67108864)) + .unwrap() + }; + let root_dir = volume.open_root_dir().unwrap(); + + let func = |entry: &embedded_sdmmc::DirEntry| { + println!("{:?}", entry.name); + }; + + root_dir.iterate_dir(func).unwrap(); + loop { core::hint::spin_loop(); } } + +pub struct SDCard { + rca: u32, + is_sdhc: bool, + device: Device, +} + +impl SDCard { + pub fn init() -> Option { + let (is_sdhc, mut device) = try_init_sd(); + + let resp_rca = device.send_command(&Request::SEND_RCA).ok()?; + let rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); + + Some(Self { + rca, + is_sdhc, + device, + }) + } +} + +impl DeviceExt for Device { + fn read_sectors( + &self, + sectors: &mut [[u8; 512]], + offset: usize, + ) -> Result<(), ogc_rs::ios::Error> { + let resp_rca = self.send_command(&Request::SEND_RCA)?; + let rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); + self.send_command(&Request::select(rca))?; + + const SDIO_CMD_READMULTIBLOCK: u32 = 0x12; + const SDIO_CMD_TYPE_AC: u32 = 3; + const SDIO_RESPONSE_TYPE_R1: u32 = 1; + + // SDIO requires 32 byte alignment + // On hardware this probably needs to be in the IPC memory space :shrug: + let mut aligned_buffer = ogc_rs::utils::alloc_aligned_buffer(sectors.as_flattened_mut()); + + //let req = Request::read_multiblock(offset, sectors, &mut aligned_buffer); + + self.send_command(&Request::new( + SDIO_CMD_READMULTIBLOCK, + SDIO_CMD_TYPE_AC, + SDIO_RESPONSE_TYPE_R1, + offset as u32, + sectors.len() as u32, + 512, + aligned_buffer.as_mut_ptr(), + ))?; + + self.send_command(&Request::DE_SELECT)?; + + sectors + .as_flattened_mut() + .copy_from_slice(&mut aligned_buffer); + + Ok(()) + } + + fn write_sectors( + &self, + sectors: &[[u8; 512]], + offset: usize, + ) -> Result<(), ogc_rs::ios::Error> { + let resp_rca = self.send_command(&Request::SEND_RCA)?; + let rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); + self.send_command(&Request::select(rca))?; + + const SDIO_CMD_WRITEMULTIBLOCK: u32 = 0x19; + const SDIO_CMD_TYPE_AC: u32 = 3; + const SDIO_RESPONSE_TYPE_R1: u32 = 1; + + // SDIO requires 32 byte alignment + // On hardware this probably needs to be in the IPC memory space :shrug: + let mut aligned_buffer = ogc_rs::utils::alloc_aligned_buffer(sectors.as_flattened()); + + self.send_command(&Request::new( + SDIO_CMD_WRITEMULTIBLOCK, + SDIO_CMD_TYPE_AC, + SDIO_RESPONSE_TYPE_R1, + offset as u32, + sectors.len() as u32, + 512, + aligned_buffer.as_mut_ptr(), + ))?; + + self.send_command(&Request::DE_SELECT)?; + + // sectors + // .as_flattened_mut() + // .copy_from_slice(&mut aligned_buffer); + // + Ok(()) + } +} + +trait DeviceExt { + fn read_sectors( + &self, + sectors: &mut [[u8; 512]], + offset: usize, + ) -> Result<(), ogc_rs::ios::Error>; + + fn write_sectors(&self, sectors: &[[u8; 512]], offset: usize) + -> Result<(), ogc_rs::ios::Error>; +} + +pub fn try_init_sd() -> (bool, Device) { + const SD_STATUS_INSERTED: u32 = 0b1; + const SD_STATUS_INITALIZED: u32 = 0b1_0000_0000_0000_0000; + const SD_STATUS_SDHC: u32 = 0x100000; + + const HOST_CTRL_4BIT: u32 = 2; + + const HOST_CONTROLLER_REG_SOFT_RESET: u8 = 47; + const HOST_CONTROLLER_REG_PWR_CTRL: u8 = 41; + const HOST_CONTROLLER_REG_HOST_CTRL: u8 = 40; + const HOST_CONTROLLER_REG_CLK_CTRL: u8 = 44; + const HOST_CONTROLLER_REG_TIMEOUT_CTRL: u8 = 46; + // + // const SDIO_CMD_APPCMD: u32 = 55; + // const SDIO_CMD_SELECT: u32 = 7; + // const SDIO_CMD_SEND_CID: u32 = 2; + // const SDIO_CMD_SEND_RCA: u32 = 3; + // const SDIO_APPCMD_SENDOPCOND: u32 = 41; + // const SDIO_APPCMD_SET_BUS_WIDTH: u32 = 6; + // const SDIO_CMD_SET_BLOCK_LENGTH: u32 = 16; + // + // const SDIO_CMD_TYPE_AC: u32 = 3; + // + // const SDIO_RESPONSE_TYPE_R1: u32 = 1; + // const SDIO_RESPONSE_TYPE_R2: u32 = 3; + // const SDIO_RESPONSE_TYPE_R5: u32 = 6; + // const SDIO_RESPONSE_TYPE_R1B: u32 = 2; + // const SDIO_RESPONSE_TYPE_R3: u32 = 4; + // + let mut device = Device::open().unwrap(); + + const SOFTWARE_RESET_REGISTER: u8 = 0x2F; + bitflags::bitflags! { + pub struct SoftwareResetRegister: u8 { + const RESET_ALL = 0b1; + const RESET_CMD = 0b10; + const RESET_DATA = 0b100; + } + } + + // Reset + let mut rca = device.reset().unwrap(); + let status = device.get_status().unwrap(); + let is_sdhc = status & SD_STATUS_SDHC == SD_STATUS_SDHC; + + if status & SD_STATUS_INSERTED != SD_STATUS_INSERTED { + println!("SD Card not found"); + panic!(); + } + + if status & SD_STATUS_INITALIZED != SD_STATUS_INITALIZED { + // Drop and reopen the device + drop(device); + let mut device = Device::open().unwrap(); + + // Reset host controller using device + let reset = SoftwareResetRegister::RESET_ALL + | SoftwareResetRegister::RESET_CMD + | SoftwareResetRegister::RESET_DATA; + + device + .write_to_host_controller_register( + SOFTWARE_RESET_REGISTER, + core::mem::size_of::().try_into().unwrap(), + reset.bits().into(), + ) + .unwrap(); + + // Wait until all bits are 0 + while device + .read_from_host_controller_register(SOFTWARE_RESET_REGISTER, 1) + .unwrap() + != 0 + { + core::hint::spin_loop(); + } + + bitflags::bitflags! { + pub struct NormalInterruptStatusEnableRegister: u16 { + const COMMAND_COMPLETE_STATUS_ENABLE = 0b1; + const TRANSFER_COMPLETE_STATUS_ENABLE = 0b10; + const BLOCK_GAP_EVENT_STATUS_ENABLE = 0b100; + const DMA_INTERRUPT_STATUS_ENABLE = 0b1000; + const BUFFER_WRITE_READY_STATUS_ENABLE = 0b10000; + const BUFFER_READ_READY_STATUS_ENABLE = 0b100000; + const CARD_INSERTION_STATUS_ENABLE = 0b1000000; + const CARD_REMOVAL_STATUS_ENABLE = 0b10000000; + const CARD_INTERRUPT_STATUS_ENABLE = 0b100000000; + const INT_A_STATUS_ENABLE = 0b1000000000; + const INT_B_STATUS_ENABLE = 0b10000000000; + const INT_C_STATUS_ENABLE = 0b100000000000; + const RETUNING_EVENT_STATUS_ENABLE = 0b1000000000000; + const FX_EVENT_STATUS_ENABLE = 0b10000000000000; + } + } + + bitflags::bitflags! { + pub struct ErrorInterruptStatusEnableRegister: u16 { + const COMMAND_TIMEOUT_ERROR_STATUS_ENABLE = 0b1; + const COMMAND_CRC_ERROR_STATUS_ENABLE = 0b10; + const COMMAND_END_BIT_ERROR_STATUS_ENABLE = 0b100; + const COMMAND_INDEX_ERROR_STATUS_ENABLE = 0b1000; + const DATA_TIMEOUT_ERROR_STATUS_ENABLE = 0b10000; + const DATA_CRC_ERROR_STATUS_ENABLE = 0b100000; + const DATA_END_BIT_ERROR_STATUS_ENABLE = 0b1000000; + const CURRENT_LIMIT_ERROR_STATUS_ENABLE = 0b10000000; + const AUTO_CMD_ERROR_STATUS_ENABLE = 0b100000000; + const ADMA_ERROR_STATUS_ENABLE = 0b1000000000; + const TUNING_STATUS_ERROR_STATUS_ENABLE = 0b10000000000; + const REPSONSE_ERROR_STATUS_ENABLE = 0b100000000000; + } + } + + let normal_interrupt_status = + NormalInterruptStatusEnableRegister::COMMAND_COMPLETE_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::TRANSFER_COMPLETE_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::BLOCK_GAP_EVENT_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::DMA_INTERRUPT_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::BUFFER_WRITE_READY_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::BUFFER_READ_READY_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::CARD_INTERRUPT_STATUS_ENABLE; + + let error_interrupt_status = + ErrorInterruptStatusEnableRegister::COMMAND_TIMEOUT_ERROR_STATUS_ENABLE + | ErrorInterruptStatusEnableRegister::COMMAND_CRC_ERROR_STATUS_ENABLE + | ErrorInterruptStatusEnableRegister::DATA_END_BIT_ERROR_STATUS_ENABLE + | ErrorInterruptStatusEnableRegister::CURRENT_LIMIT_ERROR_STATUS_ENABLE; + + let status: u32 = u32::from(normal_interrupt_status.bits()) << 16 + | u32::from(error_interrupt_status.bits()); + + const NORMAL_INTERRUPT_STATUS_REGISTER: u8 = 0x34; + //const ERROR_INTERRUPT_STATUS_REGISTER: u8 = 0x36; + + const NORMAL_INTERRUPT_SIGNAL_STATUS_REGISTER: u8 = 0x38; + //const ERROR_INTERRUPT_SIGNAL_STATUS_REGISTER: u8 = 0x3A; + + // This writes to `NORMAL_INTERRUPT_STATUS_REGISTER` and `ERROR_INTERRUPT_STATUS_REGISTER` as + // one call + let _ = + device.write_to_host_controller_register(NORMAL_INTERRUPT_STATUS_REGISTER, 4, status); + + // This writes to both `NORMAL_INTERRUPT_SIGNAL_STATUS_REGISTER` and + // `ERROR_INTERRUPT_STATUS_REGISTER` as one call + let _ = device.write_to_host_controller_register( + NORMAL_INTERRUPT_SIGNAL_STATUS_REGISTER, + 4, + status, + ); + + bitflags::bitflags! { + pub struct PowerControlRegister: u8 { + const SD_BUS_POWER_VDD1 = 0b1; + const SD_BUS_VOLTAGE_SELECT_18V = 0b1010; + const SD_BUS_VOLTAGE_SELECT_3V = 0b1100; + const SD_BUS_VOLTAGE_SELECT_33V = 0b1110; + } + } + + let select = PowerControlRegister::SD_BUS_VOLTAGE_SELECT_33V; + + // Set power + device + .write_to_host_controller_register( + HOST_CONTROLLER_REG_PWR_CTRL, + 1, + select.bits().into(), + ) + .unwrap(); + device + .write_to_host_controller_register( + HOST_CONTROLLER_REG_PWR_CTRL, + 1, + (select | PowerControlRegister::SD_BUS_POWER_VDD1) + .bits() + .into(), + ) + .unwrap(); + + bitflags::bitflags! { + pub struct ClockControlRegister: u16 { + const INTERNAL_CLOCK_ENABLE = 0b1; + const INTERNAL_CLOCK_STABLE = 0b10; + const SD_CLOCK_ENABLE = 0b100; + const PLL_ENABLE = 0b1000; + const CLK_DIV_BY_2 = 0b100000000; + const CLK_DIV_BY_4 = 0b1000000000; + const CLK_DIV_BY_8 = 0b10000000000; + const CLK_DIV_BY_16 = 0b100000000000; + const CLK_DIV_BY_32 = 0b1000000000000; + const CLK_DIV_BY_64 = 0b10000000000000; + const CLK_DIV_BY_128 = 0b100000000000000; + const CLK_DIV_BY_256 = 0b1000000000000000; + } + } + + // Clock + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0) + .unwrap(); + + let clock = + ClockControlRegister::INTERNAL_CLOCK_ENABLE | ClockControlRegister::CLK_DIV_BY_2; + + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, clock.bits().into()) + .unwrap(); + + while device + .read_from_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2) + .unwrap() + & u32::from(ClockControlRegister::INTERNAL_CLOCK_STABLE.bits()) + != u32::from(ClockControlRegister::INTERNAL_CLOCK_STABLE.bits()) + { + core::hint::spin_loop(); + } + + device + .write_to_host_controller_register( + HOST_CONTROLLER_REG_CLK_CTRL, + 2, + (clock + | ClockControlRegister::INTERNAL_CLOCK_STABLE + | ClockControlRegister::SD_CLOCK_ENABLE) + .bits() + .into(), + ) + .unwrap(); + + //max timeout for stand sd cards not sdxc + //CLK x 2^27 + const TIMEOUT_CLOCK: u32 = 0b1110; + + // Timeout + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_TIMEOUT_CTRL, 1, TIMEOUT_CLOCK) + .unwrap(); + + let _ = device.send_command(&Request::GO_IDLE).unwrap(); + let resp = device.send_command(&Request::SEND_IF_COND).unwrap(); + + let resp_bytes = u32::from_be_bytes(resp.bytes()[0..4].try_into().unwrap()); + if resp_bytes & 0xFF != 0xAA { + println!("Response from IF_COND: {}", resp_bytes); + } + + let is_sdhc = loop { + let _ = device.send_command(&Request::APP_CMD).unwrap(); + let resp = device.send_command(&Request::SEND_OP_COND).unwrap(); + let resp_bytes = u32::from_be_bytes(resp.bytes()[0..4].try_into().unwrap()); + if resp_bytes & 1 << 31 == 1 << 31 { + break resp_bytes & 1 << 30 == 1 << 30; + } + }; + + let resp_cid = device.send_command(&Request::send_cid(rca)).unwrap(); + println!("{:?}", resp_cid); + + let resp_rca = device.send_command(&Request::SEND_RCA).unwrap(); + println!("{:?}", resp_rca); + rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); + + let mut host_ctrl = device + .read_from_host_controller_register(HOST_CONTROLLER_REG_HOST_CTRL, 1) + .unwrap(); + host_ctrl &= 0xff; + host_ctrl &= !HOST_CTRL_4BIT; + host_ctrl |= HOST_CTRL_4BIT; + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_HOST_CTRL, 1, host_ctrl) + .unwrap(); + + device.enable_clock(true).unwrap(); + + device.send_command(&Request::select(rca)).unwrap(); + { + device + .send_command(&Request::set_block_length(512)) + .unwrap(); + device.send_command(&Request::appcmd_with_rca(rca)).unwrap(); + device.send_command(&Request::set_bus_width(4)).unwrap(); + } + device.send_command(&Request::DE_SELECT).unwrap(); + + println!("END OF INIT"); + + return (is_sdhc, device); + } + + return (is_sdhc, device); +} + +#[derive(Debug)] +pub struct BPB { + bytes_per_sector: u16, + sectors_per_cluster: u8, + reserved_sector_count: u16, + fat_count: u8, + fat16_max_root_dir_entry_count: u16, + sector_count: u16, + media_type: u8, + sectors_per_fat_count: u16, + sectors_per_track: u16, + head_count: u16, + hidden_sector_count: u32, + sector_count_fat32: u32, + sectors_per_fat: u32, + drive_flags: u16, + version: u16, + cluster_root_dir_start: u32, + fs_info_sector: u16, + backup_sector: u16, +} + +impl BPB { + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { + bytes_per_sector: u16::from_le_bytes(bytes[0..2].try_into().unwrap()), + sectors_per_cluster: bytes[2], + reserved_sector_count: u16::from_le_bytes(bytes[3..5].try_into().unwrap()), + fat_count: bytes[5], + fat16_max_root_dir_entry_count: u16::from_le_bytes(bytes[6..8].try_into().unwrap()), + sector_count: u16::from_le_bytes(bytes[8..10].try_into().unwrap()), + media_type: bytes[10], + sectors_per_fat_count: u16::from_le_bytes(bytes[11..13].try_into().unwrap()), + sectors_per_track: u16::from_le_bytes(bytes[13..15].try_into().unwrap()), + head_count: u16::from_le_bytes(bytes[15..17].try_into().unwrap()), + hidden_sector_count: u32::from_le_bytes(bytes[17..21].try_into().unwrap()), + sector_count_fat32: u32::from_le_bytes(bytes[21..25].try_into().unwrap()), + sectors_per_fat: u32::from_le_bytes(bytes[25..29].try_into().unwrap()), + drive_flags: u16::from_le_bytes(bytes[29..31].try_into().unwrap()), + version: u16::from_le_bytes(bytes[31..33].try_into().unwrap()), + cluster_root_dir_start: u32::from_le_bytes(bytes[33..37].try_into().unwrap()), + fs_info_sector: u16::from_le_bytes(bytes[37..39].try_into().unwrap()), + backup_sector: u16::from_le_bytes(bytes[39..41].try_into().unwrap()), + } + } +} + +#[derive(Debug)] +pub struct FSInfo { + free_cluster_count: u32, + recent_cluster: u32, +} + +impl FSInfo { + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { + free_cluster_count: u32::from_le_bytes(bytes[488..492].try_into().unwrap()), + recent_cluster: u32::from_le_bytes(bytes[492..496].try_into().unwrap()), + } + } +} diff --git a/ogc-sys/src/ogc.rs b/ogc-sys/src/ogc.rs index 86751d4..45fb306 100644 --- a/ogc-sys/src/ogc.rs +++ b/ogc-sys/src/ogc.rs @@ -77,13 +77,13 @@ pub const true_: u32 = 1; pub const false_: u32 = 0; pub const __bool_true_false_are_defined: u32 = 1; pub const _NEWLIB_VERSION_H__: u32 = 1; -pub const _NEWLIB_VERSION: &[u8; 6] = b"4.4.0\0"; +pub const _NEWLIB_VERSION: &[u8; 6] = b"4.5.0\0"; pub const __NEWLIB__: u32 = 4; -pub const __NEWLIB_MINOR__: u32 = 4; +pub const __NEWLIB_MINOR__: u32 = 5; pub const __NEWLIB_PATCHLEVEL__: u32 = 0; pub const _DEFAULT_SOURCE: u32 = 1; pub const _POSIX_SOURCE: u32 = 1; -pub const _POSIX_C_SOURCE: u32 = 200809; +pub const _POSIX_C_SOURCE: u32 = 202405; pub const _ATFILE_SOURCE: u32 = 1; pub const __ATFILE_VISIBLE: u32 = 1; pub const __BSD_VISIBLE: u32 = 1; @@ -91,7 +91,7 @@ pub const __GNU_VISIBLE: u32 = 0; pub const __ISO_C_VISIBLE: u32 = 2011; pub const __LARGEFILE_VISIBLE: u32 = 0; pub const __MISC_VISIBLE: u32 = 1; -pub const __POSIX_VISIBLE: u32 = 200809; +pub const __POSIX_VISIBLE: u32 = 202405; pub const __SVID_VISIBLE: u32 = 1; pub const __XSI_VISIBLE: u32 = 0; pub const __SSP_FORTIFY_LEVEL: u32 = 0; @@ -242,6 +242,14 @@ pub const COLOR_MONEYGREEN: u32 = 3279471478; pub const COLOR_SKYBLUE: u32 = 3096885357; pub const COLOR_CREAM: u32 = 3900434563; pub const COLOR_MEDGRAY: u32 = 2592250496; +pub const CONSOLE_COLOR_BLACK: u32 = 0; +pub const CONSOLE_COLOR_RED: u32 = 1; +pub const CONSOLE_COLOR_GREEN: u32 = 2; +pub const CONSOLE_COLOR_YELLOW: u32 = 3; +pub const CONSOLE_COLOR_BLUE: u32 = 4; +pub const CONSOLE_COLOR_MAGENTA: u32 = 5; +pub const CONSOLE_COLOR_CYAN: u32 = 6; +pub const CONSOLE_COLOR_WHITE: u32 = 7; pub const FEATURE_MEDIUM_CANREAD: u32 = 1; pub const FEATURE_MEDIUM_CANWRITE: u32 = 2; pub const FEATURE_GAMECUBE_SLOTA: u32 = 16; @@ -1129,6 +1137,7 @@ pub const _REENT_CHECK_VERIFY: u32 = 1; pub const _UNBUF_STREAM_OPT: u32 = 1; pub const _WANT_IO_C99_FORMATS: u32 = 1; pub const _WANT_IO_LONG_LONG: u32 = 1; +pub const _WANT_IO_POS_ARGS: u32 = 1; pub const _WANT_REGISTER_FINI: u32 = 1; pub const _WANT_USE_GDTOA: u32 = 1; pub const _WIDE_ORIENT: u32 = 1; @@ -1243,6 +1252,8 @@ pub const CLOCK_DISABLED: u32 = 0; pub const CLOCK_ALLOWED: u32 = 1; pub const CLOCK_DISALLOWED: u32 = 0; pub const TIMER_ABSTIME: u32 = 4; +pub const CLOCK_REALTIME: u32 = 1; +pub const CLOCK_MONOTONIC: u32 = 4; pub const SYS_BASE_CACHED: u32 = 2147483648; pub const SYS_BASE_UNCACHED: u32 = 3221225472; pub const SYS_WD_NULL: u32 = 4294967295; @@ -1297,6 +1308,8 @@ pub const VI_MAX_WIDTH_MPAL: u32 = 720; pub const VI_MAX_HEIGHT_MPAL: u32 = 480; pub const VI_MAX_WIDTH_EURGB60: u32 = 720; pub const VI_MAX_HEIGHT_EURGB60: u32 = 480; +pub const VI_MAX_WIDTH_DEBUG: u32 = 720; +pub const VI_MAX_HEIGHT_DEBUG: u32 = 480; pub const HW_IPC_PPCBASE: u32 = 3439329280; pub const HW_IPC_PPC_SEND: u32 = 1; pub const HW_IPC_PPC_MSG_ACK: u32 = 2; @@ -1326,8 +1339,11 @@ pub const ES_SIG_ECDSA: u32 = 65538; pub const ES_CERT_RSA4096: u32 = 0; pub const ES_CERT_RSA2048: u32 = 1; pub const ES_CERT_ECDSA: u32 = 2; +pub const ES_KEY_NANDFS: u32 = 2; pub const ES_KEY_COMMON: u32 = 4; +pub const ES_KEY_BACKUP: u32 = 5; pub const ES_KEY_SDCARD: u32 = 6; +pub const ES_KEY_KOREAN: u32 = 11; pub const MAX_NUM_TMD_CONTENTS: u32 = 512; pub const STM_EVENT_RESET: u32 = 131072; pub const STM_EVENT_POWER: u32 = 2048; @@ -2654,6 +2670,110 @@ extern "C" { #[doc = "CON_EnableGecko(int channel, int safe)\n Enable or disable the USB gecko console.\n\n # Arguments\n\n* `channel` (direction in) - EXI channel, or -1 �to disable the gecko console\n * `safe` (direction in) - If true, use safe mode (wait for peer)\n\n # Returns\n\nnone"] pub fn CON_EnableGecko(channel: ::libc::c_int, safe: ::libc::c_int); } +#[doc = "A callback for printing a character."] +pub type ConsolePrint = ::core::option::Option< + unsafe extern "C" fn(con: *mut ::libc::c_void, c: ::libc::c_int) -> bool, +>; +#[doc = "A font struct for the console."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ConsoleFont { + #[doc = "< A pointer to the font graphics"] + pub gfx: *mut u8_, + #[doc = "< Offset to the first valid character in the font table"] + pub asciiOffset: u16_, + #[doc = "< Number of characters in the font graphics"] + pub numChars: u16_, +} +#[doc = "Console structure used to store the state of a console render context.\n\n Default values from consoleGetDefault();\n PrintConsole defaultConsole =\n {\n \t//Font:\n \t{\n \t\t(u8*)default_font_bin, //font gfx\n \t\t0, //first ascii character in the set\n \t\t128, //number of characters in the font set\n\t},\n\t0,0, //cursorX cursorY\n\t0,0, //prevcursorX prevcursorY\n\t40, //console width\n\t30, //console height\n\t0, //window x\n\t0, //window y\n\t32, //window width\n\t24, //window height\n\t3, //tab size\n\t0, //font character offset\n\t0, //print callback\n\tfalse //console initialized\n };\n "] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PrintConsole { + #[doc = "< Font of the console"] + pub font: ConsoleFont, + #[doc = "< Framebuffer address"] + pub destbuffer: *mut ::libc::c_void, + pub con_xres: ::libc::c_int, + pub con_yres: ::libc::c_int, + pub con_stride: ::libc::c_int, + pub target_x: ::libc::c_int, + pub target_y: ::libc::c_int, + pub tgt_stride: ::libc::c_int, + #[doc = "< Current X location of the cursor (as a tile offset by default)"] + pub cursorX: ::libc::c_int, + #[doc = "< Current Y location of the cursor (as a tile offset by default)"] + pub cursorY: ::libc::c_int, + #[doc = "< Internal state"] + pub prevCursorX: ::libc::c_int, + #[doc = "< Internal state"] + pub prevCursorY: ::libc::c_int, + #[doc = "< Width of the console hardware layer in characters"] + pub con_cols: ::libc::c_int, + #[doc = "< Height of the console hardware layer in characters"] + pub con_rows: ::libc::c_int, + #[doc = "< Window X location in characters (not implemented)"] + pub windowX: ::libc::c_int, + #[doc = "< Window Y location in characters (not implemented)"] + pub windowY: ::libc::c_int, + #[doc = "< Window width in characters (not implemented)"] + pub windowWidth: ::libc::c_int, + #[doc = "< Window height in characters (not implemented)"] + pub windowHeight: ::libc::c_int, + #[doc = "< Size of a tab"] + pub tabSize: ::libc::c_int, + #[doc = "< Foreground color"] + pub fg: ::libc::c_uint, + #[doc = "< Background color"] + pub bg: ::libc::c_uint, + #[doc = "< Attribute flags"] + pub flags: ::libc::c_uint, + #[doc = "< Callback for printing a character. Should return true if it has handled rendering the graphics (else the print engine will attempt to render via tiles)."] + pub PrintChar: ConsolePrint, + #[doc = "< True if the console is initialized"] + pub consoleInitialised: bool, +} +#[doc = "< Swallows prints to stderr"] +pub const debugDevice_NULL: debugDevice = 0; +#[doc = "< Outputs stderr debug statements using exi uart"] +pub const debugDevice_EXI: debugDevice = 1; +#[doc = "< Directs stderr debug statements to console window"] +pub const debugDevice_CONSOLE: debugDevice = 2; +#[doc = "Console debug devices supported by libogc."] +pub type debugDevice = ::libc::c_uint; +extern "C" { + #[doc = "Loads the font into the console.\n # Arguments\n\n* `console` - Pointer to the console to update, if NULL it will update the current console.\n * `font` - The font to load."] + pub fn consoleSetFont(console: *mut PrintConsole, font: *mut ConsoleFont); +} +extern "C" { + #[doc = "Sets the print window.\n # Arguments\n\n* `console` - Console to set, if NULL it will set the current console window.\n * `x` - X location of the window.\n * `y` - Y location of the window.\n * `width` - Width of the window.\n * `height` - Height of the window."] + pub fn consoleSetWindow( + console: *mut PrintConsole, + x: ::libc::c_uint, + y: ::libc::c_uint, + width: ::libc::c_uint, + height: ::libc::c_uint, + ); +} +extern "C" { + #[doc = "Gets a pointer to the console with the default values.\n This should only be used when using a single console or without changing the console that is returned, otherwise use consoleInit().\n # Returns\n\nA pointer to the console with the default values."] + pub fn consoleGetDefault() -> *mut PrintConsole; +} +extern "C" { + #[doc = "Make the specified console the render target.\n # Arguments\n\n* `console` - A pointer to the console struct (must have been initialized with consoleInit(PrintConsole* console)).\n # Returns\n\nA pointer to the previous console."] + pub fn consoleSelect(console: *mut PrintConsole) -> *mut PrintConsole; +} +extern "C" { + #[doc = "Initialise the console.\n # Arguments\n\n* `console` - A pointer to the console data to initialize (if it's NULL, the default console will be used).\n # Returns\n\nA pointer to the current console."] + pub fn consoleInit(console: *mut PrintConsole) -> *mut PrintConsole; +} +extern "C" { + #[doc = "Initializes debug console output on stderr to the specified device.\n # Arguments\n\n* `device` - The debug device (or devices) to output debug print statements to."] + pub fn consoleDebugInit(device: debugDevice); +} +extern "C" { + #[doc = "Clears the screen by using iprintf(\""] + pub fn consoleClear(); +} pub type sec_t = u32; pub type FN_MEDIUM_STARTUP = ::core::option::Option bool>; pub type FN_MEDIUM_ISINSERTED = ::core::option::Option bool>; @@ -4678,6 +4798,8 @@ extern "C" { extern "C" { pub fn TPL_CloseTPLFile(tdf: *mut TPLFile); } +pub type __gnuc_va_list = __builtin_va_list; +pub type va_list = __gnuc_va_list; pub type wchar_t = ::libc::c_int; #[repr(C)] #[repr(align(16))] @@ -4773,13 +4895,6 @@ pub type _flock_t = _LOCK_RECURSIVE_T; pub struct __locale_t { _unused: [u8; 0], } -extern "C" { - pub fn memset( - arg1: *mut ::libc::c_void, - arg2: ::libc::c_int, - arg3: ::libc::c_ulong, - ) -> *mut ::libc::c_void; -} #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct _Bigint { @@ -4922,7 +5037,7 @@ pub struct _reent { #[derive(Debug, Copy, Clone)] pub struct _reent__bindgen_ty_1 { pub _reent: __BindgenUnionField<_reent__bindgen_ty_1__bindgen_ty_1>, - pub bindgen_union_field: [u64; 25usize], + pub bindgen_union_field: [u64; 29usize], } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -4945,6 +5060,7 @@ pub struct _reent__bindgen_ty_1__bindgen_ty_1 { pub _wcrtomb_state: _mbstate_t, pub _wcsrtombs_state: _mbstate_t, pub _h_errno: ::libc::c_int, + pub _getlocalename_l_buf: [::libc::c_char; 32usize], } extern "C" { pub static mut _impure_ptr: *mut _reent; @@ -5616,7 +5732,15 @@ extern "C" { extern "C" { pub fn SYS_Time() -> u64_; } +#[doc = "Printing callback used by dietPrint.\n> **Note:** The incoming `buf` may be NULL, in which case the callback is\nexpected to output the number of space characters given by `size.`"] +pub type DietPrintFn = + ::core::option::Option; extern "C" { + #[doc = "! Prints the specified formatted text (works like vprintf)."] + pub fn dietPrintV(fmt: *const ::libc::c_char, va: *mut [u32; 3usize]); +} +extern "C" { + #[doc = "! Prints the specified formatted text (works like printf)."] pub fn kprintf(str_: *const ::libc::c_char, ...); } extern "C" { @@ -5761,6 +5885,16 @@ extern "C" { extern "C" { pub static mut TVEurgb60Hz480ProgAa: GXRModeObj; } +extern "C" { + #[doc = "< Video and render mode configuration for 480 lines,progressive,singlefield RGB mode"] + pub static mut TVRgb480Prog: GXRModeObj; +} +extern "C" { + pub static mut TVRgb480ProgSoft: GXRModeObj; +} +extern "C" { + pub static mut TVRgb480ProgAa: GXRModeObj; +} #[doc = "void (*VIRetraceCallback)(u32 retraceCnt)\n function pointer typedef for the user's retrace callback\n # Arguments\n\n* `retraceCnt` (direction in) - current retrace count"] pub type VIRetraceCallback = ::core::option::Option; pub type VIPositionCallback = ::core::option::Option; @@ -6393,13 +6527,37 @@ extern "C" { pub fn ES_GetTitleContentsCount(titleID: u64_, num: *mut u32_) -> s32; } extern "C" { - pub fn ES_GetTitleContents(titleID: u64_, data: *mut u8_, size: u32_) -> s32; + pub fn ES_GetTitleContents(titleID: u64_, contents: *mut u32_, num: u32_) -> s32; } extern "C" { pub fn ES_GetTMDViewSize(titleID: u64_, size: *mut u32_) -> s32; } extern "C" { - pub fn ES_GetTMDView(titleID: u64_, data: *mut u8_, size: u32_) -> s32; + pub fn ES_GetTMDView(titleID: u64_, view: *mut tmd_view, size: u32_) -> s32; +} +extern "C" { + pub fn ES_GetConsumptionCount(ticketID: u64_, n_limits: *mut u32_) -> s32; +} +extern "C" { + pub fn ES_GetConsumption(ticketID: u64_, limits: *mut tiklimit, n_limits: u32_) -> s32; +} +extern "C" { + pub fn ES_DiGetTMDViewSize( + s_tmd: *const signed_blob, + tmd_size: u32_, + view_size: *mut u32_, + ) -> s32; +} +extern "C" { + pub fn ES_DiGetTMDView( + s_tmd: *const signed_blob, + tmd_size: u32_, + view: *mut tmd_view, + view_size: u32_, + ) -> s32; +} +extern "C" { + pub fn ES_DiGetTicketView(s_tik: *const signed_blob, view: *mut tikview) -> s32; } extern "C" { pub fn ES_GetNumSharedContents(cnt: *mut u32_) -> s32; @@ -6424,6 +6582,16 @@ extern "C" { keyid: *mut u32_, ) -> s32; } +extern "C" { + pub fn ES_DiVerifyWithTicketView( + certificates: *const signed_blob, + certificates_size: u32_, + s_tmd: *const signed_blob, + tmd_size: u32_, + ticket_view: *const tikview, + keynum: *mut u32_, + ) -> s32; +} extern "C" { pub fn ES_AddTicket( tik: *const signed_blob, @@ -6438,7 +6606,22 @@ extern "C" { pub fn ES_DeleteTicket(view: *const tikview) -> s32; } extern "C" { - pub fn ES_AddTitleTMD(tmd: *const signed_blob, tmd_size: u32_) -> s32; + pub fn ES_ExportTitleInit(titleID: u64_, tmd_out: *mut signed_blob, tmd_size: u32_) -> s32; +} +extern "C" { + pub fn ES_ExportContentBegin(titleID: u64_, cid: u32_) -> s32; +} +extern "C" { + pub fn ES_ExportContentData(cfd: s32, data: *mut ::libc::c_void, data_size: u32_) -> s32; +} +extern "C" { + pub fn ES_ExportContentEnd(cfd: s32) -> s32; +} +extern "C" { + pub fn ES_ExportTitleDone() -> s32; +} +extern "C" { + pub fn ES_ReimportTitleInit(tmd: *const signed_blob, tmd_size: u32_) -> s32; } extern "C" { pub fn ES_AddTitleStart( @@ -6454,7 +6637,7 @@ extern "C" { pub fn ES_AddContentStart(titleID: u64_, cid: u32_) -> s32; } extern "C" { - pub fn ES_AddContentData(cid: s32, data: *mut u8_, data_size: u32_) -> s32; + pub fn ES_AddContentData(cid: s32, data: *const ::libc::c_void, data_size: u32_) -> s32; } extern "C" { pub fn ES_AddContentFinish(cid: u32_) -> s32; @@ -6483,10 +6666,10 @@ extern "C" { pub fn ES_OpenContent(index: u16_) -> s32; } extern "C" { - pub fn ES_OpenTitleContent(titleID: u64_, views: *mut tikview, index: u16_) -> s32; + pub fn ES_OpenTitleContent(titleID: u64_, views: *const tikview, index: u16_) -> s32; } extern "C" { - pub fn ES_ReadContent(cfd: s32, data: *mut u8_, data_size: u32_) -> s32; + pub fn ES_ReadContent(cfd: s32, data: *mut ::libc::c_void, data_size: u32_) -> s32; } extern "C" { pub fn ES_SeekContent(cfd: s32, where_: s32, whence: s32) -> s32; @@ -6503,23 +6686,37 @@ extern "C" { extern "C" { pub fn ES_Encrypt( keynum: u32_, - iv: *mut u8_, - source: *mut u8_, + iv: *mut u32_, + source: *const ::libc::c_void, size: u32_, - dest: *mut u8_, + dest: *mut ::libc::c_void, ) -> s32; } extern "C" { pub fn ES_Decrypt( keynum: u32_, - iv: *mut u8_, - source: *mut u8_, + iv: *mut u32_, + source: *const ::libc::c_void, size: u32_, - dest: *mut u8_, + dest: *mut ::libc::c_void, ) -> s32; } extern "C" { - pub fn ES_Sign(source: *mut u8_, size: u32_, sig: *mut u8_, certs: *mut u8_) -> s32; + pub fn ES_Sign( + source: *const ::libc::c_void, + size: u32_, + ap_signature: *mut u8_, + ap_certificate: *mut signed_blob, + ) -> s32; +} +extern "C" { + pub fn ES_VerifySign( + source: *const ::libc::c_void, + size: u32_, + ap_signature: *const u8_, + certificates: *const signed_blob, + certificates_size: u32_, + ) -> s32; } extern "C" { pub fn ES_GetDeviceCert(outbuf: *mut u8_) -> s32; @@ -6531,7 +6728,10 @@ extern "C" { pub fn ES_GetBoot2Version(version: *mut u32_) -> s32; } extern "C" { - pub fn ES_NextCert(certs: *const signed_blob) -> *mut signed_blob; + pub fn ES_CheckHasKoreanKey() -> s32; +} +extern "C" { + pub fn ES_NextCert(certs: *const signed_blob) -> *const signed_blob; } pub type stmcallback = ::core::option::Option; extern "C" { @@ -7431,6 +7631,9 @@ extern "C" { extern "C" { pub static mut __io_usbstorage: DISC_INTERFACE; } +extern "C" { + pub static mut __io_usbstorage_sector_size: u32_; +} extern "C" { pub fn WII_Initialize() -> s32; } diff --git a/powerpc-unknown-eabi.json b/powerpc-unknown-eabi.json index e28c9fd..cc2c32a 100644 --- a/powerpc-unknown-eabi.json +++ b/powerpc-unknown-eabi.json @@ -28,6 +28,6 @@ "target-family": "unix", "target-mcount": "_mcount", "target-c-int-width": 32, - "target-pointer-width": "32", + "target-pointer-width": 32, "vendor": "nintendo" } diff --git a/src/ios.rs b/src/ios.rs index 4921688..7e2f08f 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -17,9 +17,13 @@ pub mod fs; /// E-Ticket System IOS Device /// -/// `/dev/es` device hellper functions. +/// `/dev/es` device helper functions. pub mod es; +/// SDIO IOS Device +/// `/dev/sdio/slot0` device helper functions +pub mod sdio; + #[repr(u32)] /// Interprocess Control / IOS File Mode pub enum Mode { diff --git a/src/ios/sdio.rs b/src/ios/sdio.rs new file mode 100644 index 0000000..a410e2c --- /dev/null +++ b/src/ios/sdio.rs @@ -0,0 +1,482 @@ +/// SDIO supported Ioctls +pub enum Ioctl { + /// Write to a SD host controller register + WriteHostControllerRegister, + /// Read from a SD host controller register + ReadHostControllerRegister, + /// Reset SD card + ResetSDCard, + /// Set SD card clock + SetClock, + /// Send SDIO command + SendCommand, + /// Get SD card status + GetStatus, + /// Get operating conditions register + GetOperatingConditionsRegister, +} + +impl From for i32 { + fn from(value: Ioctl) -> Self { + match value { + Ioctl::WriteHostControllerRegister => 1, + Ioctl::ReadHostControllerRegister => 2, + Ioctl::ResetSDCard => 4, + Ioctl::SetClock => 6, + Ioctl::SendCommand => 7, + Ioctl::GetStatus => 11, + Ioctl::GetOperatingConditionsRegister => 12, + } + } +} +/// Try from Ioctl Error +/// This happens when you don't provide a proper i32 to map to an [`Ioctl`] +pub struct TryFromIoctlError; +impl TryFrom for Ioctl { + type Error = TryFromIoctlError; + fn try_from(value: i32) -> Result { + match value { + 1 => Ok(Self::WriteHostControllerRegister), + 2 => Ok(Self::ReadHostControllerRegister), + 4 => Ok(Self::ResetSDCard), + 6 => Ok(Self::SetClock), + 7 => Ok(Self::SendCommand), + 11 => Ok(Self::GetStatus), + 12 => Ok(Self::GetOperatingConditionsRegister), + _ => Err(TryFromIoctlError), + } + } +} +//#[repr(C, align(32))] +/// SDIO request +pub struct Request { + command: u32, + command_type: u32, + response_type: u32, + arg: u32, + block_count: u32, + block_size: u32, + dma_addr: *mut u8, + is_dma: u32, + pad0: u32, +} + +impl Request { + const SDIO_CMD_GO_IDLE: u32 = 0; + const SDIO_RESPONSE_TYPE_R6: u32 = 7; + const SDIO_CMD_SEND_IF_COND: u32 = 8; + + const SDIO_CMD_APPCMD: u32 = 55; + const SDIO_CMD_SELECT: u32 = 7; + const SDIO_CMD_SEND_CID: u32 = 2; + const SDIO_CMD_SEND_RCA: u32 = 3; + const SDIO_APPCMD_SENDOPCOND: u32 = 41; + const SDIO_APPCMD_SET_BUS_WIDTH: u32 = 6; + const SDIO_CMD_SET_BLOCK_LENGTH: u32 = 16; + + const SDIO_CMD_TYPE_AC: u32 = 3; + + const SDIO_RESPONSE_TYPE_R1: u32 = 1; + const SDIO_RESPONSE_TYPE_R2: u32 = 3; + const SDIO_RESPONSE_TYPE_R5: u32 = 6; + const SDIO_RESPONSE_TYPE_R1B: u32 = 2; + const SDIO_RESPONSE_TYPE_R3: u32 = 4; + + /// `SDIO_CMD_GO_IDLE` + pub const GO_IDLE: Request = + Request::new(Self::SDIO_CMD_GO_IDLE, 0, 0, 0, 0, 0, core::ptr::null_mut()); + + /// `SDIO_CMD_SEND_IF_COND` + pub const SEND_IF_COND: Request = Request::new( + Self::SDIO_CMD_SEND_IF_COND, + 0, + Self::SDIO_RESPONSE_TYPE_R6, + 0x1AA, + 0, + 0, + core::ptr::null_mut(), + ); + + /// `SDIO_CMD_APPCMD` + pub const APP_CMD: Request = Request::new( + Self::SDIO_CMD_APPCMD, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + 0, + 0, + 0, + core::ptr::null_mut(), + ); + + /// `SDIO_CMD_APPCMD_SEND_OP_COND` + pub const SEND_OP_COND: Request = Request::new( + Self::SDIO_APPCMD_SENDOPCOND, + 0, + Self::SDIO_RESPONSE_TYPE_R3, + 0x4030_0000, + 0, + 0, + core::ptr::null_mut(), + ); + + /// `SDIO_CMD_DESELECT` + pub const DE_SELECT: Request = Request::new( + Self::SDIO_CMD_SELECT, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1B, + 0, + 0, + 0, + core::ptr::null_mut(), + ); + + /// `SDIO_CMD_SENDRCA` + pub const SEND_RCA: Request = Request::new( + Self::SDIO_CMD_SEND_RCA, + 0, + Self::SDIO_RESPONSE_TYPE_R5, + 0, + 0, + 0, + core::ptr::null_mut(), + ); + + /// `SDIO_CMD_APPCMD_SET_BUS_WIDTH` + #[must_use] + pub const fn set_bus_width(width: u32) -> Request { + Request::new( + Self::SDIO_APPCMD_SET_BUS_WIDTH, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + width, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// `SDIO_CMD_SEND_CID` + #[must_use] + pub const fn send_cid(rca: u32) -> Request { + Request::new( + Self::SDIO_CMD_SEND_CID, + 0, + Self::SDIO_RESPONSE_TYPE_R2, + rca, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// `SDIO_CMD_SET_BLOCK_LENGTH` + #[must_use] + pub const fn set_block_length(length: u32) -> Request { + Request::new( + Self::SDIO_CMD_SET_BLOCK_LENGTH, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + length, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// Select the SD Card with `rca` + #[must_use] + pub const fn select(rca: u32) -> Request { + Request::new( + Self::SDIO_CMD_SELECT, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1B, + rca, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// Build a APPCMD with RCA as the arg + #[must_use] + pub const fn appcmd_with_rca(rca: u32) -> Request { + Request::new( + Self::SDIO_CMD_APPCMD, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + rca, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// Create a new `Request` for the SDIO device + #[must_use] + pub const fn new( + command: u32, + command_type: u32, + response_type: u32, + arg: u32, + block_count: u32, + block_size: u32, + dma_addr: *mut u8, + ) -> Self { + let is_dma = !dma_addr.is_null() as u32; + Self { + command, + command_type, + response_type, + arg, + block_count, + block_size, + dma_addr, + is_dma, + pad0: 0, + } + } +} + +/// SDIO response +pub struct Response { + buf: [u8; 16], +} + +impl Response { + /// Create a response from a byte buffer + #[must_use] + pub fn from_bytes(bytes: &[u8; 16]) -> Self { + Self { buf: *bytes } + } + + /// Get bytes of response + #[must_use] + pub fn bytes(&self) -> &[u8; 16] { + &self.buf + } +} + +pub use dev::Device; +mod dev { + use core::mem::ManuallyDrop; + + use crate::ios::{ + self, + sdio::{Ioctl, Request, Response}, + FileDescriptor, + }; + + type RawFd = core::ffi::c_int; + + struct ValidRawFd { + fd: RawFd, + } + impl ValidRawFd { + pub fn new(fd: RawFd) -> Option { + if fd.is_positive() || fd == 0 { + Some(ValidRawFd { fd }) + } else { + None + } + } + + pub fn as_raw_fd(&self) -> RawFd { + self.fd + } + + pub fn into_raw_fd(self) -> RawFd { + self.fd + } + } + + struct OwnedFd { + fd: ValidRawFd, + } + + impl OwnedFd { + pub unsafe fn from_raw_fd(fd: RawFd) -> Option { + ValidRawFd::new(fd).map(|fd| OwnedFd { fd }) + } + + pub fn into_raw_fd(self) -> RawFd { + ManuallyDrop::new(self).fd.as_raw_fd() + } + + pub fn as_file_descriptor(&self) -> FileDescriptor { + FileDescriptor(self.fd.as_raw_fd()) + } + } + + impl Drop for OwnedFd { + fn drop(&mut self) { + let _ = ios::close(ios::FileDescriptor(self.fd.as_raw_fd())); + } + } + + /// `/dev/sdio/slot0` Device + pub struct Device { + fd: OwnedFd, + } + + impl Device { + /// Try to open `/dev/sdio/slot0` + /// # Errors + /// See [`ios::Error`] + pub fn open() -> Result { + let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::Read)?; + let fd = unsafe { OwnedFd::from_raw_fd(sdio.0) }.ok_or(ios::Error::Invalid)?; + Ok(Self { fd }) + } + + /// Write to a SD host controller register + /// # Errors + /// See [`ios::Error`] + pub fn write_to_host_controller_register( + &mut self, + register: u8, + size: u8, + data: u32, + ) -> Result<(), ios::Error> { + let mut buffer = [0u8; 24]; + buffer[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); + buffer[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + buffer[16..20].copy_from_slice(&data.to_be_bytes()); + + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::WriteHostControllerRegister, + &buffer, + &mut [], + )?; + + Ok(()) + } + + /// Read from a SD host controller register + /// # Errors + /// See [`ios::Error`] + pub fn read_from_host_controller_register( + &mut self, + register: u8, + size: u8, + ) -> Result { + let mut value = [0u8; 4]; + let mut query = [0u8; 24]; + query[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); + query[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::ReadHostControllerRegister, + &query, + &mut value, + )?; + + Ok(u32::from_be_bytes(value)) + } + /// Reset SD card + /// # Errors + /// See [`ios::Error`] + pub fn reset(&mut self) -> Result { + let mut buffer = [0u8; 4]; + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::ResetSDCard, + &[], + &mut buffer, + )?; + + Ok(u32::from_be_bytes(buffer)) + } + + /// Enable SD card clock + /// # Errors + /// See [`ios::Error`] + pub fn enable_clock(&mut self, enable: bool) -> Result<(), ios::Error> { + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::SetClock, + &u32::from(enable).to_be_bytes(), + &mut [], + )?; + + Ok(()) + } + + /// Send SDIO command + /// # Errors + /// See [`ios::Error`] + pub fn send_command(&self, request: &Request) -> Result { + let mut in_buf: [u8; _] = [0u8; core::mem::size_of::()]; + in_buf[0..4].copy_from_slice(&request.command.to_be_bytes()); + in_buf[4..8].copy_from_slice(&request.command_type.to_be_bytes()); + in_buf[8..12].copy_from_slice(&request.response_type.to_be_bytes()); + in_buf[12..16].copy_from_slice(&request.arg.to_be_bytes()); + in_buf[16..20].copy_from_slice(&request.block_count.to_be_bytes()); + in_buf[20..24].copy_from_slice(&request.block_size.to_be_bytes()); + in_buf[24..28].copy_from_slice(&request.dma_addr.expose_provenance().to_be_bytes()); + in_buf[28..32].copy_from_slice(&request.is_dma.to_be_bytes()); + //in_buf[32..36].copy_from_slice(&request.pad0.to_be_bytes()); + + let mut out_buf: [u8; 16] = [0u8; 16]; + + if !request.dma_addr.is_null() && request.is_dma != 0 { + let dma_bytes = unsafe { + core::slice::from_raw_parts( + request.dma_addr, + usize::try_from(request.block_count * request.block_size) + .map_err(|_| ios::Error::Invalid)?, + ) + }; + + ios::ioctlv::<2, 1, 3>( + self.fd.as_file_descriptor(), + Ioctl::SendCommand, + &[&in_buf, dma_bytes], + &mut [&mut out_buf], + )?; + } else { + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::SendCommand, + &in_buf, + &mut out_buf, + )?; + } + let resp = Response::from_bytes(&out_buf); + Ok(resp) + } + + /// Read SD card status returning the relative card address + /// # Errors + /// See [`ios::Error`] + pub fn get_status(&mut self) -> Result { + let mut buffer = [0u8; 4]; + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::GetStatus, + &[], + &mut buffer, + )?; + + Ok(u32::from_be_bytes( + buffer[0..4].try_into().map_err(|_| ios::Error::Invalid)?, + )) + } + + /// Get operation conditions register + /// # Errors + /// See [`ios::Error`] + pub fn get_operating_conditions_register(&mut self) -> Result { + let mut buffer = [0u8; 4]; + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::GetOperatingConditionsRegister, + &[], + &mut buffer, + )?; + + Ok(u32::from_be_bytes(buffer)) + } + } +}