diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c540ac7..75b29b7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,18 +19,5 @@ jobs: - uses: actions/checkout@v1 - name: Build run: cargo build --no-default-features --features ${{matrix.features}} --verbose - - name: Unpack Disk - run: gunzip -fk disk.img.gz - - name: Run Test Mount example - run: cargo run --no-default-features --features ${{matrix.features}} --example test_mount ./disk.img - - name: Unpack Disk - run: gunzip -fk disk.img.gz - - name: Run Create Test example - run: cargo run --no-default-features --features ${{matrix.features}} --example create_test ./disk.img - - name: Unpack Disk - run: gunzip -fk disk.img.gz - - name: Run Write Test example - run: cargo run --no-default-features --features ${{matrix.features}} --example write_test ./disk.img - - name: Run Unit Tests + - name: Run Tests run: cargo test --no-default-features --features ${{matrix.features}} --verbose - diff --git a/CHANGELOG.md b/CHANGELOG.md index 4823930..f08a337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,26 +2,45 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning]. -## [Unreleased](https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.5.0...develop) +## [Unreleased] + +* None + +## [Version 0.6.0] - 2023-10-20 ### Changed -- None +- Writing to a file no longer flushes file metadata to the Directory Entry. + Instead closing a file now flushes file metadata to the Directory Entry. + Requires mutable access to the Volume ([#94]). +- Files now have the correct length when modified, not appended ([#72]). +- Calling `SdCard::get_card_type` will now perform card initialisation ([#87] and [#90]). +- Removed warning about unused arguments. +- Types are now documented at the top level ([#86]). +- Renamed `Cluster` to `ClusterId` and stopped you adding two together + +[#72]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/72 +[#86]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/86 +[#87]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/87 +[#90]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/90 +[#94]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/94 ### Added -- None +- New examples, `append_file`, `create_file`, `delete_file`, `list_dir`, `shell` +- New test cases `tests/directories.rs`, `tests/read_file.rs` ### Removed -- None +- __Breaking Change__: `Controller` alias for `VolumeManager` removed. +- __Breaking Change__: `VolumeManager::open_dir_entry` removed, as it was unsafe to the user to randomly pick a starting cluster. +- Old examples `create_test`, `test_mount`, `write_test`, `delete_test` -## [Version 0.5.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.5.0) - 2023-05-20 +## [Version 0.5.0] - 2023-05-20 -### Changes +### Changed - __Breaking Change__: Renamed `Controller` to `VolumeManager`, to better describe what it does. - __Breaking Change__: Renamed `SdMmcSpi` to `SdCard` @@ -39,13 +58,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - __Breaking Change__: Removed `BlockSpi` type - card initialisation now handled as an internal state variable -## [Version 0.4.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.4.0) - 2023-01-18 +## [Version 0.4.0] - 2023-01-18 + +### Changed -### Changes -- Optionally use [defmt](https://github.com/knurling-rs/defmt) for logging. +- Optionally use [defmt] s/defmt) for logging. Controlled by `defmt-log` feature flag. - __Breaking Change__: Use SPI blocking traits instead to ease SPI peripheral sharing. - See: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/28 + See: - Added `Controller::has_open_handles` and `Controller::free` methods. - __Breaking Change__: Changed interface to enforce correct SD state at compile time. - __Breaking Change__: Added custom error type for `File` operations. @@ -56,35 +76,46 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add new constructor method `Controller::new_with_limits(block_device: D, timesource: T) -> Controller` to create a `Controller` with custom limits. -## [Version 0.3.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.3.0) - 2019-12-16 +## [Version 0.3.0] - 2019-12-16 -### Changes +### Changed -* Updated to `v2` embedded-hal traits. -* Added open support for all modes. -* Added write support for files. -* Added `Info_Sector` tracking for FAT32. -* Change directory iteration to look in all the directory's clusters. -* Added `write_test` and `create_test`. -* De-duplicated FAT16 and FAT32 code (https://github.com/thejpster/embedded-sdmmc-rs/issues/10) +- Updated to `v2` embedded-hal traits. +- Added open support for all modes. +- Added write support for files. +- Added `Info_Sector` tracking for FAT32. +- Change directory iteration to look in all the directory's clusters. +- Added `write_test` and `create_test`. +- De-duplicated FAT16 and FAT32 code () -## [Version 0.2.1](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.2.1) - 2019-02-19 +## [Version 0.2.1] - 2019-02-19 -### Changes +### Changed -* Added `readme=README.md` to `Cargo.toml` +- Added `readme=README.md` to `Cargo.toml` -## [Version 0.2.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.2.0) - 2019-01-24 +## [Version 0.2.0] - 2019-01-24 -### Changes +### Changed -* Reduce delay waiting for response. Big speed improvements. +- Reduce delay waiting for response. Big speed improvements. -## [Version 0.1.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.1.1) - 2018-12-23 +## [Version 0.1.0] - 2018-12-23 -### Changes +### Changed -* Can read blocks from an SD Card using an `embedded_hal::SPI` device and a +- Can read blocks from an SD Card using an `embedded_hal::SPI` device and a `embedded_hal::OutputPin` for Chip Select. -* Can read partition tables and open a FAT32 or FAT16 formatted partition. -* Can open and iterate the root directory of a FAT16 formatted partition. +- Can read partition tables and open a FAT32 or FAT16 formatted partition. +- Can open and iterate the root directory of a FAT16 formatted partition. + +[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: http://semver.org/spec/v2.0.0.html +[Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.6.0...develop +[Version 0.6.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.6.0...v0.5.0 +[Version 0.5.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.5.0...v0.4.0 +[Version 0.4.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.4.0...v0.3.0 +[Version 0.3.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.3.0...v0.2.1 +[Version 0.2.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.1...v0.2.0 +[Version 0.2.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.0...v0.1.1 +[Version 0.1.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.1.1 diff --git a/Cargo.toml b/Cargo.toml index 86f2bcf..8bd40d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,21 @@ license = "MIT OR Apache-2.0" name = "embedded-sdmmc" readme = "README.md" repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" -version = "0.5.0" +version = "0.6.0" [dependencies] byteorder = {version = "1", default-features = false} defmt = {version = "0.3", optional = true} embedded-hal = "0.2.3" +heapless = "0.7" log = {version = "0.4", default-features = false, optional = true} [dev-dependencies] env_logger = "0.9" hex-literal = "0.3" +flate2 = "1.0" +sha256 = "1.4" +chrono = "0.4" [features] default = ["log"] diff --git a/README.md b/README.md index 273c368..81bdfc2 100644 --- a/README.md +++ b/README.md @@ -23,36 +23,35 @@ let mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); // It doesn't hold a reference to the Volume Manager and so must be passed back // to every Volume Manager API call. This makes it easier to handle multiple // volumes in parallel. -let mut volume0 = volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; +let volume0 = volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (passing in the volume we're using). let root_dir = volume_mgr.open_root_dir(&volume0)?; // Open a file called "MY_FILE.TXT" in the root directory -let mut my_file = volume_mgr.open_file_in_dir( - &mut volume0, - &root_dir, +let my_file = volume_mgr.open_file_in_dir( + root_dir, "MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly, )?; // Print the contents of the file -while !my_file.eof() { +while !volume_manager.file_eof(my_file).unwrap() { let mut buffer = [0u8; 32]; let num_read = volume_mgr.read(&volume0, &mut my_file, &mut buffer)?; for b in &buffer[0..num_read] { print!("{}", *b as char); } } -volume_mgr.close_file(&volume0, my_file)?; -volume_mgr.close_dir(&volume0, root_dir); +volume_mgr.close_file(my_file)?; +volume_mgr.close_dir(root_dir)?; ``` ### Open directories and files -By default the `VolumeManager` will initialize with a maximum number of `4` open directories and files. This can be customized by specifying the `MAX_DIR` and `MAX_FILES` generic consts of the `VolumeManager`: +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 and 12 open files -let mut cont: VolumeManager<_, _, 6, 12> = VolumeManager::new_with_limits(block, time_source); +// 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); ``` ## Supported features diff --git a/examples/append_file.rs b/examples/append_file.rs new file mode 100644 index 0000000..608fa2c --- /dev/null +++ b/examples/append_file.rs @@ -0,0 +1,49 @@ +//! Append File Example. +//! +//! ```bash +//! $ cargo run --example append_file -- ./disk.img +//! $ cargo run --example append_file -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. 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. +//! +//! ```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}; + +fn main() -> Result<(), embedded_sdmmc::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 volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume_mgr.open_root_dir(volume)?; + println!("\nCreating file {}...", FILE_TO_APPEND); + let f = volume_mgr.open_file_in_dir(root_dir, FILE_TO_APPEND, Mode::ReadWriteAppend)?; + volume_mgr.write(f, b"\r\n\r\nThis has been added to your file.\r\n")?; + volume_mgr.close_file(f)?; + volume_mgr.close_dir(root_dir)?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/create_file.rs b/examples/create_file.rs new file mode 100644 index 0000000..1d426eb --- /dev/null +++ b/examples/create_file.rs @@ -0,0 +1,52 @@ +//! Create File Example. +//! +//! ```bash +//! $ cargo run --example create_file -- ./disk.img +//! $ cargo run --example create_file -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. 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. +//! +//! ```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}; + +fn main() -> Result<(), embedded_sdmmc::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 volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume_mgr.open_root_dir(volume)?; + println!("\nCreating file {}...", FILE_TO_CREATE); + // This will panic if the file already exists: use ReadWriteCreateOrAppend + // or ReadWriteCreateOrTruncate instead if you want to modify an existing + // file. + let f = volume_mgr.open_file_in_dir(root_dir, FILE_TO_CREATE, Mode::ReadWriteCreate)?; + volume_mgr.write(f, b"Hello, this is a new file on disk\r\n")?; + volume_mgr.close_file(f)?; + volume_mgr.close_dir(root_dir)?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/create_test.rs b/examples/create_test.rs deleted file mode 100644 index 239cded..0000000 --- a/examples/create_test.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! # Tests the Embedded SDMMC Library -//! ```bash -//! $ cargo run --example create_test -- /dev/mmcblk0 -//! $ cargo run --example create_test -- /dev/sda -//! ``` -//! -//! 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. -//! -//! ```bash -//! zcat ./disk.img.gz > ./disk.img -//! $ cargo run --example create_test -- ./disk.img -//! ``` - -extern crate embedded_sdmmc; - -const FILE_TO_CREATE: &'static str = "CREATE.TXT"; - -use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, - VolumeManager, -}; -use std::cell::RefCell; -use std::fs::{File, OpenOptions}; -use std::io::prelude::*; -use std::io::SeekFrom; -use std::path::Path; - -#[derive(Debug)] -struct LinuxBlockDevice { - file: RefCell, - print_blocks: bool, -} - -impl LinuxBlockDevice { - fn new

