diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 75b29b7a..45b6e5d3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,7 +6,10 @@ jobs: formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Check formatting run: cargo fmt -- --check @@ -14,10 +17,17 @@ jobs: runs-on: ubuntu-latest strategy: matrix: + # Always run MSRV too! + rust: ["stable", "1.76"] features: ['log', 'defmt-log', '""'] steps: - - uses: actions/checkout@v1 + - 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/CHANGELOG.md b/CHANGELOG.md index 04b9fb82..8ff7a2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,26 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic ## [Unreleased] +## [Version 0.9.0] - 2025-06-08 + ### Changed -- None +- __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 -- None +- `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 -- None +- __Breaking Change__: Removed the `reason: &str` argument from `BlockDevice` ## [Version 0.8.2] - 2025-06-07 @@ -180,7 +189,8 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic [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.8.2...develop +[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 diff --git a/Cargo.toml b/Cargo.toml index 6ddcc1d5..11b6ff6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,16 @@ license = "MIT OR Apache-2.0" name = "embedded-sdmmc" readme = "README.md" repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" -version = "0.8.2" +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} diff --git a/README.md b/README.md index 41985d25..9bd06b1e 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,23 @@ designed for readability and simplicity over performance. 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 = embedded_sdmmc::SdCard::new(sdmmc_spi, delay); +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 mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); +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 mut volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; +let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (mutably borrows from the volume). -let mut root_dir = volume0.open_root_dir()?; +let root_dir = volume0.open_root_dir()?; // Open a file called "MY_FILE.TXT" in the root directory // This mutably borrows the directory. -let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; +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]; @@ -37,13 +38,26 @@ while !my_file.is_eof() { } ``` +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 mut cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); +let cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); ``` ## Supported features diff --git a/examples/append_file.rs b/examples/append_file.rs index f5a3bc43..54b7577e 100644 --- a/examples/append_file.rs +++ b/examples/append_file.rs @@ -5,37 +5,35 @@ //! $ cargo run --example append_file -- /dev/mmcblk0 //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! 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 //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; const FILE_TO_APPEND: &str = "README.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + 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 mut f = root_dir.open_file_in_dir(FILE_TO_APPEND, Mode::ReadWriteAppend)?; + 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(()) } diff --git a/examples/big_dir.rs b/examples/big_dir.rs index b355c3c4..bfc7e83c 100644 --- a/examples/big_dir.rs +++ b/examples/big_dir.rs @@ -1,34 +1,48 @@ -extern crate embedded_sdmmc; +//! 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, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; -fn main() -> Result<(), embedded_sdmmc::Error> { +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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr - .open_volume(embedded_sdmmc::VolumeIdx(1)) - .unwrap(); + 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 mut root_dir = volume.open_root_dir().unwrap(); + 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 mut file = root_dir - .open_file_in_dir( - file_name.as_str(), - embedded_sdmmc::Mode::ReadWriteCreateOrTruncate, - ) + 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"); diff --git a/examples/create_file.rs b/examples/create_file.rs index 81263ceb..7f3cfb48 100644 --- a/examples/create_file.rs +++ b/examples/create_file.rs @@ -5,40 +5,38 @@ //! $ cargo run --example create_file -- /dev/mmcblk0 //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! 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 //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; const FILE_TO_CREATE: &str = "CREATE.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + 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 mut f = root_dir.open_file_in_dir(FILE_TO_CREATE, Mode::ReadWriteCreate)?; + 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(()) } diff --git a/examples/delete_file.rs b/examples/delete_file.rs index 743b2d54..3df1978e 100644 --- a/examples/delete_file.rs +++ b/examples/delete_file.rs @@ -8,35 +8,33 @@ //! 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. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! 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 //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; const FILE_TO_DELETE: &str = "README.TXT"; -use embedded_sdmmc::{Error, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + 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!"); diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs index 5abb99f5..6eefe23d 100644 --- a/examples/linux/mod.rs +++ b/examples/linux/mod.rs @@ -34,22 +34,14 @@ impl LinuxBlockDevice { impl BlockDevice for LinuxBlockDevice { type Error = std::io::Error; - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> Result<(), Self::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 ({}) {:?}: {:?}", - reason, start_block_idx, &block - ); + println!("Read block {:?}: {:?}", start_block_idx, &block); } } Ok(()) diff --git a/examples/list_dir.rs b/examples/list_dir.rs index 18c121d4..e12807af 100644 --- a/examples/list_dir.rs +++ b/examples/list_dir.rs @@ -22,34 +22,34 @@ //! $ //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! 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 //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; -use embedded_sdmmc::{Directory, VolumeIdx, VolumeManager}; +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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; + 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(()) @@ -58,10 +58,7 @@ fn main() -> Result<(), Error> { /// Recursively print a directory listing for the open directory given. /// /// The path is for display purposes only. -fn list_dir( - mut directory: Directory, - path: &str, -) -> Result<(), Error> { +fn list_dir(directory: Directory<'_>, path: &str) -> Result<(), Error> { println!("Listing {}", path); let mut children = Vec::new(); directory.iterate_dir(|entry| { @@ -77,8 +74,8 @@ fn list_dir( } ); if entry.attributes.is_directory() - && entry.name != embedded_sdmmc::ShortFileName::parent_dir() - && entry.name != embedded_sdmmc::ShortFileName::this_dir() + && entry.name != ShortFileName::parent_dir() + && entry.name != ShortFileName::this_dir() { children.push(entry.name.clone()); } diff --git a/examples/read_file.rs b/examples/read_file.rs index 1a958c1c..0800de94 100644 --- a/examples/read_file.rs +++ b/examples/read_file.rs @@ -22,37 +22,38 @@ //! 00000100 [54, 0a, 0d] |T...............| //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! 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 //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; const FILE_TO_READ: &str = "README.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + 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 mut f = root_dir.open_file_in_dir(FILE_TO_READ, Mode::ReadOnly)?; + 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(); diff --git a/examples/readme_test.rs b/examples/readme_test.rs index 7ae7384d..0d63d80b 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -7,6 +7,8 @@ use core::cell::RefCell; +use embedded_sdmmc::{Error, SdCardError, TimeSource, Timestamp}; + pub struct DummyCsPin; impl embedded_hal::digital::ErrorType for DummyCsPin { @@ -80,9 +82,9 @@ impl embedded_hal::delay::DelayNs for FakeDelayer { struct FakeTimesource(); -impl embedded_sdmmc::TimeSource for FakeTimesource { - fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { - embedded_sdmmc::Timestamp { +impl TimeSource for FakeTimesource { + fn get_timestamp(&self) -> Timestamp { + Timestamp { year_since_1970: 0, zero_indexed_month: 0, zero_indexed_day: 0, @@ -94,24 +96,24 @@ impl embedded_sdmmc::TimeSource for FakeTimesource { } #[derive(Debug, Clone)] -enum Error { - Filesystem(embedded_sdmmc::Error), - Disk(embedded_sdmmc::SdCardError), +enum MyError { + Filesystem(Error), + Disk(SdCardError), } -impl From> for Error { - fn from(value: embedded_sdmmc::Error) -> Error { - Error::Filesystem(value) +impl From> for MyError { + fn from(value: Error) -> MyError { + MyError::Filesystem(value) } } -impl From for Error { - fn from(value: embedded_sdmmc::SdCardError) -> Error { - Error::Disk(value) +impl From for MyError { + fn from(value: SdCardError) -> MyError { + MyError::Disk(value) } } -fn main() -> Result<(), Error> { +fn main() -> Result<(), MyError> { // BEGIN Fake stuff that will be replaced with real peripherals let spi_bus = RefCell::new(FakeSpiBus()); let delay = FakeDelayer(); @@ -119,22 +121,23 @@ fn main() -> Result<(), Error> { 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 = embedded_sdmmc::SdCard::new(sdmmc_spi, delay); + 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 mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); + 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 mut volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (mutably borrows from the volume). - let mut root_dir = volume0.open_root_dir()?; + let root_dir = volume0.open_root_dir()?; // Open a file called "MY_FILE.TXT" in the root directory // This mutably borrows the directory. - let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; + 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]; @@ -143,6 +146,7 @@ fn main() -> Result<(), Error> { print!("{}", *b as char); } } + Ok(()) } diff --git a/examples/shell.rs b/examples/shell.rs index 0e41ce1c..42682769 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -3,6 +3,20 @@ //! 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 @@ -69,12 +83,15 @@ //! | `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::io::prelude::*; +use std::{cell::RefCell, io::prelude::*}; use embedded_sdmmc::{ - Error as EsError, RawDirectory, RawVolume, ShortFileName, VolumeIdx, VolumeManager, + 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; @@ -182,21 +199,21 @@ struct VolumeState { } struct Context { - volume_mgr: VolumeManager, - volumes: [Option; 4], + volume_mgr: VolumeManager, + volumes: RefCell<[Option; 4]>, current_volume: usize, } impl Context { fn current_path(&self) -> Vec { - let Some(s) = &self.volumes[self.current_volume] else { + let Some(s) = &self.volumes.borrow()[self.current_volume] else { return vec![]; }; s.path.clone() } /// Print some help text - fn help(&mut self) -> Result<(), Error> { + fn help(&self) -> Result<(), Error> { println!("Commands:"); println!("\thelp -> this help text"); println!("\t: -> change volume/partition"); @@ -219,38 +236,50 @@ impl Context { } /// Print volume manager status - fn stat(&mut self) -> Result<(), Error> { + fn stat(&self) -> Result<(), Error> { println!("Status:\n{:#?}", self.volume_mgr); Ok(()) } /// Print a directory listing - fn dir(&mut self, path: &Path) -> Result<(), Error> { + fn dir(&self, path: &Path) -> Result<(), Error> { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); - dir.iterate_dir(|entry| { - println!( - "{:12} {:9} {} {} {:08X?} {:?}", - entry.name, entry.size, entry.ctime, entry.mtime, entry.cluster, entry.attributes - ); + 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(&mut self, path: &Path) -> Result<(), Error> { + 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) + Self::tree_dir(dir) } /// Print a recursive directory listing for the given open directory. /// /// Will close the given directory. - fn tree_dir(&mut self, dir: RawDirectory) -> Result<(), Error> { - let mut dir = dir.to_directory(&mut self.volume_mgr); + fn tree_dir(dir: Directory) -> Result<(), Error> { let mut children = Vec::new(); dir.iterate_dir(|entry| { println!( @@ -264,25 +293,12 @@ impl Context { children.push(entry.name.clone()); } })?; - // Be sure to close this, no matter what happens - let dir = dir.to_raw_directory(); for child in children { println!("Entering {}", child); - let child_dir = match self.volume_mgr.open_dir(dir, &child) { - Ok(child_dir) => child_dir, - Err(e) => { - self.volume_mgr.close_dir(dir).expect("close open dir"); - return Err(e); - } - }; - let result = self.tree_dir(child_dir); + let child_dir = dir.open_dir(&child)?; + Self::tree_dir(child_dir)?; println!("Returning from {}", child); - if let Err(e) = result { - self.volume_mgr.close_dir(dir).expect("close open dir"); - return Err(e); - } } - self.volume_mgr.close_dir(dir).expect("close open dir"); Ok(()) } @@ -293,23 +309,25 @@ impl Context { /// 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(&mut self, full_path: &Path) -> Result<(), Error> { + fn cd(&self, full_path: &Path) -> Result<(), Error> { let volume_idx = self.resolve_volume(full_path)?; - let d = self.resolve_existing_directory(full_path)?; - let Some(s) = &mut self.volumes[volume_idx] else { - self.volume_mgr.close_dir(d).expect("close open dir"); + 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; + 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()); } @@ -318,10 +336,9 @@ impl Context { } /// print a text file - fn cat(&mut self, filename: &Path) -> Result<(), Error> { + fn cat(&self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); - let mut f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + 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]; @@ -339,10 +356,9 @@ impl Context { } /// print a binary file - fn hexdump(&mut self, filename: &Path) -> Result<(), Error> { + fn hexdump(&self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); - let mut f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + 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]; @@ -376,9 +392,8 @@ impl Context { } /// create a directory - fn mkdir(&mut self, dir_name: &Path) -> Result<(), Error> { + fn mkdir(&self, dir_name: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(dir_name)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); dir.make_dir_in_dir(filename) } @@ -426,11 +441,10 @@ impl Context { /// * 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(&mut self, full_path: &Path) -> Result { - let (dir, fragment) = self.resolve_filename(full_path)?; - let mut work_dir = dir.to_directory(&mut self.volume_mgr); - work_dir.change_dir(fragment)?; - Ok(work_dir.to_raw_directory()) + 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. @@ -455,33 +469,30 @@ impl Context { /// * 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<'path>( - &mut self, + fn resolve_filename<'a, 'path>( + &'a self, full_path: &'path Path, - ) -> Result<(RawDirectory, &'path str), Error> { + ) -> Result<(Directory<'a>, &'path str), Error> { let volume_idx = self.resolve_volume(full_path)?; - let Some(s) = &mut self.volumes[volume_idx] else { + 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(&mut self.volume_mgr) + .to_directory(&self.volume_mgr) } else { // relative to CWD self.volume_mgr .open_dir(s.directory, ".")? - .to_directory(&mut self.volume_mgr) + .to_directory(&self.volume_mgr) }; for fragment in full_path.iterate_dirs() { work_dir.change_dir(fragment)?; } - Ok(( - work_dir.to_raw_directory(), - full_path.basename().unwrap_or("."), - )) + Ok((work_dir, full_path.basename().unwrap_or("."))) } /// Convert a volume index to a letter @@ -498,7 +509,7 @@ impl Context { impl Drop for Context { fn drop(&mut self) { - for v in self.volumes.iter_mut() { + for v in self.volumes.borrow_mut().iter_mut() { if let Some(v) = v { println!("Closing directory {:?}", v.directory); self.volume_mgr @@ -525,7 +536,7 @@ fn main() -> Result<(), Error> { let mut ctx = Context { volume_mgr: VolumeManager::new_with_limits(lbd, Clock, 100), - volumes: [None, None, None, None], + volumes: RefCell::new([None, None, None, None]), current_volume: 0, }; @@ -533,10 +544,14 @@ fn main() -> Result<(), Error> { for volume_no in 0..4 { match ctx.volume_mgr.open_raw_volume(VolumeIdx(volume_no)) { Ok(volume) => { - println!("Volume # {}: found", Context::volume_to_letter(volume_no)); + 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[volume_no] = Some(VolumeState { + ctx.volumes.borrow_mut()[volume_no] = Some(VolumeState { directory: root_dir, volume, path: vec![], diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 45644572..674ae55d 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -16,45 +16,6 @@ pub struct Block { pub contents: [u8; Block::LEN], } -/// 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); - -/// 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); - -/// An iterator returned from `Block::range`. -pub struct BlockIter { - inclusive_end: BlockIdx, - current: BlockIdx, -} - -/// 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, - reason: &str, - ) -> 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; -} - 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 @@ -72,64 +33,6 @@ impl Block { } } -impl Default for Block { - fn default() -> Self { - Self::new() - } -} - -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::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 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 - } -} - -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 core::ops::Deref for Block { type Target = [u8; 512]; fn deref(&self) -> &[u8; 512] { @@ -164,6 +67,117 @@ impl core::fmt::Debug for Block { } } +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 @@ -179,6 +193,65 @@ impl BlockIdx { } } +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. /// @@ -205,6 +278,12 @@ impl BlockCount { } } +/// 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. diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index f06f23eb..c7e83b62 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -85,12 +85,13 @@ impl<'a> Bpb<'a> { // FAT16/FAT32 functions /// Get the Volume Label string for this volume - pub fn volume_label(&self) -> &[u8] { - if self.fat_type != FatType::Fat32 { - &self.data[43..=53] - } else { - &self.data[71..=81] + 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 @@ -98,10 +99,9 @@ impl<'a> Bpb<'a> { /// 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 { - if self.fat_type != FatType::Fat32 { - None - } else { - Some(BlockCount(u32::from(self.fs_info()))) + match self.fat_type { + FatType::Fat16 => None, + FatType::Fat32 => Some(BlockCount(u32::from(self.fs_info()))), } } diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 35641cb6..c27a8cdd 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -14,36 +14,6 @@ pub enum FatType { Fat32, } -pub(crate) struct BlockCache { - block: Block, - idx: Option, -} -impl BlockCache { - pub fn empty() -> Self { - BlockCache { - block: Block::new(), - idx: None, - } - } - pub(crate) fn read( - &mut self, - block_device: &D, - block_idx: BlockIdx, - reason: &str, - ) -> Result<&Block, Error> - where - D: BlockDevice, - { - if Some(block_idx) != self.idx { - self.idx = Some(block_idx); - block_device - .read(core::slice::from_mut(&mut self.block), block_idx, reason) - .map_err(Error::DeviceError)?; - } - Ok(&self.block) - } -} - mod bpb; mod info; mod ondiskdirentry; @@ -54,8 +24,6 @@ pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; pub use ondiskdirentry::OnDiskDirEntry; pub use volume::{parse_volume, FatVolume, VolumeName}; -use crate::{Block, BlockDevice, BlockIdx, Error}; - // **************************************************************************** // // Unit Tests @@ -116,7 +84,7 @@ mod test { fn test_dir_entries() { #[derive(Debug)] enum Expected { - Lfn(bool, u8, [char; 13]), + Lfn(bool, u8, u8, [u16; 13]), Short(DirEntry), } let raw_data = r#" @@ -137,9 +105,14 @@ mod test { 422e0064007400620000000f0059ffffffffffffffffffffffff0000ffffffff B..d.t.b.....Y.................. 01620063006d00320037000f0059300038002d0072007000690000002d006200 .b.c.m.2.7...Y0.8.-.r.p.i...-.b. "#; + let results = [ Expected::Short(DirEntry { - name: ShortFileName::create_from_str_mixed_case("boot").unwrap(), + 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), @@ -151,9 +124,10 @@ mod test { Expected::Lfn( true, 1, + 0x47, [ - 'o', 'v', 'e', 'r', 'l', 'a', 'y', 's', '\u{0000}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', + '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 { @@ -169,16 +143,20 @@ mod test { Expected::Lfn( true, 2, + 0x79, [ - '-', 'p', 'l', 'u', 's', '.', 'd', 't', 'b', '\u{0000}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', + '-' 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', 'c', 'm', '2', '7', '0', '8', '-', 'r', 'p', 'i', '-', 'b', + '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 { @@ -194,8 +172,11 @@ mod test { Expected::Lfn( true, 1, + 0x12, [ - 'C', 'O', 'P', 'Y', 'I', 'N', 'G', '.', 'l', 'i', 'n', 'u', 'x', + '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 { @@ -211,16 +192,31 @@ mod test { Expected::Lfn( true, 2, + 0x67, [ - 'c', 'o', 'm', '\u{0}', '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', + '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', 'I', 'C', 'E', 'N', 'C', 'E', '.', 'b', 'r', 'o', 'a', 'd', + '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 { @@ -236,16 +232,20 @@ mod test { Expected::Lfn( true, 2, + 0x19, [ - '-', 'b', '.', 'd', 't', 'b', '\u{0000}', '\u{ffff}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', '\u{ffff}', + '-' 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', 'c', 'm', '2', '7', '0', '9', '-', 'r', 'p', 'i', '-', '2', + '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 { @@ -261,16 +261,20 @@ mod test { Expected::Lfn( true, 2, + 0x59, [ - '.', 'd', 't', 'b', '\u{0000}', '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', + '.' 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', 'c', 'm', '2', '7', '0', '8', '-', 'r', 'p', 'i', '-', 'b', + '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, ], ), ]; @@ -279,12 +283,13 @@ mod test { for (part, expected) in data.chunks(OnDiskDirEntry::LEN).zip(results.iter()) { let on_disk_entry = OnDiskDirEntry::new(part); match expected { - Expected::Lfn(start, index, contents) if on_disk_entry.is_lfn() => { - let (calc_start, calc_index, calc_contents) = + 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); @@ -349,7 +354,7 @@ mod test { 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.volume_label(), *b"boot "); assert_eq!(bpb.fat_size(), 32); assert_eq!(bpb.total_blocks(), 122_880); assert_eq!(bpb.fat_type, FatType::Fat16); diff --git a/src/fat/ondiskdirentry.rs b/src/fat/ondiskdirentry.rs index 49b8bb20..83707e47 100644 --- a/src/fat/ondiskdirentry.rs +++ b/src/fat/ondiskdirentry.rs @@ -78,47 +78,27 @@ impl<'a> OnDiskDirEntry<'a> { } /// If this is an LFN, get the contents so we can re-assemble the filename. - pub fn lfn_contents(&self) -> Option<(bool, u8, [char; 13])> { + pub fn lfn_contents(&self) -> Option<(bool, u8, u8, [u16; 13])> { if self.is_lfn() { - let mut buffer = [' '; 13]; let is_start = (self.data[0] & 0x40) != 0; let sequence = self.data[0] & 0x1F; - // LFNs store UCS-2, so we can map from 16-bit char to 32-bit char without problem. - buffer[0] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[1..=2]))).unwrap(); - buffer[1] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[3..=4]))).unwrap(); - buffer[2] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[5..=6]))).unwrap(); - buffer[3] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[7..=8]))).unwrap(); - buffer[4] = core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[9..=10]))) - .unwrap(); - buffer[5] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[14..=15]))) - .unwrap(); - buffer[6] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[16..=17]))) - .unwrap(); - buffer[7] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[18..=19]))) - .unwrap(); - buffer[8] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[20..=21]))) - .unwrap(); - buffer[9] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[22..=23]))) - .unwrap(); - buffer[10] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[24..=25]))) - .unwrap(); - buffer[11] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[28..=29]))) - .unwrap(); - buffer[12] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[30..=31]))) - .unwrap(); - Some((is_start, sequence, buffer)) + 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 } diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 00d0ed65..ed39b6a0 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -6,34 +6,136 @@ use crate::{ Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, RESERVED_ENTRIES, }, - trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, ClusterId, DirEntry, - DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, + 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; -use super::BlockCache; - -/// The name given to a particular FAT formatted volume. +/// 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(Clone, PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone)] pub struct VolumeName { - data: [u8; 11], + pub(crate) contents: [u8; Self::TOTAL_LEN], } impl VolumeName { - /// Create a new VolumeName - pub fn new(data: [u8; 11]) -> VolumeName { - VolumeName { data } + 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::Debug for VolumeName { - fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { - match core::str::from_utf8(&self.data) { - Ok(s) => write!(fmt, "{:?}", s), - Err(_e) => write!(fmt, "{:?}", &self.data), +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) } } @@ -55,8 +157,8 @@ pub struct FatVolume { /// 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 (if present). Relative to start of - /// partition (so add `self.lba_offset` before passing to volume manager) + /// 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, @@ -70,7 +172,10 @@ pub struct FatVolume { impl FatVolume { /// Write a new entry in the FAT - pub fn update_info_sector(&mut self, block_device: &D) -> Result<(), Error> + pub fn update_info_sector( + &mut self, + block_cache: &mut BlockCache, + ) -> Result<(), Error> where D: BlockDevice, { @@ -82,20 +187,18 @@ impl FatVolume { if self.free_clusters_count.is_none() && self.next_free_cluster.is_none() { return Ok(()); } - let mut blocks = [Block::new()]; - block_device - .read(&mut blocks, fat32_info.info_location, "read_info_sector") + trace!("Reading info sector"); + let block = block_cache + .read_mut(fat32_info.info_location) .map_err(Error::DeviceError)?; - let block = &mut blocks[0]; 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()); } - block_device - .write(&blocks, fat32_info.info_location) - .map_err(Error::DeviceError)?; + trace!("Writing info sector"); + block_cache.write_back()?; } } Ok(()) @@ -112,27 +215,26 @@ impl FatVolume { /// Write a new entry in the FAT fn update_fat( &mut self, - block_device: &D, + block_cache: &mut BlockCache, cluster: ClusterId, new_value: ClusterId, ) -> Result<(), Error> where D: BlockDevice, { - let mut blocks = [Block::new()]; - let this_fat_block_num; let mut second_fat_block_num = None; match &self.fat_specific_info { FatSpecificInfo::Fat16(_fat16_info) => { let fat_offset = cluster.0 * 2; - this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); - if let Some(fat_start) = self.second_fat_start { + 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 + fat_start.offset_bytes(fat_offset)); + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); } let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - block_device - .read(&mut blocks, this_fat_block_num, "read_fat") + 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 { @@ -143,21 +245,22 @@ impl FatVolume { _ => new_value.0 as u16, }; LittleEndian::write_u16( - &mut blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 1], + &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; - this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); - if let Some(fat_start) = self.second_fat_start { + 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 + fat_start.offset_bytes(fat_offset)); + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); } let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - block_device - .read(&mut blocks, this_fat_block_num, "read_fat") + 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, @@ -165,23 +268,20 @@ impl FatVolume { ClusterId::EMPTY => 0x0000_0000, _ => new_value.0, }; - let existing = LittleEndian::read_u32( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], - ); + 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 blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 3], new, ); } } - block_device - .write(&blocks, this_fat_block_num) - .map_err(Error::DeviceError)?; - if let Some(second_fat_block_num) = second_fat_block_num { - block_device - .write(&blocks, second_fat_block_num) - .map_err(Error::DeviceError)?; + trace!("Updating FAT"); + if let Some(duplicate) = second_fat_block_num { + block_cache.write_back_with_duplicate(duplicate)?; + } else { + block_cache.write_back()?; } Ok(()) } @@ -189,9 +289,8 @@ impl FatVolume { /// Look in the FAT to see which cluster comes next. pub(crate) fn next_cluster( &self, - block_device: &D, + block_cache: &mut BlockCache, cluster: ClusterId, - fat_block_cache: &mut BlockCache, ) -> Result> where D: BlockDevice, @@ -204,8 +303,8 @@ impl FatVolume { 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; - let block = - fat_block_cache.read(block_device, this_fat_block_num, "next_cluster")?; + 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 { @@ -227,8 +326,8 @@ impl FatVolume { 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; - let block = - fat_block_cache.read(block_device, this_fat_block_num, "next_cluster")?; + 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; @@ -293,7 +392,7 @@ impl FatVolume { /// needed pub(crate) fn write_new_directory_entry( &mut self, - block_device: &D, + block_cache: &mut BlockCache, time_source: &T, dir_cluster: ClusterId, name: ShortFileName, @@ -324,17 +423,16 @@ impl FatVolume { }; // Walk the directory - let mut blocks = [Block::new()]; while let Some(cluster) = current_cluster { - for block in first_dir_block_num.range(dir_size) { - block_device - .read(&mut blocks, block, "read_dir") + 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)?; - let entries_per_block = Block::LEN / OnDiskDirEntry::LEN; - for entry in 0..entries_per_block { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + 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(); @@ -343,34 +441,30 @@ impl FatVolume { attributes, ClusterId::EMPTY, ctime, - block, - start as u32, + block_idx, + (i * OnDiskDirEntry::LEN) as u32, ); - blocks[0][start..start + 32] + dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); - block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; + trace!("Updating directory"); + block_cache.write_back()?; return Ok(entry); } } } if cluster != ClusterId::ROOT_DIR { - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - Err(Error::EndOfFile) => { - let c = - self.alloc_cluster(block_device, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + 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; } @@ -385,23 +479,23 @@ impl FatVolume { _ => Some(dir_cluster), }; let mut first_dir_block_num = self.cluster_to_block(dir_cluster); - let mut blocks = [Block::new()]; 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 in first_dir_block_num.range(dir_size) { + for block_idx in first_dir_block_num.range(dir_size) { // Read a block of directory entries - block_device - .read(&mut blocks, block, "read_dir") + 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 entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + 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(); @@ -410,34 +504,31 @@ impl FatVolume { attributes, ClusterId(0), ctime, - block, - start as u32, + block_idx, + (i * OnDiskDirEntry::LEN) as u32, ); - blocks[0][start..start + 32] + dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); - block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; + 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. - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - Err(Error::EndOfFile) => { - let c = self.alloc_cluster(block_device, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + 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. @@ -450,9 +541,9 @@ impl FatVolume { /// Useful for performing directory listings. pub(crate) fn iterate_dir( &self, - block_device: &D, - dir: &DirectoryInfo, - func: F, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + mut func: F, ) -> Result<(), Error> where F: FnMut(&DirEntry), @@ -460,35 +551,148 @@ impl FatVolume { { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { - self.iterate_fat16(dir, fat16_info, block_device, func) + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, _| func(de)) } FatSpecificInfo::Fat32(fat32_info) => { - self.iterate_fat32(dir, fat32_info, block_device, func) + 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: &DirectoryInfo, + dir_info: &DirectoryInfo, fat16_info: &Fat16Info, - block_device: &D, + block_cache: &mut BlockCache, mut func: F, ) -> Result<(), Error> where - F: FnMut(&DirEntry), + 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.cluster); - let mut first_dir_block_num = match dir.cluster { + 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.cluster), + _ => self.cluster_to_block(dir_info.cluster), }; - let dir_size = match dir.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) @@ -496,27 +700,25 @@ impl FatVolume { _ => BlockCount(u32::from(self.blocks_per_cluster)), }; - let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block_idx in first_dir_block_num.range(dir_size) { - let block = block_cache.read(block_device, block_idx, "read_dir")?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&block[start..end]); + 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() && !dir_entry.is_lfn() { + } else if dir_entry.is_valid() { // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + let start = (i * OnDiskDirEntry::LEN) as u32; let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); - func(&entry); + func(&entry, &dir_entry); } } } if cluster != ClusterId::ROOT_DIR { - current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { + current_cluster = match self.next_cluster(block_cache, cluster) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -532,45 +734,40 @@ impl FatVolume { fn iterate_fat32( &self, - dir: &DirectoryInfo, + dir_info: &DirectoryInfo, fat32_info: &Fat32Info, - block_device: &D, + block_cache: &mut BlockCache, mut func: F, ) -> Result<(), Error> where - F: FnMut(&DirEntry), + 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.cluster { + let mut current_cluster = match dir_info.cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_info.cluster), }; - let mut blocks = [Block::new()]; - let mut block_cache = BlockCache::empty(); 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))) { - block_device - .read(&mut blocks, block, "read_dir") - .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + 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() && !dir_entry.is_lfn() { + } else if dir_entry.is_valid() { // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); - let entry = dir_entry.get_entry(FatType::Fat32, block, start); - func(&entry); + 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_device, cluster, &mut block_cache) { + current_cluster = match self.next_cluster(block_cache, cluster) { Ok(n) => Some(n), _ => None, }; @@ -581,8 +778,8 @@ impl FatVolume { /// Get an entry from the given directory pub(crate) fn find_directory_entry( &self, - block_device: &D, - dir: &DirectoryInfo, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, match_name: &ShortFileName, ) -> Result> where @@ -594,12 +791,12 @@ impl FatVolume { // 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 { + 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.cluster), + _ => self.cluster_to_block(dir_info.cluster), }; - let dir_size = match dir.cluster { + let dir_size = match dir_info.cluster { ClusterId::ROOT_DIR => { let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; @@ -608,11 +805,10 @@ impl FatVolume { _ => BlockCount(u32::from(self.blocks_per_cluster)), }; - let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { match self.find_entry_in_block( - block_device, + block_cache, FatType::Fat16, match_name, block, @@ -622,14 +818,13 @@ impl FatVolume { } } if cluster != ClusterId::ROOT_DIR { - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + 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; } @@ -637,16 +832,15 @@ impl FatVolume { Err(Error::NotFound) } FatSpecificInfo::Fat32(fat32_info) => { - let mut current_cluster = match dir.cluster { + let mut current_cluster = match dir_info.cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_info.cluster), }; - let mut block_cache = BlockCache::empty(); 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_device, + block_cache, FatType::Fat32, match_name, block, @@ -655,11 +849,10 @@ impl FatVolume { x => return x, } } - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => Some(n), - _ => None, - } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } } Err(Error::NotFound) } @@ -669,30 +862,26 @@ impl FatVolume { /// Finds an entry in a given block of directory entries. fn find_entry_in_block( &self, - block_device: &D, + block_cache: &mut BlockCache, fat_type: FatType, match_name: &ShortFileName, - block: BlockIdx, + block_idx: BlockIdx, ) -> Result> where D: BlockDevice, { - let mut blocks = [Block::new()]; - block_device - .read(&mut blocks, block, "read_dir") - .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + 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 - // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); - return Ok(dir_entry.get_entry(fat_type, block, start)); + // 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) @@ -701,8 +890,8 @@ impl FatVolume { /// Delete an entry from the given directory pub(crate) fn delete_directory_entry( &self, - block_device: &D, - dir: &DirectoryInfo, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, match_name: &ShortFileName, ) -> Result<(), Error> where @@ -714,12 +903,12 @@ impl FatVolume { // 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 { + 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.cluster), + _ => self.cluster_to_block(dir_info.cluster), }; - let dir_size = match dir.cluster { + let dir_size = match dir_info.cluster { ClusterId::ROOT_DIR => { let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; @@ -731,8 +920,8 @@ impl FatVolume { // Walk the directory while let Some(cluster) = current_cluster { // Scan the cluster / root dir a block at a time - for block in first_dir_block_num.range(dir_size) { - match self.delete_entry_in_block(block_device, match_name, block) { + 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 } @@ -745,15 +934,13 @@ impl FatVolume { } // if it's not the root dir, find the next cluster so we can keep looking if cluster != ClusterId::ROOT_DIR { - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + 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; } @@ -763,16 +950,18 @@ impl FatVolume { 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.cluster { + let mut current_cluster = match dir_info.cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_info.cluster), }; // Walk the directory while let Some(cluster) = current_cluster { // Scan the cluster a block at a time - let block_idx = self.cluster_to_block(cluster); - for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { - match self.delete_entry_in_block(block_device, match_name, block) { + 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; @@ -785,12 +974,10 @@ impl FatVolume { } } // Find the next cluster - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => Some(n), - _ => None, - } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } } // Ok, give up } @@ -806,30 +993,28 @@ impl FatVolume { /// to a special value. fn delete_entry_in_block( &self, - block_device: &D, + block_cache: &mut BlockCache, match_name: &ShortFileName, - block: BlockIdx, + block_idx: BlockIdx, ) -> Result<(), Error> where D: BlockDevice, { - let mut blocks = [Block::new()]; - block_device - .read(&mut blocks, block, "read_dir") + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + 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 mut blocks = blocks; - blocks[0].contents[start] = 0xE5; - return block_device - .write(&blocks, block) - .map_err(Error::DeviceError); + 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) @@ -838,14 +1023,13 @@ impl FatVolume { /// Finds the next free cluster after the start_cluster and before end_cluster pub(crate) fn find_next_free_cluster( &self, - block_device: &D, + block_cache: &mut BlockCache, start_cluster: ClusterId, end_cluster: ClusterId, ) -> Result> where D: BlockDevice, { - let mut blocks = [Block::new()]; let mut current_cluster = start_cluster; match &self.fat_specific_info { FatSpecificInfo::Fat16(_fat16_info) => { @@ -863,13 +1047,12 @@ impl FatVolume { 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); - block_device - .read(&mut blocks, this_fat_block_num, "next_cluster") + 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( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 1], + &block[this_fat_ent_offset..=this_fat_ent_offset + 1], ); if fat_entry == 0 { return Ok(current_cluster); @@ -894,13 +1077,12 @@ impl FatVolume { 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); - block_device - .read(&mut blocks, this_fat_block_num, "next_cluster") + 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( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], + &block[this_fat_ent_offset..=this_fat_ent_offset + 3], ) & 0x0FFF_FFFF; if fat_entry == 0 { return Ok(current_cluster); @@ -918,7 +1100,7 @@ impl FatVolume { /// Tries to allocate a cluster pub(crate) fn alloc_cluster( &mut self, - block_device: &D, + block_cache: &mut BlockCache, prev_cluster: Option, zero: bool, ) -> Result> @@ -936,31 +1118,29 @@ impl FatVolume { start_cluster, end_cluster ); - let new_cluster = - match self.find_next_free_cluster(block_device, 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_device, - ClusterId(RESERVED_ENTRIES), - end_cluster, - )? - } - Err(e) => return Err(e), - }; - self.update_fat(block_device, new_cluster, ClusterId::END_OF_FILE)?; + 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_device, cluster, new_cluster)?; + self.update_fat(block_cache, cluster, new_cluster)?; } trace!( "Finding next free between {:?}..={:?}", @@ -968,11 +1148,11 @@ impl FatVolume { end_cluster ); self.next_free_cluster = - match self.find_next_free_cluster(block_device, new_cluster, end_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_device, + block_cache, ClusterId(RESERVED_ENTRIES), end_cluster, ) { @@ -983,17 +1163,17 @@ impl FatVolume { 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 blocks = [Block::new()]; - let first_block = self.cluster_to_block(new_cluster); + let start_block_idx = self.cluster_to_block(new_cluster); let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); - for block in first_block.range(num_blocks) { - block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; + 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); @@ -1003,7 +1183,7 @@ impl FatVolume { /// 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_device: &D, + block_cache: &mut BlockCache, cluster: ClusterId, ) -> Result<(), Error> where @@ -1014,8 +1194,7 @@ impl FatVolume { return Ok(()); } let mut next = { - let mut block_cache = BlockCache::empty(); - match self.next_cluster(block_device, cluster, &mut block_cache) { + match self.next_cluster(block_cache, cluster) { Ok(n) => n, Err(Error::EndOfFile) => return Ok(()), Err(e) => return Err(e), @@ -1028,16 +1207,15 @@ impl FatVolume { } else { self.next_free_cluster = Some(next); } - self.update_fat(block_device, cluster, ClusterId::END_OF_FILE)?; + self.update_fat(block_cache, cluster, ClusterId::END_OF_FILE)?; loop { - let mut block_cache = BlockCache::empty(); - match self.next_cluster(block_device, next, &mut block_cache) { + match self.next_cluster(block_cache, next) { Ok(n) => { - self.update_fat(block_device, next, ClusterId::EMPTY)?; + self.update_fat(block_cache, next, ClusterId::EMPTY)?; next = n; } Err(Error::EndOfFile) => { - self.update_fat(block_device, next, ClusterId::EMPTY)?; + self.update_fat(block_cache, next, ClusterId::EMPTY)?; break; } Err(e) => return Err(e), @@ -1052,7 +1230,7 @@ impl FatVolume { /// Writes a Directory Entry to the disk pub(crate) fn write_entry_to_disk( &self, - block_device: &D, + block_cache: &mut BlockCache, entry: &DirEntry, ) -> Result<(), Error> where @@ -1062,18 +1240,97 @@ impl FatVolume { FatSpecificInfo::Fat16(_) => FatType::Fat16, FatSpecificInfo::Fat32(_) => FatType::Fat32, }; - let mut blocks = [Block::new()]; - block_device - .read(&mut blocks, entry.entry_block, "read") + trace!("Reading directory for update"); + let block = block_cache + .read_mut(entry.entry_block) .map_err(Error::DeviceError)?; - let block = &mut blocks[0]; let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); - block_device - .write(&blocks, entry.entry_block) - .map_err(Error::DeviceError)?; + 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(()) } } @@ -1081,7 +1338,7 @@ impl FatVolume { /// 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_device: &D, + block_cache: &mut BlockCache, lba_start: BlockIdx, num_blocks: BlockCount, ) -> Result> @@ -1089,12 +1346,15 @@ where D: BlockDevice, D::Error: core::fmt::Debug, { - let mut blocks = [Block::new()]; - block_device - .read(&mut blocks, lba_start, "read_bpb") - .map_err(Error::DeviceError)?; - let block = &blocks[0]; + trace!("Reading BPB"); + let block = block_cache.read(lba_start).map_err(Error::DeviceError)?; let bpb = Bpb::create_from_bytes(block).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 { @@ -1104,24 +1364,19 @@ where let root_dir_blocks = ((u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32) + (Block::LEN_U32 - 1)) / Block::LEN_U32; - let fat_start = BlockCount(u32::from(bpb.reserved_block_count())); 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 mut volume = FatVolume { + let volume = FatVolume { lba_start, num_blocks, - name: VolumeName { data: [0u8; 11] }, - blocks_per_cluster: bpb.blocks_per_cluster(), - first_data_block: (first_data_block), - fat_start: BlockCount(u32::from(bpb.reserved_block_count())), - second_fat_start: if bpb.num_fats() == 2 { - Some(BlockCount( - u32::from(bpb.reserved_block_count()) + bpb.fat_size(), - )) - } else { - None + 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(), @@ -1130,56 +1385,61 @@ where first_root_dir_block, }), }; - volume.name.data[..].copy_from_slice(bpb.volume_label()); Ok(VolumeType::Fat(volume)) } FatType::Fat32 => { // FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz); - let first_data_block = u32::from(bpb.reserved_block_count()) - + (u32::from(bpb.num_fats()) * bpb.fat_size()); - + 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 info_blocks = [Block::new()]; - block_device - .read( - &mut info_blocks, - lba_start + info_location, - "read_info_sector", - ) - .map_err(Error::DeviceError)?; - let info_block = &info_blocks[0]; - let info_sector = - InfoSector::create_from_bytes(info_block).map_err(Error::FormatError)?; - let mut volume = FatVolume { lba_start, num_blocks, - name: VolumeName { data: [0u8; 11] }, - blocks_per_cluster: bpb.blocks_per_cluster(), - first_data_block: BlockCount(first_data_block), - fat_start: BlockCount(u32::from(bpb.reserved_block_count())), - second_fat_start: if bpb.num_fats() == 2 { - Some(BlockCount( - u32::from(bpb.reserved_block_count()) + bpb.fat_size(), - )) - } else { - None + name: VolumeName { + contents: bpb.volume_label(), }, - free_clusters_count: info_sector.free_clusters_count(), - next_free_cluster: info_sector.next_free_cluster(), + 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()), }), }; - volume.name.data[..].copy_from_slice(bpb.volume_label()); + + // 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/src/filesystem/attributes.rs b/src/filesystem/attributes.rs index a6df7578..e22dcd18 100644 --- a/src/filesystem/attributes.rs +++ b/src/filesystem/attributes.rs @@ -71,31 +71,33 @@ impl Attributes { 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() { - write!(f, "LFN")?; + output.push_str("LFN").unwrap(); } else { if self.is_directory() { - write!(f, "D")?; + output.push_str("D").unwrap(); } else { - write!(f, "F")?; + output.push_str("F").unwrap(); } if self.is_read_only() { - write!(f, "R")?; + output.push_str("R").unwrap(); } if self.is_hidden() { - write!(f, "H")?; + output.push_str("H").unwrap(); } if self.is_system() { - write!(f, "S")?; + output.push_str("S").unwrap(); } if self.is_volume() { - write!(f, "V")?; + output.push_str("V").unwrap(); } if self.is_archive() { - write!(f, "A")?; + output.push_str("A").unwrap(); } } - Ok(()) + f.pad(&output) } } diff --git a/src/filesystem/cluster.rs b/src/filesystem/cluster.rs index 34d85909..14f11263 100644 --- a/src/filesystem/cluster.rs +++ b/src/filesystem/cluster.rs @@ -3,7 +3,7 @@ /// 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(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ClusterId(pub(crate) u32); impl ClusterId { @@ -33,6 +33,34 @@ impl core::ops::AddAssign for ClusterId { } } +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/src/filesystem/directory.rs b/src/filesystem/directory.rs index b965995f..527807bc 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -1,8 +1,6 @@ -use core::convert::TryFrom; - use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; -use crate::filesystem::{Attributes, ClusterId, SearchId, ShortFileName, Timestamp}; +use crate::filesystem::{Attributes, ClusterId, Handle, LfnBuffer, ShortFileName, Timestamp}; use crate::{Error, RawVolume, VolumeManager}; use super::ToShortFileName; @@ -47,7 +45,7 @@ pub struct DirEntry { /// 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) SearchId); +pub struct RawDirectory(pub(crate) Handle); impl RawDirectory { /// Convert a raw directory into a droppable [`Directory`] @@ -59,7 +57,7 @@ impl RawDirectory { const MAX_VOLUMES: usize, >( self, - volume_mgr: &mut VolumeManager, + volume_mgr: &VolumeManager, ) -> Directory where D: crate::BlockDevice, @@ -89,7 +87,7 @@ pub struct Directory< T: crate::TimeSource, { raw_directory: RawDirectory, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, } impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> @@ -101,7 +99,7 @@ where /// Create a new `Directory` from a `RawDirectory` pub fn new( raw_directory: RawDirectory, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, ) -> Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { Directory { raw_directory, @@ -113,7 +111,7 @@ where /// /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. pub fn open_dir( - &mut self, + &self, name: N, ) -> Result, Error> where @@ -137,7 +135,7 @@ where } /// Look in a directory for a named file. - pub fn find_directory_entry(&mut self, name: N) -> Result> + pub fn find_directory_entry(&self, name: N) -> Result> where N: ToShortFileName, { @@ -146,16 +144,53 @@ where } /// Call a callback function for each directory entry in a directory. - pub fn iterate_dir(&mut self, func: F) -> Result<(), Error> + /// + /// 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( - &mut self, + &self, name: N, mode: crate::Mode, ) -> Result, crate::Error> @@ -169,7 +204,7 @@ where } /// Delete a closed file with the given filename, if it exists. - pub fn delete_file_in_dir(&mut self, name: N) -> Result<(), Error> + pub fn delete_file_in_dir(&self, name: N) -> Result<(), Error> where N: ToShortFileName, { @@ -177,7 +212,7 @@ where } /// Make a directory inside this directory - pub fn make_dir_in_dir(&mut self, name: N) -> Result<(), Error> + pub fn make_dir_in_dir(&self, name: N) -> Result<(), Error> where N: ToShortFileName, { @@ -240,10 +275,10 @@ where #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Clone)] pub(crate) struct DirectoryInfo { - /// Unique ID for this directory. - pub(crate) directory_id: RawDirectory, - /// The unique ID for the volume this directory is on - pub(crate) volume_id: RawVolume, + /// 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, } @@ -262,16 +297,12 @@ impl DirEntry { [0u8; 2] } else { // Safe due to the AND operation - u16::try_from((cluster_number >> 16) & 0x0000_FFFF) - .unwrap() - .to_le_bytes() + (((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 = u16::try_from(cluster_number & 0x0000_FFFF) - .unwrap() - .to_le_bytes(); + 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 diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 4cb763f3..31d28548 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -1,5 +1,8 @@ //! 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)] @@ -40,17 +43,19 @@ impl ToShortFileName for &str { } } -/// An MS-DOS 8.3 filename. 7-bit ASCII only. All lower-case is converted to -/// upper-case by default. +/// 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; 11], + pub(crate) contents: [u8; Self::TOTAL_LEN], } impl ShortFileName { - const FILENAME_BASE_MAX_LEN: usize = 8; - const FILENAME_MAX_LEN: usize = 11; + 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 { @@ -68,22 +73,24 @@ impl ShortFileName { /// Get base name (without extension) of the file. pub fn base_name(&self) -> &[u8] { - Self::bytes_before_space(&self.contents[..Self::FILENAME_BASE_MAX_LEN]) + 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::FILENAME_BASE_MAX_LEN..]) + Self::bytes_before_space(&self.contents[Self::BASE_LEN..]) } fn bytes_before_space(bytes: &[u8]) -> &[u8] { - bytes.split(|b| *b == b' ').next().unwrap_or(&bytes[0..0]) + 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::FILENAME_MAX_LEN], + contents: [b' '; Self::TOTAL_LEN], }; // Special case `..`, which means "parent directory". @@ -98,47 +105,52 @@ impl ShortFileName { let mut idx = 0; let mut seen_dot = false; - for ch in name.bytes() { + for ch in name.chars() { match ch { // Microsoft say these are the invalid characters - 0x00..=0x1F - | 0x20 - | 0x22 - | 0x2A - | 0x2B - | 0x2C - | 0x2F - | 0x3A - | 0x3B - | 0x3C - | 0x3D - | 0x3E - | 0x3F - | 0x5B - | 0x5C - | 0x5D - | 0x7C => { + '\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 - b'.' => { - if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) { - idx = Self::FILENAME_BASE_MAX_LEN; + '.' => { + // 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 ch = ch.to_ascii_uppercase(); + let b = ch.to_ascii_uppercase() as u8; if seen_dot { - if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) { - sfn.contents[idx] = ch; + if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) { + sfn.contents[idx] = b; } else { return Err(FilenameError::NameTooLong); } - } else if idx < Self::FILENAME_BASE_MAX_LEN { - sfn.contents[idx] = ch; + } else if idx < Self::BASE_LEN { + sfn.contents[idx] = b; } else { return Err(FilenameError::NameTooLong); } @@ -152,65 +164,26 @@ impl ShortFileName { Ok(sfn) } - /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry. - /// Use this for volume labels with mixed case. - pub fn create_from_str_mixed_case(name: &str) -> Result { - let mut sfn = ShortFileName { - contents: [b' '; Self::FILENAME_MAX_LEN], - }; - let mut idx = 0; - let mut seen_dot = false; - for ch in name.bytes() { - match ch { - // Microsoft say these are the invalid characters - 0x00..=0x1F - | 0x20 - | 0x22 - | 0x2A - | 0x2B - | 0x2C - | 0x2F - | 0x3A - | 0x3B - | 0x3C - | 0x3D - | 0x3E - | 0x3F - | 0x5B - | 0x5C - | 0x5D - | 0x7C => { - return Err(FilenameError::InvalidCharacter); - } - // Denotes the start of the file extension - b'.' => { - if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) { - idx = Self::FILENAME_BASE_MAX_LEN; - seen_dot = true; - } else { - return Err(FilenameError::MisplacedPeriod); - } - } - _ => { - if seen_dot { - if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) { - sfn.contents[idx] = ch; - } else { - return Err(FilenameError::NameTooLong); - } - } else if idx < Self::FILENAME_BASE_MAX_LEN { - sfn.contents[idx] = ch; - } else { - return Err(FilenameError::NameTooLong); - } - idx += 1; - } - } + /// 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, } - if idx == 0 { - return Err(FilenameError::FilenameEmpty); + } + + /// 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); } - Ok(sfn) + result } } @@ -219,10 +192,12 @@ impl core::fmt::Display for ShortFileName { let mut printed = 0; for (i, &c) in self.contents.iter().enumerate() { if c != b' ' { - if i == Self::FILENAME_BASE_MAX_LEN { + 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; } @@ -245,6 +220,139 @@ impl core::fmt::Debug for ShortFileName { } } +/// 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 @@ -337,6 +445,58 @@ mod test { 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"); + } } // **************************************************************************** diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index 5e524775..870d85df 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,7 +1,9 @@ +use super::TimeSource; use crate::{ - filesystem::{ClusterId, DirEntry, SearchId}, - Error, RawVolume, VolumeManager, + filesystem::{ClusterId, DirEntry, Handle}, + BlockDevice, Error, RawVolume, VolumeManager, }; +use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; /// A handle for an open file on disk. /// @@ -21,13 +23,13 @@ use crate::{ /// 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) SearchId); +pub struct RawFile(pub(crate) Handle); impl RawFile { /// Convert a raw file into a droppable [`File`] pub fn to_file( self, - volume_mgr: &mut VolumeManager, + volume_mgr: &VolumeManager, ) -> File where D: crate::BlockDevice, @@ -51,7 +53,7 @@ where T: crate::TimeSource, { raw_file: RawFile, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, } impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> @@ -63,7 +65,7 @@ where /// Create a new `File` from a `RawFile` pub fn new( raw_file: RawFile, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, ) -> File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { File { raw_file, @@ -74,12 +76,12 @@ where /// Read from the file /// /// Returns how many bytes were read, or an error. - pub fn read(&mut self, buffer: &mut [u8]) -> Result> { + pub fn read(&self, buffer: &mut [u8]) -> Result> { self.volume_mgr.read(self.raw_file, buffer) } /// Write to the file - pub fn write(&mut self, buffer: &[u8]) -> Result<(), crate::Error> { + pub fn write(&self, buffer: &[u8]) -> Result<(), crate::Error> { self.volume_mgr.write(self.raw_file, buffer) } @@ -91,18 +93,18 @@ where } /// Seek a file with an offset from the current position. - pub fn seek_from_current(&mut self, offset: i32) -> Result<(), crate::Error> { + 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(&mut self, offset: u32) -> Result<(), crate::Error> { + 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(&mut self, offset: u32) -> Result<(), crate::Error> { + pub fn seek_from_end(&self, offset: u32) -> Result<(), crate::Error> { self.volume_mgr.file_seek_from_end(self.raw_file, offset) } @@ -128,7 +130,7 @@ where } /// Flush any written data by updating the directory entry. - pub fn flush(&mut self) -> Result<(), Error> { + pub fn flush(&self) -> Result<(), Error> { self.volume_mgr.flush_file(self.raw_file) } @@ -165,6 +167,80 @@ where } } +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> @@ -207,10 +283,10 @@ pub enum Mode { #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Clone)] pub(crate) struct FileInfo { - /// Unique ID for this file - pub(crate) file_id: RawFile, - /// The unique ID for the volume this directory is on - pub(crate) volume_id: RawVolume, + /// 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 diff --git a/src/filesystem/search_id.rs b/src/filesystem/handles.rs similarity index 56% rename from src/filesystem/search_id.rs rename to src/filesystem/handles.rs index 30c10182..dd379038 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/handles.rs @@ -1,11 +1,19 @@ +//! Contains the Handles and the HandleGenerator. + use core::num::Wrapping; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -/// Unique ID used to search for files and directories in the open Volume/File/Directory lists -pub struct SearchId(pub(crate) u32); +/// 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 Search ID generator. +/// A Handle Generator. /// /// This object will always return a different ID. /// @@ -13,23 +21,23 @@ pub struct SearchId(pub(crate) u32); /// 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 SearchIdGenerator { +pub struct HandleGenerator { next_id: Wrapping, } -impl SearchIdGenerator { - /// Create a new generator of Search IDs. +impl HandleGenerator { + /// Create a new generator of Handles. pub const fn new(offset: u32) -> Self { Self { next_id: Wrapping(offset), } } - /// Generate a new, unique [`SearchId`]. - pub fn get(&mut self) -> SearchId { + /// Generate a new, unique [`Handle`]. + pub fn generate(&mut self) -> Handle { let id = self.next_id; self.next_id += 1; - SearchId(id.0) + Handle(id.0) } } diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 03baa678..668ac86b 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -4,22 +4,22 @@ //! most (if not all) supported filesystems. /// Maximum file size supported by this library -pub const MAX_FILE_SIZE: u32 = core::u32::MAX; +pub const MAX_FILE_SIZE: u32 = u32::MAX; mod attributes; mod cluster; mod directory; mod filename; mod files; -mod search_id; +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, ShortFileName, ToShortFileName}; +pub use self::filename::{FilenameError, LfnBuffer, ShortFileName, ToShortFileName}; pub use self::files::{File, FileError, Mode, RawFile}; -pub use self::search_id::{SearchId, SearchIdGenerator}; +pub use self::handles::{Handle, HandleGenerator}; pub use self::timestamp::{TimeSource, Timestamp}; pub(crate) use self::directory::DirectoryInfo; diff --git a/src/lib.rs b/src/lib.rs index f1c4e029..c6af4e95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,10 +27,10 @@ //! { //! let sdcard = SdCard::new(spi, delay); //! println!("Card size is {} bytes", sdcard.num_bytes()?); -//! let mut volume_mgr = VolumeManager::new(sdcard, ts); -//! let mut volume0 = volume_mgr.open_volume(VolumeIdx(0))?; +//! let volume_mgr = VolumeManager::new(sdcard, ts); +//! let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; //! println!("Volume 0: {:?}", volume0); -//! let mut root_dir = volume0.open_root_dir()?; +//! 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]; @@ -43,12 +43,31 @@ //! } //! ``` //! +//! 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. +//! `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. @@ -73,18 +92,20 @@ pub mod fat; pub mod filesystem; pub mod sdcard; -use filesystem::SearchId; +use core::fmt::Debug; +use embedded_io::ErrorKind; +use filesystem::Handle; #[doc(inline)] -pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; +pub use crate::blockdevice::{Block, BlockCache, BlockCount, BlockDevice, BlockIdx}; #[doc(inline)] -pub use crate::fat::FatVolume; +pub use crate::fat::{FatVolume, VolumeName}; #[doc(inline)] pub use crate::filesystem::{ - Attributes, ClusterId, DirEntry, Directory, File, FilenameError, Mode, RawDirectory, RawFile, - ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, + Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, Mode, RawDirectory, + RawFile, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, }; use filesystem::DirectoryInfo; @@ -200,6 +221,46 @@ where 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 @@ -211,10 +272,19 @@ where } } -/// A partition with a filesystem within it. +/// 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(SearchId); +pub struct RawVolume(Handle); impl RawVolume { /// Convert a raw volume into a droppable [`Volume`] @@ -226,7 +296,7 @@ impl RawVolume { const MAX_VOLUMES: usize, >( self, - volume_mgr: &mut VolumeManager, + volume_mgr: &VolumeManager, ) -> Volume where D: crate::BlockDevice, @@ -236,7 +306,7 @@ impl RawVolume { } } -/// An open volume on disk, which closes on drop. +/// 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. @@ -250,7 +320,7 @@ where T: crate::TimeSource, { raw_volume: RawVolume, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, } impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> @@ -262,7 +332,7 @@ where /// Create a new `Volume` from a `RawVolume` pub fn new( raw_volume: RawVolume, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, ) -> Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { Volume { raw_volume, @@ -275,7 +345,7 @@ where /// You can then read the directory entries with `iterate_dir`, or you can /// use `open_file_in_dir`. pub fn open_root_dir( - &mut self, + &self, ) -> Result, Error> { let d = self.volume_mgr.open_root_dir(self.raw_volume)?; Ok(d.to_directory(self.volume_mgr)) @@ -337,9 +407,9 @@ where #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, PartialEq, Eq)] pub(crate) struct VolumeInfo { - /// Search ID for this volume. - volume_id: RawVolume, - /// TODO: some kind of index + /// 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, @@ -369,6 +439,9 @@ 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; diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 65930438..553791f9 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -162,19 +162,9 @@ where /// 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, - _reason: &str, - ) -> Result<(), Self::Error> { + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { let mut inner = self.inner.borrow_mut(); - debug!( - "Read {} blocks @ {} for {}", - blocks.len(), - start_block_idx.0, - _reason - ); + debug!("Read {} blocks @ {}", blocks.len(), start_block_idx.0,); inner.check_init()?; inner.read(blocks, start_block_idx) } @@ -351,9 +341,7 @@ where return Err(Error::ReadError); } - for b in buffer.iter_mut() { - *b = 0xFF; - } + buffer.fill(0xFF); self.transfer_bytes(buffer)?; // These two bytes are always sent. They are either a valid CRC, or diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index ef659eef..3f5f895c 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -2,21 +2,23 @@ //! //! The volume manager handles partitions and open files on a block device. -use byteorder::{ByteOrder, LittleEndian}; +use core::cell::RefCell; use core::convert::TryFrom; +use core::ops::DerefMut; -use crate::fat::{self, BlockCache, OnDiskDirEntry, RESERVED_ENTRIES}; +use byteorder::{ByteOrder, LittleEndian}; +use heapless::Vec; -use crate::filesystem::{ - Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, Mode, RawDirectory, RawFile, - SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, -}; use crate::{ - debug, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, Volume, - VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, - PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, + 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, }; -use heapless::Vec; /// Wraps a block device and gives access to the FAT-formatted volumes within /// it. @@ -35,12 +37,8 @@ pub struct VolumeManager< T: TimeSource, ::Error: core::fmt::Debug, { - pub(crate) block_device: D, - pub(crate) time_source: T, - id_generator: SearchIdGenerator, - open_volumes: Vec, - open_dirs: Vec, - open_files: Vec, + time_source: T, + data: RefCell>, } impl VolumeManager @@ -84,18 +82,25 @@ where ) -> VolumeManager { debug!("Creating new embedded-sdmmc::VolumeManager"); VolumeManager { - block_device, time_source, - id_generator: SearchIdGenerator::new(id_offset), - open_volumes: Vec::new(), - open_dirs: Vec::new(), - open_files: Vec::new(), + 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(&mut self) -> &mut D { - &mut self.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. @@ -103,7 +108,7 @@ where /// 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( - &mut self, + &self, volume_idx: VolumeIdx, ) -> Result, Error> { let v = self.open_raw_volume(volume_idx)?; @@ -117,7 +122,7 @@ where /// /// This function gives you a `RawVolume` and you must close the volume by /// calling `VolumeManager::close_volume`. - pub fn open_raw_volume(&mut self, volume_idx: VolumeIdx) -> Result> { + 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; @@ -130,22 +135,24 @@ where const PARTITION_INFO_LBA_START_INDEX: usize = 8; const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; - if self.open_volumes.is_full() { + 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 self.open_volumes.iter() { + for v in data.open_volumes.iter() { if v.idx == volume_idx { return Err(Error::VolumeAlreadyOpen); } } let (part_type, lba_start, num_blocks) = { - let mut blocks = [Block::new()]; - self.block_device - .read(&mut blocks, BlockIdx(0), "read_mbr") + trace!("Reading partition table"); + let block = data + .block_cache + .read(BlockIdx(0)) .map_err(Error::DeviceError)?; - let block = &blocks[0]; // 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 { @@ -188,16 +195,17 @@ where PARTITION_ID_FAT32_CHS_LBA | PARTITION_ID_FAT32_LBA | PARTITION_ID_FAT16_LBA - | PARTITION_ID_FAT16 => { - let volume = fat::parse_volume(&self.block_device, lba_start, num_blocks)?; - let id = RawVolume(self.id_generator.get()); + | 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 { - volume_id: id, + raw_volume: id, idx: volume_idx, volume_type: volume, }; // We already checked for space - self.open_volumes.push(info).unwrap(); + data.open_volumes.push(info).unwrap(); Ok(id) } _ => Err(Error::FormatError("Partition type not supported")), @@ -208,20 +216,25 @@ where /// /// You can then read the directory entries with `iterate_dir`, or you can /// use `open_file_in_dir`. - pub fn open_root_dir(&mut self, volume: RawVolume) -> Result> { + 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(self.id_generator.get()); + let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { - volume_id: volume, + raw_volume: volume, cluster: ClusterId::ROOT_DIR, - directory_id, + raw_directory: directory_id, }; - self.open_dirs + data.open_dirs .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; + debug!("Opened root on {:?}, got {:?}", volume, directory_id); + Ok(directory_id) } @@ -231,44 +244,51 @@ where /// /// Passing "." as the name results in opening the `parent_dir` a second time. pub fn open_dir( - &mut self, + &self, parent_dir: RawDirectory, name: N, ) -> Result> where N: ToShortFileName, { - if self.open_dirs.is_full() { + 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 = self.get_dir_by_id(parent_dir)?; - let volume_idx = self.get_volume_by_id(self.open_dirs[parent_dir_idx].volume_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)?; - let parent_dir_info = &self.open_dirs[parent_dir_idx]; // Open the directory + + // Should we short-cut? (root dir doesn't have ".") if short_file_name == ShortFileName::this_dir() { - // short-cut (root dir doesn't have ".") - let directory_id = RawDirectory(self.id_generator.get()); + let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { - directory_id, - volume_id: self.open_volumes[volume_idx].volume_id, - cluster: parent_dir_info.cluster, + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, + cluster: data.open_dirs[parent_dir_idx].cluster, }; - self.open_dirs + data.open_dirs .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; return Ok(directory_id); } - let dir_entry = match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.find_directory_entry(&self.block_device, parent_dir_info, &short_file_name)? - } + // 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); @@ -281,14 +301,14 @@ where // no cached state and so opening a directory twice is allowable. // Remember this open directory. - let directory_id = RawDirectory(self.id_generator.get()); + let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { - directory_id, - volume_id: self.open_volumes[volume_idx].volume_id, + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, cluster: dir_entry.cluster, }; - self.open_dirs + data.open_dirs .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; @@ -297,10 +317,13 @@ where /// 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(&mut self, directory: RawDirectory) -> Result<(), Error> { - for (idx, info) in self.open_dirs.iter().enumerate() { - if directory == info.directory_id { - self.open_dirs.swap_remove(idx); + 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(()); } } @@ -310,166 +333,147 @@ where /// Close a volume /// /// You can't close it if there are any files or directories open on it. - pub fn close_volume(&mut self, volume: RawVolume) -> Result<(), Error> { - for f in self.open_files.iter() { - if f.volume_id == volume { + 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 self.open_dirs.iter() { - if d.volume_id == volume { + for d in data.open_dirs.iter() { + if d.raw_volume == volume { return Err(Error::VolumeStillInUse); } } - let volume_idx = self.get_volume_by_id(volume)?; + let volume_idx = data.get_volume_by_id(volume)?; - match &mut self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat_volume) => { - fat_volume.update_info_sector(&self.block_device)?; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.update_info_sector(&mut data.block_cache)?; } } - self.open_volumes.swap_remove(volume_idx); + data.open_volumes.swap_remove(volume_idx); Ok(()) } /// Look in a directory for a named file. pub fn find_directory_entry( - &mut self, + &self, directory: RawDirectory, name: N, ) -> Result> where N: ToShortFileName, { - let directory_idx = self.get_dir_by_id(directory)?; - let volume_idx = self.get_volume_by_id(self.open_dirs[directory_idx].volume_id)?; - match &self.open_volumes[volume_idx].volume_type { + 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(&self.block_device, &self.open_dirs[directory_idx], &sfn) + 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( - &mut self, + &self, directory: RawDirectory, - func: F, + mut func: F, ) -> Result<(), Error> where F: FnMut(&DirEntry), { - let directory_idx = self.get_dir_by_id(directory)?; - let volume_idx = self.get_volume_by_id(self.open_dirs[directory_idx].volume_id)?; - match &self.open_volumes[volume_idx].volume_type { + 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(&self.block_device, &self.open_dirs[directory_idx], func) + 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); + } + }, + ) } } } - /// Open a file from a DirEntry. This is obtained by calling iterate_dir. + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. /// - /// # Safety + /// 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. /// - /// The DirEntry must be a valid DirEntry read from disk, and not just - /// random numbers. - unsafe fn open_dir_entry( - &mut self, - volume: RawVolume, - dir_entry: DirEntry, - mode: Mode, - ) -> Result> { - // This check is load-bearing - we do an unchecked push later. - if self.open_files.is_full() { - return Err(Error::TooManyOpenFiles); - } - - if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly { - return Err(Error::ReadOnly); - } - - if dir_entry.attributes.is_directory() { - return Err(Error::OpenedDirAsFile); - } + ///
+ /// + /// 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(); - // Check it's not already open - if self.file_is_open(volume, &dir_entry) { - return Err(Error::FileAlreadyOpen); - } + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; - let mode = solve_mode_variant(mode, true); - let file_id = RawFile(self.id_generator.get()); - - let file = match mode { - Mode::ReadOnly => FileInfo { - file_id, - volume_id: volume, - current_cluster: (0, dir_entry.cluster), - current_offset: 0, - mode, - entry: dir_entry, - dirty: false, - }, - Mode::ReadWriteAppend => { - let mut file = FileInfo { - file_id, - volume_id: volume, - 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 { - file_id, - volume_id: volume, - current_cluster: (0, dir_entry.cluster), - current_offset: 0, - mode, - entry: dir_entry, - dirty: false, - }; - let volume_idx = self.get_volume_by_id(volume)?; - match &mut self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.truncate_cluster_chain(&self.block_device, file.entry.cluster)? - } - }; - file.update_length(0); - match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - file.entry.mtime = self.time_source.get_timestamp(); - fat.write_entry_to_disk(&self.block_device, &file.entry)?; - } - }; - - file + 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, + ) } - _ => return Err(Error::Unsupported), - }; - - // Remember this open file - can't be full as we checked already - unsafe { - self.open_files.push_unchecked(file); } - - Ok(file_id) } /// Open a file with the given full path. A file can only be opened once. pub fn open_file_in_dir( - &mut self, + &self, directory: RawDirectory, name: N, mode: Mode, @@ -477,22 +481,26 @@ where 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 self.open_files.is_full() { + if data.open_files.is_full() { return Err(Error::TooManyOpenFiles); } - let directory_idx = self.get_dir_by_id(directory)?; - let directory_info = &self.open_dirs[directory_idx]; - let volume_id = self.open_dirs[directory_idx].volume_id; - let volume_idx = self.get_volume_by_id(volume_id)?; - let volume_info = &self.open_volumes[volume_idx]; + 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(&self.block_device, directory_info, &sfn) - } + VolumeType::Fat(fat) => fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ), }; let dir_entry = match dir_entry { @@ -517,7 +525,7 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - if self.file_is_open(volume_info.volume_id, dir_entry) { + if data.file_is_open(volume_info.raw_volume, dir_entry) { return Err(Error::FileAlreadyOpen); } } @@ -529,23 +537,24 @@ where 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 = self.get_volume_by_id(volume_id)?; - let entry = match &mut self.open_volumes[volume_idx].volume_type { + 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( - &self.block_device, + &mut data.block_cache, &self.time_source, - directory_info.cluster, + cluster, sfn, att, )?, }; - let file_id = RawFile(self.id_generator.get()); + let file_id = RawFile(data.id_generator.generate()); let file = FileInfo { - file_id, - volume_id, + raw_file: file_id, + raw_volume: volume_id, current_cluster: (0, entry.cluster), current_offset: 0, mode, @@ -555,7 +564,7 @@ where // Remember this open file - can't be full as we checked already unsafe { - self.open_files.push_unchecked(file); + data.open_files.push_unchecked(file); } Ok(file_id) @@ -563,95 +572,205 @@ where _ => { // Safe to unwrap, since we actually have an entry if we got here let dir_entry = dir_entry.unwrap(); - // Safety: We read this dir entry off disk and didn't change it - unsafe { self.open_dir_entry(volume_id, dir_entry, mode) } + + 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( - &mut self, + &self, directory: RawDirectory, name: N, ) -> Result<(), Error> where N: ToShortFileName, { - let dir_idx = self.get_dir_by_id(directory)?; - let dir_info = &self.open_dirs[dir_idx]; - let volume_idx = self.get_volume_by_id(dir_info.volume_id)?; + 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 &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(&self.block_device, dir_info, &sfn), + 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 self.file_is_open(dir_info.volume_id, &dir_entry) { + if data.file_is_open(dir_info.raw_volume, &dir_entry) { return Err(Error::FileAlreadyOpen); } - let volume_idx = self.get_volume_by_id(dir_info.volume_id)?; - match &self.open_volumes[volume_idx].volume_type { + 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(&self.block_device, dir_info, &sfn)? + fat.delete_directory_entry(&mut data.block_cache, dir_info, &sfn)? } } Ok(()) } - /// Check if a file is open + /// Get the volume label /// - /// Returns `true` if it's open, `false`, otherwise. - fn file_is_open(&self, volume: RawVolume, dir_entry: &DirEntry) -> bool { - for f in self.open_files.iter() { - if f.volume_id == volume - && f.entry.entry_block == dir_entry.entry_block - && f.entry.entry_offset == dir_entry.entry_offset - { - return true; + /// 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())); + } } } - false + 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(&mut self, file: RawFile, buffer: &mut [u8]) -> Result> { - let file_idx = self.get_file_by_id(file)?; - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + 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 && !self.open_files[file_idx].eof() { - let mut current_cluster = self.open_files[file_idx].current_cluster; - let (block_idx, block_offset, block_avail) = self.find_data_on_disk( + 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, - self.open_files[file_idx].entry.cluster, - self.open_files[file_idx].current_offset, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, )?; - self.open_files[file_idx].current_cluster = current_cluster; - let mut blocks = [Block::new()]; - self.block_device - .read(&mut blocks, block_idx, "read") + 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 block = &blocks[0]; let to_copy = block_avail .min(space) - .min(self.open_files[file_idx].left() as usize); + .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; - self.open_files[file_idx] + data.open_files[file_idx] .seek_from_current(to_copy as i32) .unwrap(); } @@ -659,63 +778,66 @@ where } /// Write to a open file. - pub fn write(&mut self, file: RawFile, buffer: &[u8]) -> Result<(), Error> { + 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 = self.get_file_by_id(file)?; - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + 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 self.open_files[file_idx].mode == Mode::ReadOnly { + if data.open_files[file_idx].mode == Mode::ReadOnly { return Err(Error::ReadOnly); } - self.open_files[file_idx].dirty = true; + data.open_files[file_idx].dirty = true; - if self.open_files[file_idx].entry.cluster.0 < RESERVED_ENTRIES { + 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 - self.open_files[file_idx].entry.cluster = - match self.open_volumes[volume_idx].volume_type { + data.open_files[file_idx].entry.cluster = + match data.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { - fat.alloc_cluster(&self.block_device, None, false)? + fat.alloc_cluster(&mut data.block_cache, None, false)? } }; debug!( "Alloc first cluster {:?}", - self.open_files[file_idx].entry.cluster + data.open_files[file_idx].entry.cluster ); } // Clone this so we can touch our other structures. - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; - if (self.open_files[file_idx].current_cluster.1) < self.open_files[file_idx].entry.cluster { + if (data.open_files[file_idx].current_cluster.1) < data.open_files[file_idx].entry.cluster { debug!("Rewinding to start"); - self.open_files[file_idx].current_cluster = - (0, self.open_files[file_idx].entry.cluster); + 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 - self.open_files[file_idx].current_offset) + 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 = self.open_files[file_idx].current_cluster; + 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 = self.open_files[file_idx].current_offset; - let (block_idx, block_offset, block_avail) = match self.find_data_on_disk( + 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, - self.open_files[file_idx].entry.cluster, + data.open_files[file_idx].entry.cluster, current_offset, ) { Ok(vars) => { @@ -727,21 +849,25 @@ where } Err(Error::EndOfFile) => { debug!("Extending file"); - match self.open_volumes[volume_idx].volume_type { + match data.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { if fat - .alloc_cluster(&self.block_device, Some(current_cluster.1), false) + .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 = self + let new_offset = data .find_data_on_disk( volume_idx, &mut current_cluster, - self.open_files[file_idx].entry.cluster, - self.open_files[file_idx].current_offset, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, ) .map_err(|_| Error::AllocationError)?; debug!("New offset {:?}", new_offset); @@ -751,68 +877,71 @@ where } Err(e) => return Err(e), }; - let mut blocks = [Block::new()]; let to_copy = core::cmp::min(block_avail, bytes_to_write - written); - if block_offset != 0 || to_copy != block_avail { - debug!("Partial block read/modify/write"); - self.block_device - .read(&mut blocks, block_idx, "read") - .map_err(Error::DeviceError)?; - } - let block = &mut blocks[0]; + 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); - self.block_device - .write(&blocks, block_idx) - .map_err(Error::DeviceError)?; + data.block_cache.write_back()?; written += to_copy; - self.open_files[file_idx].current_cluster = current_cluster; + data.open_files[file_idx].current_cluster = current_cluster; let to_copy = to_copy as u32; - let new_offset = self.open_files[file_idx].current_offset + to_copy; - if new_offset > self.open_files[file_idx].entry.size { + 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 - self.open_files[file_idx].update_length(new_offset); + data.open_files[file_idx].update_length(new_offset); } - self.open_files[file_idx] + data.open_files[file_idx] .seek_from_start(new_offset) .unwrap(); // Entry update deferred to file close, for performance. } - self.open_files[file_idx].entry.attributes.set_archive(true); - self.open_files[file_idx].entry.mtime = self.time_source.get_timestamp(); + 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(&mut self, file: RawFile) -> Result<(), Error> { + pub fn close_file(&self, file: RawFile) -> Result<(), Error> { let flush_result = self.flush_file(file); - let file_idx = self.get_file_by_id(file)?; - self.open_files.swap_remove(file_idx); + 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(&mut self, file: RawFile) -> Result<(), Error> { - let file_info = self - .open_files - .iter() - .find(|info| info.file_id == file) - .ok_or(Error::BadHandle)?; - - if file_info.dirty { - let volume_idx = self.get_volume_by_id(file_info.volume_id)?; - match self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(ref mut fat) => { + 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(&self.block_device)?; - debug!("Updating dir entry {:?}", file_info.entry); - if file_info.entry.size != 0 { + 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!(file_info.entry.cluster.0 != 0); + assert!(data.open_files[file_id].entry.cluster.0 != 0); } - fat.write_entry_to_disk(&self.block_device, &file_info.entry)?; + fat.write_entry_to_disk( + &mut data.block_cache, + &data.open_files[file_id].entry, + )?; } }; } @@ -821,28 +950,28 @@ where /// Check if any files or folders are open. pub fn has_open_handles(&self) -> bool { - !(self.open_dirs.is_empty() || self.open_files.is_empty()) + 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) { - (self.block_device, self.time_source) + 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 file_idx = self.get_file_by_id(file)?; - Ok(self.open_files[file_idx].eof()) + 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( - &mut self, - file: RawFile, - offset: u32, - ) -> Result<(), Error> { - let file_idx = self.get_file_by_id(file)?; - self.open_files[file_idx] + 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(()) @@ -850,25 +979,23 @@ where /// Seek a file with an offset from the current position. pub fn file_seek_from_current( - &mut self, + &self, file: RawFile, offset: i32, ) -> Result<(), Error> { - let file_idx = self.get_file_by_id(file)?; - self.open_files[file_idx] + 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( - &mut self, - file: RawFile, - offset: u32, - ) -> Result<(), Error> { - let file_idx = self.get_file_by_id(file)?; - self.open_files[file_idx] + 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(()) @@ -876,35 +1003,40 @@ where /// Get the length of a file pub fn file_length(&self, file: RawFile) -> Result> { - let file_idx = self.get_file_by_id(file)?; - Ok(self.open_files[file_idx].length()) + 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 file_idx = self.get_file_by_id(file)?; - Ok(self.open_files[file_idx].current_offset) + 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( - &mut self, + &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 self.open_dirs.is_full() { + if data.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); } - let parent_directory_idx = self.get_dir_by_id(directory)?; - let parent_directory_info = &self.open_dirs[parent_directory_idx]; - let volume_id = self.open_dirs[parent_directory_idx].volume_id; - let volume_idx = self.get_volume_by_id(volume_id)?; - let volume_info = &self.open_volumes[volume_idx]; + 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); @@ -916,7 +1048,7 @@ where // Does an entry exist with this name? let maybe_dir_entry = match &volume_info.volume_type { VolumeType::Fat(fat) => { - fat.find_directory_entry(&self.block_device, parent_directory_info, &sfn) + fat.find_directory_entry(&mut data.block_cache, parent_directory_info, &sfn) } }; @@ -939,111 +1071,93 @@ where let att = Attributes::create_from_fat(Attributes::DIRECTORY); // Need mutable access for this - match &mut self.open_volumes[volume_idx].volume_type { + match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { debug!("Making dir entry"); - let mut new_dir_entry_in_parent = fat.write_new_directory_entry( - &self.block_device, + fat.make_dir( + &mut data.block_cache, &self.time_source, parent_directory_info.cluster, sfn, att, )?; - if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { - new_dir_entry_in_parent.cluster = - fat.alloc_cluster(&self.block_device, None, false)?; - // update the parent dir with the cluster of the new dir - fat.write_entry_to_disk(&self.block_device, &new_dir_entry_in_parent)?; - } - let new_dir_start_block = fat.cluster_to_block(new_dir_entry_in_parent.cluster); - debug!("Made new dir entry {:?}", new_dir_entry_in_parent); - let now = self.time_source.get_timestamp(); - let fat_type = fat.get_fat_type(); - // A blank block - let mut blocks = [Block::new()]; - // 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; - blocks[0][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: { - // On FAT16, indicate parent is root using Cluster(0) - if parent_directory_info.cluster == ClusterId::ROOT_DIR { - ClusterId::EMPTY - } else { - parent_directory_info.cluster - } - }, - size: 0, - entry_block: new_dir_start_block, - entry_offset: OnDiskDirEntry::LEN_U32, - }; - debug!("New dir has {:?}", dot_dot_entry_in_child); - blocks[0][offset..offset + OnDiskDirEntry::LEN] - .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); - - self.block_device - .write(&blocks, new_dir_start_block) - .map_err(Error::DeviceError)?; - - // Now zero the rest of the cluster - for b in blocks[0].iter_mut() { - *b = 0; - } - for block in new_dir_start_block - .range(BlockCount(u32::from(fat.blocks_per_cluster))) - .skip(1) - { - self.block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; - } } }; Ok(()) } +} - fn get_volume_by_id(&self, volume: RawVolume) -> Result> { +/// 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.volume_id == volume { + if v.raw_volume == raw_volume { return Ok(idx); } } Err(Error::BadHandle) } - fn get_dir_by_id(&self, directory: RawDirectory) -> Result> { + 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.directory_id == directory { + if d.raw_directory == raw_directory { return Ok(idx); } } Err(Error::BadHandle) } - fn get_file_by_id(&self, file: RawFile) -> Result> { + 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.file_id == file { + if f.raw_file == raw_file { return Ok(idx); } } @@ -1060,12 +1174,15 @@ where /// * the byte offset into that block for the data we want, and /// * how many bytes remain in that block. fn find_data_on_disk( - &self, + &mut self, volume_idx: usize, start: &mut (u32, ClusterId), file_start: ClusterId, desired_offset: u32, - ) -> Result<(BlockIdx, usize, usize), Error> { + ) -> 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(), }; @@ -1080,12 +1197,9 @@ where let offset_from_cluster = desired_offset - start.0; // walk through the FAT chain let num_clusters = offset_from_cluster / bytes_per_cluster; - let mut block_cache = BlockCache::empty(); for _ in 0..num_clusters { start.1 = match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.next_cluster(&self.block_device, start.1, &mut block_cache)? - } + VolumeType::Fat(fat) => fat.next_cluster(&mut self.block_cache, start.1)?, }; start.0 += bytes_per_cluster; } @@ -1131,7 +1245,7 @@ fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { #[cfg(test)] mod tests { use super::*; - use crate::filesystem::SearchId; + use crate::filesystem::Handle; use crate::Timestamp; struct DummyBlockDevice; @@ -1161,12 +1275,7 @@ mod tests { type Error = Error; /// Read one or more blocks, starting at the given block index. - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { + 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 { @@ -1371,16 +1480,16 @@ mod tests { #[test] fn partition0() { - let mut c: VolumeManager = + let c: VolumeManager = VolumeManager::new_with_limits(DummyBlockDevice, Clock, 0xAA00_0000); let v = c.open_raw_volume(VolumeIdx(0)).unwrap(); - let expected_id = RawVolume(SearchId(0xAA00_0000)); + let expected_id = RawVolume(Handle(0xAA00_0000)); assert_eq!(v, expected_id); assert_eq!( - &c.open_volumes[0], + &c.data.borrow().open_volumes[0], &VolumeInfo { - volume_id: expected_id, + raw_volume: expected_id, idx: VolumeIdx(0), volume_type: VolumeType::Fat(crate::FatVolume { lba_start: BlockIdx(1), @@ -1389,7 +1498,7 @@ mod tests { first_data_block: BlockCount(15136), fat_start: BlockCount(32), second_fat_start: Some(BlockCount(32 + 0x0000_1D80)), - name: fat::VolumeName::new(*b"Pictures "), + name: fat::VolumeName::create_from_str("Pictures").unwrap(), free_clusters_count: None, next_free_cluster: None, cluster_count: 965_788, diff --git a/tests/directories.rs b/tests/directories.rs index df9154f4..e10a9018 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -1,6 +1,6 @@ //! Directory related tests -use embedded_sdmmc::{Mode, ShortFileName}; +use embedded_sdmmc::{LfnBuffer, Mode, ShortFileName}; mod utils; @@ -38,7 +38,7 @@ impl PartialEq for ExpectedDirEntry { fn fat16_root_directory_listing() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -48,59 +48,104 @@ fn fat16_root_directory_listing() { .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, - }, - 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, - }, - 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, - }, - 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, - }, + ( + 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(root_dir, |d| { - listing.push(d.clone()); + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); }) .expect("iterate directory"); - assert_eq!(expected.len(), listing.len()); for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { assert_eq!( - expected_entry, given_entry, + 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 mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -151,7 +196,6 @@ fn fat16_sub_directory_listing() { }) .expect("iterate directory"); - assert_eq!(expected.len(), listing.len()); for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { assert_eq!( expected_entry, given_entry, @@ -159,13 +203,20 @@ fn fat16_sub_directory_listing() { 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 mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -175,59 +226,124 @@ fn fat32_root_directory_listing() { .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, - }, - 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, - }, - 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, - }, - 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, - }, + ( + 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(root_dir, |d| { - listing.push(d.clone()); + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); }) .expect("iterate directory"); - assert_eq!(expected.len(), listing.len()); for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { assert_eq!( - expected_entry, given_entry, + 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 mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -267,7 +383,7 @@ fn open_dir_twice() { fn open_too_many_dirs() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: embedded_sdmmc::VolumeManager< + let volume_mgr: embedded_sdmmc::VolumeManager< utils::RamDisk>, utils::TestTimeSource, 1, @@ -292,7 +408,7 @@ fn open_too_many_dirs() { fn find_dir_entry() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -322,7 +438,7 @@ fn find_dir_entry() { fn delete_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -367,7 +483,7 @@ fn delete_file() { fn make_directory() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) diff --git a/tests/disk.img.gz b/tests/disk.img.gz index 8df362bc..507ec948 100644 Binary files a/tests/disk.img.gz and b/tests/disk.img.gz differ diff --git a/tests/open_files.rs b/tests/open_files.rs index 10e21818..6b927bc0 100644 --- a/tests/open_files.rs +++ b/tests/open_files.rs @@ -8,7 +8,7 @@ mod utils; fn open_files() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + 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)) @@ -95,11 +95,11 @@ fn open_files() { fn open_non_raw() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); - let mut root_dir = volume.open_root_dir().expect("open root dir"); - let mut f = root_dir + 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"); diff --git a/tests/read_file.rs b/tests/read_file.rs index b7c80c95..904964f3 100644 --- a/tests/read_file.rs +++ b/tests/read_file.rs @@ -11,7 +11,7 @@ static TEST_DAT_SHA256_SUM: &[u8] = fn read_file_512_blocks() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -53,7 +53,7 @@ fn read_file_512_blocks() { fn read_file_all() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -87,7 +87,7 @@ fn read_file_all() { fn read_file_prime_blocks() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -130,7 +130,7 @@ fn read_file_prime_blocks() { fn read_file_backwards() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -188,13 +188,13 @@ fn read_file_backwards() { fn read_file_with_odd_seek() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); - let mut volume = volume_mgr + let volume = volume_mgr .open_volume(embedded_sdmmc::VolumeIdx(0)) .unwrap(); - let mut root_dir = volume.open_root_dir().unwrap(); - let mut f = root_dir + 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(); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 22ed2eab..3f89a0ea 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -85,12 +85,7 @@ where { type Error = Error; - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::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; diff --git a/tests/volume.rs b/tests/volume.rs index 633a8d25..c2ea49b9 100644 --- a/tests/volume.rs +++ b/tests/volume.rs @@ -6,7 +6,7 @@ mod utils; fn open_all_volumes() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: embedded_sdmmc::VolumeManager< + let volume_mgr: embedded_sdmmc::VolumeManager< utils::RamDisk>, utils::TestTimeSource, 4, @@ -58,6 +58,12 @@ fn open_all_volumes() { 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)), @@ -76,7 +82,7 @@ fn open_all_volumes() { fn close_volume_too_early() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) diff --git a/tests/write_file.rs b/tests/write_file.rs index 6fd4d059..9a5e4ab9 100644 --- a/tests/write_file.rs +++ b/tests/write_file.rs @@ -8,7 +8,7 @@ mod utils; fn append_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + 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)) @@ -59,7 +59,7 @@ fn append_file() { fn flush_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + 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)) @@ -106,7 +106,7 @@ fn flush_file() { fn random_access_write_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + 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))