(device_name: P, print_blocks: bool) -> Result - where - P: AsRef, - { - Ok(LinuxBlockDevice { - file: RefCell::new( - OpenOptions::new() - .read(true) - .write(true) - .open(device_name)?, - ), - print_blocks, - }) - } -} - -impl BlockDevice for LinuxBlockDevice { - type Error = std::io::Error; - - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> 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 - ); - } - } - Ok(()) - } - - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { - self.file - .borrow_mut() - .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; - for block in blocks.iter() { - self.file.borrow_mut().write_all(&block.contents)?; - if self.print_blocks { - println!("Wrote: {:?}", &block); - } - } - Ok(()) - } - - fn num_blocks(&self) -> Result { - let num_blocks = self.file.borrow().metadata().unwrap().len() / 512; - Ok(BlockCount(num_blocks as u32)) - } -} - -struct Clock; - -impl TimeSource for Clock { - fn get_timestamp(&self) -> Timestamp { - Timestamp { - year_since_1970: 0, - zero_indexed_month: 0, - zero_indexed_day: 0, - hours: 0, - minutes: 0, - seconds: 0, - } - } -} - -fn main() { - 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) - .unwrap(); - println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); - for volume_idx in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); - println!("volume {}: {:#?}", volume_idx, volume); - if let Ok(mut volume) = volume { - let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(&volume, &root_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - println!("\nCreating file {}...", FILE_TO_CREATE); - // This will panic if the file already exists, use ReadWriteCreateOrAppend or - // ReadWriteCreateOrTruncate instead - let mut f = volume_mgr - .open_file_in_dir( - &mut volume, - &root_dir, - FILE_TO_CREATE, - Mode::ReadWriteCreate, - ) - .unwrap(); - println!("\nReading from file"); - println!("FILE STARTS:"); - while !f.eof() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - } - } - println!("EOF"); - - let buffer1 = b"\nFile Appended\n"; - let mut buffer: Vec = vec![]; - for _ in 0..64 { - for _ in 0..15 { - buffer.push(b'a'); - } - buffer.push(b'\n'); - } - println!("\nAppending to file"); - let num_written1 = volume_mgr.write(&mut volume, &mut f, &buffer1[..]).unwrap(); - let num_written = volume_mgr.write(&mut volume, &mut f, &buffer[..]).unwrap(); - println!("Number of bytes written: {}\n", num_written + num_written1); - volume_mgr.close_file(&volume, f).unwrap(); - - let mut f = volume_mgr - .open_file_in_dir( - &mut volume, - &root_dir, - FILE_TO_CREATE, - Mode::ReadWriteCreateOrAppend, - ) - .unwrap(); - f.seek_from_start(0).unwrap(); - - println!("\tFinding {}...", FILE_TO_CREATE); - println!( - "\tFound {}?: {:?}", - FILE_TO_CREATE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_CREATE) - ); - println!("\nReading from file"); - println!("FILE STARTS:"); - while !f.eof() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - } - } - println!("EOF"); - volume_mgr.close_file(&volume, f).unwrap(); - } - } -} diff --git a/examples/delete_file.rs b/examples/delete_file.rs new file mode 100644 index 0000000..8b7b1cb --- /dev/null +++ b/examples/delete_file.rs @@ -0,0 +1,51 @@ +//! Delete File Example. +//! +//! ```bash +//! $ cargo run --example delete_file -- ./disk.img +//! $ cargo run --example delete_file -- /dev/mmcblk0 +//! ``` +//! +//! NOTE: THIS EXAMPLE DELETES A FILE CALLED README.TXT. IF YOU DO NOT WANT THAT +//! FILE DELETED FROM YOUR DISK IMAGE, DO NOT RUN THIS EXAMPLE. +//! +//! If you pass a block device it should be unmounted. 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. +//! +//! ```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}; + +fn main() -> Result<(), embedded_sdmmc::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 volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume_mgr.open_root_dir(volume)?; + println!("Deleting file {}...", FILE_TO_DELETE); + volume_mgr.delete_file_in_dir(root_dir, FILE_TO_DELETE)?; + println!("Deleted!"); + volume_mgr.close_dir(root_dir)?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/delete_test.rs b/examples/delete_test.rs deleted file mode 100644 index 189f683..0000000 --- a/examples/delete_test.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! # Tests the Embedded SDMMC Library -//! ```bash -//! $ cargo run --example delete_test -- /dev/mmcblk0 -//! $ cargo run --example delete_test -- /dev/sda -//! ``` -//! -//! 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. -//! -//! ```bash -//! zcat ./disk.img.gz > ./disk.img -//! $ cargo run --example delete_test -- ./disk.img -//! ``` - -extern crate embedded_sdmmc; - -const FILE_TO_DELETE: &'static str = "DELETE.TXT"; - -use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, - VolumeManager, -}; -use std::cell::RefCell; -use std::fs::{File, OpenOptions}; -use std::io::prelude::*; -use std::io::SeekFrom; -use std::path::Path; - -#[derive(Debug)] -struct LinuxBlockDevice { - file: RefCell, - print_blocks: bool, -} - -impl LinuxBlockDevice { - fn new

(device_name: P, print_blocks: bool) -> Result - where - P: AsRef, - { - Ok(LinuxBlockDevice { - file: RefCell::new( - OpenOptions::new() - .read(true) - .write(true) - .open(device_name)?, - ), - print_blocks, - }) - } -} - -impl BlockDevice for LinuxBlockDevice { - type Error = std::io::Error; - - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> 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 - ); - } - } - Ok(()) - } - - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { - self.file - .borrow_mut() - .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; - for block in blocks.iter() { - self.file.borrow_mut().write_all(&block.contents)?; - if self.print_blocks { - println!("Wrote: {:?}", &block); - } - } - Ok(()) - } - - fn num_blocks(&self) -> Result { - let num_blocks = self.file.borrow().metadata().unwrap().len() / 512; - Ok(BlockCount(num_blocks as u32)) - } -} - -struct Clock; - -impl TimeSource for Clock { - fn get_timestamp(&self) -> Timestamp { - Timestamp { - year_since_1970: 0, - zero_indexed_month: 0, - zero_indexed_day: 0, - hours: 0, - minutes: 0, - seconds: 0, - } - } -} - -fn main() { - 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) - .unwrap(); - println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); - for volume_idx in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); - println!("volume {}: {:#?}", volume_idx, volume); - if let Ok(mut volume) = volume { - let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(&volume, &root_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - println!("\nCreating file {}...", FILE_TO_DELETE); - // This will panic if the file already exists, use ReadWriteCreateOrAppend or - // ReadWriteCreateOrTruncate instead - let f = volume_mgr - .open_file_in_dir( - &mut volume, - &root_dir, - FILE_TO_DELETE, - Mode::ReadWriteCreate, - ) - .unwrap(); - - println!("\tFinding {}...", FILE_TO_DELETE); - println!( - "\tFound {}?: {:?}", - FILE_TO_DELETE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) - ); - - match volume_mgr.delete_file_in_dir(&volume, &root_dir, FILE_TO_DELETE) { - Ok(()) => (), - Err(error) => println!("\tCannot delete file: {:?}", error), - } - println!("\tClosing {}...", FILE_TO_DELETE); - volume_mgr.close_file(&volume, f).unwrap(); - - match volume_mgr.delete_file_in_dir(&volume, &root_dir, FILE_TO_DELETE) { - Ok(()) => println!("\tDeleted {}.", FILE_TO_DELETE), - Err(error) => println!("\tCannot delete {}: {:?}", FILE_TO_DELETE, error), - } - println!("\tFinding {}...", FILE_TO_DELETE); - println!( - "\tFound {}?: {:?}", - FILE_TO_DELETE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) - ); - } - } -} diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs new file mode 100644 index 0000000..5abb99f --- /dev/null +++ b/examples/linux/mod.rs @@ -0,0 +1,99 @@ +//! Helpers for using embedded-sdmmc on Linux + +use chrono::Timelike; +use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx, TimeSource, Timestamp}; +use std::cell::RefCell; +use std::fs::{File, OpenOptions}; +use std::io::prelude::*; +use std::io::SeekFrom; +use std::path::Path; + +#[derive(Debug)] +pub struct LinuxBlockDevice { + file: RefCell, + print_blocks: bool, +} + +impl LinuxBlockDevice { + pub fn new

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

+//! 64MB.DAT 67108864 2018-12-09 19:21:38 +//! FSEVEN~1 0 2023-09-21 11:32:04 +//! Listing /TEST +//! . 0 2018-12-09 19:21:02 +//! .. 0 2018-12-09 19:21:02 +//! TEST.DAT 3500 2018-12-09 19:22:12 +//! Listing /FSEVEN~1 +//! . 0 2023-09-21 11:32:22 +//! .. 0 2023-09-21 11:32:04 +//! FSEVEN~1 36 2023-09-21 11:32:04 +//! $ +//! ``` +//! +//! If you pass a block device it should be unmounted. 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. +//! +//! ```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}; + +type Error = 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 volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume_mgr.open_root_dir(volume)?; + list_dir(&mut volume_mgr, root_dir, "/")?; + volume_mgr.close_dir(root_dir)?; + Ok(()) +} + +/// Recursively print a directory listing for the open directory given. +/// +/// The path is for display purposes only. +fn list_dir( + volume_mgr: &mut VolumeManager, + directory: Directory, + path: &str, +) -> Result<(), Error> { + println!("Listing {}", path); + let mut children = Vec::new(); + volume_mgr.iterate_dir(directory, |entry| { + println!( + "{:12} {:9} {} {}", + entry.name, + entry.size, + entry.mtime, + if entry.attributes.is_directory() { + "" + } else { + "" + } + ); + if entry.attributes.is_directory() { + if entry.name != embedded_sdmmc::ShortFileName::parent_dir() + && entry.name != embedded_sdmmc::ShortFileName::this_dir() + { + children.push(entry.name.clone()); + } + } + })?; + for child_name in children { + let child_dir = volume_mgr.open_dir(directory, &child_name)?; + let child_path = if path == "/" { + format!("/{}", child_name) + } else { + format!("{}/{}", path, child_name) + }; + list_dir(volume_mgr, child_dir, &child_path)?; + volume_mgr.close_dir(child_dir)?; + } + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/read_file.rs b/examples/read_file.rs new file mode 100644 index 0000000..a771f59 --- /dev/null +++ b/examples/read_file.rs @@ -0,0 +1,85 @@ +//! Read File Example. +//! +//! ```bash +//! $ cargo run --example read_file -- ./disk.img +//! Reading file README.TXT... +//! 00000000 [54, 68, 69, 73, 20, 69, 73, 20, 61, 20, 46, 41, 54, 31, 36, 20] |This.is.a.FAT16.| +//! 00000010 [70, 61, 74, 69, 74, 69, 6f, 6e, 2e, 20, 49, 74, 20, 63, 6f, 6e] |patition..It.con| +//! 00000020 [74, 61, 69, 6e, 73, 20, 66, 6f, 75, 72, 20, 66, 69, 6c, 65, 73] |tains.four.files| +//! 00000030 [20, 61, 6e, 64, 20, 61, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |.and.a.directory| +//! 00000040 [2e, 0a, 0a, 2a, 20, 54, 68, 69, 73, 20, 66, 69, 6c, 65, 20, 28] |...*.This.file.(| +//! 00000050 [52, 45, 41, 44, 4d, 45, 2e, 54, 58, 54, 29, 0a, 2a, 20, 41, 20] |README.TXT).*.A.| +//! 00000060 [36, 34, 20, 4d, 69, 42, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c] |64.MiB.file.full| +//! 00000070 [20, 6f, 66, 20, 7a, 65, 72, 6f, 73, 20, 28, 36, 34, 4d, 42, 2e] |.of.zeros.(64MB.| +//! 00000080 [44, 41, 54, 29, 2e, 0a, 2a, 20, 41, 20, 33, 35, 30, 30, 20, 62] |DAT)..*.A.3500.b| +//! 00000090 [79, 74, 65, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c, 20, 6f, 66] |yte.file.full.of| +//! 000000a0 [20, 72, 61, 6e, 64, 6f, 6d, 20, 64, 61, 74, 61, 2e, 0a, 2a, 20] |.random.data..*.| +//! 000000b0 [41, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79, 20, 63, 61, 6c, 6c] |A.directory.call| +//! 000000c0 [65, 64, 20, 54, 45, 53, 54, 0a, 2a, 20, 41, 20, 7a, 65, 72, 6f] |ed.TEST.*.A.zero| +//! 000000d0 [20, 62, 79, 74, 65, 20, 66, 69, 6c, 65, 20, 69, 6e, 20, 74, 68] |.byte.file.in.th| +//! 000000e0 [65, 20, 54, 45, 53, 54, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |e.TEST.directory| +//! 000000f0 [20, 63, 61, 6c, 6c, 65, 64, 20, 45, 4d, 50, 54, 59, 2e, 44, 41] |.called.EMPTY.DA| +//! 00000100 [54, 0a, 0d] |T...............| +//! ``` +//! +//! If you pass a block device it should be unmounted. 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. +//! +//! ```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}; + +fn main() -> Result<(), embedded_sdmmc::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 volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume_mgr.open_root_dir(volume)?; + println!("\nReading file {}...", FILE_TO_READ); + let f = volume_mgr.open_file_in_dir(root_dir, FILE_TO_READ, Mode::ReadOnly)?; + volume_mgr.close_dir(root_dir)?; + while !volume_mgr.file_eof(f)? { + let mut buffer = [0u8; 16]; + let offset = volume_mgr.file_offset(f)?; + let mut len = volume_mgr.read(f, &mut buffer)?; + print!("{:08x} {:02x?}", offset, &buffer[0..len]); + while len < buffer.len() { + print!(" "); + len += 1; + } + print!(" |"); + for b in buffer.iter() { + let ch = char::from(*b); + if ch.is_ascii_graphic() { + print!("{}", ch); + } else { + print!("."); + } + } + println!("|"); + } + volume_mgr.close_file(f)?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/readme_test.rs b/examples/readme_test.rs index bb9ffbc..beba096 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -14,7 +14,7 @@ impl embedded_hal::blocking::spi::Transfer for FakeSpi { impl embedded_hal::blocking::spi::Write for FakeSpi { type Error = core::convert::Infallible; - fn write<'w>(&mut self, _words: &'w [u8]) -> Result<(), Self::Error> { + fn write(&mut self, _words: &[u8]) -> Result<(), Self::Error> { Ok(()) } } @@ -90,26 +90,28 @@ fn main() -> Result<(), Error> { // It doesn't hold a reference to the Volume Manager and so must be passed back // to every Volume Manager API call. This makes it easier to handle multiple // volumes in parallel. - let mut volume0 = volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; + let volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (passing in the volume we're using). - let root_dir = volume_mgr.open_root_dir(&volume0)?; + let root_dir = volume_mgr.open_root_dir(volume0)?; // Open a file called "MY_FILE.TXT" in the root directory - let mut my_file = volume_mgr.open_file_in_dir( - &mut volume0, - &root_dir, - "MY_FILE.TXT", - embedded_sdmmc::Mode::ReadOnly, - )?; + let my_file = + volume_mgr.open_file_in_dir(root_dir, "MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; // Print the contents of the file - while !my_file.eof() { + while !volume_mgr.file_eof(my_file).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume0, &mut my_file, &mut buffer)?; + let num_read = volume_mgr.read(my_file, &mut buffer)?; for b in &buffer[0..num_read] { print!("{}", *b as char); } } - volume_mgr.close_file(&volume0, my_file)?; - volume_mgr.close_dir(&volume0, root_dir); + volume_mgr.close_file(my_file)?; + volume_mgr.close_dir(root_dir)?; Ok(()) } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/shell.rs b/examples/shell.rs new file mode 100644 index 0000000..35e9e13 --- /dev/null +++ b/examples/shell.rs @@ -0,0 +1,252 @@ +//! A simple shell demo for embedded-sdmmc +//! +//! Presents a basic command prompt which implements some basic MS-DOS style shell commands. + +use std::io::prelude::*; + +use embedded_sdmmc::{Directory, Error, Volume, VolumeIdx, VolumeManager}; + +use crate::linux::{Clock, LinuxBlockDevice}; + +mod linux; + +struct VolumeState { + directory: Directory, + volume: Volume, + path: Vec, +} + +struct Context { + volume_mgr: VolumeManager, + volumes: [Option; 4], + current_volume: usize, +} + +impl Context { + fn current_path(&self) -> Vec { + let Some(s) = &self.volumes[self.current_volume] else { + return vec![]; + }; + s.path.clone() + } + + fn process_line(&mut self, line: &str) -> Result<(), Error> { + if line == "help" { + println!("Commands:"); + println!("\thelp -> this help text"); + println!("\t: -> change volume/partition"); + println!("\tdir -> do a directory listing"); + println!("\tstat -> print volume manager status"); + println!("\tcat -> print a text file"); + println!("\thexdump -> print a binary file"); + println!("\tcd .. -> go up a level"); + println!("\tcd -> change into "); + println!("\tquit -> exits the program"); + } else if line == "0:" { + self.current_volume = 0; + } else if line == "1:" { + self.current_volume = 1; + } else if line == "2:" { + self.current_volume = 2; + } else if line == "3:" { + self.current_volume = 3; + } else if line == "stat" { + println!("Status:\n{:#?}", self.volume_mgr); + } else if line == "dir" { + let Some(s) = &self.volumes[self.current_volume] else { + println!("That volume isn't available"); + return Ok(()); + }; + self.volume_mgr.iterate_dir(s.directory, |entry| { + println!( + "{:12} {:9} {} {:?}", + entry.name, entry.size, entry.mtime, entry.attributes + ); + })?; + } else if let Some(arg) = line.strip_prefix("cd ") { + let Some(s) = &mut self.volumes[self.current_volume] else { + println!("This volume isn't available"); + return Ok(()); + }; + let d = self.volume_mgr.open_dir(s.directory, arg)?; + self.volume_mgr.close_dir(s.directory)?; + s.directory = d; + if arg == ".." { + s.path.pop(); + } else { + s.path.push(arg.to_owned()); + } + } else if let Some(arg) = line.strip_prefix("cat ") { + let Some(s) = &mut self.volumes[self.current_volume] else { + println!("This volume isn't available"); + return Ok(()); + }; + let f = self.volume_mgr.open_file_in_dir( + s.directory, + arg, + embedded_sdmmc::Mode::ReadOnly, + )?; + let mut inner = || -> Result<(), Error> { + let mut data = Vec::new(); + while !self.volume_mgr.file_eof(f)? { + let mut buffer = vec![0u8; 65536]; + let n = self.volume_mgr.read(f, &mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + if let Ok(s) = std::str::from_utf8(&data) { + println!("{}", s); + } else { + println!("I'm afraid that file isn't UTF-8 encoded"); + } + Ok(()) + }; + let r = inner(); + self.volume_mgr.close_file(f)?; + r?; + } else if let Some(arg) = line.strip_prefix("hexdump ") { + let Some(s) = &mut self.volumes[self.current_volume] else { + println!("This volume isn't available"); + return Ok(()); + }; + let f = self.volume_mgr.open_file_in_dir( + s.directory, + arg, + embedded_sdmmc::Mode::ReadOnly, + )?; + let mut inner = || -> Result<(), Error> { + let mut data = Vec::new(); + while !self.volume_mgr.file_eof(f)? { + let mut buffer = vec![0u8; 65536]; + let n = self.volume_mgr.read(f, &mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + for (idx, chunk) in data.chunks(16).enumerate() { + print!("{:08x} | ", idx * 16); + for b in chunk { + print!("{:02x} ", b); + } + for _padding in 0..(16 - chunk.len()) { + print!(" "); + } + print!("| "); + for b in chunk { + print!( + "{}", + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + ); + } + println!(); + } + Ok(()) + }; + let r = inner(); + self.volume_mgr.close_file(f)?; + r?; + } else { + println!("Unknown command {line:?} - try 'help' for help"); + } + Ok(()) + } +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + println!("Opening '{filename}'..."); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let stdin = std::io::stdin(); + + let mut ctx = Context { + volume_mgr: VolumeManager::new_with_limits(lbd, Clock, 100), + volumes: [None, None, None, None], + current_volume: 0, + }; + + let mut current_volume = None; + for volume_no in 0..4 { + match ctx.volume_mgr.open_volume(VolumeIdx(volume_no)) { + Ok(volume) => { + println!("Volume # {}: found", volume_no,); + match ctx.volume_mgr.open_root_dir(volume) { + Ok(root_dir) => { + ctx.volumes[volume_no] = Some(VolumeState { + directory: root_dir, + volume, + path: vec![], + }); + if current_volume.is_none() { + current_volume = Some(volume_no); + } + } + Err(e) => { + println!("Failed to open root directory: {e:?}"); + ctx.volume_mgr.close_volume(volume).expect("close volume"); + } + } + } + Err(e) => { + println!("Failed to open volume {volume_no}: {e:?}"); + } + } + } + + match current_volume { + Some(n) => { + // Default to the first valid partition + ctx.current_volume = n; + } + None => { + println!("No volumes found in file. Sorry."); + return Ok(()); + } + }; + + loop { + print!("{}:/", ctx.current_volume); + print!("{}", ctx.current_path().join("/")); + print!("> "); + std::io::stdout().flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + if line == "quit" { + break; + } else if let Err(e) = ctx.process_line(line) { + println!("Error: {:?}", e); + } + } + + for (idx, s) in ctx.volumes.into_iter().enumerate() { + if let Some(state) = s { + println!("Closing current dir for {idx}..."); + let r = ctx.volume_mgr.close_dir(state.directory); + if let Err(e) = r { + println!("Error closing directory: {e:?}"); + } + println!("Unmounting {idx}..."); + let r = ctx.volume_mgr.close_volume(state.volume); + if let Err(e) = r { + println!("Error closing volume: {e:?}"); + } + } + } + + println!("Bye!"); + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/test_mount.rs b/examples/test_mount.rs deleted file mode 100644 index f0eddf4..0000000 --- a/examples/test_mount.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! # Tests the Embedded SDMMC Library -//! -//! This example should be given a file or block device as the first and only -//! argument. It will attempt to mount all four possible primary MBR -//! partitions, one at a time, prints the root directory and will print a file -//! called "README.TXT". It will then list the contents of the "TEST" -//! sub-directory. -//! -//! ```bash -//! $ cargo run --example test_mount -- /dev/mmcblk0 -//! $ cargo run --example test_mount -- /dev/sda -//! ``` -//! -//! 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. -//! -//! ```bash -//! zcat ./disk.img.gz > ./disk.img -//! $ cargo run --example test_mount -- ./disk.img -//! ``` - -extern crate embedded_sdmmc; - -const FILE_TO_PRINT: &'static str = "README.TXT"; -const FILE_TO_CHECKSUM: &'static str = "64MB.DAT"; - -use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, - VolumeManager, -}; -use std::cell::RefCell; -use std::fs::File; -use std::io::prelude::*; -use std::io::SeekFrom; -use std::path::Path; - -#[derive(Debug)] -struct LinuxBlockDevice { - file: RefCell, - print_blocks: bool, -} - -impl LinuxBlockDevice { - fn new

(device_name: P, print_blocks: bool) -> Result - where - P: AsRef, - { - Ok(LinuxBlockDevice { - file: RefCell::new(File::open(device_name)?), - print_blocks, - }) - } -} - -impl BlockDevice for LinuxBlockDevice { - type Error = std::io::Error; - - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> 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 - ); - } - } - Ok(()) - } - - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { - self.file - .borrow_mut() - .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; - for block in blocks.iter() { - self.file.borrow_mut().write_all(&block.contents)?; - if self.print_blocks { - println!("Wrote: {:?}", &block); - } - } - Ok(()) - } - - fn num_blocks(&self) -> Result { - let num_blocks = self.file.borrow().metadata().unwrap().len() / 512; - Ok(BlockCount(num_blocks as u32)) - } -} - -struct Clock; - -impl TimeSource for Clock { - fn get_timestamp(&self) -> Timestamp { - Timestamp { - year_since_1970: 0, - zero_indexed_month: 0, - zero_indexed_day: 0, - hours: 0, - minutes: 0, - seconds: 0, - } - } -} - -fn main() { - env_logger::init(); - let mut args = std::env::args().skip(1); - let filename = args.next().unwrap_or("/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) - .unwrap(); - println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); - for i in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(i)); - println!("volume {}: {:#?}", i, volume); - if let Ok(mut volume) = volume { - let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(&volume, &root_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - println!("\tFinding {}...", FILE_TO_PRINT); - println!( - "\tFound {}?: {:?}", - FILE_TO_PRINT, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_PRINT) - ); - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_PRINT, Mode::ReadOnly) - .unwrap(); - println!("FILE STARTS:"); - while !f.eof() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - } - } - println!("EOF"); - // Can't open file twice - assert!(volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_PRINT, Mode::ReadOnly) - .is_err()); - volume_mgr.close_file(&volume, f).unwrap(); - - let test_dir = volume_mgr.open_dir(&volume, &root_dir, "TEST").unwrap(); - // Check we can't open it twice - assert!(volume_mgr.open_dir(&volume, &root_dir, "TEST").is_err()); - // Print the contents - println!("\tListing TEST directory:"); - volume_mgr - .iterate_dir(&volume, &test_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - volume_mgr.close_dir(&volume, test_dir); - - // Checksum example file. We just sum the bytes, as a quick and dirty checksum. - // We also read in a weird block size, just to exercise the offset calculation code. - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_CHECKSUM, Mode::ReadOnly) - .unwrap(); - println!("Checksuming {} bytes of {}", f.length(), FILE_TO_CHECKSUM); - let mut csum = 0u32; - while !f.eof() { - let mut buffer = [0u8; 2047]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - csum += u32::from(*b); - } - } - println!("Checksum over {} bytes: {}", f.length(), csum); - volume_mgr.close_file(&volume, f).unwrap(); - - assert!(volume_mgr.open_root_dir(&volume).is_err()); - volume_mgr.close_dir(&volume, root_dir); - assert!(volume_mgr.open_root_dir(&volume).is_ok()); - } - } -} diff --git a/examples/write_test.rs b/examples/write_test.rs deleted file mode 100644 index 47acfec..0000000 --- a/examples/write_test.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! # Tests the Embedded SDMMC Library -//! ```bash -//! $ cargo run --example write_test -- /dev/mmcblk0 -//! $ cargo run --example write_test -- /dev/sda -//! ``` -//! -//! 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! -//! -//! ```bash -//! gunzip -kf ./disk.img.gz -//! $ cargo run --example write_test -- ./disk.img -//! ``` - -extern crate embedded_sdmmc; - -const FILE_TO_WRITE: &str = "README.TXT"; - -use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, - VolumeManager, -}; -use std::cell::RefCell; -use std::fs::{File, OpenOptions}; -use std::io::prelude::*; -use std::io::SeekFrom; -use std::path::Path; - -#[derive(Debug)] -struct LinuxBlockDevice { - file: RefCell, - print_blocks: bool, -} - -impl LinuxBlockDevice { - fn new

(device_name: P, print_blocks: bool) -> Result - where - P: AsRef, - { - Ok(LinuxBlockDevice { - file: RefCell::new( - OpenOptions::new() - .read(true) - .write(true) - .open(device_name)?, - ), - print_blocks, - }) - } -} - -impl BlockDevice for LinuxBlockDevice { - type Error = std::io::Error; - - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> 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 - ); - } - } - Ok(()) - } - - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { - self.file - .borrow_mut() - .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; - for block in blocks.iter() { - self.file.borrow_mut().write_all(&block.contents)?; - if self.print_blocks { - println!("Wrote: {:?}", &block); - } - } - Ok(()) - } - - fn num_blocks(&self) -> Result { - let num_blocks = self.file.borrow().metadata().unwrap().len() / 512; - Ok(BlockCount(num_blocks as u32)) - } -} - -struct Clock; - -impl TimeSource for Clock { - fn get_timestamp(&self) -> Timestamp { - Timestamp { - year_since_1970: 0, - zero_indexed_month: 0, - zero_indexed_day: 0, - hours: 0, - minutes: 0, - seconds: 0, - } - } -} - -fn main() { - env_logger::init(); - let mut args = std::env::args().skip(1); - let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); - println!("Opening {:?}", filename); - let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); - let lbd = LinuxBlockDevice::new(filename, print_blocks) - .map_err(Error::DeviceError) - .unwrap(); - println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); - for volume_idx in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); - println!("volume {}: {:#?}", volume_idx, volume); - if let Ok(mut volume) = volume { - let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(&volume, &root_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - - // This will panic if the file doesn't exist, use ReadWriteCreateOrTruncate or - // ReadWriteCreateOrAppend instead. ReadWriteCreate also creates a file, but it returns an - // error if the file already exists - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadOnly) - .unwrap(); - println!("\nReading from file {}\n", FILE_TO_WRITE); - println!("FILE STARTS:"); - while !f.eof() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - } - } - println!("EOF\n"); - volume_mgr.close_file(&volume, f).unwrap(); - - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) - .unwrap(); - - let buffer1 = b"\nFile Appended\n"; - let buffer = [b'a'; 8192]; - println!("\nAppending to file"); - let num_written1 = volume_mgr.write(&mut volume, &mut f, &buffer1[..]).unwrap(); - let num_written = volume_mgr.write(&mut volume, &mut f, &buffer[..]).unwrap(); - println!("Number of bytes written: {}\n", num_written + num_written1); - - f.seek_from_start(0).unwrap(); - println!("\tFinding {}...", FILE_TO_WRITE); - println!( - "\tFound {}?: {:?}", - FILE_TO_WRITE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_WRITE) - ); - println!("\nFILE STARTS:"); - while !f.eof() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - } - } - println!("EOF"); - volume_mgr.close_file(&volume, f).unwrap(); - - println!("\nTruncating file"); - let mut f = volume_mgr - .open_file_in_dir( - &mut volume, - &root_dir, - FILE_TO_WRITE, - Mode::ReadWriteTruncate, - ) - .unwrap(); - - let buffer = b"Hello\n"; - let num_written = volume_mgr.write(&mut volume, &mut f, &buffer[..]).unwrap(); - println!("\nNumber of bytes written: {}\n", num_written); - - println!("\tFinding {}...", FILE_TO_WRITE); - println!( - "\tFound {}?: {:?}", - FILE_TO_WRITE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_WRITE) - ); - f.seek_from_start(0).unwrap(); - println!("\nFILE STARTS:"); - while !f.eof() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - } - } - println!("EOF"); - volume_mgr.close_file(&volume, f).unwrap(); - } - } -} diff --git a/src/blockdevice.rs b/src/blockdevice.rs index b52b552..76bd235 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -177,6 +177,24 @@ impl BlockIdx { } impl BlockCount { + /// How many blocks are required to hold this many bytes. + /// + /// ``` + /// # use embedded_sdmmc::BlockCount; + /// assert_eq!(BlockCount::from_bytes(511), BlockCount(1)); + /// assert_eq!(BlockCount::from_bytes(512), BlockCount(1)); + /// assert_eq!(BlockCount::from_bytes(513), BlockCount(2)); + /// assert_eq!(BlockCount::from_bytes(1024), BlockCount(2)); + /// assert_eq!(BlockCount::from_bytes(1025), BlockCount(3)); + /// ``` + pub const fn from_bytes(byte_count: u32) -> BlockCount { + let mut count = byte_count / Block::LEN_U32; + if (count * Block::LEN_U32) != byte_count { + count += 1; + } + BlockCount(count) + } + /// Take a number of blocks and increment by the integer number of blocks /// required to get to the block that holds the byte at the given offset. pub fn offset_bytes(self, offset: u32) -> Self { @@ -187,7 +205,7 @@ impl BlockCount { impl BlockIter { /// Create a new `BlockIter`, from the given start block, through (and /// including) the given end block. - pub fn new(start: BlockIdx, inclusive_end: BlockIdx) -> BlockIter { + pub const fn new(start: BlockIdx, inclusive_end: BlockIdx) -> BlockIter { BlockIter { inclusive_end, current: start, diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index 0cc7be3..cc77900 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -1,7 +1,7 @@ //! Boot Parameter Block use crate::{ - blockdevice::{Block, BlockCount}, + blockdevice::BlockCount, fat::{FatType, OnDiskDirEntry}, }; use byteorder::{ByteOrder, LittleEndian}; @@ -29,13 +29,12 @@ impl<'a> Bpb<'a> { return Err("Bad BPB footer"); } - let root_dir_blocks = ((u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32) - + (Block::LEN_U32 - 1)) - / Block::LEN_U32; - let data_blocks = bpb.total_blocks() - - (u32::from(bpb.reserved_block_count()) - + (u32::from(bpb.num_fats()) * bpb.fat_size()) - + root_dir_blocks); + let root_dir_blocks = + BlockCount::from_bytes(u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32).0; + let non_data_blocks = u32::from(bpb.reserved_block_count()) + + (u32::from(bpb.num_fats()) * bpb.fat_size()) + + root_dir_blocks; + let data_blocks = bpb.total_blocks() - non_data_blocks; bpb.cluster_count = data_blocks / u32::from(bpb.blocks_per_cluster()); if bpb.cluster_count < 4085 { return Err("FAT12 is unsupported"); @@ -132,3 +131,9 @@ impl<'a> Bpb<'a> { self.cluster_count } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/info.rs b/src/fat/info.rs index 1f2a623..f9f8e2c 100644 --- a/src/fat/info.rs +++ b/src/fat/info.rs @@ -1,9 +1,9 @@ -use crate::{BlockCount, BlockIdx, Cluster}; +use crate::{BlockCount, BlockIdx, ClusterId}; use byteorder::{ByteOrder, LittleEndian}; /// Indentifies the supported types of FAT format #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum FatSpecificInfo { /// Fat16 Format Fat16(Fat16Info), @@ -13,18 +13,18 @@ pub enum FatSpecificInfo { /// FAT32 specific data #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Fat32Info { /// The root directory does not have a reserved area in FAT32. This is the /// cluster it starts in (nominally 2). - pub(crate) first_root_dir_cluster: Cluster, + pub(crate) first_root_dir_cluster: ClusterId, /// Block idx of the info sector pub(crate) info_location: BlockIdx, } /// FAT16 specific data #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Fat16Info { /// The block the root directory starts in. Relative to start of partition /// (so add `self.lba_offset` before passing to volume manager) @@ -78,11 +78,17 @@ impl<'a> InfoSector<'a> { } /// Return the number of the next free cluster, if known. - pub fn next_free_cluster(&self) -> Option { + pub fn next_free_cluster(&self) -> Option { match self.next_free() { // 0 and 1 are reserved clusters 0xFFFF_FFFF | 0 | 1 => None, - n => Some(Cluster(n)), + n => Some(ClusterId(n)), } } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 6b74628..35641cb 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -1,4 +1,4 @@ -//! embedded-sdmmc-rs - FAT16/FAT32 file system implementation +//! FAT16/FAT32 file system implementation //! //! Implements the File Allocation Table file system. Supports FAT16 and FAT32 volumes. @@ -14,6 +14,36 @@ 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; @@ -24,11 +54,19 @@ 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 +// +// **************************************************************************** + #[cfg(test)] mod test { use super::*; - use crate::{Attributes, BlockIdx, Cluster, DirEntry, ShortFileName, Timestamp}; + use crate::{Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; fn parse(input: &str) -> Vec { let mut output = Vec::new(); @@ -105,7 +143,7 @@ mod test { mtime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), ctime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), attributes: Attributes::create_from_fat(Attributes::VOLUME), - cluster: Cluster(0), + cluster: ClusterId(0), size: 0, entry_block: BlockIdx(0), entry_offset: 0, @@ -123,7 +161,7 @@ mod test { mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), attributes: Attributes::create_from_fat(Attributes::DIRECTORY), - cluster: Cluster(3), + cluster: ClusterId(3), size: 0, entry_block: BlockIdx(0), entry_offset: 0, @@ -148,7 +186,7 @@ mod test { mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), attributes: Attributes::create_from_fat(Attributes::ARCHIVE), - cluster: Cluster(9), + cluster: ClusterId(9), size: 11120, entry_block: BlockIdx(0), entry_offset: 0, @@ -165,7 +203,7 @@ mod test { mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), attributes: Attributes::create_from_fat(Attributes::ARCHIVE), - cluster: Cluster(5), + cluster: ClusterId(5), size: 18693, entry_block: BlockIdx(0), entry_offset: 0, @@ -190,7 +228,7 @@ mod test { mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), attributes: Attributes::create_from_fat(Attributes::ARCHIVE), - cluster: Cluster(8), + cluster: ClusterId(8), size: 1494, entry_block: BlockIdx(0), entry_offset: 0, @@ -215,7 +253,7 @@ mod test { mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), attributes: Attributes::create_from_fat(Attributes::ARCHIVE), - cluster: Cluster(15), + cluster: ClusterId(15), size: 12108, entry_block: BlockIdx(0), entry_offset: 0, @@ -317,3 +355,9 @@ mod test { assert_eq!(bpb.fat_type, FatType::Fat16); } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/ondiskdirentry.rs b/src/fat/ondiskdirentry.rs index 5410a80..14be74c 100644 --- a/src/fat/ondiskdirentry.rs +++ b/src/fat/ondiskdirentry.rs @@ -1,6 +1,6 @@ //! Directory Entry as stored on-disk -use crate::{fat::FatType, Attributes, BlockIdx, Cluster, DirEntry, ShortFileName, Timestamp}; +use crate::{fat::FatType, Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; use byteorder::{ByteOrder, LittleEndian}; /// Represents a 32-byte directory entry as stored on-disk in a directory file. @@ -129,16 +129,16 @@ impl<'a> OnDiskDirEntry<'a> { } /// Which cluster, if any, does this file start at? Assumes this is from a FAT32 volume. - pub fn first_cluster_fat32(&self) -> Cluster { + pub fn first_cluster_fat32(&self) -> ClusterId { let cluster_no = (u32::from(self.first_cluster_hi()) << 16) | u32::from(self.first_cluster_lo()); - Cluster(cluster_no) + ClusterId(cluster_no) } /// Which cluster, if any, does this file start at? Assumes this is from a FAT16 volume. - fn first_cluster_fat16(&self) -> Cluster { + fn first_cluster_fat16(&self) -> ClusterId { let cluster_no = u32::from(self.first_cluster_lo()); - Cluster(cluster_no) + ClusterId(cluster_no) } /// Convert the on-disk format into a DirEntry @@ -148,17 +148,26 @@ impl<'a> OnDiskDirEntry<'a> { entry_block: BlockIdx, entry_offset: u32, ) -> DirEntry { + let attributes = Attributes::create_from_fat(self.raw_attr()); let mut result = DirEntry { name: ShortFileName { contents: [0u8; 11], }, mtime: Timestamp::from_fat(self.write_date(), self.write_time()), ctime: Timestamp::from_fat(self.create_date(), self.create_time()), - attributes: Attributes::create_from_fat(self.raw_attr()), - cluster: if fat_type == FatType::Fat32 { - self.first_cluster_fat32() - } else { - self.first_cluster_fat16() + attributes, + cluster: { + let cluster = if fat_type == FatType::Fat32 { + self.first_cluster_fat32() + } else { + self.first_cluster_fat16() + }; + if cluster == ClusterId::EMPTY && attributes.is_directory() { + // FAT16/FAT32 uses a cluster ID of `0` in the ".." entry to mean 'root directory' + ClusterId::ROOT_DIR + } else { + cluster + } }, size: self.file_size(), entry_block, @@ -168,3 +177,9 @@ impl<'a> OnDiskDirEntry<'a> { result } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 3afa5a8..c946ec8 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1,4 +1,4 @@ -//! FAT volume +//! FAT-specific volume support. use crate::{ debug, @@ -6,15 +6,17 @@ use crate::{ Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, RESERVED_ENTRIES, }, - trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, DirEntry, - Directory, Error, ShortFileName, TimeSource, VolumeManager, VolumeType, + trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, ClusterId, DirEntry, + DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; +use super::BlockCache; + /// The name given to a particular FAT formatted volume. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct VolumeName { data: [u8; 11], } @@ -35,9 +37,9 @@ impl core::fmt::Debug for VolumeName { } } -/// Identifies a FAT16 Volume on the disk. +/// Identifies a FAT16 or FAT32 Volume on the disk. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(PartialEq, Eq, Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct FatVolume { /// The block number of the start of the partition. All other BlockIdx values are relative to this. pub(crate) lba_start: BlockIdx, @@ -56,7 +58,7 @@ pub struct FatVolume { /// Expected number of free clusters pub(crate) free_clusters_count: Option, /// Number of the next expected free cluster - pub(crate) next_free_cluster: Option, + pub(crate) next_free_cluster: Option, /// Total number of clusters pub(crate) cluster_count: u32, /// Type of FAT @@ -65,23 +67,20 @@ pub struct FatVolume { impl FatVolume { /// Write a new entry in the FAT - pub fn update_info_sector( - &mut self, - volume_mgr: &mut VolumeManager, - ) -> Result<(), Error> + pub fn update_info_sector(&mut self, block_device: &D) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { match &self.fat_specific_info { - FatSpecificInfo::Fat16(_) => {} + FatSpecificInfo::Fat16(_) => { + // FAT16 volumes don't have an info sector + } FatSpecificInfo::Fat32(fat32_info) => { if self.free_clusters_count.is_none() && self.next_free_cluster.is_none() { return Ok(()); } let mut blocks = [Block::new()]; - volume_mgr - .block_device + block_device .read(&mut blocks, fat32_info.info_location, "read_info_sector") .map_err(Error::DeviceError)?; let block = &mut blocks[0]; @@ -91,8 +90,7 @@ impl FatVolume { if let Some(next_free_cluster) = self.next_free_cluster { block[492..496].copy_from_slice(&next_free_cluster.0.to_le_bytes()); } - volume_mgr - .block_device + block_device .write(&blocks, fat32_info.info_location) .map_err(Error::DeviceError)?; } @@ -109,15 +107,14 @@ impl FatVolume { } /// Write a new entry in the FAT - fn update_fat( + fn update_fat( &mut self, - volume_mgr: &mut VolumeManager, - cluster: Cluster, - new_value: Cluster, + block_device: &D, + cluster: ClusterId, + new_value: ClusterId, ) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { let mut blocks = [Block::new()]; let this_fat_block_num; @@ -126,15 +123,15 @@ impl FatVolume { let fat_offset = cluster.0 * 2; 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; - volume_mgr - .block_device + block_device .read(&mut blocks, this_fat_block_num, "read_fat") .map_err(Error::DeviceError)?; + // See let entry = match new_value { - Cluster::INVALID => 0xFFF6, - Cluster::BAD => 0xFFF7, - Cluster::EMPTY => 0x0000, - Cluster::END_OF_FILE => 0xFFFF, + ClusterId::INVALID => 0xFFF6, + ClusterId::BAD => 0xFFF7, + ClusterId::EMPTY => 0x0000, + ClusterId::END_OF_FILE => 0xFFFF, _ => new_value.0 as u16, }; LittleEndian::write_u16( @@ -147,14 +144,13 @@ impl FatVolume { let fat_offset = cluster.0 * 4; 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; - volume_mgr - .block_device + block_device .read(&mut blocks, this_fat_block_num, "read_fat") .map_err(Error::DeviceError)?; let entry = match new_value { - Cluster::INVALID => 0x0FFF_FFF6, - Cluster::BAD => 0x0FFF_FFF7, - Cluster::EMPTY => 0x0000_0000, + ClusterId::INVALID => 0x0FFF_FFF6, + ClusterId::BAD => 0x0FFF_FFF7, + ClusterId::EMPTY => 0x0000_0000, _ => new_value.0, }; let existing = LittleEndian::read_u32( @@ -167,36 +163,31 @@ impl FatVolume { ); } } - volume_mgr - .block_device + block_device .write(&blocks, this_fat_block_num) .map_err(Error::DeviceError)?; Ok(()) } /// Look in the FAT to see which cluster comes next. - pub(crate) fn next_cluster( + pub(crate) fn next_cluster( &self, - volume_mgr: &VolumeManager, - cluster: Cluster, - ) -> Result> + block_device: &D, + cluster: ClusterId, + fat_block_cache: &mut BlockCache, + ) -> Result> where D: BlockDevice, - T: TimeSource, { - let mut blocks = [Block::new()]; match &self.fat_specific_info { FatSpecificInfo::Fat16(_fat16_info) => { let fat_offset = cluster.0 * 2; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - volume_mgr - .block_device - .read(&mut blocks, this_fat_block_num, "next_cluster") - .map_err(Error::DeviceError)?; - let fat_entry = LittleEndian::read_u16( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 1], - ); + let block = + fat_block_cache.read(block_device, this_fat_block_num, "next_cluster")?; + let fat_entry = + LittleEndian::read_u16(&block[this_fat_ent_offset..=this_fat_ent_offset + 1]); match fat_entry { 0xFFF7 => { // Bad cluster @@ -208,7 +199,7 @@ impl FatVolume { } f => { // Seems legit - Ok(Cluster(u32::from(f))) + Ok(ClusterId(u32::from(f))) } } } @@ -216,17 +207,15 @@ 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; - volume_mgr - .block_device - .read(&mut blocks, this_fat_block_num, "next_cluster") - .map_err(Error::DeviceError)?; - let fat_entry = LittleEndian::read_u32( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], - ) & 0x0FFF_FFFF; + let block = + fat_block_cache.read(block_device, this_fat_block_num, "next_cluster")?; + let fat_entry = + LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) + & 0x0FFF_FFFF; match fat_entry { 0x0000_0000 => { // Jumped to free space - Err(Error::JumpedFree) + Err(Error::UnterminatedFatChain) } 0x0FFF_FFF7 => { // Bad cluster @@ -238,7 +227,7 @@ impl FatVolume { } f => { // Seems legit - Ok(Cluster(f)) + Ok(ClusterId(f)) } } } @@ -253,12 +242,12 @@ impl FatVolume { /// Converts a cluster number (or `Cluster`) to a block number (or /// `BlockIdx`). Gives an absolute `BlockIdx` you can pass to the /// volume manager. - pub(crate) fn cluster_to_block(&self, cluster: Cluster) -> BlockIdx { + pub(crate) fn cluster_to_block(&self, cluster: ClusterId) -> BlockIdx { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { let block_num = match cluster { - Cluster::ROOT_DIR => fat16_info.first_root_dir_block, - Cluster(c) => { + ClusterId::ROOT_DIR => fat16_info.first_root_dir_block, + ClusterId(c) => { // FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector; let first_block_of_cluster = BlockCount((c - 2) * u32::from(self.blocks_per_cluster)); @@ -269,7 +258,7 @@ impl FatVolume { } FatSpecificInfo::Fat32(fat32_info) => { let cluster_num = match cluster { - Cluster::ROOT_DIR => fat32_info.first_root_dir_cluster.0, + ClusterId::ROOT_DIR => fat32_info.first_root_dir_cluster.0, c => c.0, }; // FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector; @@ -282,10 +271,11 @@ impl FatVolume { /// Finds a empty entry space and writes the new entry to it, allocates a new cluster if it's /// needed - pub(crate) fn write_new_directory_entry( + pub(crate) fn write_new_directory_entry( &mut self, - volume_mgr: &mut VolumeManager, - dir: &Directory, + block_device: &D, + time_source: &T, + dir: &DirectoryInfo, name: ShortFileName, attributes: Attributes, ) -> Result> @@ -295,64 +285,72 @@ impl FatVolume { { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir.cluster); let mut first_dir_block_num = match dir.cluster { - Cluster::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, _ => self.cluster_to_block(dir.cluster), }; - let mut current_cluster = Some(dir.cluster); - let mut blocks = [Block::new()]; - let dir_size = match dir.cluster { - Cluster::ROOT_DIR => BlockCount( - ((u32::from(fat16_info.root_entries_count) * 32) + (Block::LEN as u32 - 1)) - / Block::LEN as u32, - ), + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } _ => BlockCount(u32::from(self.blocks_per_cluster)), }; + + // Walk the directory + let mut blocks = [Block::new()]; while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - volume_mgr - .block_device + block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { + 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]); // 0x00 or 0xE5 represents a free entry if !dir_entry.is_valid() { - let ctime = volume_mgr.timesource.get_timestamp(); + let ctime = time_source.get_timestamp(); let entry = DirEntry::new( name, attributes, - Cluster(0), + ClusterId::EMPTY, ctime, block, start as u32, ); blocks[0][start..start + 32] .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); - volume_mgr - .block_device + block_device .write(&blocks, block) .map_err(Error::DeviceError)?; return Ok(entry); } } } - if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - Err(Error::EndOfFile) => { - let c = self.alloc_cluster(volume_mgr, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + 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, + }; } else { current_cluster = None; } @@ -360,8 +358,10 @@ impl FatVolume { Err(Error::NotEnoughSpace) } FatSpecificInfo::Fat32(fat32_info) => { + // All directories on FAT32 have a cluster chain but the root + // dir starts in a specified cluster. let mut first_dir_block_num = match dir.cluster { - Cluster::ROOT_DIR => self.cluster_to_block(fat32_info.first_root_dir_cluster), + ClusterId::ROOT_DIR => self.cluster_to_block(fat32_info.first_root_dir_cluster), _ => self.cluster_to_block(dir.cluster), }; let mut current_cluster = Some(dir.cluster); @@ -370,8 +370,7 @@ impl FatVolume { let dir_size = BlockCount(u32::from(self.blocks_per_cluster)); while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - volume_mgr - .block_device + block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; for entry in 0..Block::LEN / OnDiskDirEntry::LEN { @@ -380,37 +379,38 @@ impl FatVolume { let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); // 0x00 or 0xE5 represents a free entry if !dir_entry.is_valid() { - let ctime = volume_mgr.timesource.get_timestamp(); + let ctime = time_source.get_timestamp(); let entry = DirEntry::new( name, attributes, - Cluster(0), + ClusterId(0), ctime, block, start as u32, ); blocks[0][start..start + 32] .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); - volume_mgr - .block_device + block_device .write(&blocks, block) .map_err(Error::DeviceError)?; return Ok(entry); } } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - Err(Error::EndOfFile) => { - let c = self.alloc_cluster(volume_mgr, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + 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, + }; } Err(Error::NotEnoughSpace) } @@ -419,61 +419,64 @@ impl FatVolume { /// Calls callback `func` with every valid entry in the given directory. /// Useful for performing directory listings. - pub(crate) fn iterate_dir( + pub(crate) fn iterate_dir( &self, - volume_mgr: &VolumeManager, - dir: &Directory, + block_device: &D, + dir: &DirectoryInfo, mut func: F, ) -> Result<(), Error> where F: FnMut(&DirEntry), D: BlockDevice, - T: TimeSource, { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir.cluster); let mut first_dir_block_num = match dir.cluster { - Cluster::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, _ => self.cluster_to_block(dir.cluster), }; - let mut current_cluster = Some(dir.cluster); let dir_size = match dir.cluster { - Cluster::ROOT_DIR => BlockCount( - ((u32::from(fat16_info.root_entries_count) * 32) + (Block::LEN as u32 - 1)) - / Block::LEN as u32, - ), + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } _ => BlockCount(u32::from(self.blocks_per_cluster)), }; - let mut blocks = [Block::new()]; + + let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { - for block in first_dir_block_num.range(dir_size) { - volume_mgr - .block_device - .read(&mut blocks, block, "read_dir") - .map_err(Error::DeviceError)?; + 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(&blocks[0][start..end]); + let dir_entry = OnDiskDirEntry::new(&block[start..end]); if dir_entry.is_end() { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { // Safe, since Block::LEN always fits on a u32 let start = u32::try_from(start).unwrap(); - let entry = dir_entry.get_entry(FatType::Fat16, block, start); + let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); func(&entry); } } } - if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + 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, + }; } else { current_cluster = None; } @@ -481,16 +484,18 @@ impl FatVolume { Ok(()) } FatSpecificInfo::Fat32(fat32_info) => { + // All directories on FAT32 have a cluster chain but the root + // dir starts in a specified cluster. let mut current_cluster = match dir.cluster { - Cluster::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), _ => Some(dir.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))) { - volume_mgr - .block_device + block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; for entry in 0..Block::LEN / OnDiskDirEntry::LEN { @@ -508,10 +513,11 @@ impl FatVolume { } } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => Some(n), - _ => None, - }; + current_cluster = + match self.next_cluster(block_device, cluster, &mut block_cache) { + Ok(n) => Some(n), + _ => None, + }; } Ok(()) } @@ -519,52 +525,57 @@ impl FatVolume { } /// Get an entry from the given directory - pub(crate) fn find_directory_entry( + pub(crate) fn find_directory_entry( &self, - volume_mgr: &mut VolumeManager, - dir: &Directory, - name: &str, + block_device: &D, + dir: &DirectoryInfo, + match_name: &ShortFileName, ) -> Result> where D: BlockDevice, - T: TimeSource, { - let match_name = ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. let mut current_cluster = Some(dir.cluster); let mut first_dir_block_num = match dir.cluster { - Cluster::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, _ => self.cluster_to_block(dir.cluster), }; let dir_size = match dir.cluster { - Cluster::ROOT_DIR => BlockCount( - ((u32::from(fat16_info.root_entries_count) * 32) + (Block::LEN as u32 - 1)) - / Block::LEN as u32, - ), + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } _ => BlockCount(u32::from(self.blocks_per_cluster)), }; + 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( - volume_mgr, + block_device, FatType::Fat16, - &match_name, + match_name, block, ) { - Err(Error::NotInBlock) => continue, + Err(Error::FileNotFound) => continue, x => return x, } } - if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + 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, + }; } else { current_cluster = None; } @@ -573,47 +584,47 @@ impl FatVolume { } FatSpecificInfo::Fat32(fat32_info) => { let mut current_cluster = match dir.cluster { - Cluster::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), _ => Some(dir.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( - volume_mgr, + block_device, FatType::Fat32, - &match_name, + match_name, block, ) { - Err(Error::NotInBlock) => continue, + Err(Error::FileNotFound) => continue, x => return x, } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => Some(n), - _ => None, - } + current_cluster = + match self.next_cluster(block_device, cluster, &mut block_cache) { + Ok(n) => Some(n), + _ => None, + } } Err(Error::FileNotFound) } } } - /// Finds an entry in a given block - fn find_entry_in_block( + /// Finds an entry in a given block of directory entries. + fn find_entry_in_block( &self, - volume_mgr: &mut VolumeManager, + block_device: &D, fat_type: FatType, match_name: &ShortFileName, block: BlockIdx, ) -> Result> where D: BlockDevice, - T: TimeSource, { let mut blocks = [Block::new()]; - volume_mgr - .block_device + block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; for entry in 0..Block::LEN / OnDiskDirEntry::LEN { @@ -622,7 +633,7 @@ impl FatVolume { let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); if dir_entry.is_end() { // Can quit early - return Err(Error::FileNotFound); + break; } else if dir_entry.matches(match_name) { // Found it // Safe, since Block::LEN always fits on a u32 @@ -630,94 +641,126 @@ impl FatVolume { return Ok(dir_entry.get_entry(fat_type, block, start)); } } - Err(Error::NotInBlock) + Err(Error::FileNotFound) } /// Delete an entry from the given directory - pub(crate) fn delete_directory_entry( + pub(crate) fn delete_directory_entry( &self, - volume_mgr: &mut VolumeManager, - dir: &Directory, - name: &str, + block_device: &D, + dir: &DirectoryInfo, + match_name: &ShortFileName, ) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { - let match_name = ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. let mut current_cluster = Some(dir.cluster); let mut first_dir_block_num = match dir.cluster { - Cluster::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, _ => self.cluster_to_block(dir.cluster), }; let dir_size = match dir.cluster { - Cluster::ROOT_DIR => BlockCount( - ((u32::from(fat16_info.root_entries_count) * 32) + (Block::LEN as u32 - 1)) - / Block::LEN as u32, - ), + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } _ => BlockCount(u32::from(self.blocks_per_cluster)), }; + // Walk the directory while let Some(cluster) = current_cluster { + // Scan the cluster / root dir a block at a time for block in first_dir_block_num.range(dir_size) { - match self.delete_entry_in_block(volume_mgr, &match_name, block) { - Err(Error::NotInBlock) => continue, - x => return x, + match self.delete_entry_in_block(block_device, match_name, block) { + Err(Error::FileNotFound) => { + // Carry on + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } } } - if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + // 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, + }; } else { current_cluster = None; } } - Err(Error::FileNotFound) + // Ok, give up } FatSpecificInfo::Fat32(fat32_info) => { + // Root directories on FAT32 start at a specified cluster, but + // they can have any length. let mut current_cluster = match dir.cluster { - Cluster::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), _ => Some(dir.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(volume_mgr, &match_name, block) { - Err(Error::NotInBlock) => continue, - x => return x, + match self.delete_entry_in_block(block_device, match_name, block) { + Err(Error::FileNotFound) => { + // Carry on + continue; + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => Some(n), - _ => None, - } + // 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, + } } - Err(Error::FileNotFound) + // Ok, give up } } + // If we get here we never found the right entry in any of the + // blocks that made up the directory + Err(Error::FileNotFound) } - /// Deletes an entry in a given block - fn delete_entry_in_block( + /// Deletes a directory entry from a block of directory entries. + /// + /// Entries are marked as deleted by setting the first byte of the file name + /// to a special value. + fn delete_entry_in_block( &self, - volume_mgr: &mut VolumeManager, + block_device: &D, match_name: &ShortFileName, block: BlockIdx, ) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { let mut blocks = [Block::new()]; - volume_mgr - .block_device + block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; for entry in 0..Block::LEN / OnDiskDirEntry::LEN { @@ -726,30 +769,27 @@ impl FatVolume { let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); if dir_entry.is_end() { // Can quit early - return Err(Error::FileNotFound); + break; } else if dir_entry.matches(match_name) { let mut blocks = blocks; blocks[0].contents[start] = 0xE5; - volume_mgr - .block_device + return block_device .write(&blocks, block) - .map_err(Error::DeviceError)?; - return Ok(()); + .map_err(Error::DeviceError); } } - Err(Error::NotInBlock) + Err(Error::FileNotFound) } /// Finds the next free cluster after the start_cluster and before end_cluster - pub(crate) fn find_next_free_cluster( + pub(crate) fn find_next_free_cluster( &self, - volume_mgr: &mut VolumeManager, - start_cluster: Cluster, - end_cluster: Cluster, - ) -> Result> + block_device: &D, + start_cluster: ClusterId, + end_cluster: ClusterId, + ) -> Result> where D: BlockDevice, - T: TimeSource, { let mut blocks = [Block::new()]; let mut current_cluster = start_cluster; @@ -769,8 +809,7 @@ 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); - volume_mgr - .block_device + block_device .read(&mut blocks, this_fat_block_num, "next_cluster") .map_err(Error::DeviceError)?; @@ -801,8 +840,7 @@ 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); - volume_mgr - .block_device + block_device .read(&mut blocks, this_fat_block_num, "next_cluster") .map_err(Error::DeviceError)?; @@ -824,48 +862,51 @@ impl FatVolume { } /// Tries to allocate a cluster - pub(crate) fn alloc_cluster( + pub(crate) fn alloc_cluster( &mut self, - volume_mgr: &mut VolumeManager, - prev_cluster: Option, + block_device: &D, + prev_cluster: Option, zero: bool, - ) -> Result> + ) -> Result> where D: BlockDevice, - T: TimeSource, { debug!("Allocating new cluster, prev_cluster={:?}", prev_cluster); - let end_cluster = Cluster(self.cluster_count + RESERVED_ENTRIES); + let end_cluster = ClusterId(self.cluster_count + RESERVED_ENTRIES); let start_cluster = match self.next_free_cluster { Some(cluster) if cluster.0 < end_cluster.0 => cluster, - _ => Cluster(RESERVED_ENTRIES), + _ => ClusterId(RESERVED_ENTRIES), }; trace!( "Finding next free between {:?}..={:?}", start_cluster, end_cluster ); - let new_cluster = match self.find_next_free_cluster(volume_mgr, start_cluster, end_cluster) - { - Ok(cluster) => cluster, - Err(_) if start_cluster.0 > RESERVED_ENTRIES => { - debug!( - "Retrying, finding next free between {:?}..={:?}", - Cluster(RESERVED_ENTRIES), - end_cluster - ); - self.find_next_free_cluster(volume_mgr, Cluster(RESERVED_ENTRIES), end_cluster)? - } - Err(e) => return Err(e), - }; - self.update_fat(volume_mgr, new_cluster, Cluster::END_OF_FILE)?; + 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)?; if let Some(cluster) = prev_cluster { trace!( "Updating old cluster {:?} to {:?} in FAT", cluster, new_cluster ); - self.update_fat(volume_mgr, cluster, new_cluster)?; + self.update_fat(block_device, cluster, new_cluster)?; } trace!( "Finding next free between {:?}..={:?}", @@ -873,12 +914,12 @@ impl FatVolume { end_cluster ); self.next_free_cluster = - match self.find_next_free_cluster(volume_mgr, new_cluster, end_cluster) { + match self.find_next_free_cluster(block_device, new_cluster, end_cluster) { Ok(cluster) => Some(cluster), Err(_) if new_cluster.0 > RESERVED_ENTRIES => { match self.find_next_free_cluster( - volume_mgr, - Cluster(RESERVED_ENTRIES), + block_device, + ClusterId(RESERVED_ENTRIES), end_cluster, ) { Ok(cluster) => Some(cluster), @@ -896,8 +937,7 @@ impl FatVolume { let first_block = self.cluster_to_block(new_cluster); let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); for block in first_block.range(num_blocks) { - volume_mgr - .block_device + block_device .write(&blocks, block) .map_err(Error::DeviceError)?; } @@ -907,23 +947,25 @@ 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( + pub(crate) fn truncate_cluster_chain( &mut self, - volume_mgr: &mut VolumeManager, - cluster: Cluster, + block_device: &D, + cluster: ClusterId, ) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { if cluster.0 < RESERVED_ENTRIES { // file doesn't have any valid cluster allocated, there is nothing to do return Ok(()); } - let mut next = match self.next_cluster(volume_mgr, cluster) { - Ok(n) => n, - Err(Error::EndOfFile) => return Ok(()), - Err(e) => return Err(e), + let mut next = { + let mut block_cache = BlockCache::empty(); + match self.next_cluster(block_device, cluster, &mut block_cache) { + Ok(n) => n, + Err(Error::EndOfFile) => return Ok(()), + Err(e) => return Err(e), + } }; if let Some(ref mut next_free_cluster) = self.next_free_cluster { if next_free_cluster.0 > next.0 { @@ -932,15 +974,16 @@ impl FatVolume { } else { self.next_free_cluster = Some(next); } - self.update_fat(volume_mgr, cluster, Cluster::END_OF_FILE)?; + self.update_fat(block_device, cluster, ClusterId::END_OF_FILE)?; loop { - match self.next_cluster(volume_mgr, next) { + let mut block_cache = BlockCache::empty(); + match self.next_cluster(block_device, next, &mut block_cache) { Ok(n) => { - self.update_fat(volume_mgr, next, Cluster::EMPTY)?; + self.update_fat(block_device, next, ClusterId::EMPTY)?; next = n; } Err(Error::EndOfFile) => { - self.update_fat(volume_mgr, next, Cluster::EMPTY)?; + self.update_fat(block_device, next, ClusterId::EMPTY)?; break; } Err(e) => return Err(e), @@ -955,19 +998,17 @@ 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( - volume_mgr: &mut VolumeManager, +pub fn parse_volume( + block_device: &D, lba_start: BlockIdx, num_blocks: BlockCount, ) -> Result> where D: BlockDevice, - T: TimeSource, D::Error: core::fmt::Debug, { let mut blocks = [Block::new()]; - volume_mgr - .block_device + block_device .read(&mut blocks, lba_start, "read_bpb") .map_err(Error::DeviceError)?; let block = &blocks[0]; @@ -1011,8 +1052,7 @@ where // Safe to unwrap since this is a Fat32 Type let info_location = bpb.fs_info_block().unwrap(); let mut info_blocks = [Block::new()]; - volume_mgr - .block_device + block_device .read( &mut info_blocks, lba_start + info_location, @@ -1035,7 +1075,7 @@ where cluster_count: bpb.total_clusters(), fat_specific_info: FatSpecificInfo::Fat32(Fat32Info { info_location: lba_start + info_location, - first_root_dir_cluster: Cluster(bpb.first_root_dir_cluster()), + first_root_dir_cluster: ClusterId(bpb.first_root_dir_cluster()), }), }; volume.name.data[..].copy_from_slice(bpb.volume_label()); @@ -1043,3 +1083,9 @@ where } } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/attributes.rs b/src/filesystem/attributes.rs index b24ee3c..a6df757 100644 --- a/src/filesystem/attributes.rs +++ b/src/filesystem/attributes.rs @@ -98,3 +98,9 @@ impl core::fmt::Debug for Attributes { Ok(()) } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/cluster.rs b/src/filesystem/cluster.rs index c2cbef3..34d8590 100644 --- a/src/filesystem/cluster.rs +++ b/src/filesystem/cluster.rs @@ -1,44 +1,40 @@ -/// Represents a cluster on disk. +/// Identifies a cluster on disk. +/// +/// A cluster is a consecutive group of blocks. Each cluster has a a numeric ID. +/// Some numeric IDs are reserved for special purposes. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Cluster(pub(crate) u32); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct ClusterId(pub(crate) u32); -impl Cluster { +impl ClusterId { /// Magic value indicating an invalid cluster value. - pub const INVALID: Cluster = Cluster(0xFFFF_FFF6); + pub const INVALID: ClusterId = ClusterId(0xFFFF_FFF6); /// Magic value indicating a bad cluster. - pub const BAD: Cluster = Cluster(0xFFFF_FFF7); + pub const BAD: ClusterId = ClusterId(0xFFFF_FFF7); /// Magic value indicating a empty cluster. - pub const EMPTY: Cluster = Cluster(0x0000_0000); + pub const EMPTY: ClusterId = ClusterId(0x0000_0000); /// Magic value indicating the cluster holding the root directory (which /// doesn't have a number in FAT16 as there's a reserved region). - pub const ROOT_DIR: Cluster = Cluster(0xFFFF_FFFC); + pub const ROOT_DIR: ClusterId = ClusterId(0xFFFF_FFFC); /// Magic value indicating that the cluster is allocated and is the final cluster for the file - pub const END_OF_FILE: Cluster = Cluster(0xFFFF_FFFF); + pub const END_OF_FILE: ClusterId = ClusterId(0xFFFF_FFFF); } -impl core::ops::Add for Cluster { - type Output = Cluster; - fn add(self, rhs: u32) -> Cluster { - Cluster(self.0 + rhs) +impl core::ops::Add for ClusterId { + type Output = ClusterId; + fn add(self, rhs: u32) -> ClusterId { + ClusterId(self.0 + rhs) } } -impl core::ops::AddAssign for Cluster { +impl core::ops::AddAssign for ClusterId { fn add_assign(&mut self, rhs: u32) { self.0 += rhs; } } -impl core::ops::Add for Cluster { - type Output = Cluster; - fn add(self, rhs: Cluster) -> Cluster { - Cluster(self.0 + rhs.0) - } -} - -impl core::ops::AddAssign for Cluster { - fn add_assign(&mut self, rhs: Cluster) { - self.0 += rhs.0; - } -} +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index ea7a9e5..744e12d 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -2,7 +2,8 @@ use core::convert::TryFrom; use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; -use crate::filesystem::{Attributes, Cluster, ShortFileName, Timestamp}; +use crate::filesystem::{Attributes, ClusterId, SearchId, ShortFileName, Timestamp}; +use crate::Volume; /// Represents a directory entry, which tells you about /// other files and directories. @@ -18,7 +19,7 @@ pub struct DirEntry { /// The file attributes (Read Only, Archive, etc) pub attributes: Attributes, /// The starting cluster of the file. The FAT tells us the following Clusters. - pub cluster: Cluster, + pub cluster: ClusterId, /// The size of the file in bytes. pub size: u32, /// The disk block of this entry @@ -28,13 +29,35 @@ pub struct DirEntry { } /// Represents an open directory on disk. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and if you drop it, the VolumeManager will think you +/// still have the directory open, and it won't let you open the directory +/// again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_dir`] to close it +/// cleanly. +/// +/// If you want your directories to close themselves on drop, create your own +/// `Directory` type that wraps this one and also holds a `VolumeManager` +/// reference. You'll then also need to put your `VolumeManager` in some kind of +/// Mutex or RefCell, and deal with the fact you can't put them both in the same +/// struct any more because one refers to the other. Basically, it's complicated +/// and there's a reason we did it this way. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug)] -pub struct Directory { +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Directory(pub(crate) SearchId); + +/// Holds information about an open file on disk +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub(crate) struct DirectoryInfo { + /// Unique ID for this directory. + pub(crate) directory_id: Directory, + /// The unique ID for the volume this directory is on + pub(crate) volume_id: Volume, /// The starting point of the directory listing. - pub(crate) cluster: Cluster, - /// Dir Entry of this directory, None for the root directory - pub(crate) entry: Option, + pub(crate) cluster: ClusterId, } impl DirEntry { @@ -69,7 +92,7 @@ impl DirEntry { pub(crate) fn new( name: ShortFileName, attributes: Attributes, - cluster: Cluster, + cluster: ClusterId, ctime: Timestamp, entry_block: BlockIdx, entry_offset: u32, @@ -87,4 +110,8 @@ impl DirEntry { } } -impl Directory {} +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index fec87fa..120eaa5 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -1,10 +1,4 @@ -/// An MS-DOS 8.3 filename. 7-bit ASCII only. 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], -} +//! Filename related types /// Various filename related errors that can occur. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] @@ -22,12 +16,56 @@ pub enum FilenameError { Utf8Error, } -impl FilenameError {} +/// Describes things we can convert to short 8.3 filenames +pub trait ToShortFileName { + /// Try and convert this value into a [`ShortFileName`]. + fn to_short_filename(self) -> Result; +} + +impl ToShortFileName for ShortFileName { + fn to_short_filename(self) -> Result { + Ok(self) + } +} + +impl ToShortFileName for &ShortFileName { + fn to_short_filename(self) -> Result { + Ok(self.clone()) + } +} + +impl ToShortFileName for &str { + fn to_short_filename(self) -> Result { + ShortFileName::create_from_str(self) + } +} + +/// An MS-DOS 8.3 filename. 7-bit ASCII only. 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], +} impl ShortFileName { const FILENAME_BASE_MAX_LEN: usize = 8; const FILENAME_MAX_LEN: usize = 11; + /// Get a short file name containing "..", which means "parent directory". + pub const fn parent_dir() -> Self { + Self { + contents: *b".. ", + } + } + + /// Get a short file name containing "..", which means "this directory". + pub const fn this_dir() -> Self { + Self { + contents: *b". ", + } + } + /// Get base name (name without extension) of file name pub fn base_name(&self) -> &[u8] { Self::bytes_before_space(&self.contents[..Self::FILENAME_BASE_MAX_LEN]) @@ -47,6 +85,12 @@ impl ShortFileName { let mut sfn = ShortFileName { contents: [b' '; Self::FILENAME_MAX_LEN], }; + + // Special case `..`, which means "parent directory". + if name == ".." { + return Ok(ShortFileName::parent_dir()); + } + let mut idx = 0; let mut seen_dot = false; for ch in name.bytes() { @@ -196,6 +240,12 @@ impl core::fmt::Debug for ShortFileName { } } +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + #[cfg(test)] mod test { use super::*; @@ -276,3 +326,9 @@ mod test { assert!(ShortFileName::create_from_str("12345678.ABCD").is_err()); } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index a619ed5..6d66125 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,21 +1,46 @@ -use crate::filesystem::{Cluster, DirEntry}; +use crate::{ + filesystem::{ClusterId, DirEntry, SearchId}, + Volume, +}; /// Represents an open file on disk. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and cannot update the directory entry if you drop it. +/// Additionally, the VolumeManager will think you still have the file open if +/// you just drop it, and it won't let you open the file again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_file`] to close it +/// cleanly. +/// +/// If you want your files to close themselves on drop, create your own File +/// type that wraps this one and also holds a `VolumeManager` reference. You'll +/// then also need to put your `VolumeManager` in some kind of Mutex or RefCell, +/// and deal with the fact you can't put them both in the same struct any more +/// because one refers to the other. Basically, it's complicated and there's a +/// reason we did it this way. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug)] -pub struct File { - /// The starting point of the file. - pub(crate) starting_cluster: Cluster, +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct File(pub(crate) SearchId); + +/// Internal metadata about an open file +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub(crate) struct FileInfo { + /// Unique ID for this file + pub(crate) file_id: File, + /// The unique ID for the volume this directory is on + pub(crate) volume_id: Volume, /// The current cluster, and how many bytes that short-cuts us - pub(crate) current_cluster: (u32, Cluster), + pub(crate) current_cluster: (u32, ClusterId), /// How far through the file we've read (in bytes). pub(crate) current_offset: u32, - /// The length of the file, in bytes. - pub(crate) length: u32, /// What mode the file was opened in pub(crate) mode: Mode, /// DirEntry of this file pub(crate) entry: DirEntry, + /// Did we write to this file? + pub(crate) dirty: bool, } /// Errors related to file operations @@ -44,24 +69,24 @@ pub enum Mode { ReadWriteCreateOrAppend, } -impl File { +impl FileInfo { /// Are we at the end of the file? pub fn eof(&self) -> bool { - self.current_offset == self.length + self.current_offset == self.entry.size } /// How long is the file? pub fn length(&self) -> u32 { - self.length + self.entry.size } /// Seek to a new position in the file, relative to the start of the file. pub fn seek_from_start(&mut self, offset: u32) -> Result<(), FileError> { - if offset <= self.length { + if offset <= self.entry.size { self.current_offset = offset; if offset < self.current_cluster.0 { // Back to start - self.current_cluster = (0, self.starting_cluster); + self.current_cluster = (0, self.entry.cluster); } Ok(()) } else { @@ -71,11 +96,11 @@ impl File { /// Seek to a new position in the file, relative to the end of the file. pub fn seek_from_end(&mut self, offset: u32) -> Result<(), FileError> { - if offset <= self.length { - self.current_offset = self.length - offset; + if offset <= self.entry.size { + self.current_offset = self.entry.size - offset; if offset < self.current_cluster.0 { // Back to start - self.current_cluster = (0, self.starting_cluster); + self.current_cluster = (0, self.entry.cluster); } Ok(()) } else { @@ -86,7 +111,7 @@ impl File { /// Seek to a new position in the file, relative to the current position. pub fn seek_from_current(&mut self, offset: i32) -> Result<(), FileError> { let new_offset = i64::from(self.current_offset) + i64::from(offset); - if new_offset >= 0 && new_offset <= i64::from(self.length) { + if new_offset >= 0 && new_offset <= i64::from(self.entry.size) { self.current_offset = new_offset as u32; Ok(()) } else { @@ -96,11 +121,18 @@ impl File { /// Amount of file left to read. pub fn left(&self) -> u32 { - self.length - self.current_offset + self.entry.size - self.current_offset } + /// Update the file's length. pub(crate) fn update_length(&mut self, new: u32) { - self.length = new; + self.entry.size = new; self.entry.size = new; } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index c1b2a81..69076f1 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -1,4 +1,4 @@ -//! embedded-sdmmc-rs - Generic File System structures +//! Generic File System structures //! //! Implements generic file system components. These should be applicable to //! most (if not all) supported filesystems. @@ -11,11 +11,22 @@ mod cluster; mod directory; mod filename; mod files; +mod search_id; mod timestamp; pub use self::attributes::Attributes; -pub use self::cluster::Cluster; +pub use self::cluster::ClusterId; pub use self::directory::{DirEntry, Directory}; -pub use self::filename::{FilenameError, ShortFileName}; +pub use self::filename::{FilenameError, ShortFileName, ToShortFileName}; pub use self::files::{File, FileError, Mode}; +pub use self::search_id::{SearchId, SearchIdGenerator}; pub use self::timestamp::{TimeSource, Timestamp}; + +pub(crate) use self::directory::DirectoryInfo; +pub(crate) use self::files::FileInfo; + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs new file mode 100644 index 0000000..30c1018 --- /dev/null +++ b/src/filesystem/search_id.rs @@ -0,0 +1,40 @@ +use core::num::Wrapping; + +#[derive(Clone, Copy, Debug, 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); + +/// A Search ID generator. +/// +/// This object will always return a different ID. +/// +/// Well, it will wrap after `2**32` IDs. But most systems won't open that many +/// files, and if they do, they are unlikely to hold one file open and then +/// open/close `2**32 - 1` others. +#[derive(Debug)] +pub struct SearchIdGenerator { + next_id: Wrapping, +} + +impl SearchIdGenerator { + /// Create a new generator of Search IDs. + pub const fn new(offset: u32) -> Self { + Self { + next_id: Wrapping(offset), + } + } + + /// Generate a new, unique [`SearchId`]. + pub fn get(&mut self) -> SearchId { + let id = self.next_id; + self.next_id += 1; + SearchId(id.0) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/timestamp.rs b/src/filesystem/timestamp.rs index 30c00d3..3e70cc0 100644 --- a/src/filesystem/timestamp.rs +++ b/src/filesystem/timestamp.rs @@ -132,3 +132,9 @@ impl core::fmt::Display for Timestamp { ) } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/lib.rs b/src/lib.rs index bb2a86f..61070fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,32 +53,32 @@ //! println!("Card size {} bytes", sdcard.num_bytes()?); //! let mut volume_mgr = VolumeManager::new(sdcard, time_source); //! println!("Card size is still {} bytes", volume_mgr.device().num_bytes()?); -//! let mut volume0 = volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; +//! let volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; //! println!("Volume 0: {:?}", volume0); -//! let root_dir = volume_mgr.open_root_dir(&volume0)?; -//! let mut my_file = volume_mgr.open_file_in_dir( -//! &mut volume0, &root_dir, "MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; -//! while !my_file.eof() { +//! let root_dir = volume_mgr.open_root_dir(volume0)?; +//! let my_file = volume_mgr.open_file_in_dir( +//! root_dir, "MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; +//! while !volume_mgr.file_eof(my_file).unwrap() { //! let mut buffer = [0u8; 32]; -//! let num_read = volume_mgr.read(&volume0, &mut my_file, &mut buffer)?; +//! let num_read = volume_mgr.read(my_file, &mut buffer)?; //! for b in &buffer[0..num_read] { //! print!("{}", *b as char); //! } //! } -//! volume_mgr.close_file(&volume0, my_file)?; -//! volume_mgr.close_dir(&volume0, root_dir); +//! volume_mgr.close_file(my_file)?; +//! volume_mgr.close_dir(root_dir)?; //! # 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. //! -//! Make sure that either the `log` feature or the `defmt-log` feature is -//! enabled. +//! You cannot enable both the `log` feature and the `defmt-log` feature. #![cfg_attr(not(test), no_std)] #![deny(missing_docs)] @@ -101,21 +101,32 @@ pub mod fat; pub mod filesystem; pub mod sdcard; +use filesystem::SearchId; + +#[doc(inline)] pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; + +#[doc(inline)] pub use crate::fat::FatVolume; + +#[doc(inline)] pub use crate::filesystem::{ - Attributes, Cluster, DirEntry, Directory, File, FilenameError, Mode, ShortFileName, TimeSource, - Timestamp, MAX_FILE_SIZE, + Attributes, ClusterId, DirEntry, Directory, File, FilenameError, Mode, ShortFileName, + TimeSource, Timestamp, MAX_FILE_SIZE, }; + +use filesystem::DirectoryInfo; + +#[doc(inline)] pub use crate::sdcard::Error as SdCardError; + +#[doc(inline)] pub use crate::sdcard::SdCard; mod volume_mgr; +#[doc(inline)] pub use volume_mgr::VolumeManager; -#[deprecated] -pub use volume_mgr::VolumeManager as Controller; - #[cfg(all(feature = "defmt-log", feature = "log"))] compile_error!("Cannot enable both log and defmt-log"); @@ -167,22 +178,30 @@ where NoSuchVolume, /// The given filename was bad FilenameError(FilenameError), + /// Out of memory opening volumes + TooManyOpenVolumes, /// Out of memory opening directories TooManyOpenDirs, /// Out of memory opening files TooManyOpenFiles, + /// Bad handle given + BadHandle, /// That file doesn't exist FileNotFound, - /// You can't open a file twice + /// You can't open a file twice or delete an open file FileAlreadyOpen, /// You can't open a directory twice DirAlreadyOpen, /// You can't open a directory as a file OpenedDirAsFile, + /// You can't open a file as a directory + OpenedFileAsDir, /// You can't delete a directory as a file DeleteDirAsFile, - /// You can't delete an open file - FileIsOpen, + /// You can't close a volume with open files or directories + VolumeStillInUse, + /// You can't open a volume twice + VolumeAlreadyOpen, /// We can't do that yet Unsupported, /// Tried to read beyond end of file @@ -195,16 +214,16 @@ where NotEnoughSpace, /// Cluster was not properly allocated by the library AllocationError, - /// Jumped to free space during fat traversing - JumpedFree, + /// Jumped to free space during FAT traversing + UnterminatedFatChain, /// Tried to open Read-Only file with write mode ReadOnly, /// Tried to create an existing file FileAlreadyExists, /// Bad block size - only 512 byte blocks supported BadBlockSize(u16), - /// Entry not found in the block - NotInBlock, + /// Bad offset given when seeking + InvalidOffset, } impl From for Error @@ -218,9 +237,18 @@ where /// Represents a partition with a filesystem within it. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Volume(SearchId); + +/// Internal information about a Volume +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, PartialEq, Eq)] -pub struct Volume { +pub(crate) struct VolumeInfo { + /// Search ID for this volume. + volume_id: Volume, + /// TODO: some kind of index idx: VolumeIdx, + /// What kind of volume this is volume_type: VolumeType, } @@ -234,26 +262,13 @@ pub enum VolumeType { } /// A `VolumeIdx` is a number which identifies a volume (or partition) on a -/// disk. `VolumeIdx(0)` is the first primary partition on an MBR partitioned /// disk. +/// +/// `VolumeIdx(0)` is the first primary partition on an MBR partitioned disk. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct VolumeIdx(pub usize); -// **************************************************************************** -// -// Public Data -// -// **************************************************************************** - -// None - -// **************************************************************************** -// -// Private Types -// -// **************************************************************************** - /// Marker for a FAT32 partition. Sometimes also use for FAT16 formatted /// partitions. const PARTITION_ID_FAT32_LBA: u8 = 0x0C; @@ -272,274 +287,7 @@ const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B; // // **************************************************************************** -#[cfg(test)] -mod tests { - use super::*; - - struct DummyBlockDevice; - - struct Clock; - - #[derive(Debug)] - enum Error { - Unknown, - } - - impl TimeSource for Clock { - fn get_timestamp(&self) -> Timestamp { - // TODO: Return actual time - Timestamp { - year_since_1970: 0, - zero_indexed_month: 0, - zero_indexed_day: 0, - hours: 0, - minutes: 0, - seconds: 0, - } - } - } - - impl BlockDevice for DummyBlockDevice { - type Error = Error; - - /// Read one or more blocks, starting at the given block index. - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { - // Actual blocks taken from an SD card, except I've changed the start and length of partition 0. - static BLOCKS: [Block; 3] = [ - Block { - contents: [ - 0xfa, 0xb8, 0x00, 0x10, 0x8e, 0xd0, 0xbc, 0x00, 0xb0, 0xb8, 0x00, 0x00, - 0x8e, 0xd8, 0x8e, 0xc0, // 0x000 - 0xfb, 0xbe, 0x00, 0x7c, 0xbf, 0x00, 0x06, 0xb9, 0x00, 0x02, 0xf3, 0xa4, - 0xea, 0x21, 0x06, 0x00, // 0x010 - 0x00, 0xbe, 0xbe, 0x07, 0x38, 0x04, 0x75, 0x0b, 0x83, 0xc6, 0x10, 0x81, - 0xfe, 0xfe, 0x07, 0x75, // 0x020 - 0xf3, 0xeb, 0x16, 0xb4, 0x02, 0xb0, 0x01, 0xbb, 0x00, 0x7c, 0xb2, 0x80, - 0x8a, 0x74, 0x01, 0x8b, // 0x030 - 0x4c, 0x02, 0xcd, 0x13, 0xea, 0x00, 0x7c, 0x00, 0x00, 0xeb, 0xfe, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x040 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x050 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x060 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x070 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x080 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x090 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0A0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0B0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0C0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0D0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0E0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0F0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x130 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x140 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x150 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x160 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1A0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xca, 0xde, 0x06, - 0x00, 0x00, 0x00, 0x04, // 0x1B0 - 0x01, 0x04, 0x0c, 0xfe, 0xc2, 0xff, 0x01, 0x00, 0x00, 0x00, 0x33, 0x22, - 0x11, 0x00, 0x00, 0x00, // 0x1C0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1D0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1E0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x55, 0xaa, // 0x1F0 - ], - }, - Block { - contents: [ - 0xeb, 0x58, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, - 0x02, 0x08, 0x20, 0x00, // 0x000 - 0x02, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x10, 0x00, 0x04, 0x00, - 0x00, 0x08, 0x00, 0x00, // 0x010 - 0x00, 0x20, 0x76, 0x00, 0x80, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, // 0x020 - 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x030 - 0x80, 0x01, 0x29, 0x0b, 0xa8, 0x89, 0x27, 0x50, 0x69, 0x63, 0x74, 0x75, - 0x72, 0x65, 0x73, 0x20, // 0x040 - 0x20, 0x20, 0x46, 0x41, 0x54, 0x33, 0x32, 0x20, 0x20, 0x20, 0x0e, 0x1f, - 0xbe, 0x77, 0x7c, 0xac, // 0x050 - 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, - 0x5e, 0xeb, 0xf0, 0x32, // 0x060 - 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, - 0x69, 0x73, 0x20, 0x6e, // 0x070 - 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x20, 0x64, 0x69, // 0x080 - 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, - 0x69, 0x6e, 0x73, 0x65, // 0x090 - 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x20, 0x66, 0x6c, // 0x0A0 - 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, - 0x65, 0x73, 0x73, 0x20, // 0x0B0 - 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, - 0x72, 0x79, 0x20, 0x61, // 0x0C0 - 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0D0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0E0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x0F0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x130 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x140 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x150 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x160 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1A0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1B0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1C0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1D0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // 0x1E0 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x55, 0xaa, // 0x1F0 - ], - }, - Block { - contents: hex!( - "52 52 61 41 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - 00 00 00 00 72 72 41 61 FF FF FF FF FF FF FF FF - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" - ), - }, - ]; - println!( - "Reading block {} to {}", - start_block_idx.0, - start_block_idx.0 as usize + blocks.len() - ); - for (idx, block) in blocks.iter_mut().enumerate() { - let block_idx = start_block_idx.0 as usize + idx; - if block_idx < BLOCKS.len() { - *block = BLOCKS[block_idx].clone(); - } else { - return Err(Error::Unknown); - } - } - Ok(()) - } - - /// Write one or more blocks, starting at the given block index. - fn write(&self, _blocks: &[Block], _start_block_idx: BlockIdx) -> Result<(), Self::Error> { - unimplemented!(); - } - - /// Determine how many blocks this device can hold. - fn num_blocks(&self) -> Result { - Ok(BlockCount(2)) - } - } - - #[test] - fn partition0() { - let mut c: VolumeManager = - VolumeManager::new_with_limits(DummyBlockDevice, Clock); - - let v = c.get_volume(VolumeIdx(0)).unwrap(); - assert_eq!( - v, - Volume { - idx: VolumeIdx(0), - volume_type: VolumeType::Fat(FatVolume { - lba_start: BlockIdx(1), - num_blocks: BlockCount(0x0011_2233), - blocks_per_cluster: 8, - first_data_block: BlockCount(15136), - fat_start: BlockCount(32), - name: fat::VolumeName::new(*b"Pictures "), - free_clusters_count: None, - next_free_cluster: None, - cluster_count: 965_788, - fat_specific_info: fat::FatSpecificInfo::Fat32(fat::Fat32Info { - first_root_dir_cluster: Cluster(2), - info_location: BlockIdx(1) + BlockCount(1), - }) - }) - } - ); - } -} +// None // **************************************************************************** // diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 350c774..517e19c 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -11,15 +11,15 @@ use crate::{trace, Block, BlockCount, BlockDevice, BlockIdx}; use core::cell::RefCell; use proto::*; -// ============================================================================= +// **************************************************************************** // Imports -// ============================================================================= +// **************************************************************************** use crate::{debug, warn}; -// ============================================================================= +// **************************************************************************** // Types and Implementations -// ============================================================================= +// **************************************************************************** /// Represents an SD Card on an SPI bus. /// @@ -49,12 +49,18 @@ where { /// Create a new SD/MMC Card driver using a raw SPI interface. /// + /// The card will not be initialised at this time. Initialisation is + /// deferred until a method is called on the object. + /// /// Uses the default options. pub fn new(spi: SPI, cs: CS, delayer: DELAYER) -> SdCard { Self::new_with_options(spi, cs, delayer, AcquireOpts::default()) } /// Construct a new SD/MMC Card driver, using a raw SPI interface and the given options. + /// + /// The card will not be initialised at this time. Initialisation is + /// deferred until a method is called on the object. pub fn new_with_options( spi: SPI, cs: CS, @@ -72,8 +78,13 @@ where } } - /// Get a temporary borrow on the underlying SPI device. Useful if you - /// need to re-clock the SPI. + /// Get a temporary borrow on the underlying SPI device. + /// + /// The given closure will be called exactly once, and will be passed a + /// mutable reference to the underlying SPI object. + /// + /// Useful if you need to re-clock the SPI, but does not perform card + /// initialisation. pub fn spi(&self, func: F) -> T where F: FnOnce(&mut SPI) -> T, @@ -83,6 +94,8 @@ where } /// Return the usable size of this SD card in bytes. + /// + /// This will trigger card (re-)initialisation. pub fn num_bytes(&self) -> Result { let mut inner = self.inner.borrow_mut(); inner.check_init()?; @@ -90,6 +103,8 @@ where } /// Can this card erase single blocks? + /// + /// This will trigger card (re-)initialisation. pub fn erase_single_block_enabled(&self) -> Result { let mut inner = self.inner.borrow_mut(); inner.check_init()?; @@ -105,17 +120,30 @@ where } /// Get the card type. + /// + /// This will trigger card (re-)initialisation. pub fn get_card_type(&self) -> Option { - let inner = self.inner.borrow(); + let mut inner = self.inner.borrow_mut(); + inner.check_init().ok()?; inner.card_type } /// Tell the driver the card has been initialised. /// + /// This is here in case you were previously using the SD Card, and then a + /// previous instance of this object got destroyed but you know for certain + /// the SD Card remained powered up and initialised, and you'd just like to + /// read/write to/from the card again without going through the + /// initialisation sequence again. + /// /// # Safety /// - /// Only do this if the card has actually been initialised and is of the - /// indicated type, otherwise corruption may occur. + /// Only do this if the SD Card has actually been initialised. That is, if + /// you have been through the card initialisation sequence as specified in + /// the SD Card Specification by sending each appropriate command in turn, + /// either manually or using another variable of this [`SdCard`]. The card + /// must also be of the indicated type. Failure to uphold this will cause + /// data corruption. pub unsafe fn mark_card_as_init(&self, card_type: CardType) { let mut inner = self.inner.borrow_mut(); inner.card_type = Some(card_type); @@ -133,6 +161,8 @@ where type Error = Error; /// Read one or more blocks, starting at the given block index. + /// + /// This will trigger card (re-)initialisation. fn read( &self, blocks: &mut [Block], @@ -151,6 +181,8 @@ where } /// Write one or more blocks, starting at the given block index. + /// + /// This will trigger card (re-)initialisation. fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { let mut inner = self.inner.borrow_mut(); debug!("Writing {} blocks @ {}", blocks.len(), start_block_idx.0); @@ -159,6 +191,8 @@ where } /// Determine how many blocks this device can hold. + /// + /// This will trigger card (re-)initialisation. fn num_blocks(&self) -> Result { let mut inner = self.inner.borrow_mut(); inner.check_init()?; @@ -237,6 +271,13 @@ where return Err(Error::WriteError); } } else { + // > It is recommended using this command preceding CMD25, some of the cards will be faster for Multiple + // > Write Blocks operation. Note that the host should send ACMD23 just before WRITE command if the host + // > wants to use the pre-erased feature + s.card_acmd(ACMD23, blocks.len() as u32)?; + // wait for card to be ready before sending the next command + s.wait_not_busy(Delay::new_write())?; + // Start a multi-block write s.card_command(CMD25, start_idx)?; for block in blocks.iter() { @@ -401,9 +442,9 @@ where // Assert CS s.cs_low()?; // Enter SPI mode. - let mut delay = Delay::new_command(); - for attempts in 1.. { - trace!("Enter SPI mode, attempt: {}..", attempts); + let mut delay = Delay::new(s.options.acquire_retries); + for _attempts in 1.. { + trace!("Enter SPI mode, attempt: {}..", _attempts); match s.card_command(CMD0, 0) { Err(Error::TimeoutCommand(0)) => { // Try again? @@ -586,11 +627,18 @@ pub struct AcquireOpts { /// On by default because without it you might get silent data corruption on /// your card. pub use_crc: bool, + + /// Sets the number of times we will retry to acquire the card before giving up and returning + /// `Err(Error::CardNotFound)`. By default, card acquisition will be retried 50 times. + pub acquire_retries: u32, } impl Default for AcquireOpts { fn default() -> Self { - AcquireOpts { use_crc: true } + AcquireOpts { + use_crc: true, + acquire_retries: 50, + } } } diff --git a/src/sdcard/proto.rs b/src/sdcard/proto.rs index 1d34b0f..6418574 100644 --- a/src/sdcard/proto.rs +++ b/src/sdcard/proto.rs @@ -60,6 +60,12 @@ pub const CMD55: u8 = 0x37; pub const CMD58: u8 = 0x3A; /// CRC_ON_OFF - enable or disable CRC checking pub const CMD59: u8 = 0x3B; +/// Pre-erased before writing +/// +/// > It is recommended using this command preceding CMD25, some of the cards will be faster for Multiple +/// > Write Blocks operation. Note that the host should send ACMD23 just before WRITE command if the host +/// > wants to use the pre-erased feature +pub const ACMD23: u8 = 0x17; /// SD_SEND_OP_COMD - Sends host capacity support information and activates /// the card's initialization process pub const ACMD41: u8 = 0x29; @@ -235,6 +241,12 @@ pub fn crc16(data: &[u8]) -> u16 { crc } +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + #[cfg(test)] mod test { use super::*; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 9c24913..5abfcbb 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -1,28 +1,43 @@ -//! The Volume Manager handles partitions and open files on a block device. +//! The Volume Manager implementation. +//! +//! The volume manager handles partitions and open files on a block device. use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; -use crate::fat::{self, RESERVED_ENTRIES}; +use crate::fat::{self, BlockCache, RESERVED_ENTRIES}; + use crate::filesystem::{ - Attributes, Cluster, DirEntry, Directory, File, Mode, ShortFileName, TimeSource, MAX_FILE_SIZE, + Attributes, ClusterId, DirEntry, Directory, DirectoryInfo, File, FileInfo, Mode, + SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ - debug, Block, BlockCount, BlockDevice, BlockIdx, Error, Volume, VolumeIdx, VolumeType, - PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, + debug, Block, BlockCount, BlockDevice, BlockIdx, Error, Volume, VolumeIdx, VolumeInfo, + VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, + PARTITION_ID_FAT32_LBA, }; +use heapless::Vec; -/// A `VolumeManager` wraps a block device and gives access to the volumes within it. -pub struct VolumeManager -where +/// A `VolumeManager` wraps a block device and gives access to the FAT-formatted +/// volumes within it. +#[derive(Debug)] +pub struct VolumeManager< + D, + T, + const MAX_DIRS: usize = 4, + const MAX_FILES: usize = 4, + const MAX_VOLUMES: usize = 1, +> where D: BlockDevice, T: TimeSource, ::Error: core::fmt::Debug, { pub(crate) block_device: D, - pub(crate) timesource: T, - open_dirs: [(VolumeIdx, Cluster); MAX_DIRS], - open_files: [(VolumeIdx, Cluster); MAX_FILES], + pub(crate) time_source: T, + id_generator: SearchIdGenerator, + open_volumes: Vec, + open_dirs: Vec, + open_files: Vec, } impl VolumeManager @@ -36,14 +51,17 @@ where /// files. /// /// This creates a `VolumeManager` with default values - /// MAX_DIRS = 4, MAX_FILES = 4. Call `VolumeManager::new_with_limits(block_device, timesource)` + /// MAX_DIRS = 4, MAX_FILES = 4, MAX_VOLUMES = 1. Call `VolumeManager::new_with_limits(block_device, time_source)` /// if you need different limits. - pub fn new(block_device: D, timesource: T) -> VolumeManager { - Self::new_with_limits(block_device, timesource) + pub fn new(block_device: D, time_source: T) -> VolumeManager { + // Pick a random starting point for the IDs that's not zero, because + // zero doesn't stand out in the logs. + Self::new_with_limits(block_device, time_source, 5000) } } -impl VolumeManager +impl + VolumeManager where D: BlockDevice, T: TimeSource, @@ -52,16 +70,23 @@ where /// Create a new Volume Manager using a generic `BlockDevice`. From this /// object we can open volumes (partitions) and with those we can open /// files. + /// + /// You can also give an offset for all the IDs this volume manager + /// generates, which might help you find the IDs in your logs when + /// debugging. pub fn new_with_limits( block_device: D, - timesource: T, - ) -> VolumeManager { + time_source: T, + id_offset: u32, + ) -> VolumeManager { debug!("Creating new embedded-sdmmc::VolumeManager"); VolumeManager { block_device, - timesource, - open_dirs: [(VolumeIdx(0), Cluster::INVALID); MAX_DIRS], - open_files: [(VolumeIdx(0), Cluster::INVALID); MAX_FILES], + time_source, + id_generator: SearchIdGenerator::new(id_offset), + open_volumes: Vec::new(), + open_dirs: Vec::new(), + open_files: Vec::new(), } } @@ -74,7 +99,7 @@ where /// Record. We do not support GUID Partition Table disks. Nor do we /// support any concept of drive letters - that is for a higher layer to /// handle. - pub fn get_volume(&mut self, volume_idx: VolumeIdx) -> Result> { + pub fn open_volume(&mut 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; @@ -87,6 +112,16 @@ where const PARTITION_INFO_LBA_START_INDEX: usize = 8; const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; + if self.open_volumes.is_full() { + return Err(Error::TooManyOpenVolumes); + } + + for v in self.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 @@ -136,44 +171,44 @@ where | PARTITION_ID_FAT32_LBA | PARTITION_ID_FAT16_LBA | PARTITION_ID_FAT16 => { - let volume = fat::parse_volume(self, lba_start, num_blocks)?; - Ok(Volume { + let volume = fat::parse_volume(&self.block_device, lba_start, num_blocks)?; + let id = Volume(self.id_generator.get()); + let info = VolumeInfo { + volume_id: id, idx: volume_idx, volume_type: volume, - }) + }; + // We already checked for space + self.open_volumes.push(info).unwrap(); + Ok(id) } _ => Err(Error::FormatError("Partition type not supported")), } } - /// Open a directory. - /// - /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + /// Open the volume's root directory. /// - /// TODO: Work out how to prevent damage occuring to the file system while - /// this directory handle is open. In particular, stop this directory - /// being unlinked. - pub fn open_root_dir(&mut self, volume: &Volume) -> Result> { - // Find a free directory entry, and check the root dir isn't open. As - // we already know the root dir's magic cluster number, we can do both - // checks in one loop. - let mut open_dirs_row = None; - for (i, d) in self.open_dirs.iter().enumerate() { - if *d == (volume.idx, Cluster::ROOT_DIR) { + /// 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: Volume) -> Result> { + for dir in self.open_dirs.iter() { + if dir.cluster == ClusterId::ROOT_DIR && dir.volume_id == volume { return Err(Error::DirAlreadyOpen); } - if d.1 == Cluster::INVALID { - open_dirs_row = Some(i); - break; - } } - let open_dirs_row = open_dirs_row.ok_or(Error::TooManyOpenDirs)?; - // Remember this open directory - self.open_dirs[open_dirs_row] = (volume.idx, Cluster::ROOT_DIR); - Ok(Directory { - cluster: Cluster::ROOT_DIR, - entry: None, - }) + + let directory_id = Directory(self.id_generator.get()); + let dir_info = DirectoryInfo { + volume_id: volume, + cluster: ClusterId::ROOT_DIR, + directory_id, + }; + + self.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + Ok(directory_id) } /// Open a directory. @@ -183,145 +218,205 @@ where /// TODO: Work out how to prevent damage occuring to the file system while /// this directory handle is open. In particular, stop this directory /// being unlinked. - pub fn open_dir( + pub fn open_dir( &mut self, - volume: &Volume, - parent_dir: &Directory, - name: &str, - ) -> Result> { - // Find a free open directory table row - let mut open_dirs_row = None; - for (i, d) in self.open_dirs.iter().enumerate() { - if d.1 == Cluster::INVALID { - open_dirs_row = Some(i); - } + parent_dir: Directory, + name: N, + ) -> Result> + where + N: ToShortFileName, + { + if self.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); } - let open_dirs_row = open_dirs_row.ok_or(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 short_file_name = name.to_short_filename().map_err(Error::FilenameError)?; // Open the directory - let dir_entry = match &volume.volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(self, parent_dir, name)?, + let parent_dir_info = &self.open_dirs[parent_dir_idx]; + 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)? + } }; + debug!("Found dir entry: {:?}", dir_entry); + if !dir_entry.attributes.is_directory() { - return Err(Error::OpenedDirAsFile); + return Err(Error::OpenedFileAsDir); } // Check it's not already open - for (_i, dir_table_row) in self.open_dirs.iter().enumerate() { - if *dir_table_row == (volume.idx, dir_entry.cluster) { + for d in self.open_dirs.iter() { + if d.volume_id == self.open_volumes[volume_idx].volume_id + && d.cluster == dir_entry.cluster + { return Err(Error::DirAlreadyOpen); } } - // Remember this open directory - self.open_dirs[open_dirs_row] = (volume.idx, dir_entry.cluster); - Ok(Directory { + + // Remember this open directory. + let directory_id = Directory(self.id_generator.get()); + let dir_info = DirectoryInfo { + directory_id, + volume_id: self.open_volumes[volume_idx].volume_id, cluster: dir_entry.cluster, - entry: Some(dir_entry), - }) + }; + + self.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + Ok(directory_id) } /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. - pub fn close_dir(&mut self, volume: &Volume, dir: Directory) { - let target = (volume.idx, dir.cluster); - for d in self.open_dirs.iter_mut() { - if *d == target { - d.1 = Cluster::INVALID; - break; + pub fn close_dir(&mut self, directory: Directory) -> Result<(), Error> { + for (idx, info) in self.open_dirs.iter().enumerate() { + if directory == info.directory_id { + self.open_dirs.swap_remove(idx); + return Ok(()); + } + } + Err(Error::BadHandle) + } + + /// Close a volume + /// + /// You can't close it if there are any files or directories open on it. + pub fn close_volume(&mut self, volume: Volume) -> Result<(), Error> { + for f in self.open_files.iter() { + if f.volume_id == volume { + return Err(Error::VolumeStillInUse); + } + } + + for d in self.open_dirs.iter() { + if d.volume_id == volume { + return Err(Error::VolumeStillInUse); } } + + let volume_idx = self.get_volume_by_id(volume)?; + self.open_volumes.swap_remove(volume_idx); + + Ok(()) } /// Look in a directory for a named file. - pub fn find_directory_entry( + pub fn find_directory_entry( &mut self, - volume: &Volume, - dir: &Directory, - name: &str, - ) -> Result> { - match &volume.volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(self, dir, name), + directory: Directory, + 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 { + 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) + } } } /// Call a callback function for each directory entry in a directory. - pub fn iterate_dir( - &mut self, - volume: &Volume, - dir: &Directory, - func: F, - ) -> Result<(), Error> + pub fn iterate_dir(&mut self, directory: Directory, func: F) -> Result<(), Error> where F: FnMut(&DirEntry), { - match &volume.volume_type { - VolumeType::Fat(fat) => fat.iterate_dir(self, dir, func), + 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 { + VolumeType::Fat(fat) => { + fat.iterate_dir(&self.block_device, &self.open_dirs[directory_idx], func) + } } } - /// Open a file from DirEntry. This is obtained by calling iterate_dir. A file can only be opened once. - pub fn open_dir_entry( + /// Open a file from a DirEntry. This is obtained by calling iterate_dir. + /// + /// # Safety + /// + /// The DirEntry must be a valid DirEntry read from disk, and not just + /// random numbers. + unsafe fn open_dir_entry( &mut self, - volume: &mut Volume, + volume: Volume, dir_entry: DirEntry, mode: Mode, ) -> Result> { - let open_files_row = self.get_open_files_row()?; - // Check it's not already open - for dir_table_row in self.open_files.iter() { - if *dir_table_row == (volume.idx, dir_entry.cluster) { - return Err(Error::DirAlreadyOpen); - } + // 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); } - if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly { - return Err(Error::ReadOnly); + + // Check it's not already open + if self.file_is_open(volume, &dir_entry) { + return Err(Error::FileAlreadyOpen); } let mode = solve_mode_variant(mode, true); + let file_id = File(self.id_generator.get()); + let file = match mode { - Mode::ReadOnly => File { - starting_cluster: dir_entry.cluster, + Mode::ReadOnly => FileInfo { + file_id, + volume_id: volume, current_cluster: (0, dir_entry.cluster), current_offset: 0, - length: dir_entry.size, mode, entry: dir_entry, + dirty: false, }, Mode::ReadWriteAppend => { - let mut file = File { - starting_cluster: dir_entry.cluster, + let mut file = FileInfo { + file_id, + volume_id: volume, current_cluster: (0, dir_entry.cluster), current_offset: 0, - length: dir_entry.size, 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 = File { - starting_cluster: dir_entry.cluster, + let mut file = FileInfo { + file_id, + volume_id: volume, current_cluster: (0, dir_entry.cluster), current_offset: 0, - length: dir_entry.size, mode, entry: dir_entry, + dirty: false, }; - match &mut volume.volume_type { + 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, file.starting_cluster)? + fat.truncate_cluster_chain(&self.block_device, file.entry.cluster)? } }; file.update_length(0); - // TODO update entry Timestamps - match &volume.volume_type { + match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { + file.entry.mtime = self.time_source.get_timestamp(); let fat_type = fat.get_fat_type(); self.write_entry_to_disk(fat_type, &file.entry)?; } @@ -331,36 +426,70 @@ where } _ => return Err(Error::Unsupported), }; - // Remember this open file - self.open_files[open_files_row] = (volume.idx, file.starting_cluster); - Ok(file) + + // 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( + pub fn open_file_in_dir( &mut self, - volume: &mut Volume, - dir: &Directory, - name: &str, + directory: Directory, + name: N, mode: Mode, - ) -> Result> { - let dir_entry = match &volume.volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(self, dir, name), + ) -> Result> + where + N: ToShortFileName, + { + // This check is load-bearing - we do an unchecked push later. + if self.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 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) + } }; - let open_files_row = self.get_open_files_row()?; let dir_entry = match dir_entry { - Ok(entry) => Some(entry), + Ok(entry) => { + // we are opening an existing file + Some(entry) + } Err(_) if (mode == Mode::ReadWriteCreate) | (mode == Mode::ReadWriteCreateOrTruncate) | (mode == Mode::ReadWriteCreateOrAppend) => { + // We are opening a non-existant file, but that's OK because they + // asked us to create it None } - _ => return Err(Error::FileNotFound), + _ => { + // We are opening a non-existant file, and that's not OK. + return Err(Error::FileNotFound); + } }; + // Check if it's open already + if let Some(dir_entry) = &dir_entry { + if self.file_is_open(volume_info.volume_id, dir_entry) { + return Err(Error::FileAlreadyOpen); + } + } + let mode = solve_mode_variant(mode, dir_entry.is_some()); match mode { @@ -368,158 +497,190 @@ where if dir_entry.is_some() { return Err(Error::FileAlreadyExists); } - let file_name = - ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; let att = Attributes::create_from_fat(0); - let entry = match &mut volume.volume_type { - VolumeType::Fat(fat) => { - fat.write_new_directory_entry(self, dir, file_name, att)? - } + let volume_idx = self.get_volume_by_id(volume_id)?; + let entry = match &mut self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.write_new_directory_entry( + &self.block_device, + &self.time_source, + directory_info, + sfn, + att, + )?, }; - let file = File { - starting_cluster: entry.cluster, + let file_id = File(self.id_generator.get()); + + let file = FileInfo { + file_id, + volume_id, current_cluster: (0, entry.cluster), current_offset: 0, - length: entry.size, mode, entry, + dirty: false, }; - // Remember this open file - self.open_files[open_files_row] = (volume.idx, file.starting_cluster); - Ok(file) + + // Remember this open file - can't be full as we checked already + unsafe { + self.open_files.push_unchecked(file); + } + + Ok(file_id) } _ => { // Safe to unwrap, since we actually have an entry if we got here let dir_entry = dir_entry.unwrap(); - // FIXME: if 2 files are in the same cluster this will cause an error when opening - // a file for a first time in a different than `ReadWriteCreate` mode. - self.open_dir_entry(volume, dir_entry, mode) - } - } - } - - /// Get the next entry in open_files list - fn get_open_files_row(&self) -> Result> { - // Find a free directory entry - let mut open_files_row = None; - for (i, d) in self.open_files.iter().enumerate() { - if d.1 == Cluster::INVALID { - open_files_row = Some(i); + // Safety: We read this dir entry off disk and didn't change it + unsafe { self.open_dir_entry(volume_id, dir_entry, mode) } } } - open_files_row.ok_or(Error::TooManyOpenDirs) } - /// Delete a closed file with the given full path, if exists. - pub fn delete_file_in_dir( + /// Delete a closed file with the given filename, if it exists. + pub fn delete_file_in_dir( &mut self, - volume: &Volume, - dir: &Directory, - name: &str, - ) -> Result<(), Error> { - debug!( - "delete_file(volume={:?}, dir={:?}, filename={:?}", - volume, dir, name - ); - let dir_entry = match &volume.volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(self, dir, name), + directory: Directory, + 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 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), }?; if dir_entry.attributes.is_directory() { return Err(Error::DeleteDirAsFile); } - let target = (volume.idx, dir_entry.cluster); - for d in self.open_files.iter_mut() { - if *d == target { - return Err(Error::FileIsOpen); + if self.file_is_open(dir_info.volume_id, &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 { + VolumeType::Fat(fat) => { + fat.delete_directory_entry(&self.block_device, dir_info, &sfn)? } } - match &volume.volume_type { - VolumeType::Fat(fat) => fat.delete_directory_entry(self, dir, name), + Ok(()) + } + + /// Check if a file is open + /// + /// Returns `true` if it's open, `false`, otherwise. + fn file_is_open(&self, volume: Volume, 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; + } } + false } /// Read from an open file. - pub fn read( - &mut self, - volume: &Volume, - file: &mut File, - buffer: &mut [u8], - ) -> Result> { + pub fn read(&mut self, file: File, 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)?; // 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 && !file.eof() { - let (block_idx, block_offset, block_avail) = - self.find_data_on_disk(volume, &mut file.current_cluster, file.current_offset)?; + 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( + volume_idx, + &mut current_cluster, + self.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") .map_err(Error::DeviceError)?; let block = &blocks[0]; - let to_copy = block_avail.min(space).min(file.left() as usize); + let to_copy = block_avail + .min(space) + .min(self.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; - file.seek_from_current(to_copy as i32).unwrap(); + self.open_files[file_idx] + .seek_from_current(to_copy as i32) + .unwrap(); } Ok(read) } /// Write to a open file. - pub fn write( - &mut self, - volume: &mut Volume, - file: &mut File, - buffer: &[u8], - ) -> Result> { + pub fn write(&mut self, file: File, buffer: &[u8]) -> Result> { #[cfg(feature = "defmt-log")] - debug!( - "write(volume={:?}, file={:?}, buffer={:x}", - volume, file, buffer - ); + debug!("write(file={:?}, buffer={:x}", file, buffer); #[cfg(feature = "log")] - debug!( - "write(volume={:?}, file={:?}, buffer={:x?}", - volume, file, buffer - ); + debug!("write(file={:?}, buffer={:x?}", file, buffer); + + // 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)?; - if file.mode == Mode::ReadOnly { + if self.open_files[file_idx].mode == Mode::ReadOnly { return Err(Error::ReadOnly); } - if file.starting_cluster.0 < RESERVED_ENTRIES { + + self.open_files[file_idx].dirty = true; + + if self.open_files[file_idx].entry.cluster.0 < RESERVED_ENTRIES { // file doesn't have a valid allocated cluster (possible zero-length file), allocate one - file.starting_cluster = match &mut volume.volume_type { - VolumeType::Fat(fat) => fat.alloc_cluster(self, None, false)?, - }; - file.entry.cluster = file.starting_cluster; - debug!("Alloc first cluster {:?}", file.starting_cluster); + self.open_files[file_idx].entry.cluster = + match self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(ref mut fat) => { + fat.alloc_cluster(&self.block_device, None, false)? + } + }; + debug!( + "Alloc first cluster {:?}", + self.open_files[file_idx].entry.cluster + ); } - if (file.current_cluster.1).0 < file.starting_cluster.0 { + + // Clone this so we can touch our other structures. + let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + + if (self.open_files[file_idx].current_cluster.1) < self.open_files[file_idx].entry.cluster { debug!("Rewinding to start"); - file.current_cluster = (0, file.starting_cluster); + self.open_files[file_idx].current_cluster = + (0, self.open_files[file_idx].entry.cluster); } - let bytes_until_max = usize::try_from(MAX_FILE_SIZE - file.current_offset) - .map_err(|_| Error::ConversionError)?; + let bytes_until_max = + usize::try_from(MAX_FILE_SIZE - self.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 = file.current_cluster; + let mut current_cluster = self.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(volume, &mut current_cluster, file.current_offset) { + match self.find_data_on_disk(volume_idx, &mut current_cluster, current_offset) { Ok(vars) => { debug!( "Found block_idx={:?}, block_offset={:?}, block_avail={}", @@ -529,10 +690,14 @@ where } Err(Error::EndOfFile) => { debug!("Extending file"); - match &mut volume.volume_type { + match self.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { if fat - .alloc_cluster(self, Some(current_cluster.1), false) + .alloc_cluster( + &self.block_device, + Some(current_cluster.1), + false, + ) .is_err() { return Ok(written); @@ -540,9 +705,9 @@ where debug!("Allocated new FAT cluster, finding offsets..."); let new_offset = self .find_data_on_disk( - volume, + volume_idx, &mut current_cluster, - file.current_offset, + self.open_files[file_idx].current_offset, ) .map_err(|_| Error::AllocationError)?; debug!("New offset {:?}", new_offset); @@ -568,69 +733,164 @@ where .write(&blocks, block_idx) .map_err(Error::DeviceError)?; written += to_copy; - file.current_cluster = current_cluster; - let to_copy = i32::try_from(to_copy).map_err(|_| Error::ConversionError)?; - // TODO: Should we do this once when the whole file is written? - file.update_length(file.length + (to_copy as u32)); - file.seek_from_current(to_copy).unwrap(); - file.entry.attributes.set_archive(true); - file.entry.mtime = self.timesource.get_timestamp(); - debug!("Updating FAT info sector"); - match &mut volume.volume_type { - VolumeType::Fat(fat) => { - fat.update_info_sector(self)?; - debug!("Updating dir entry"); - self.write_entry_to_disk(fat.get_fat_type(), &file.entry)?; - } + self.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 { + // We made it longer + self.open_files[file_idx].update_length(new_offset); } + self.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(); Ok(written) } /// Close a file with the given full path. - pub fn close_file(&mut self, volume: &Volume, file: File) -> Result<(), Error> { - let target = (volume.idx, file.starting_cluster); - for d in self.open_files.iter_mut() { - if *d == target { - d.1 = Cluster::INVALID; + pub fn close_file(&mut self, file: File) -> Result<(), Error> { + let mut found_idx = None; + for (idx, info) in self.open_files.iter().enumerate() { + if file == info.file_id { + found_idx = Some((info, idx)); break; } } + + let (file_info, file_idx) = found_idx.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) => { + 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 { + // If you have a length, you must have a cluster + assert!(file_info.entry.cluster.0 != 0); + } + let fat_type = fat.get_fat_type(); + self.write_entry_to_disk(fat_type, &file_info.entry)?; + } + }; + } + + self.open_files.swap_remove(file_idx); Ok(()) } /// Check if any files or folders are open. pub fn has_open_handles(&self) -> bool { - !self - .open_dirs - .iter() - .chain(self.open_files.iter()) - .all(|(_, c)| c == &Cluster::INVALID) + !(self.open_dirs.is_empty() || self.open_files.is_empty()) } /// Consume self and return BlockDevice and TimeSource pub fn free(self) -> (D, T) { - (self.block_device, self.timesource) + (self.block_device, self.time_source) + } + + /// Check if a file is at End Of File. + pub fn file_eof(&self, file: File) -> Result> { + let file_idx = self.get_file_by_id(file)?; + Ok(self.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: File, offset: u32) -> Result<(), Error> { + let file_idx = self.get_file_by_id(file)?; + self.open_files[file_idx] + .seek_from_start(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Seek a file with an offset from the current position. + pub fn file_seek_from_current( + &mut self, + file: File, + offset: i32, + ) -> Result<(), Error> { + let file_idx = self.get_file_by_id(file)?; + self.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: File, offset: u32) -> Result<(), Error> { + let file_idx = self.get_file_by_id(file)?; + self.open_files[file_idx] + .seek_from_end(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Get the length of a file + pub fn file_length(&self, file: File) -> Result> { + let file_idx = self.get_file_by_id(file)?; + Ok(self.open_files[file_idx].length()) + } + + /// Get the current offset of a file + pub fn file_offset(&self, file: File) -> Result> { + let file_idx = self.get_file_by_id(file)?; + Ok(self.open_files[file_idx].current_offset) + } + + fn get_volume_by_id(&self, volume: Volume) -> Result> { + for (idx, v) in self.open_volumes.iter().enumerate() { + if v.volume_id == volume { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + fn get_dir_by_id(&self, directory: Directory) -> Result> { + for (idx, d) in self.open_dirs.iter().enumerate() { + if d.directory_id == directory { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + fn get_file_by_id(&self, file: File) -> Result> { + for (idx, f) in self.open_files.iter().enumerate() { + if f.file_id == file { + return Ok(idx); + } + } + Err(Error::BadHandle) } /// This function turns `desired_offset` into an appropriate block to be /// read. It either calculates this based on the start of the file, or /// from the last cluster we read - whichever is better. fn find_data_on_disk( - &mut self, - volume: &Volume, - start: &mut (u32, Cluster), + &self, + volume_idx: usize, + start: &mut (u32, ClusterId), desired_offset: u32, ) -> Result<(BlockIdx, usize, usize), Error> { - let bytes_per_cluster = match &volume.volume_type { + let bytes_per_cluster = match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.bytes_per_cluster(), }; // How many clusters forward do we need to go? let offset_from_cluster = desired_offset - start.0; let num_clusters = offset_from_cluster / bytes_per_cluster; + let mut block_cache = BlockCache::empty(); for _ in 0..num_clusters { - start.1 = match &volume.volume_type { - VolumeType::Fat(fat) => fat.next_cluster(self, start.1)?, + start.1 = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.next_cluster(&self.block_device, start.1, &mut block_cache)? + } }; start.0 += bytes_per_cluster; } @@ -638,7 +898,7 @@ where let offset_from_cluster = desired_offset - start.0; assert!(offset_from_cluster < bytes_per_cluster); let num_blocks = BlockCount(offset_from_cluster / Block::LEN_U32); - let block_idx = match &volume.volume_type { + let block_idx = match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.cluster_to_block(start.1), } + num_blocks; let block_offset = (desired_offset % Block::LEN_U32) as usize; @@ -648,7 +908,7 @@ where /// Writes a Directory Entry to the disk fn write_entry_to_disk( - &mut self, + &self, fat_type: fat::FatType, entry: &DirEntry, ) -> Result<(), Error> { @@ -687,3 +947,289 @@ fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { } mode } + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod tests { + use super::*; + use crate::filesystem::SearchId; + use crate::Timestamp; + + struct DummyBlockDevice; + + struct Clock; + + #[derive(Debug)] + enum Error { + Unknown, + } + + impl TimeSource for Clock { + fn get_timestamp(&self) -> Timestamp { + // TODO: Return actual time + Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } + } + + impl BlockDevice for DummyBlockDevice { + type Error = Error; + + /// Read one or more blocks, starting at the given block index. + fn read( + &self, + blocks: &mut [Block], + start_block_idx: BlockIdx, + _reason: &str, + ) -> Result<(), Self::Error> { + // Actual blocks taken from an SD card, except I've changed the start and length of partition 0. + static BLOCKS: [Block; 3] = [ + Block { + contents: [ + 0xfa, 0xb8, 0x00, 0x10, 0x8e, 0xd0, 0xbc, 0x00, 0xb0, 0xb8, 0x00, 0x00, + 0x8e, 0xd8, 0x8e, 0xc0, // 0x000 + 0xfb, 0xbe, 0x00, 0x7c, 0xbf, 0x00, 0x06, 0xb9, 0x00, 0x02, 0xf3, 0xa4, + 0xea, 0x21, 0x06, 0x00, // 0x010 + 0x00, 0xbe, 0xbe, 0x07, 0x38, 0x04, 0x75, 0x0b, 0x83, 0xc6, 0x10, 0x81, + 0xfe, 0xfe, 0x07, 0x75, // 0x020 + 0xf3, 0xeb, 0x16, 0xb4, 0x02, 0xb0, 0x01, 0xbb, 0x00, 0x7c, 0xb2, 0x80, + 0x8a, 0x74, 0x01, 0x8b, // 0x030 + 0x4c, 0x02, 0xcd, 0x13, 0xea, 0x00, 0x7c, 0x00, 0x00, 0xeb, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x040 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x050 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x060 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x080 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x090 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0B0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0F0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xca, 0xde, 0x06, + 0x00, 0x00, 0x00, 0x04, // 0x1B0 + 0x01, 0x04, 0x0c, 0xfe, 0xc2, 0xff, 0x01, 0x00, 0x00, 0x00, 0x33, 0x22, + 0x11, 0x00, 0x00, 0x00, // 0x1C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x55, 0xaa, // 0x1F0 + ], + }, + Block { + contents: [ + 0xeb, 0x58, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, + 0x02, 0x08, 0x20, 0x00, // 0x000 + 0x02, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x10, 0x00, 0x04, 0x00, + 0x00, 0x08, 0x00, 0x00, // 0x010 + 0x00, 0x20, 0x76, 0x00, 0x80, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, // 0x020 + 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x030 + 0x80, 0x01, 0x29, 0x0b, 0xa8, 0x89, 0x27, 0x50, 0x69, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x20, // 0x040 + 0x20, 0x20, 0x46, 0x41, 0x54, 0x33, 0x32, 0x20, 0x20, 0x20, 0x0e, 0x1f, + 0xbe, 0x77, 0x7c, 0xac, // 0x050 + 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, + 0x5e, 0xeb, 0xf0, 0x32, // 0x060 + 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, + 0x69, 0x73, 0x20, 0x6e, // 0x070 + 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x20, 0x64, 0x69, // 0x080 + 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, + 0x69, 0x6e, 0x73, 0x65, // 0x090 + 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x20, 0x66, 0x6c, // 0x0A0 + 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x20, // 0x0B0 + 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, + 0x72, 0x79, 0x20, 0x61, // 0x0C0 + 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0F0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1B0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x55, 0xaa, // 0x1F0 + ], + }, + Block { + contents: hex!( + "52 52 61 41 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 72 72 41 61 FF FF FF FF FF FF FF FF + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" + ), + }, + ]; + println!( + "Reading block {} to {}", + start_block_idx.0, + start_block_idx.0 as usize + blocks.len() + ); + for (idx, block) in blocks.iter_mut().enumerate() { + let block_idx = start_block_idx.0 as usize + idx; + if block_idx < BLOCKS.len() { + *block = BLOCKS[block_idx].clone(); + } else { + return Err(Error::Unknown); + } + } + Ok(()) + } + + /// Write one or more blocks, starting at the given block index. + fn write(&self, _blocks: &[Block], _start_block_idx: BlockIdx) -> Result<(), Self::Error> { + unimplemented!(); + } + + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result { + Ok(BlockCount(2)) + } + } + + #[test] + fn partition0() { + let mut c: VolumeManager = + VolumeManager::new_with_limits(DummyBlockDevice, Clock, 0xAA00_0000); + + let v = c.open_volume(VolumeIdx(0)).unwrap(); + let expected_id = Volume(SearchId(0xAA00_0000)); + assert_eq!(v, expected_id); + assert_eq!( + &c.open_volumes[0], + &VolumeInfo { + volume_id: expected_id, + idx: VolumeIdx(0), + volume_type: VolumeType::Fat(crate::FatVolume { + lba_start: BlockIdx(1), + num_blocks: BlockCount(0x0011_2233), + blocks_per_cluster: 8, + first_data_block: BlockCount(15136), + fat_start: BlockCount(32), + name: fat::VolumeName::new(*b"Pictures "), + free_clusters_count: None, + next_free_cluster: None, + cluster_count: 965_788, + fat_specific_info: fat::FatSpecificInfo::Fat32(fat::Fat32Info { + first_root_dir_cluster: ClusterId(2), + info_location: BlockIdx(1) + BlockCount(1), + }) + }) + } + ); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/directories.rs b/tests/directories.rs new file mode 100644 index 0000000..12abb85 --- /dev/null +++ b/tests/directories.rs @@ -0,0 +1,326 @@ +//! Directory related tests + +use embedded_sdmmc::ShortFileName; + +mod utils; + +#[derive(Debug, Clone)] +struct ExpectedDirEntry { + name: String, + mtime: String, + ctime: String, + size: u32, + is_dir: bool, +} + +impl PartialEq for ExpectedDirEntry { + fn eq(&self, other: &embedded_sdmmc::DirEntry) -> bool { + if other.name.to_string() != self.name { + return false; + } + if format!("{}", other.mtime) != self.mtime { + return false; + } + if format!("{}", other.ctime) != self.ctime { + return false; + } + if other.size != self.size { + return false; + } + if other.attributes.is_directory() != self.is_dir { + return false; + } + true + } +} + +#[test] +fn fat16_root_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + + let expected = [ + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2018-12-09 19:22:34"), + ctime: String::from("2018-12-09 19:22:34"), + size: 258, + is_dir: false, + }, + 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, + }, + ]; + + let mut listing = Vec::new(); + + volume_mgr + .iterate_dir(root_dir, |d| { + listing.push(d.clone()); + }) + .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, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } +} + +#[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 fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("open test dir"); + + let expected = [ + ExpectedDirEntry { + name: String::from("."), + mtime: String::from("2018-12-09 19:21:02"), + ctime: String::from("2018-12-09 19:21:02"), + size: 0, + is_dir: true, + }, + ExpectedDirEntry { + name: String::from(".."), + mtime: String::from("2018-12-09 19:21:02"), + ctime: String::from("2018-12-09 19:21:02"), + size: 0, + is_dir: true, + }, + ExpectedDirEntry { + name: String::from("TEST.DAT"), + mtime: String::from("2018-12-09 19:22:12"), + ctime: String::from("2018-12-09 19:22:12"), + size: 3500, + is_dir: false, + }, + ]; + + let mut listing = Vec::new(); + let mut count = 0; + + volume_mgr + .iterate_dir(test_dir, |d| { + if count == 0 { + assert!(d.name == ShortFileName::this_dir()); + } else if count == 1 { + assert!(d.name == ShortFileName::parent_dir()); + } + count += 1; + listing.push(d.clone()); + }) + .expect("iterate directory"); + + assert_eq!(expected.len(), listing.len()); + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry, given_entry, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } +} + +#[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 fat32_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let expected = [ + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + 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, + }, + ]; + + let mut listing = Vec::new(); + + volume_mgr + .iterate_dir(root_dir, |d| { + listing.push(d.clone()); + }) + .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, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } +} + +#[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 fat32_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let r = volume_mgr.open_root_dir(fat32_volume); + let Err(embedded_sdmmc::Error::DirAlreadyOpen) = r else { + panic!("Expected to fail opening the root dir twice: {r:?}"); + }; + + let r = volume_mgr.open_dir(root_dir, "README.TXT"); + let Err(embedded_sdmmc::Error::OpenedFileAsDir) = r else { + panic!("Expected to fail opening file as dir: {r:?}"); + }; + + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("open test dir"); + + let r = volume_mgr.open_dir(root_dir, "TEST"); + let Err(embedded_sdmmc::Error::DirAlreadyOpen) = r else { + panic!("Expected to fail opening the dir twice: {r:?}"); + }; + + volume_mgr.close_dir(root_dir).expect("close root dir"); + volume_mgr.close_dir(test_dir).expect("close test dir"); + + let r = volume_mgr.close_dir(test_dir); + let Err(embedded_sdmmc::Error::BadHandle) = r else { + panic!("Expected to fail closing the dir twice: {r:?}"); + }; +} + +#[test] +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< + utils::RamDisk>, + utils::TestTimeSource, + 1, + 4, + 2, + > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); + + let fat32_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let Err(embedded_sdmmc::Error::TooManyOpenDirs) = volume_mgr.open_dir(root_dir, "TEST") else { + panic!("Expected to fail at opening too many dirs"); + }; +} + +#[test] +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 fat32_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let dir_entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("Find directory entry"); + assert!(dir_entry.attributes.is_archive()); + assert!(!dir_entry.attributes.is_directory()); + assert!(!dir_entry.attributes.is_hidden()); + assert!(!dir_entry.attributes.is_lfn()); + assert!(!dir_entry.attributes.is_system()); + assert!(!dir_entry.attributes.is_volume()); + + let r = volume_mgr.find_directory_entry(root_dir, "README.TXS"); + let Err(embedded_sdmmc::Error::FileNotFound) = r else { + panic!("Expected not to find file: {r:?}"); + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/disk.img.gz b/tests/disk.img.gz similarity index 72% rename from disk.img.gz rename to tests/disk.img.gz index 9d2e00e..8df362b 100644 Binary files a/disk.img.gz and b/tests/disk.img.gz differ diff --git a/tests/open_files.rs b/tests/open_files.rs new file mode 100644 index 0000000..0e07d0c --- /dev/null +++ b/tests/open_files.rs @@ -0,0 +1,93 @@ +//! File opening related tests + +use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; + +mod utils; + +#[test] +fn open_files() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + let r = volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly); + let Err(Error::FileAlreadyOpen) = r else { + panic!("Expected to not open file twice: {r:?}"); + }; + + volume_mgr.close_file(f).expect("close file"); + + // Open with SFN + + let dir_entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find file"); + + let f = volume_mgr + .open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadWriteCreateOrAppend) + .expect("open file with dir entry"); + + let r = volume_mgr.open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadOnly); + let Err(Error::FileAlreadyOpen) = r else { + panic!("Expected to not open file twice: {r:?}"); + }; + + // Can still spot duplicates even if name given the other way around + let r = volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly); + let Err(Error::FileAlreadyOpen) = r else { + panic!("Expected to not open file twice: {r:?}"); + }; + + let f2 = volume_mgr + .open_file_in_dir(root_dir, "64MB.DAT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Hit file limit + let r = volume_mgr.open_file_in_dir(root_dir, "EMPTY.DAT", Mode::ReadOnly); + let Err(Error::TooManyOpenFiles) = r else { + panic!("Expected to run out of file handles: {r:?}"); + }; + + volume_mgr.close_file(f).expect("close file"); + volume_mgr.close_file(f2).expect("close file"); + + // File not found + let r = volume_mgr.open_file_in_dir(root_dir, "README.TXS", Mode::ReadOnly); + let Err(Error::FileNotFound) = r else { + panic!("Expected to not open missing file: {r:?}"); + }; + + // Create a new file + let f3 = volume_mgr + .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadWriteCreate) + .expect("open file"); + + volume_mgr.write(f3, b"12345").expect("write to file"); + volume_mgr.write(f3, b"67890").expect("write to file"); + volume_mgr.close_file(f3).expect("close file"); + + // Open our new file + let f3 = volume_mgr + .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadOnly) + .expect("open file"); + // Should have 10 bytes in it + assert_eq!(volume_mgr.file_length(f3).expect("file length"), 10); + volume_mgr.close_file(f3).expect("close file"); + + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/read_file.rs b/tests/read_file.rs new file mode 100644 index 0000000..0a6bffb --- /dev/null +++ b/tests/read_file.rs @@ -0,0 +1,177 @@ +//! Reading related tests + +mod utils; + +static TEST_DAT_SHA256_SUM: &str = + "59e3468e3bef8bfe37e60a8221a1896e105b80a61a23637612ac8cd24ca04a75"; + +#[test] +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 fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = Vec::new(); + + let mut partial = false; + while !volume_mgr.file_eof(test_file).expect("check eof") { + let mut buffer = [0u8; 512]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); + if len != buffer.len() { + if partial { + panic!("Two partial reads!"); + } else { + partial = true; + } + } + contents.extend(&buffer[0..len]); + } + + let hash = sha256::digest(contents); + assert_eq!(&hash, TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_all() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = vec![0u8; 4096]; + let len = volume_mgr + .read(test_file, &mut contents) + .expect("read data"); + if len != 3500 { + panic!("Failed to read all of TEST.DAT"); + } + + let hash = sha256::digest(&contents[0..3500]); + assert_eq!(&hash, TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_prime_blocks() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = Vec::new(); + + let mut partial = false; + while !volume_mgr.file_eof(test_file).expect("check eof") { + // Exercise the alignment code by reading in chunks of 53 bytes + let mut buffer = [0u8; 53]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); + if len != buffer.len() { + if partial { + panic!("Two partial reads!"); + } else { + partial = true; + } + } + contents.extend(&buffer[0..len]); + } + + let hash = sha256::digest(contents); + assert_eq!(&hash, TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_backwards() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = std::collections::VecDeque::new(); + + const CHUNK_SIZE: u32 = 100; + let length = volume_mgr.file_length(test_file).expect("file length"); + let mut offset = length - CHUNK_SIZE; + let mut read = 0; + + // We're going to read the file backwards in chunks of 100 bytes. This + // checks we didn't make any assumptions about only going forwards. + while read < length { + volume_mgr + .file_seek_from_start(test_file, offset) + .expect("seek"); + let mut buffer = [0u8; CHUNK_SIZE as usize]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read"); + assert_eq!(len, CHUNK_SIZE as usize); + contents.push_front(buffer.to_vec()); + read += CHUNK_SIZE; + if offset >= CHUNK_SIZE { + offset -= CHUNK_SIZE; + } + } + + assert_eq!(read, length); + assert_eq!(offset, 0); + + let flat: Vec = contents.iter().flatten().copied().collect(); + + let hash = sha256::digest(flat); + assert_eq!(&hash, TEST_DAT_SHA256_SUM); +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..b74db45 --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,186 @@ +//! Useful library code for tests + +use std::io::prelude::*; + +use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx}; + +/// This file contains: +/// +/// ```console +/// $ fdisk ./disk.img +/// Disk: ./disk.img geometry: 520/32/63 [1048576 sectors] +/// Signature: 0xAA55 +/// Starting Ending +/// #: id cyl hd sec - cyl hd sec [ start - size] +/// ------------------------------------------------------------------------ +/// 1: 0E 0 32 33 - 16 113 33 [ 2048 - 262144] DOS FAT-16 +/// 2: 0C 16 113 34 - 65 69 4 [ 264192 - 784384] Win95 FAT32L +/// 3: 00 0 0 0 - 0 0 0 [ 0 - 0] unused +/// 4: 00 0 0 0 - 0 0 0 [ 0 - 0] unused +/// $ ls -l /Volumes/P-FAT16 +/// total 131080 +/// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT +/// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT +/// -rwxrwxrwx@ 1 jonathan staff 258 9 Dec 2018 README.TXT +/// drwxrwxrwx 1 jonathan staff 2048 9 Dec 2018 TEST +/// $ ls -l /Volumes/P-FAT16/TEST +/// total 8 +/// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT +/// $ ls -l /Volumes/P-FAT32 +/// total 131088 +/// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT +/// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT +/// -rwxrwxrwx@ 1 jonathan staff 258 21 Sep 09:48 README.TXT +/// drwxrwxrwx 1 jonathan staff 4096 9 Dec 2018 TEST +/// $ ls -l /Volumes/P-FAT32/TEST +/// total 8 +/// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT +/// ``` +/// +/// It will unpack to a Vec that is 1048576 * 512 = 512 MiB in size. +pub static DISK_SOURCE: &[u8] = include_bytes!("../disk.img.gz"); + +#[derive(Debug)] +pub enum Error { + /// Failed to read the source image + Io(std::io::Error), + /// Failed to unzip the source image + Decode(flate2::DecompressError), + /// Asked for a block we don't have + OutOfBounds(BlockIdx), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: flate2::DecompressError) -> Self { + Self::Decode(value) + } +} + +/// Implements the block device traits for a chunk of bytes in RAM. +/// +/// The slice should be a multiple of `embedded_sdmmc::Block::LEN` bytes in +/// length. If it isn't the trailing data is discarded. +pub struct RamDisk { + contents: std::cell::RefCell, +} + +impl RamDisk { + fn new(contents: T) -> RamDisk { + RamDisk { + contents: std::cell::RefCell::new(contents), + } + } +} + +impl BlockDevice for RamDisk +where + T: AsMut<[u8]> + AsRef<[u8]>, +{ + type Error = Error; + + fn read( + &self, + blocks: &mut [Block], + start_block_idx: BlockIdx, + _reason: &str, + ) -> Result<(), Self::Error> { + let borrow = self.contents.borrow(); + let contents: &[u8] = borrow.as_ref(); + let mut block_idx = start_block_idx; + for block in blocks.iter_mut() { + let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; + let end_offset = start_offset + embedded_sdmmc::Block::LEN; + if end_offset > contents.len() { + return Err(Error::OutOfBounds(block_idx)); + } + block + .as_mut_slice() + .copy_from_slice(&contents[start_offset..end_offset]); + block_idx.0 += 1; + } + Ok(()) + } + + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut borrow = self.contents.borrow_mut(); + let contents: &mut [u8] = borrow.as_mut(); + let mut block_idx = start_block_idx; + for block in blocks.iter() { + let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; + let end_offset = start_offset + embedded_sdmmc::Block::LEN; + if end_offset > contents.len() { + return Err(Error::OutOfBounds(block_idx)); + } + contents[start_offset..end_offset].copy_from_slice(block.as_slice()); + block_idx.0 += 1; + } + Ok(()) + } + + fn num_blocks(&self) -> Result { + let borrow = self.contents.borrow(); + let contents: &[u8] = borrow.as_ref(); + let len_blocks = contents.len() as usize / embedded_sdmmc::Block::LEN; + if len_blocks > u32::MAX as usize { + panic!("Test disk too large! Only 2**32 blocks allowed"); + } + Ok(BlockCount(len_blocks as u32)) + } +} + +/// Unpack the fixed, static, disk image. +fn unpack_disk(gzip_bytes: &[u8]) -> Result, Error> { + let disk_cursor = std::io::Cursor::new(gzip_bytes); + let mut gz_decoder = flate2::read::GzDecoder::new(disk_cursor); + let mut output = Vec::with_capacity(512 * 1024 * 1024); + gz_decoder.read_to_end(&mut output)?; + Ok(output) +} + +/// Turn some gzipped bytes into a block device, +pub fn make_block_device(gzip_bytes: &[u8]) -> Result>, Error> { + let data = unpack_disk(gzip_bytes)?; + Ok(RamDisk::new(data)) +} + +pub struct TestTimeSource { + fixed: embedded_sdmmc::Timestamp, +} + +impl embedded_sdmmc::TimeSource for TestTimeSource { + fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { + self.fixed.clone() + } +} + +/// Make a time source that gives a fixed time. +/// +/// It always claims to be 4 April 2003, at 13:30:05. +/// +/// This is an interesting time, because FAT will round it down to 13:30:04 due +/// to only have two-second resolution. Hey, Real Time Clocks were optional back +/// in 1981. +pub fn make_time_source() -> TestTimeSource { + TestTimeSource { + fixed: embedded_sdmmc::Timestamp { + year_since_1970: 33, + zero_indexed_month: 3, + zero_indexed_day: 3, + hours: 13, + minutes: 30, + seconds: 5, + }, + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/volume.rs b/tests/volume.rs new file mode 100644 index 0000000..a1a0455 --- /dev/null +++ b/tests/volume.rs @@ -0,0 +1,109 @@ +//! Volume related tests + +mod utils; + +#[test] +fn open_all_volumes() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr: embedded_sdmmc::VolumeManager< + utils::RamDisk>, + utils::TestTimeSource, + 4, + 4, + 2, + > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); + + // Open Volume 0 + let fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + + // Fail to Open Volume 0 again + let r = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0)); + let Err(embedded_sdmmc::Error::VolumeAlreadyOpen) = r else { + panic!("Should have failed to open volume {:?}", r); + }; + + volume_mgr.close_volume(fat16_volume).expect("close fat16"); + + // Open Volume 1 + let fat32_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + // Fail to Volume 1 again + let r = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(1)); + let Err(embedded_sdmmc::Error::VolumeAlreadyOpen) = r else { + panic!("Should have failed to open volume {:?}", r); + }; + + // Open Volume 0 again + let fat16_volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + + // Open any volume - too many volumes (0 and 1 are open) + let r = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0)); + let Err(embedded_sdmmc::Error::TooManyOpenVolumes) = r else { + panic!("Should have failed to open volume {:?}", r); + }; + + volume_mgr.close_volume(fat16_volume).expect("close fat16"); + volume_mgr.close_volume(fat32_volume).expect("close fat32"); + + // This isn't a valid volume + let r = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(2)); + let Err(embedded_sdmmc::Error::FormatError(_e)) = r else { + panic!("Should have failed to open volume {:?}", r); + }; + + // This isn't a valid volume + let r = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(9)); + let Err(embedded_sdmmc::Error::NoSuchVolume) = r else { + panic!("Should have failed to open volume {:?}", r); + }; + + let _root_dir = volume_mgr.open_root_dir(fat32_volume).expect("Open dir"); + + let r = volume_mgr.close_volume(fat32_volume); + let Err(embedded_sdmmc::Error::VolumeStillInUse) = r else { + panic!("Should have failed to close volume {:?}", r); + }; +} + +#[test] +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 = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Dir open + let r = volume_mgr.close_volume(volume); + let Err(embedded_sdmmc::Error::VolumeStillInUse) = r else { + panic!("Expected failure to close volume: {r:?}"); + }; + + let _test_file = volume_mgr + .open_file_in_dir(root_dir, "64MB.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + volume_mgr.close_dir(root_dir).unwrap(); + + // File open, not dir open + let r = volume_mgr.close_volume(volume); + let Err(embedded_sdmmc::Error::VolumeStillInUse) = r else { + panic!("Expected failure to close volume: {r:?}"); + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/write_file.rs b/tests/write_file.rs new file mode 100644 index 0000000..0594509 --- /dev/null +++ b/tests/write_file.rs @@ -0,0 +1,60 @@ +//! File opening related tests + +use embedded_sdmmc::{Mode, VolumeIdx, VolumeManager}; + +mod utils; + +#[test] +fn append_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Should be enough to cause a few more clusters to be allocated + let test_data = vec![0xCC; 1024 * 1024]; + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, 1024 * 1024); + + let offset = volume_mgr.file_offset(f).expect("offset"); + assert_eq!(offset, 1024 * 1024); + + // Now wind it back 1 byte; + volume_mgr.file_seek_from_current(f, -1).expect("Seeking"); + + let offset = volume_mgr.file_offset(f).expect("offset"); + assert_eq!(offset, (1024 * 1024) - 1); + + // Write another megabyte, making `2 MiB - 1` + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, (1024 * 1024 * 2) - 1); + + volume_mgr.close_file(f).expect("close dir"); + + // Now check the file length again + + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("Find entry"); + assert_eq!(entry.size, (1024 * 1024 * 2) - 1); + + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} + +// **************************************************************************** +// +// End Of File +// +// ****************************************************************************