From c68db897425d59a13c383564a1a4a8aa5bfc5160 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Mon, 26 Sep 2022 13:07:34 -0400 Subject: [PATCH 01/69] Add the option to skip waiting in the init sequence when issuing CMD0. --- src/sdmmc.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 1910e7b..35cdadb 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -129,11 +129,18 @@ impl Delay { pub struct AcquireOpts { /// Some cards don't support CRC mode. At least a 512MiB Transcend one. pub require_crc: bool, + /// Some cards should send CMD0 after an idle state to avoid being stuck in [`TimeoutWaitNotBusy`]. + /// See [this conversation](https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33) + /// and [this one])(https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33). + pub skip_wait_not_busy: bool, } impl Default for AcquireOpts { fn default() -> Self { - AcquireOpts { require_crc: true } + AcquireOpts { + require_crc: true, + skip_wait_not_busy: false, + } } } @@ -189,7 +196,15 @@ where while attempts > 0 { trace!("Enter SPI mode, attempt: {}..", 32i32 - attempts); - match s.card_command(CMD0, 0) { + // Select whether or not to skip waiting for the card not to be busy + // when issuing the first CMD0. See https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32 and + // https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33. + let cmd0_func = match options.skip_wait_not_busy { + true => Self::card_command_skip_wait, + false => Self::card_command, + }; + + match cmd0_func(s, CMD0, 0) { Err(Error::TimeoutCommand(0)) => { // Try again? warn!("Timed out, trying again.."); @@ -300,6 +315,13 @@ where /// Perform a command. fn card_command(&self, command: u8, arg: u32) -> Result { self.wait_not_busy()?; + self.card_command_skip_wait(command, arg) + } + + /// Perform a command without waiting for the card not to be busy. This method should almost never be used directly, except in + /// very specific circumstances. See [https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33] + /// and [https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32] for more info. + fn card_command_skip_wait(&self, command: u8, arg: u32) -> Result { let mut buf = [ 0x40 | command, (arg >> 24) as u8, From cd9e3fe2a4eb601692da12074a6a5f8d0b7bf2f7 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Thu, 6 Oct 2022 11:37:07 -0400 Subject: [PATCH 02/69] Update links to card init issue/PR --- src/sdmmc.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 35cdadb..7f041ae 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -130,8 +130,8 @@ pub struct AcquireOpts { /// Some cards don't support CRC mode. At least a 512MiB Transcend one. pub require_crc: bool, /// Some cards should send CMD0 after an idle state to avoid being stuck in [`TimeoutWaitNotBusy`]. - /// See [this conversation](https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33) - /// and [this one])(https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33). + /// See [this conversation](https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33#issue-803000031) + /// and [this one])(https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32#issue-802999340). pub skip_wait_not_busy: bool, } @@ -197,8 +197,8 @@ where trace!("Enter SPI mode, attempt: {}..", 32i32 - attempts); // Select whether or not to skip waiting for the card not to be busy - // when issuing the first CMD0. See https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32 and - // https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33. + // when issuing the first CMD0. See https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33#issue-803000031 and + // https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32#issue-802999340. let cmd0_func = match options.skip_wait_not_busy { true => Self::card_command_skip_wait, false => Self::card_command, From 3be7da402f57a26df21f6b910367b0f0ee7a77a8 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Mon, 17 Oct 2022 14:48:54 -0400 Subject: [PATCH 03/69] Change to flushing the card instead of skipping wait. --- src/sdmmc.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 7f041ae..aa66915 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -196,18 +196,15 @@ where while attempts > 0 { trace!("Enter SPI mode, attempt: {}..", 32i32 - attempts); - // Select whether or not to skip waiting for the card not to be busy - // when issuing the first CMD0. See https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33#issue-803000031 and - // https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32#issue-802999340. - let cmd0_func = match options.skip_wait_not_busy { - true => Self::card_command_skip_wait, - false => Self::card_command, - }; - - match cmd0_func(s, CMD0, 0) { + match s.card_command(CMD0, 0) { Err(Error::TimeoutCommand(0)) => { // Try again? warn!("Timed out, trying again.."); + // Try flushing the card as done here: https://github.com/greiman/SdFat/blob/master/src/SdCard/SdSpiCard.cpp#L170, + // https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/65#issuecomment-1270709448 + for _ in 0..0xFF { + s.send(0xFF)?; + } attempts -= 1; } Err(e) => { @@ -315,13 +312,7 @@ where /// Perform a command. fn card_command(&self, command: u8, arg: u32) -> Result { self.wait_not_busy()?; - self.card_command_skip_wait(command, arg) - } - /// Perform a command without waiting for the card not to be busy. This method should almost never be used directly, except in - /// very specific circumstances. See [https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33] - /// and [https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32] for more info. - fn card_command_skip_wait(&self, command: u8, arg: u32) -> Result { let mut buf = [ 0x40 | command, (arg >> 24) as u8, From ffed79e18afc56349bfd293e6770022aa52c60b1 Mon Sep 17 00:00:00 2001 From: Darrik <30670444+mdarrik@users.noreply.github.com> Date: Sun, 30 Oct 2022 21:54:07 -0700 Subject: [PATCH 04/69] fix: don't wait for not busy if CMD0 or CMD12 Also remove unused AcquireOpt option --- src/sdmmc.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index aa66915..bd8da0a 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -129,18 +129,11 @@ impl Delay { pub struct AcquireOpts { /// Some cards don't support CRC mode. At least a 512MiB Transcend one. pub require_crc: bool, - /// Some cards should send CMD0 after an idle state to avoid being stuck in [`TimeoutWaitNotBusy`]. - /// See [this conversation](https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/33#issue-803000031) - /// and [this one])(https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/32#issue-802999340). - pub skip_wait_not_busy: bool, } impl Default for AcquireOpts { fn default() -> Self { - AcquireOpts { - require_crc: true, - skip_wait_not_busy: false, - } + AcquireOpts { require_crc: true } } } @@ -311,7 +304,9 @@ where /// Perform a command. fn card_command(&self, command: u8, arg: u32) -> Result { - self.wait_not_busy()?; + if command != CMD0 && command != CMD12 { + self.wait_not_busy()?; + } let mut buf = [ 0x40 | command, From 01da43c6abdd6d8b11f185b03e270088d37fe23d Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 23 Jan 2023 15:30:14 +0200 Subject: [PATCH 05/69] chore: fix README usage example Signed-off-by: Lachezar Lechev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a47e22f..4623240 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ designed for readability and simplicity over performance. You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. ```rust -let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs), time_source); +let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs); write!(uart, "Init SD card...").unwrap(); match spi_dev.acquire() { Ok(block) => { From a821037c0cdb7f5862dc8a1ea9455c493d6534e6 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 10 Apr 2023 11:27:37 +0300 Subject: [PATCH 06/69] docs: add fixme for opening files bug Signed-off-by: Lachezar Lechev --- src/controller.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controller.rs b/src/controller.rs index ba3008d..e50ec72 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -376,6 +376,8 @@ where _ => { // 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) } } From 6e904b6c159f9a322f0ce3ebaa6c66159e647858 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 21 Apr 2023 16:49:45 +0100 Subject: [PATCH 07/69] Rename Controller to VolumeManager. Better reflects what it actually does. --- CHANGELOG.md | 2 + README.md | 14 ++- examples/create_test.rs | 30 +++--- examples/delete_test.rs | 24 ++--- examples/test_mount.rs | 42 ++++----- examples/write_test.rs | 40 ++++---- src/fat/info.rs | 3 +- src/fat/volume.rs | 134 ++++++++++++++------------- src/lib.rs | 18 ++-- src/sdmmc.rs | 2 +- src/{controller.rs => volume_mgr.rs} | 28 +++--- 11 files changed, 173 insertions(+), 164 deletions(-) rename src/{controller.rs => volume_mgr.rs} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0cc5ce..9f01b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.4.0...develop +- Renamed `Controller` to `VolumeManager`, to better describe what it does. + ## [Version 0.4.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.4.0) ### Changes diff --git a/README.md b/README.md index 4623240..9622c83 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,15 @@ designed for readability and simplicity over performance. You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. ```rust +// Build an SD Card interface out of an SPI device let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs); +// Try and initialise the SD card write!(uart, "Init SD card...").unwrap(); match spi_dev.acquire() { Ok(block) => { - let mut cont = embedded_sdmmc::Controller::new(block, time_source); + // The SD Card initialised, and we have a `BlockSpi` object representing + // the initialised card. Now let's + let mut cont = embedded_sdmmc::VolumeManager::new(block, time_source); write!(uart, "OK!\nCard size...").unwrap(); match cont.device().card_size_bytes() { Ok(size) => writeln!(uart, "{}", size).unwrap(), @@ -33,16 +37,16 @@ match spi_dev.acquire() { ### Open directories and files -By default the `Controller` 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 `Controller`: +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`: ```rust -// Create a controller with a maximum of 6 open directories and 12 open files -let mut cont: Controller< +// Create a volume manager with a maximum of 6 open directories and 12 open files +let mut cont: VolumeManager< embedded_sdmmc::BlockSpi, DummyTimeSource, 6, 12, -> = Controller::new_with_limits(block, time_source); +> = VolumeManager::new_with_limits(block, time_source); ``` ## Supported features diff --git a/examples/create_test.rs b/examples/create_test.rs index c9ee6df..239cded 100644 --- a/examples/create_test.rs +++ b/examples/create_test.rs @@ -19,8 +19,8 @@ extern crate embedded_sdmmc; const FILE_TO_CREATE: &'static str = "CREATE.TXT"; use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Controller, Error, Mode, TimeSource, Timestamp, - VolumeIdx, + Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, + VolumeManager, }; use std::cell::RefCell; use std::fs::{File, OpenOptions}; @@ -118,14 +118,14 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut controller = Controller::new(lbd, Clock); + let mut volume_mgr = VolumeManager::new(lbd, Clock); for volume_idx in 0..=3 { - let volume = controller.get_volume(VolumeIdx(volume_idx)); + let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); println!("volume {}: {:#?}", volume_idx, volume); if let Ok(mut volume) = volume { - let root_dir = controller.open_root_dir(&volume).unwrap(); + let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); println!("\tListing root directory:"); - controller + volume_mgr .iterate_dir(&volume, &root_dir, |x| { println!("\t\tFound: {:?}", x); }) @@ -133,7 +133,7 @@ fn main() { println!("\nCreating file {}...", FILE_TO_CREATE); // This will panic if the file already exists, use ReadWriteCreateOrAppend or // ReadWriteCreateOrTruncate instead - let mut f = controller + let mut f = volume_mgr .open_file_in_dir( &mut volume, &root_dir, @@ -145,7 +145,7 @@ fn main() { println!("FILE STARTS:"); while !f.eof() { let mut buffer = [0u8; 32]; - let num_read = controller.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -164,12 +164,12 @@ fn main() { buffer.push(b'\n'); } println!("\nAppending to file"); - let num_written1 = controller.write(&mut volume, &mut f, &buffer1[..]).unwrap(); - let num_written = controller.write(&mut volume, &mut f, &buffer[..]).unwrap(); + 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); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); - let mut f = controller + let mut f = volume_mgr .open_file_in_dir( &mut volume, &root_dir, @@ -183,13 +183,13 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_CREATE, - controller.find_directory_entry(&volume, &root_dir, 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 = controller.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -198,7 +198,7 @@ fn main() { } } println!("EOF"); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); } } } diff --git a/examples/delete_test.rs b/examples/delete_test.rs index 756d819..189f683 100644 --- a/examples/delete_test.rs +++ b/examples/delete_test.rs @@ -19,8 +19,8 @@ extern crate embedded_sdmmc; const FILE_TO_DELETE: &'static str = "DELETE.TXT"; use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Controller, Error, Mode, TimeSource, Timestamp, - VolumeIdx, + Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, + VolumeManager, }; use std::cell::RefCell; use std::fs::{File, OpenOptions}; @@ -118,14 +118,14 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut controller = Controller::new(lbd, Clock); + let mut volume_mgr = VolumeManager::new(lbd, Clock); for volume_idx in 0..=3 { - let volume = controller.get_volume(VolumeIdx(volume_idx)); + let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); println!("volume {}: {:#?}", volume_idx, volume); if let Ok(mut volume) = volume { - let root_dir = controller.open_root_dir(&volume).unwrap(); + let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); println!("\tListing root directory:"); - controller + volume_mgr .iterate_dir(&volume, &root_dir, |x| { println!("\t\tFound: {:?}", x); }) @@ -133,7 +133,7 @@ fn main() { println!("\nCreating file {}...", FILE_TO_DELETE); // This will panic if the file already exists, use ReadWriteCreateOrAppend or // ReadWriteCreateOrTruncate instead - let f = controller + let f = volume_mgr .open_file_in_dir( &mut volume, &root_dir, @@ -146,17 +146,17 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_DELETE, - controller.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) + volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) ); - match controller.delete_file_in_dir(&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); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); - match controller.delete_file_in_dir(&volume, &root_dir, FILE_TO_DELETE) { + 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), } @@ -164,7 +164,7 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_DELETE, - controller.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) + volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) ); } } diff --git a/examples/test_mount.rs b/examples/test_mount.rs index fd9a5e1..f0eddf4 100644 --- a/examples/test_mount.rs +++ b/examples/test_mount.rs @@ -27,8 +27,8 @@ const FILE_TO_PRINT: &'static str = "README.TXT"; const FILE_TO_CHECKSUM: &'static str = "64MB.DAT"; use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Controller, Error, Mode, TimeSource, Timestamp, - VolumeIdx, + Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, + VolumeManager, }; use std::cell::RefCell; use std::fs::File; @@ -121,14 +121,14 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut controller = Controller::new(lbd, Clock); + let mut volume_mgr = VolumeManager::new(lbd, Clock); for i in 0..=3 { - let volume = controller.get_volume(VolumeIdx(i)); + let volume = volume_mgr.get_volume(VolumeIdx(i)); println!("volume {}: {:#?}", i, volume); if let Ok(mut volume) = volume { - let root_dir = controller.open_root_dir(&volume).unwrap(); + let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); println!("\tListing root directory:"); - controller + volume_mgr .iterate_dir(&volume, &root_dir, |x| { println!("\t\tFound: {:?}", x); }) @@ -137,15 +137,15 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_PRINT, - controller.find_directory_entry(&volume, &root_dir, FILE_TO_PRINT) + volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_PRINT) ); - let mut f = controller + 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 = controller.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -155,43 +155,43 @@ fn main() { } println!("EOF"); // Can't open file twice - assert!(controller + assert!(volume_mgr .open_file_in_dir(&mut volume, &root_dir, FILE_TO_PRINT, Mode::ReadOnly) .is_err()); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); - let test_dir = controller.open_dir(&volume, &root_dir, "TEST").unwrap(); + let test_dir = volume_mgr.open_dir(&volume, &root_dir, "TEST").unwrap(); // Check we can't open it twice - assert!(controller.open_dir(&volume, &root_dir, "TEST").is_err()); + assert!(volume_mgr.open_dir(&volume, &root_dir, "TEST").is_err()); // Print the contents println!("\tListing TEST directory:"); - controller + volume_mgr .iterate_dir(&volume, &test_dir, |x| { println!("\t\tFound: {:?}", x); }) .unwrap(); - controller.close_dir(&volume, test_dir); + 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 = controller + 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 = controller.read(&volume, &mut f, &mut buffer).unwrap(); + 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); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); - assert!(controller.open_root_dir(&volume).is_err()); - controller.close_dir(&volume, root_dir); - assert!(controller.open_root_dir(&volume).is_ok()); + 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 index 1281790..47acfec 100644 --- a/examples/write_test.rs +++ b/examples/write_test.rs @@ -18,8 +18,8 @@ extern crate embedded_sdmmc; const FILE_TO_WRITE: &str = "README.TXT"; use embedded_sdmmc::{ - Block, BlockCount, BlockDevice, BlockIdx, Controller, Error, Mode, TimeSource, Timestamp, - VolumeIdx, + Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, + VolumeManager, }; use std::cell::RefCell; use std::fs::{File, OpenOptions}; @@ -118,14 +118,14 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut controller = Controller::new(lbd, Clock); + let mut volume_mgr = VolumeManager::new(lbd, Clock); for volume_idx in 0..=3 { - let volume = controller.get_volume(VolumeIdx(volume_idx)); + let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); println!("volume {}: {:#?}", volume_idx, volume); if let Ok(mut volume) = volume { - let root_dir = controller.open_root_dir(&volume).unwrap(); + let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); println!("\tListing root directory:"); - controller + volume_mgr .iterate_dir(&volume, &root_dir, |x| { println!("\t\tFound: {:?}", x); }) @@ -134,14 +134,14 @@ fn main() { // 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 = controller + 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 = controller.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -150,17 +150,17 @@ fn main() { } } println!("EOF\n"); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); - let mut f = controller + 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 = controller.write(&mut volume, &mut f, &buffer1[..]).unwrap(); - let num_written = controller.write(&mut volume, &mut f, &buffer[..]).unwrap(); + 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(); @@ -168,12 +168,12 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_WRITE, - controller.find_directory_entry(&volume, &root_dir, 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 = controller.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -182,10 +182,10 @@ fn main() { } } println!("EOF"); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); println!("\nTruncating file"); - let mut f = controller + let mut f = volume_mgr .open_file_in_dir( &mut volume, &root_dir, @@ -195,20 +195,20 @@ fn main() { .unwrap(); let buffer = b"Hello\n"; - let num_written = controller.write(&mut volume, &mut f, &buffer[..]).unwrap(); + 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, - controller.find_directory_entry(&volume, &root_dir, 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 = controller.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -217,7 +217,7 @@ fn main() { } } println!("EOF"); - controller.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&volume, f).unwrap(); } } } diff --git a/src/fat/info.rs b/src/fat/info.rs index 85d6ad1..1f2a623 100644 --- a/src/fat/info.rs +++ b/src/fat/info.rs @@ -26,7 +26,8 @@ pub struct Fat32Info { #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, 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 controller) + /// The block the root directory starts in. Relative to start of partition + /// (so add `self.lba_offset` before passing to volume manager) pub(crate) first_root_dir_block: BlockCount, /// Number of entries in root directory (it's reserved and not in the FAT) pub(crate) root_entries_count: u16, diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 57d60e7..55514d8 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -11,8 +11,8 @@ use crate::{ Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, RESERVED_ENTRIES, }, - Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, Controller, DirEntry, Directory, - Error, ShortFileName, TimeSource, VolumeType, + Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, DirEntry, Directory, Error, + ShortFileName, TimeSource, VolumeManager, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; @@ -52,9 +52,11 @@ pub struct FatVolume { pub(crate) name: VolumeName, /// Number of 512 byte blocks (or Blocks) in a cluster pub(crate) blocks_per_cluster: u8, - /// The block the data starts in. Relative to start of partition (so add `self.lba_offset` before passing to controller) + /// The block the data starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) pub(crate) first_data_block: BlockCount, - /// The block the FAT starts in. Relative to start of partition (so add `self.lba_offset` before passing to controller) + /// The block the FAT starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) pub(crate) fat_start: BlockCount, /// Expected number of free clusters pub(crate) free_clusters_count: Option, @@ -70,7 +72,7 @@ impl FatVolume { /// Write a new entry in the FAT pub fn update_info_sector( &mut self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, ) -> Result<(), Error> where D: BlockDevice, @@ -83,7 +85,7 @@ impl FatVolume { return Ok(()); } let mut blocks = [Block::new()]; - controller + volume_mgr .block_device .read(&mut blocks, fat32_info.info_location, "read_info_sector") .map_err(Error::DeviceError)?; @@ -94,7 +96,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()); } - controller + volume_mgr .block_device .write(&blocks, fat32_info.info_location) .map_err(Error::DeviceError)?; @@ -114,7 +116,7 @@ impl FatVolume { /// Write a new entry in the FAT fn update_fat( &mut self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, cluster: Cluster, new_value: Cluster, ) -> Result<(), Error> @@ -129,7 +131,7 @@ 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; - controller + volume_mgr .block_device .read(&mut blocks, this_fat_block_num, "read_fat") .map_err(Error::DeviceError)?; @@ -150,7 +152,7 @@ impl FatVolume { let fat_offset = cluster.0 as u32 * 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; - controller + volume_mgr .block_device .read(&mut blocks, this_fat_block_num, "read_fat") .map_err(Error::DeviceError)?; @@ -170,7 +172,7 @@ impl FatVolume { ); } } - controller + volume_mgr .block_device .write(&blocks, this_fat_block_num) .map_err(Error::DeviceError)?; @@ -180,7 +182,7 @@ impl FatVolume { /// Look in the FAT to see which cluster comes next. pub(crate) fn next_cluster( &self, - controller: &Controller, + volume_mgr: &VolumeManager, cluster: Cluster, ) -> Result> where @@ -193,7 +195,7 @@ impl FatVolume { let fat_offset = cluster.0 * 2; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - controller + volume_mgr .block_device .read(&mut blocks, this_fat_block_num, "next_cluster") .map_err(Error::DeviceError)?; @@ -219,7 +221,7 @@ 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; - controller + volume_mgr .block_device .read(&mut blocks, this_fat_block_num, "next_cluster") .map_err(Error::DeviceError)?; @@ -255,7 +257,7 @@ impl FatVolume { /// Converts a cluster number (or `Cluster`) to a block number (or /// `BlockIdx`). Gives an absolute `BlockIdx` you can pass to the - /// controller. + /// volume manager. pub(crate) fn cluster_to_block(&self, cluster: Cluster) -> BlockIdx { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { @@ -287,7 +289,7 @@ impl FatVolume { /// needed pub(crate) fn write_new_directory_entry( &mut self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, dir: &Directory, name: ShortFileName, attributes: Attributes, @@ -314,7 +316,7 @@ impl FatVolume { }; while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - controller + volume_mgr .block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; @@ -324,7 +326,7 @@ 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 = controller.timesource.get_timestamp(); + let ctime = volume_mgr.timesource.get_timestamp(); let entry = DirEntry::new( name, attributes, @@ -335,7 +337,7 @@ impl FatVolume { ); blocks[0][start..start + 32] .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); - controller + volume_mgr .block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -344,13 +346,13 @@ impl FatVolume { } } if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(controller, cluster) { + 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(controller, Some(cluster), true)?; + let c = self.alloc_cluster(volume_mgr, Some(cluster), true)?; first_dir_block_num = self.cluster_to_block(c); Some(c) } @@ -373,7 +375,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) { - controller + volume_mgr .block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; @@ -383,7 +385,7 @@ 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 = controller.timesource.get_timestamp(); + let ctime = volume_mgr.timesource.get_timestamp(); let entry = DirEntry::new( name, attributes, @@ -394,7 +396,7 @@ impl FatVolume { ); blocks[0][start..start + 32] .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); - controller + volume_mgr .block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -402,13 +404,13 @@ impl FatVolume { } } } - current_cluster = match self.next_cluster(controller, cluster) { + 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(controller, Some(cluster), true)?; + let c = self.alloc_cluster(volume_mgr, Some(cluster), true)?; first_dir_block_num = self.cluster_to_block(c); Some(c) } @@ -424,7 +426,7 @@ impl FatVolume { /// Useful for performing directory listings. pub(crate) fn iterate_dir( &self, - controller: &Controller, + volume_mgr: &VolumeManager, dir: &Directory, mut func: F, ) -> Result<(), Error> @@ -450,7 +452,7 @@ impl FatVolume { let mut blocks = [Block::new()]; while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - controller + volume_mgr .block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; @@ -470,7 +472,7 @@ impl FatVolume { } } if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(controller, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -492,7 +494,7 @@ impl FatVolume { 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))) { - controller + volume_mgr .block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; @@ -511,7 +513,7 @@ impl FatVolume { } } } - current_cluster = match self.next_cluster(controller, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster) { Ok(n) => Some(n), _ => None, }; @@ -524,7 +526,7 @@ impl FatVolume { /// Get an entry from the given directory pub(crate) fn find_directory_entry( &self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, dir: &Directory, name: &str, ) -> Result> @@ -551,7 +553,7 @@ impl FatVolume { while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { match self.find_entry_in_block( - controller, + volume_mgr, FatType::Fat16, &match_name, block, @@ -561,7 +563,7 @@ impl FatVolume { } } if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(controller, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -583,7 +585,7 @@ impl FatVolume { 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( - controller, + volume_mgr, FatType::Fat32, &match_name, block, @@ -592,7 +594,7 @@ impl FatVolume { x => return x, } } - current_cluster = match self.next_cluster(controller, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster) { Ok(n) => Some(n), _ => None, } @@ -605,7 +607,7 @@ impl FatVolume { /// Finds an entry in a given block fn find_entry_in_block( &self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, fat_type: FatType, match_name: &ShortFileName, block: BlockIdx, @@ -615,7 +617,7 @@ impl FatVolume { T: TimeSource, { let mut blocks = [Block::new()]; - controller + volume_mgr .block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; @@ -639,7 +641,7 @@ impl FatVolume { /// Delete an entry from the given directory pub(crate) fn delete_directory_entry( &self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, dir: &Directory, name: &str, ) -> Result<(), Error> @@ -665,13 +667,13 @@ impl FatVolume { while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - match self.delete_entry_in_block(controller, &match_name, block) { + match self.delete_entry_in_block(volume_mgr, &match_name, block) { Err(Error::NotInBlock) => continue, x => return x, } } if cluster != Cluster::ROOT_DIR { - current_cluster = match self.next_cluster(controller, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -692,12 +694,12 @@ impl FatVolume { 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.delete_entry_in_block(controller, &match_name, block) { + match self.delete_entry_in_block(volume_mgr, &match_name, block) { Err(Error::NotInBlock) => continue, x => return x, } } - current_cluster = match self.next_cluster(controller, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster) { Ok(n) => Some(n), _ => None, } @@ -710,7 +712,7 @@ impl FatVolume { /// Deletes an entry in a given block fn delete_entry_in_block( &self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, match_name: &ShortFileName, block: BlockIdx, ) -> Result<(), Error> @@ -719,7 +721,7 @@ impl FatVolume { T: TimeSource, { let mut blocks = [Block::new()]; - controller + volume_mgr .block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; @@ -733,7 +735,7 @@ impl FatVolume { } else if dir_entry.matches(match_name) { let mut blocks = blocks; blocks[0].contents[start] = 0xE5; - controller + volume_mgr .block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -746,7 +748,7 @@ impl FatVolume { /// Finds the next free cluster after the start_cluster and before end_cluster pub(crate) fn find_next_free_cluster( &self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, start_cluster: Cluster, end_cluster: Cluster, ) -> Result> @@ -772,7 +774,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); - controller + volume_mgr .block_device .read(&mut blocks, this_fat_block_num, "next_cluster") .map_err(Error::DeviceError)?; @@ -804,7 +806,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); - controller + volume_mgr .block_device .read(&mut blocks, this_fat_block_num, "next_cluster") .map_err(Error::DeviceError)?; @@ -829,7 +831,7 @@ impl FatVolume { /// Tries to allocate a cluster pub(crate) fn alloc_cluster( &mut self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, prev_cluster: Option, zero: bool, ) -> Result> @@ -848,7 +850,7 @@ impl FatVolume { start_cluster, end_cluster ); - let new_cluster = match self.find_next_free_cluster(controller, 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 => { @@ -857,18 +859,18 @@ impl FatVolume { Cluster(RESERVED_ENTRIES), end_cluster ); - self.find_next_free_cluster(controller, 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(controller, new_cluster, Cluster::END_OF_FILE)?; + self.update_fat(volume_mgr, new_cluster, Cluster::END_OF_FILE)?; if let Some(cluster) = prev_cluster { trace!( "Updating old cluster {:?} to {:?} in FAT", cluster, new_cluster ); - self.update_fat(controller, cluster, new_cluster)?; + self.update_fat(volume_mgr, cluster, new_cluster)?; } trace!( "Finding next free between {:?}..={:?}", @@ -876,11 +878,11 @@ impl FatVolume { end_cluster ); self.next_free_cluster = - match self.find_next_free_cluster(controller, new_cluster, end_cluster) { + match self.find_next_free_cluster(volume_mgr, new_cluster, end_cluster) { Ok(cluster) => Some(cluster), Err(_) if new_cluster.0 > RESERVED_ENTRIES => { match self.find_next_free_cluster( - controller, + volume_mgr, Cluster(RESERVED_ENTRIES), end_cluster, ) { @@ -899,7 +901,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) { - controller + volume_mgr .block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -912,7 +914,7 @@ impl FatVolume { /// Marks the input cluster as an EOF and all the subsequent clusters in the chain as free pub(crate) fn truncate_cluster_chain( &mut self, - controller: &mut Controller, + volume_mgr: &mut VolumeManager, cluster: Cluster, ) -> Result<(), Error> where @@ -923,7 +925,7 @@ impl FatVolume { // file doesn't have any valid cluster allocated, there is nothing to do return Ok(()); } - let mut next = match self.next_cluster(controller, cluster) { + let mut next = match self.next_cluster(volume_mgr, cluster) { Ok(n) => n, Err(Error::EndOfFile) => return Ok(()), Err(e) => return Err(e), @@ -935,15 +937,15 @@ impl FatVolume { } else { self.next_free_cluster = Some(next); } - self.update_fat(controller, cluster, Cluster::END_OF_FILE)?; + self.update_fat(volume_mgr, cluster, Cluster::END_OF_FILE)?; loop { - match self.next_cluster(controller, next) { + match self.next_cluster(volume_mgr, next) { Ok(n) => { - self.update_fat(controller, next, Cluster::EMPTY)?; + self.update_fat(volume_mgr, next, Cluster::EMPTY)?; next = n; } Err(Error::EndOfFile) => { - self.update_fat(controller, next, Cluster::EMPTY)?; + self.update_fat(volume_mgr, next, Cluster::EMPTY)?; break; } Err(e) => return Err(e), @@ -959,7 +961,7 @@ impl FatVolume { /// Load the boot parameter block from the start of the given partition and /// determine if the partition contains a valid FAT16 or FAT32 file system. pub fn parse_volume( - controller: &mut Controller, + volume_mgr: &mut VolumeManager, lba_start: BlockIdx, num_blocks: BlockCount, ) -> Result> @@ -969,7 +971,7 @@ where D::Error: core::fmt::Debug, { let mut blocks = [Block::new()]; - controller + volume_mgr .block_device .read(&mut blocks, lba_start, "read_bpb") .map_err(Error::DeviceError)?; @@ -1014,7 +1016,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()]; - controller + volume_mgr .block_device .read( &mut info_blocks, diff --git a/src/lib.rs b/src/lib.rs index 6021832..7ee8398 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ //! # } //! # impl std::fmt::Write for DummyUart { fn write_str(&mut self, s: &str) -> std::fmt::Result { Ok(()) } } //! # use std::fmt::Write; -//! # use embedded_sdmmc::Controller; +//! # use embedded_sdmmc::VolumeManager; //! # let mut uart = DummyUart; //! # let mut sdmmc_spi = DummySpi; //! # let mut sdmmc_cs = DummyCsPin; @@ -40,19 +40,19 @@ //! write!(uart, "Init SD card...").unwrap(); //! match spi_dev.acquire() { //! Ok(block) => { -//! let mut cont: Controller< +//! let mut volume_mgr: VolumeManager< //! embedded_sdmmc::BlockSpi, //! DummyTimeSource, //! 4, //! 4, -//! > = Controller::new(block, time_source); +//! > = VolumeManager::new(block, time_source); //! write!(uart, "OK!\nCard size...").unwrap(); -//! match cont.device().card_size_bytes() { +//! match volume_mgr.device().card_size_bytes() { //! Ok(size) => writeln!(uart, "{}", size).unwrap(), //! Err(e) => writeln!(uart, "Err: {:?}", e).unwrap(), //! } //! write!(uart, "Volume 0...").unwrap(); -//! match cont.get_volume(embedded_sdmmc::VolumeIdx(0)) { +//! match volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0)) { //! Ok(v) => writeln!(uart, "{:?}", v).unwrap(), //! Err(e) => writeln!(uart, "Err: {:?}", e).unwrap(), //! } @@ -160,8 +160,8 @@ where NotInBlock, } -mod controller; -pub use controller::Controller; +mod volume_mgr; +pub use volume_mgr::VolumeManager; /// Represents a partition with a filesystem within it. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] @@ -460,8 +460,8 @@ mod tests { #[test] fn partition0() { - let mut c: Controller = - Controller::new_with_limits(DummyBlockDevice, Clock); + let mut c: VolumeManager = + VolumeManager::new_with_limits(DummyBlockDevice, Clock); let v = c.get_volume(VolumeIdx(0)).unwrap(); assert_eq!( diff --git a/src/sdmmc.rs b/src/sdmmc.rs index bd8da0a..70655f7 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -143,7 +143,7 @@ where CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, { - /// Create a new SD/MMC controller using a raw SPI interface. + /// Create a new SD/MMC interface using a raw SPI interface. pub fn new(spi: SPI, cs: CS) -> SdMmcSpi { SdMmcSpi { spi: RefCell::new(spi), diff --git a/src/controller.rs b/src/volume_mgr.rs similarity index 96% rename from src/controller.rs rename to src/volume_mgr.rs index c9a91b5..fe7fe56 100644 --- a/src/controller.rs +++ b/src/volume_mgr.rs @@ -16,8 +16,8 @@ use crate::{ PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, }; -/// A `Controller` wraps a block device and gives access to the volumes within it. -pub struct Controller +/// A `VolumeManager` wraps a block device and gives access to the volumes within it. +pub struct VolumeManager where D: BlockDevice, T: TimeSource, @@ -29,39 +29,39 @@ where open_files: [(VolumeIdx, Cluster); MAX_FILES], } -impl Controller +impl VolumeManager where D: BlockDevice, T: TimeSource, ::Error: core::fmt::Debug, { - /// Create a new Disk Controller using a generic `BlockDevice`. From this - /// controller we can open volumes (partitions) and with those we can open + /// Create a new Volume Manager using a generic `BlockDevice`. From this + /// object we can open volumes (partitions) and with those we can open /// files. /// - /// This creates a Controller with default values - /// MAX_DIRS = 4, MAX_FILES = 4. Call `Controller::new_with_limits(block_device, timesource)` + /// This creates a `VolumeManager` with default values + /// MAX_DIRS = 4, MAX_FILES = 4. Call `VolumeManager::new_with_limits(block_device, timesource)` /// if you need different limits. - pub fn new(block_device: D, timesource: T) -> Controller { + pub fn new(block_device: D, timesource: T) -> VolumeManager { Self::new_with_limits(block_device, timesource) } } -impl Controller +impl VolumeManager where D: BlockDevice, T: TimeSource, ::Error: core::fmt::Debug, { - /// Create a new Disk Controller using a generic `BlockDevice`. From this - /// controller we can open volumes (partitions) and with those we can open + /// Create a new Volume Manager using a generic `BlockDevice`. From this + /// object we can open volumes (partitions) and with those we can open /// files. pub fn new_with_limits( block_device: D, timesource: T, - ) -> Controller { - debug!("Creating new embedded-sdmmc::Controller"); - Controller { + ) -> VolumeManager { + debug!("Creating new embedded-sdmmc::VolumeManager"); + VolumeManager { block_device, timesource, open_dirs: [(VolumeIdx(0), Cluster::INVALID); MAX_DIRS], From 14b62bb3db8cfe30527f8ab6c05a6320cdac0c72 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 21 Apr 2023 16:49:57 +0100 Subject: [PATCH 08/69] Update copyright in MIT license. --- LICENSE-MIT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE-MIT b/LICENSE-MIT index 6ca3a9c..ec19225 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2018 Jonathan 'theJPster' Pallant +Copyright (c) 2018-2023 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated From 95df37ec9a033eab783245d586b6af54d06599df Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 21 Apr 2023 16:50:22 +0100 Subject: [PATCH 09/69] Clean up mod-level comments. --- src/blockdevice.rs | 5 +++-- src/sdmmc.rs | 2 +- src/sdmmc_proto.rs | 2 +- src/structure.rs | 2 +- src/volume_mgr.rs | 2 ++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 61651ae..b52b552 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -1,6 +1,7 @@ -//! embedded-sdmmc-rs - Block Device support +//! Block Device support //! -//! Generic code for handling block devices. +//! Generic code for handling block devices, such as types for identifying +//! a particular block on a block device by its index. /// Represents a standard 512 byte block (also known as a sector). IBM PC /// formatted 5.25" and 3.5" floppy disks, SD/MMC cards up to 1 GiB in size diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 70655f7..4e925bd 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -1,4 +1,4 @@ -//! embedded-sdmmc-rs - SDMMC Protocol +//! The SD/MMC Protocol //! //! Implements the SD/MMC protocol on some generic SPI interface. //! diff --git a/src/sdmmc_proto.rs b/src/sdmmc_proto.rs index 18e086b..d729725 100644 --- a/src/sdmmc_proto.rs +++ b/src/sdmmc_proto.rs @@ -1,4 +1,4 @@ -//! embedded-sdmmc-rs - Constants from the SD Specifications +//! Constants from the SD Specifications //! //! Based on SdFat, under the following terms: //! diff --git a/src/structure.rs b/src/structure.rs index f2e397c..fb0e553 100644 --- a/src/structure.rs +++ b/src/structure.rs @@ -1,4 +1,4 @@ -//! embedded-sdmmc-rs - Useful macros for parsing SD/MMC structures. +//! Useful macros for parsing SD/MMC structures. macro_rules! access_field { ($self:expr, $offset:expr, $start_bit:expr, 1) => { diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index fe7fe56..c0ecc76 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -1,3 +1,5 @@ +//! The Volume Manager handles partitions and open files on a block device. + use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; From a1aa27cb8117dd286015dd45a0efce9eb849ee66 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 21 Apr 2023 17:02:05 +0100 Subject: [PATCH 10/69] Improved examples in README and lib.rs --- README.md | 46 +++++++++++++++--------------- src/lib.rs | 84 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 9622c83..5bef9e6 100644 --- a/README.md +++ b/README.md @@ -14,25 +14,30 @@ You will need something that implements the `BlockDevice` trait, which can read // Build an SD Card interface out of an SPI device let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs); // Try and initialise the SD card -write!(uart, "Init SD card...").unwrap(); -match spi_dev.acquire() { - Ok(block) => { - // The SD Card initialised, and we have a `BlockSpi` object representing - // the initialised card. Now let's - let mut cont = embedded_sdmmc::VolumeManager::new(block, time_source); - write!(uart, "OK!\nCard size...").unwrap(); - match cont.device().card_size_bytes() { - Ok(size) => writeln!(uart, "{}", size).unwrap(), - Err(e) => writeln!(uart, "Err: {:?}", e).unwrap(), - } - write!(uart, "Volume 0...").unwrap(); - match cont.get_volume(embedded_sdmmc::VolumeIdx(0)) { - Ok(v) => writeln!(uart, "{:?}", v).unwrap(), - Err(e) => writeln!(uart, "Err: {:?}", e).unwrap(), - } +let block_dev = spi_dev.acquire()?; +// The SD Card was initialised, and we have a `BlockSpi` object +// representing the initialised card. +write!(uart, "Card size is {} bytes", block_dev.card_size_bytes()?)?; +// Now let's look for volumes (also known as partitions) on our block device. +let mut cont = embedded_sdmmc::VolumeManager::new(block_dev, time_source); +// Try and access Volume 0 (i.e. the first partition) +let mut volume = cont.get_volume(embedded_sdmmc::VolumeIdx(0))?; +writeln!(uart, "Volume 0: {:?}", v)?; +// Open the root directory +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)?; +// Print the contents of the file +while !my_file.eof() { + 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); } - Err(e) => writeln!(uart, "{:?}!", e).unwrap(), } +volume_mgr.close_file(&volume0, my_file)?; +volume_mgr.close_dir(&volume0, root_dir)?; ``` ### Open directories and files @@ -41,12 +46,7 @@ By default the `VolumeManager` will initialize with a maximum number of `4` open ```rust // Create a volume manager with a maximum of 6 open directories and 12 open files -let mut cont: VolumeManager< - embedded_sdmmc::BlockSpi, - DummyTimeSource, - 6, - 12, -> = VolumeManager::new_with_limits(block, time_source); +let mut cont: VolumeManager<_, _, 6, 12> = VolumeManager::new_with_limits(block, time_source); ``` ## Supported features diff --git a/src/lib.rs b/src/lib.rs index 7ee8398..b49240d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,17 +2,21 @@ //! //! > An SD/MMC Library written in Embedded Rust //! -//! This crate is intended to allow you to read/write files on a FAT formatted SD -//! card on your Rust Embedded device, as easily as using the `SdFat` Arduino -//! library. It is written in pure-Rust, is `#![no_std]` and does not use `alloc` -//! or `collections` to keep the memory footprint low. In the first instance it is -//! designed for readability and simplicity over performance. +//! This crate is intended to allow you to read/write files on a FAT formatted +//! SD card on your Rust Embedded device, as easily as using the `SdFat` Arduino +//! library. It is written in pure-Rust, is `#![no_std]` and does not use +//! `alloc` or `collections` to keep the memory footprint low. In the first +//! instance it is designed for readability and simplicity over performance. //! //! ## Using the crate //! -//! You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. +//! You will need something that implements the `BlockDevice` trait, which can +//! read and write the 512-byte blocks (or sectors) from your card. If you were +//! to implement this over USB Mass Storage, there's no reason this crate +//! couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` +//! suitable for reading SD and SDHC cards over SPI. //! -//! ```rust +//! ```rust,no_run //! # struct DummySpi; //! # struct DummyCsPin; //! # struct DummyUart; @@ -32,41 +36,41 @@ //! # impl std::fmt::Write for DummyUart { fn write_str(&mut self, s: &str) -> std::fmt::Result { Ok(()) } } //! # use std::fmt::Write; //! # use embedded_sdmmc::VolumeManager; -//! # let mut uart = DummyUart; +//! # fn main() -> Result<(), embedded_sdmmc::Error> { //! # let mut sdmmc_spi = DummySpi; //! # let mut sdmmc_cs = DummyCsPin; //! # let time_source = DummyTimeSource; //! let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs); -//! write!(uart, "Init SD card...").unwrap(); -//! match spi_dev.acquire() { -//! Ok(block) => { -//! let mut volume_mgr: VolumeManager< -//! embedded_sdmmc::BlockSpi, -//! DummyTimeSource, -//! 4, -//! 4, -//! > = VolumeManager::new(block, time_source); -//! write!(uart, "OK!\nCard size...").unwrap(); -//! match volume_mgr.device().card_size_bytes() { -//! Ok(size) => writeln!(uart, "{}", size).unwrap(), -//! Err(e) => writeln!(uart, "Err: {:?}", e).unwrap(), -//! } -//! write!(uart, "Volume 0...").unwrap(); -//! match volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0)) { -//! Ok(v) => writeln!(uart, "{:?}", v).unwrap(), -//! Err(e) => writeln!(uart, "Err: {:?}", e).unwrap(), -//! } +//! let block = spi_dev.acquire()?; +//! println!("Card size {} bytes", block.card_size_bytes()?); +//! let mut volume_mgr = VolumeManager::new(block, time_source); +//! println!("Card size is still {} bytes", volume_mgr.device().card_size_bytes()?); +//! let mut volume0 = volume_mgr.get_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 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); //! } -//! Err(e) => writeln!(uart, "{:?}!", e).unwrap(), -//! }; +//! } +//! volume_mgr.close_file(&volume0, my_file)?; +//! volume_mgr.close_dir(&volume0, root_dir); +//! # Ok(()) +//! # } //! ``` //! //! ## Features //! -//! * `defmt-log`: By turning off the default features and enabling the `defmt-log` feature you can -//! configure this crate to log messages over defmt instead. +//! * `defmt-log`: 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. +//! Make sure that either the `log` feature or the `defmt-log` feature is +//! enabled. #![cfg_attr(not(test), no_std)] #![deny(missing_docs)] @@ -99,6 +103,12 @@ pub use crate::filesystem::{ pub use crate::sdmmc::Error as SdMmcError; pub use crate::sdmmc::{BlockSpi, SdMmcSpi}; +mod volume_mgr; +pub use volume_mgr::VolumeManager; + +#[deprecated] +pub use volume_mgr::VolumeManager as Controller; + // **************************************************************************** // // Public Types @@ -160,8 +170,14 @@ where NotInBlock, } -mod volume_mgr; -pub use volume_mgr::VolumeManager; +impl From for Error +where + E: core::fmt::Debug, +{ + fn from(value: E) -> Error { + Error::DeviceError(value) + } +} /// Represents a partition with a filesystem within it. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] From 7c5ee327ef69e7969d406d36abb6b7b6e6481693 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 14:22:15 +0100 Subject: [PATCH 11/69] Renamed some types and cleaned up some clippy lints. Changed BlockSpi to AcquiredSdCard. Changed SdMmcSpi to SdCard. --- CHANGELOG.md | 2 ++ README.md | 4 ++-- src/fat/volume.rs | 2 +- src/filesystem/filename.rs | 7 +------ src/filesystem/timestamp.rs | 2 +- src/lib.rs | 4 ++-- src/sdmmc.rs | 33 +++++++++++++++++++-------------- src/volume_mgr.rs | 2 -- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f01b48..e5bbec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.4.0...develop - Renamed `Controller` to `VolumeManager`, to better describe what it does. +- Renamed `SdMmcSpi` to `SdCard` +- Renamed `BlockSpi` to `AcquiredSdCard` ## [Version 0.4.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.4.0) diff --git a/README.md b/README.md index 5bef9e6..33a5033 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ You will need something that implements the `BlockDevice` trait, which can read ```rust // Build an SD Card interface out of an SPI device -let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs); +let mut spi_dev = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); // Try and initialise the SD card let block_dev = spi_dev.acquire()?; -// The SD Card was initialised, and we have a `BlockSpi` object +// The SD Card was initialised, and we have a `AcquiredSdCard` object // representing the initialised card. write!(uart, "Card size is {} bytes", block_dev.card_size_bytes()?)?; // Now let's look for volumes (also known as partitions) on our block device. diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 55514d8..aae83cf 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -149,7 +149,7 @@ impl FatVolume { } FatSpecificInfo::Fat32(_fat32_info) => { // FAT32 => 4 bytes per entry - let fat_offset = cluster.0 as u32 * 4; + 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 diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index e0e13d6..fec87fa 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -81,12 +81,7 @@ impl ShortFileName { } } _ => { - let ch = if (b'a'..=b'z').contains(&ch) { - // Uppercase characters only - ch - 32 - } else { - ch - }; + let ch = ch.to_ascii_uppercase(); if seen_dot { if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) { sfn.contents[idx] = ch; diff --git a/src/filesystem/timestamp.rs b/src/filesystem/timestamp.rs index a189042..30c00d3 100644 --- a/src/filesystem/timestamp.rs +++ b/src/filesystem/timestamp.rs @@ -27,7 +27,7 @@ pub struct Timestamp { impl Timestamp { /// Create a `Timestamp` from the 16-bit FAT date and time fields. pub fn from_fat(date: u16, time: u16) -> Timestamp { - let year = (1980 + (date >> 9)) as u16; + let year = 1980 + (date >> 9); let month = ((date >> 5) & 0x000F) as u8; let day = (date & 0x001F) as u8; let hours = ((time >> 11) & 0x001F) as u8; diff --git a/src/lib.rs b/src/lib.rs index b49240d..64559c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,7 @@ //! # let mut sdmmc_spi = DummySpi; //! # let mut sdmmc_cs = DummyCsPin; //! # let time_source = DummyTimeSource; -//! let mut spi_dev = embedded_sdmmc::SdMmcSpi::new(sdmmc_spi, sdmmc_cs); +//! let mut spi_dev = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); //! let block = spi_dev.acquire()?; //! println!("Card size {} bytes", block.card_size_bytes()?); //! let mut volume_mgr = VolumeManager::new(block, time_source); @@ -101,7 +101,7 @@ pub use crate::filesystem::{ Timestamp, MAX_FILE_SIZE, }; pub use crate::sdmmc::Error as SdMmcError; -pub use crate::sdmmc::{BlockSpi, SdMmcSpi}; +pub use crate::sdmmc::{AcquiredSdCard, SdCard}; mod volume_mgr; pub use volume_mgr::VolumeManager; diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 4e925bd..a30b8f5 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -22,7 +22,7 @@ const DEFAULT_DELAY_COUNT: u32 = 32_000; /// Built from an SPI peripheral and a Chip /// Select pin. We need Chip Select to be separate so we can clock out some /// bytes without Chip Select asserted (which puts the card into SPI mode). -pub struct SdMmcSpi +pub struct SdCard where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, @@ -35,15 +35,16 @@ where } /// An initialized block device used to access the SD card. -/// **Caution**: any data must be flushed manually before dropping `BlockSpi`, see `deinit`. +/// +/// **Caution**: any data must be flushed manually before dropping `AcquiredSdCard`, see `deinit`. /// Uses SPI mode. -pub struct BlockSpi<'a, SPI, CS>(&'a mut SdMmcSpi) +pub struct AcquiredSdCard<'a, SPI, CS>(&'a mut SdCard) where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug; -/// The possible errors `SdMmcSpi` can generate. +/// The possible errors this crate can generate. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Copy, Clone)] pub enum Error { @@ -77,7 +78,7 @@ pub enum Error { GpioError, } -/// The possible states `SdMmcSpi` can be in. +/// The possible states `SdCard` can be in. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum State { @@ -92,6 +93,7 @@ pub enum State { /// The different types of card we support. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Copy, Clone, PartialEq)] +#[allow(clippy::upper_case_acronyms)] enum CardType { SD1, SD2, @@ -137,15 +139,15 @@ impl Default for AcquireOpts { } } -impl SdMmcSpi +impl SdCard where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, { /// Create a new SD/MMC interface using a raw SPI interface. - pub fn new(spi: SPI, cs: CS) -> SdMmcSpi { - SdMmcSpi { + pub fn new(spi: SPI, cs: CS) -> SdCard { + SdCard { spi: RefCell::new(spi), cs: RefCell::new(cs), card_type: CardType::SD1, @@ -165,12 +167,15 @@ where } /// Initializes the card into a known state - pub fn acquire(&mut self) -> Result, Error> { + pub fn acquire(&mut self) -> Result, Error> { self.acquire_with_opts(Default::default()) } /// Initializes the card into a known state - pub fn acquire_with_opts(&mut self, options: AcquireOpts) -> Result, Error> { + pub fn acquire_with_opts( + &mut self, + options: AcquireOpts, + ) -> Result, Error> { debug!("acquiring card with opts: {:?}", options); let f = |s: &mut Self| { // Assume it hasn't worked @@ -269,7 +274,7 @@ where let result = f(self); self.cs_high()?; let _ = self.receive(); - result.map(move |()| BlockSpi(self)) + result.map(move |()| AcquiredSdCard(self)) } /// Perform a function that might error with the chipselect low. @@ -377,7 +382,7 @@ where } } -impl BlockSpi<'_, SPI, CS> +impl AcquiredSdCard<'_, SPI, CS> where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, @@ -517,7 +522,7 @@ impl> BlockDevice for T { } } -impl BlockDevice for BlockSpi<'_, SPI, CS> +impl BlockDevice for AcquiredSdCard<'_, SPI, CS> where SPI: embedded_hal::blocking::spi::Transfer, >::Error: core::fmt::Debug, @@ -595,7 +600,7 @@ where } } -impl Drop for BlockSpi<'_, SPI, CS> +impl Drop for AcquiredSdCard<'_, SPI, CS> where SPI: embedded_hal::blocking::spi::Transfer, >::Error: core::fmt::Debug, diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index c0ecc76..aa66410 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -237,7 +237,6 @@ where break; } } - drop(dir); } /// Look in a directory for a named file. @@ -603,7 +602,6 @@ where break; } } - drop(file); Ok(()) } From 261c52e8e469ef372374c7fe07ea120e7cc75a57 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 14:25:41 +0100 Subject: [PATCH 12/69] Fail if a logging feature isn't set. Avoids errors like 'info! not defined', or 'info! defined twice'. --- src/sdmmc.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index a30b8f5..62c1789 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -16,6 +16,12 @@ use log::{debug, trace, warn}; #[cfg(feature = "defmt-log")] use defmt::{debug, trace, warn}; +#[cfg(all(feature = "defmt-log", feature = "log"))] +compile_error!("Cannot enable both log and defmt-log"); + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +compile_error!("Must enable either log or defmt-log"); + const DEFAULT_DELAY_COUNT: u32 = 32_000; /// Represents an inactive SD Card interface. From aba344922a92e3689f8787873cffe76c3f334ad8 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 15:17:33 +0100 Subject: [PATCH 13/69] Reworked the API. Now there is only "SdCard" in the public API. Any attempt to use the SD card when the card type is unknown will force a re-initialisation of the SD card. You no longer need to acquire. --- README.md | 10 +- src/lib.rs | 9 +- src/sdmmc.rs | 756 +++++++++++++++++++++++++-------------------------- 3 files changed, 383 insertions(+), 392 deletions(-) diff --git a/README.md b/README.md index 33a5033..e2d1f0b 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,10 @@ You will need something that implements the `BlockDevice` trait, which can read ```rust // Build an SD Card interface out of an SPI device -let mut spi_dev = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); -// Try and initialise the SD card -let block_dev = spi_dev.acquire()?; -// The SD Card was initialised, and we have a `AcquiredSdCard` object -// representing the initialised card. -write!(uart, "Card size is {} bytes", block_dev.card_size_bytes()?)?; +let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); +write!(uart, "Card size is {} bytes", sdcard.card_size_bytes()?)?; // Now let's look for volumes (also known as partitions) on our block device. -let mut cont = embedded_sdmmc::VolumeManager::new(block_dev, time_source); +let mut cont = embedded_sdmmc::VolumeManager::new(sdcard, time_source); // Try and access Volume 0 (i.e. the first partition) let mut volume = cont.get_volume(embedded_sdmmc::VolumeIdx(0))?; writeln!(uart, "Volume 0: {:?}", v)?; diff --git a/src/lib.rs b/src/lib.rs index 64559c8..51e09f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,10 +40,9 @@ //! # let mut sdmmc_spi = DummySpi; //! # let mut sdmmc_cs = DummyCsPin; //! # let time_source = DummyTimeSource; -//! let mut spi_dev = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); -//! let block = spi_dev.acquire()?; -//! println!("Card size {} bytes", block.card_size_bytes()?); -//! let mut volume_mgr = VolumeManager::new(block, time_source); +//! let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); +//! println!("Card size {} bytes", sdcard.card_size_bytes()?); +//! let mut volume_mgr = VolumeManager::new(sdcard, time_source); //! println!("Card size is still {} bytes", volume_mgr.device().card_size_bytes()?); //! let mut volume0 = volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; //! println!("Volume 0: {:?}", volume0); @@ -101,7 +100,7 @@ pub use crate::filesystem::{ Timestamp, MAX_FILE_SIZE, }; pub use crate::sdmmc::Error as SdMmcError; -pub use crate::sdmmc::{AcquiredSdCard, SdCard}; +pub use crate::sdmmc::SdCard; mod volume_mgr; pub use volume_mgr::VolumeManager; diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 62c1789..f07a2be 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -8,7 +8,10 @@ use super::sdmmc_proto::*; use super::{Block, BlockCount, BlockDevice, BlockIdx}; use core::cell::RefCell; -use core::ops::Deref; + +// ============================================================================= +// Imports +// ============================================================================= #[cfg(feature = "log")] use log::{debug, trace, warn}; @@ -22,170 +25,332 @@ compile_error!("Cannot enable both log and defmt-log"); #[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] compile_error!("Must enable either log or defmt-log"); +// ============================================================================= +// Constants +// ============================================================================= + const DEFAULT_DELAY_COUNT: u32 = 32_000; -/// Represents an inactive SD Card interface. -/// Built from an SPI peripheral and a Chip -/// Select pin. We need Chip Select to be separate so we can clock out some -/// bytes without Chip Select asserted (which puts the card into SPI mode). +// ============================================================================= +// Types and Implementations +// ============================================================================= + +/// Represents an SD Card on an SPI bus. +/// +/// Built from an SPI peripheral and a Chip Select pin. We need Chip Select to +/// be separate so we can clock out some bytes without Chip Select asserted +/// (which puts the card into SPI mode). +/// +/// All the APIs take `&self` - mutability is handled using an inner `RefCell`. pub struct SdCard where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, { - spi: RefCell, - cs: RefCell, - card_type: CardType, - state: State, + inner: RefCell>, } -/// An initialized block device used to access the SD card. -/// -/// **Caution**: any data must be flushed manually before dropping `AcquiredSdCard`, see `deinit`. -/// Uses SPI mode. -pub struct AcquiredSdCard<'a, SPI, CS>(&'a mut SdCard) +impl SdCard where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, - >::Error: core::fmt::Debug; + >::Error: core::fmt::Debug, +{ + /// Create a new SD/MMC interface using a raw SPI interface. + pub fn new(spi: SPI, cs: CS) -> SdCard { + SdCard { + inner: RefCell::new(SdCardInner { + spi, + cs, + card_type: CardType::Unknown, + options: AcquireOpts { require_crc: true }, + }), + } + } -/// The possible errors this crate can generate. -#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone)] -pub enum Error { - /// We got an error from the SPI peripheral - Transport, - /// We failed to enable CRC checking on the SD card - CantEnableCRC, - /// We didn't get a response when reading data from the card - TimeoutReadBuffer, - /// We didn't get a response when waiting for the card to not be busy - TimeoutWaitNotBusy, - /// We didn't get a response when executing this command - TimeoutCommand(u8), - /// We didn't get a response when executing this application-specific command - TimeoutACommand(u8), - /// We got a bad response from Command 58 - Cmd58Error, - /// We failed to read the Card Specific Data register - RegisterReadError, - /// We got a CRC mismatch (card gave us, we calculated) - CrcError(u16, u16), - /// Error reading from the card - ReadError, - /// Error writing to the card - WriteError, - /// Can't perform this operation with the card in this state - BadState, - /// Couldn't find the card - CardNotFound, - /// Couldn't set a GPIO pin - GpioError, -} + /// Construct a new SD/MMC interface, using the given options. + pub fn new_with_options(spi: SPI, cs: CS, options: AcquireOpts) -> SdCard { + SdCard { + inner: RefCell::new(SdCardInner { + spi, + cs, + card_type: CardType::Unknown, + options, + }), + } + } -/// The possible states `SdCard` can be in. -#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum State { - /// Card is not initialised - NoInit, - /// Card is in an error state - Error, - /// Card is initialised and idle - Idle, -} + /// Get a temporary borrow on the underlying SPI device. Useful if you + /// need to re-clock the SPI. + pub fn spi(&self, func: F) -> T + where + F: FnOnce(&mut SPI) -> T, + { + let mut inner = self.inner.borrow_mut(); + let result = func(&mut inner.spi); + result + } -/// The different types of card we support. -#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq)] -#[allow(clippy::upper_case_acronyms)] -enum CardType { - SD1, - SD2, - SDHC, + /// Return the usable size of this SD card in bytes. + pub fn card_size_bytes(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.card_size_bytes() + } + + /// Can this card erase single blocks? + pub fn erase_single_block_enabled(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.erase_single_block_enabled() + } } -/// A terrible hack for busy-waiting the CPU while we wait for the card to -/// sort itself out. -/// -/// @TODO replace this! -struct Delay(u32); +impl BlockDevice for SdCard +where + SPI: embedded_hal::blocking::spi::Transfer, + >::Error: core::fmt::Debug, + CS: embedded_hal::digital::v2::OutputPin, +{ + type Error = Error; -impl Delay { - fn new() -> Delay { - Delay(DEFAULT_DELAY_COUNT) + /// 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> { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.read(blocks, start_block_idx, _reason) } - fn delay(&mut self, err: Error) -> Result<(), Error> { - if self.0 == 0 { - Err(err) - } else { - let dummy_var: u32 = 0; - for _ in 0..100 { - unsafe { core::ptr::read_volatile(&dummy_var) }; - } - self.0 -= 1; - Ok(()) - } + /// Write one or more blocks, starting at the given block index. + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.write(blocks, start_block_idx) } -} -/// Options for acquiring the card. -#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug)] -pub struct AcquireOpts { - /// Some cards don't support CRC mode. At least a 512MiB Transcend one. - pub require_crc: bool, + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.num_blocks() + } } -impl Default for AcquireOpts { - fn default() -> Self { - AcquireOpts { require_crc: true } - } +/// Represents an SD Card on an SPI bus. +/// +/// All the APIs required `&mut self`. +struct SdCardInner +where + SPI: embedded_hal::blocking::spi::Transfer, + CS: embedded_hal::digital::v2::OutputPin, + >::Error: core::fmt::Debug, +{ + spi: SPI, + cs: CS, + card_type: CardType, + options: AcquireOpts, } -impl SdCard +impl SdCardInner where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, { - /// Create a new SD/MMC interface using a raw SPI interface. - pub fn new(spi: SPI, cs: CS) -> SdCard { - SdCard { - spi: RefCell::new(spi), - cs: RefCell::new(cs), - card_type: CardType::SD1, - state: State::NoInit, + /// Read one or more blocks, starting at the given block index. + fn read( + &mut self, + blocks: &mut [Block], + start_block_idx: BlockIdx, + _reason: &str, + ) -> Result<(), Error> { + let start_idx = match self.card_type { + CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, + CardType::Sdhc => start_block_idx.0, + CardType::Unknown => return Err(Error::CardNotFound), + }; + self.with_chip_select(|s| { + if blocks.len() == 1 { + // Start a single-block read + s.card_command(CMD17, start_idx)?; + s.read_data(&mut blocks[0].contents)?; + } else { + // Start a multi-block read + s.card_command(CMD18, start_idx)?; + for block in blocks.iter_mut() { + s.read_data(&mut block.contents)?; + } + // Stop the read + s.card_command(CMD12, 0)?; + } + Ok(()) + }) + } + + /// Write one or more blocks, starting at the given block index. + fn write(&mut self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Error> { + let start_idx = match self.card_type { + CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, + CardType::Sdhc => start_block_idx.0, + CardType::Unknown => return Err(Error::CardNotFound), + }; + self.with_chip_select(|s| { + if blocks.len() == 1 { + // Start a single-block write + s.card_command(CMD24, start_idx)?; + s.write_data(DATA_START_BLOCK, &blocks[0].contents)?; + s.wait_not_busy()?; + if s.card_command(CMD13, 0)? != 0x00 { + return Err(Error::WriteError); + } + if s.receive()? != 0x00 { + return Err(Error::WriteError); + } + } else { + // Start a multi-block write + s.card_command(CMD25, start_idx)?; + for block in blocks.iter() { + s.wait_not_busy()?; + s.write_data(WRITE_MULTIPLE_TOKEN, &block.contents)?; + } + // Stop the write + s.wait_not_busy()?; + s.send(STOP_TRAN_TOKEN)?; + } + Ok(()) + }) + } + + /// Determine how many blocks this device can hold. + fn num_blocks(&mut self) -> Result { + let num_bytes = self.card_size_bytes()?; + let num_blocks = (num_bytes / 512) as u32; + Ok(BlockCount(num_blocks)) + } + + /// Return the usable size of this SD card in bytes. + fn card_size_bytes(&mut self) -> Result { + self.with_chip_select(|s| { + let csd = s.read_csd()?; + match csd { + Csd::V1(ref contents) => Ok(contents.card_capacity_bytes()), + Csd::V2(ref contents) => Ok(contents.card_capacity_bytes()), + } + }) + } + + /// Can this card erase single blocks? + pub fn erase_single_block_enabled(&mut self) -> Result { + self.with_chip_select(|s| { + let csd = s.read_csd()?; + match csd { + Csd::V1(ref contents) => Ok(contents.erase_single_block_enabled()), + Csd::V2(ref contents) => Ok(contents.erase_single_block_enabled()), + } + }) + } + + /// Read the 'card specific data' block. + fn read_csd(&mut self) -> Result { + match self.card_type { + CardType::SD1 => { + let mut csd = CsdV1::new(); + if self.card_command(CMD9, 0)? != 0 { + return Err(Error::RegisterReadError); + } + self.read_data(&mut csd.data)?; + Ok(Csd::V1(csd)) + } + CardType::SD2 | CardType::Sdhc => { + let mut csd = CsdV2::new(); + if self.card_command(CMD9, 0)? != 0 { + return Err(Error::RegisterReadError); + } + self.read_data(&mut csd.data)?; + Ok(Csd::V2(csd)) + } + CardType::Unknown => Err(Error::CardNotFound), } } - fn cs_high(&self) -> Result<(), Error> { - self.cs - .borrow_mut() - .set_high() - .map_err(|_| Error::GpioError) + /// Read an arbitrary number of bytes from the card. Always fills the + /// given buffer, so make sure it's the right size. + fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + // Get first non-FF byte. + let mut delay = Delay::new(); + let status = loop { + let s = self.receive()?; + if s != 0xFF { + break s; + } + delay.delay(Error::TimeoutReadBuffer)?; + }; + if status != DATA_START_BLOCK { + return Err(Error::ReadError); + } + + self.spi.transfer(buffer).map_err(|_e| Error::Transport)?; + + let mut crc = u16::from(self.receive()?); + crc <<= 8; + crc |= u16::from(self.receive()?); + + let calc_crc = crc16(buffer); + if crc != calc_crc { + return Err(Error::CrcError(crc, calc_crc)); + } + + Ok(()) } - fn cs_low(&self) -> Result<(), Error> { - self.cs.borrow_mut().set_low().map_err(|_| Error::GpioError) + /// Write an arbitrary number of bytes to the card. + fn write_data(&mut self, token: u8, buffer: &[u8]) -> Result<(), Error> { + let calc_crc = crc16(buffer); + self.send(token)?; + for &b in buffer.iter() { + self.send(b)?; + } + self.send((calc_crc >> 8) as u8)?; + self.send(calc_crc as u8)?; + let status = self.receive()?; + if (status & DATA_RES_MASK) != DATA_RES_ACCEPTED { + Err(Error::WriteError) + } else { + Ok(()) + } } - /// Initializes the card into a known state - pub fn acquire(&mut self) -> Result, Error> { - self.acquire_with_opts(Default::default()) + fn cs_high(&mut self) -> Result<(), Error> { + self.cs.set_high().map_err(|_| Error::GpioError) } - /// Initializes the card into a known state - pub fn acquire_with_opts( - &mut self, - options: AcquireOpts, - ) -> Result, Error> { - debug!("acquiring card with opts: {:?}", options); + fn cs_low(&mut self) -> Result<(), Error> { + self.cs.set_low().map_err(|_| Error::GpioError) + } + + /// Check the card is initialised. + fn check_init(&mut self) -> Result<(), Error> { + if self.card_type == CardType::Unknown { + // If we don't know what the card type is, try and initialise the + // card. This will tell us what type of card it is. + self.acquire() + } else { + Ok(()) + } + } + + /// Initializes the card into a known state (or at least tries to). + fn acquire(&mut self) -> Result<(), Error> { + debug!("acquiring card with opts: {:?}", self.options); let f = |s: &mut Self| { // Assume it hasn't worked - s.state = State::Error; + let mut card_type; trace!("Reset card.."); // Supply minimum of 74 clock cycles without CS asserted. s.cs_high()?; @@ -229,77 +394,60 @@ where return Err(Error::CardNotFound); } // Enable CRC - debug!("Enable CRC: {}", options.require_crc); - if s.card_command(CMD59, 1)? != R1_IDLE_STATE && options.require_crc { + debug!("Enable CRC: {}", s.options.require_crc); + if s.card_command(CMD59, 1)? != R1_IDLE_STATE && s.options.require_crc { return Err(Error::CantEnableCRC); } // Check card version let mut delay = Delay::new(); - loop { + let arg = loop { if s.card_command(CMD8, 0x1AA)? == (R1_ILLEGAL_COMMAND | R1_IDLE_STATE) { - s.card_type = CardType::SD1; - break; + card_type = CardType::SD1; + break 0; } s.receive()?; s.receive()?; s.receive()?; let status = s.receive()?; if status == 0xAA { - s.card_type = CardType::SD2; - break; + card_type = CardType::SD2; + break 0x4000_0000; } delay.delay(Error::TimeoutCommand(CMD8))?; - } - debug!("Card version: {:?}", s.card_type); - - let arg = match s.card_type { - CardType::SD1 => 0, - CardType::SD2 | CardType::SDHC => 0x4000_0000, }; + debug!("Card version: {:?}", card_type); let mut delay = Delay::new(); while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { delay.delay(Error::TimeoutACommand(ACMD41))?; } - if s.card_type == CardType::SD2 { + if card_type == CardType::SD2 { if s.card_command(CMD58, 0)? != 0 { return Err(Error::Cmd58Error); } if (s.receive()? & 0xC0) == 0xC0 { - s.card_type = CardType::SDHC; + card_type = CardType::Sdhc; } // Discard other three bytes s.receive()?; s.receive()?; s.receive()?; } - s.state = State::Idle; + s.card_type = card_type; Ok(()) }; let result = f(self); self.cs_high()?; let _ = self.receive(); - result.map(move |()| AcquiredSdCard(self)) - } - - /// Perform a function that might error with the chipselect low. - /// Always releases the chipselect, even if the function errors. - fn with_chip_select_mut(&self, func: F) -> Result - where - F: FnOnce(&Self) -> Result, - { - self.cs_low()?; - let result = func(self); - self.cs_high()?; result } /// Perform a function that might error with the chipselect low. /// Always releases the chipselect, even if the function errors. - fn with_chip_select(&self, func: F) -> Result + fn with_chip_select(&mut self, func: F) -> Result where - F: FnOnce(&Self) -> Result, + F: FnOnce(&mut Self) -> Result, { self.cs_low()?; let result = func(self); @@ -308,13 +456,13 @@ where } /// Perform an application-specific command. - fn card_acmd(&self, command: u8, arg: u32) -> Result { + fn card_acmd(&mut self, command: u8, arg: u32) -> Result { self.card_command(CMD55, 0)?; self.card_command(command, arg) } /// Perform a command. - fn card_command(&self, command: u8, arg: u32) -> Result { + fn card_command(&mut self, command: u8, arg: u32) -> Result { if command != CMD0 && command != CMD12 { self.wait_not_busy()?; } @@ -349,27 +497,27 @@ where } /// Receive a byte from the SD card by clocking in an 0xFF byte. - fn receive(&self) -> Result { + fn receive(&mut self) -> Result { self.transfer(0xFF) } /// Send a byte from the SD card. - fn send(&self, out: u8) -> Result<(), Error> { + fn send(&mut self, out: u8) -> Result<(), Error> { let _ = self.transfer(out)?; Ok(()) } /// Send one byte and receive one byte. - fn transfer(&self, out: u8) -> Result { - let mut spi = self.spi.borrow_mut(); - spi.transfer(&mut [out]) + fn transfer(&mut self, out: u8) -> Result { + self.spi + .transfer(&mut [out]) .map(|b| b[0]) .map_err(|_e| Error::Transport) } /// Spin until the card returns 0xFF, or we spin too many times and /// timeout. - fn wait_not_busy(&self) -> Result<(), Error> { + fn wait_not_busy(&mut self) -> Result<(), Error> { let mut delay = Delay::new(); loop { let s = self.receive()?; @@ -380,240 +528,88 @@ where } Ok(()) } - - /// Get a temporary borrow on the underlying SPI device. Useful if you - /// need to re-clock the SPI. - pub fn spi(&mut self) -> core::cell::RefMut { - self.spi.borrow_mut() - } } -impl AcquiredSdCard<'_, SPI, CS> -where - SPI: embedded_hal::blocking::spi::Transfer, - CS: embedded_hal::digital::v2::OutputPin, - >::Error: core::fmt::Debug, -{ - /// Get a temporary borrow on the underlying SPI device. Useful if you - /// need to re-clock the SPI. - pub fn spi(&mut self) -> core::cell::RefMut { - self.0.spi.borrow_mut() - } - - /// Mark the card as unused. - /// This should be kept infallible, because Drop is unable to fail. - /// See https://github.com/rust-lang/rfcs/issues/814 - // If there is any need to flush data, it should be implemented here. - fn deinit(&mut self) { - self.0.state = State::NoInit; - } - - /// Return the usable size of this SD card in bytes. - pub fn card_size_bytes(&self) -> Result { - self.0.with_chip_select(|_s| { - let csd = self.read_csd()?; - match csd { - Csd::V1(ref contents) => Ok(contents.card_capacity_bytes()), - Csd::V2(ref contents) => Ok(contents.card_capacity_bytes()), - } - }) - } - - /// Erase some blocks on the card. - pub fn erase(&mut self, _first_block: BlockIdx, _last_block: BlockIdx) -> Result<(), Error> { - unimplemented!(); - } - - /// Can this card erase single blocks? - pub fn erase_single_block_enabled(&self) -> Result { - self.0.with_chip_select(|_s| { - let csd = self.read_csd()?; - match csd { - Csd::V1(ref contents) => Ok(contents.erase_single_block_enabled()), - Csd::V2(ref contents) => Ok(contents.erase_single_block_enabled()), - } - }) - } - - /// Read the 'card specific data' block. - fn read_csd(&self) -> Result { - match self.0.card_type { - CardType::SD1 => { - let mut csd = CsdV1::new(); - if self.0.card_command(CMD9, 0)? != 0 { - return Err(Error::RegisterReadError); - } - self.read_data(&mut csd.data)?; - Ok(Csd::V1(csd)) - } - CardType::SD2 | CardType::SDHC => { - let mut csd = CsdV2::new(); - if self.0.card_command(CMD9, 0)? != 0 { - return Err(Error::RegisterReadError); - } - self.read_data(&mut csd.data)?; - Ok(Csd::V2(csd)) - } - } - } - - /// Read an arbitrary number of bytes from the card. Always fills the - /// given buffer, so make sure it's the right size. - fn read_data(&self, buffer: &mut [u8]) -> Result<(), Error> { - // Get first non-FF byte. - let mut delay = Delay::new(); - let status = loop { - let s = self.0.receive()?; - if s != 0xFF { - break s; - } - delay.delay(Error::TimeoutReadBuffer)?; - }; - if status != DATA_START_BLOCK { - return Err(Error::ReadError); - } - - for b in buffer.iter_mut() { - *b = self.0.receive()?; - } - - let mut crc = u16::from(self.0.receive()?); - crc <<= 8; - crc |= u16::from(self.0.receive()?); - - let calc_crc = crc16(buffer); - if crc != calc_crc { - return Err(Error::CrcError(crc, calc_crc)); - } - - Ok(()) - } - - /// Write an arbitrary number of bytes to the card. - fn write_data(&self, token: u8, buffer: &[u8]) -> Result<(), Error> { - let calc_crc = crc16(buffer); - self.0.send(token)?; - for &b in buffer.iter() { - self.0.send(b)?; - } - self.0.send((calc_crc >> 8) as u8)?; - self.0.send(calc_crc as u8)?; - let status = self.0.receive()?; - if (status & DATA_RES_MASK) != DATA_RES_ACCEPTED { - Err(Error::WriteError) - } else { - Ok(()) - } - } +/// Options for acquiring the card. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug)] +pub struct AcquireOpts { + /// Some cards don't support CRC mode. At least a 512MiB Transcend one. + pub require_crc: bool, } -impl> BlockDevice for T { - type Error = U::Error; - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { - self.deref().read(blocks, start_block_idx, _reason) +impl Default for AcquireOpts { + fn default() -> Self { + AcquireOpts { require_crc: true } } +} - /// Write one or more blocks, starting at the given block index. - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { - self.deref().write(blocks, start_block_idx) - } +/// The possible errors this crate can generate. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone)] +pub enum Error { + /// We got an error from the SPI peripheral + Transport, + /// We failed to enable CRC checking on the SD card + CantEnableCRC, + /// We didn't get a response when reading data from the card + TimeoutReadBuffer, + /// We didn't get a response when waiting for the card to not be busy + TimeoutWaitNotBusy, + /// We didn't get a response when executing this command + TimeoutCommand(u8), + /// We didn't get a response when executing this application-specific command + TimeoutACommand(u8), + /// We got a bad response from Command 58 + Cmd58Error, + /// We failed to read the Card Specific Data register + RegisterReadError, + /// We got a CRC mismatch (card gave us, we calculated) + CrcError(u16, u16), + /// Error reading from the card + ReadError, + /// Error writing to the card + WriteError, + /// Can't perform this operation with the card in this state + BadState, + /// Couldn't find the card + CardNotFound, + /// Couldn't set a GPIO pin + GpioError, +} - fn num_blocks(&self) -> Result { - self.deref().num_blocks() - } +/// The different types of card we support. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq)] +enum CardType { + Unknown, + SD1, + SD2, + Sdhc, } -impl BlockDevice for AcquiredSdCard<'_, SPI, CS> -where - SPI: embedded_hal::blocking::spi::Transfer, - >::Error: core::fmt::Debug, - CS: embedded_hal::digital::v2::OutputPin, -{ - type Error = Error; +/// A terrible hack for busy-waiting the CPU while we wait for the card to +/// sort itself out. +/// +/// @TODO replace this! +struct Delay(u32); - /// 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> { - let start_idx = match self.0.card_type { - CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, - CardType::SDHC => start_block_idx.0, - }; - self.0.with_chip_select(|s| { - if blocks.len() == 1 { - // Start a single-block read - s.card_command(CMD17, start_idx)?; - self.read_data(&mut blocks[0].contents)?; - } else { - // Start a multi-block read - s.card_command(CMD18, start_idx)?; - for block in blocks.iter_mut() { - self.read_data(&mut block.contents)?; - } - // Stop the read - s.card_command(CMD12, 0)?; - } - Ok(()) - }) +impl Delay { + fn new() -> Delay { + Delay(DEFAULT_DELAY_COUNT) } - /// Write one or more blocks, starting at the given block index. - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { - let start_idx = match self.0.card_type { - CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, - CardType::SDHC => start_block_idx.0, - }; - self.0.with_chip_select_mut(|s| { - if blocks.len() == 1 { - // Start a single-block write - s.card_command(CMD24, start_idx)?; - self.write_data(DATA_START_BLOCK, &blocks[0].contents)?; - s.wait_not_busy()?; - if s.card_command(CMD13, 0)? != 0x00 { - return Err(Error::WriteError); - } - if s.receive()? != 0x00 { - return Err(Error::WriteError); - } - } else { - // Start a multi-block write - s.card_command(CMD25, start_idx)?; - for block in blocks.iter() { - s.wait_not_busy()?; - self.write_data(WRITE_MULTIPLE_TOKEN, &block.contents)?; - } - // Stop the write - s.wait_not_busy()?; - s.send(STOP_TRAN_TOKEN)?; + fn delay(&mut self, err: Error) -> Result<(), Error> { + if self.0 == 0 { + Err(err) + } else { + let dummy_var: u32 = 0; + for _ in 0..100 { + unsafe { core::ptr::read_volatile(&dummy_var) }; } + self.0 -= 1; Ok(()) - }) - } - - /// Determine how many blocks this device can hold. - fn num_blocks(&self) -> Result { - let num_bytes = self.card_size_bytes()?; - let num_blocks = (num_bytes / 512) as u32; - Ok(BlockCount(num_blocks)) - } -} - -impl Drop for AcquiredSdCard<'_, SPI, CS> -where - SPI: embedded_hal::blocking::spi::Transfer, - >::Error: core::fmt::Debug, - CS: embedded_hal::digital::v2::OutputPin, -{ - fn drop(&mut self) { - self.deinit() + } } } From debe580d185f073b24be260b6f72f0b95afcd812 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 18:06:25 +0100 Subject: [PATCH 14/69] Make CSD printable with defmt --- src/sdmmc_proto.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sdmmc_proto.rs b/src/sdmmc_proto.rs index d729725..1d34b0f 100644 --- a/src/sdmmc_proto.rs +++ b/src/sdmmc_proto.rs @@ -91,20 +91,24 @@ pub const DATA_RES_MASK: u8 = 0x1F; pub const DATA_RES_ACCEPTED: u8 = 0x05; /// Card Specific Data, version 1 -#[derive(Default)] +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] pub struct CsdV1 { /// The 16-bytes of data in this Card Specific Data block pub data: [u8; 16], } /// Card Specific Data, version 2 -#[derive(Default)] +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] pub struct CsdV2 { /// The 16-bytes of data in this Card Specific Data block pub data: [u8; 16], } /// Card Specific Data +#[derive(Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] pub enum Csd { /// A version 1 CSD V1(CsdV1), From 71c2803856df2c7a7e1d4d98e5d097febf36cd52 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 18:06:39 +0100 Subject: [PATCH 15/69] Ensure we only send FF during reads. --- src/sdmmc.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index f07a2be..13616d6 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -295,6 +295,9 @@ where return Err(Error::ReadError); } + for b in buffer.iter_mut() { + *b = 0xFF; + } self.spi.transfer(buffer).map_err(|_e| Error::Transport)?; let mut crc = u16::from(self.receive()?); From a36dd9b14a130a96f708dfcbc8f15a6dc806c7a4 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 18:06:58 +0100 Subject: [PATCH 16/69] Add more debugging. --- src/sdmmc.rs | 56 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/sdmmc.rs b/src/sdmmc.rs index 13616d6..17c1dbb 100644 --- a/src/sdmmc.rs +++ b/src/sdmmc.rs @@ -57,19 +57,14 @@ where CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, { - /// Create a new SD/MMC interface using a raw SPI interface. + /// Create a new SD/MMC Card driver using a raw SPI interface. + /// + /// Uses the default options. pub fn new(spi: SPI, cs: CS) -> SdCard { - SdCard { - inner: RefCell::new(SdCardInner { - spi, - cs, - card_type: CardType::Unknown, - options: AcquireOpts { require_crc: true }, - }), - } + Self::new_with_options(spi, cs, AcquireOpts::default()) } - /// Construct a new SD/MMC interface, using the given options. + /// Construct a new SD/MMC Card driver, using a raw SPI interface and the given options. pub fn new_with_options(spi: SPI, cs: CS, options: AcquireOpts) -> SdCard { SdCard { inner: RefCell::new(SdCardInner { @@ -105,6 +100,14 @@ where inner.check_init()?; inner.erase_single_block_enabled() } + + /// Mark the card as requiring a reset. + /// + /// The next operation will assume the card has been freshly inserted. + pub fn mark_card_uninit(&self) { + let mut inner = self.inner.borrow_mut(); + inner.card_type = CardType::Unknown; + } } impl BlockDevice for SdCard @@ -120,16 +123,23 @@ where &self, blocks: &mut [Block], start_block_idx: BlockIdx, - _reason: &str, + reason: &str, ) -> Result<(), Self::Error> { let mut inner = self.inner.borrow_mut(); + debug!( + "Read {} blocks @ {} for {}", + blocks.len(), + start_block_idx.0, + reason + ); inner.check_init()?; - inner.read(blocks, start_block_idx, _reason) + inner.read(blocks, start_block_idx) } /// Write one or more blocks, starting at the given block index. fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { let mut inner = self.inner.borrow_mut(); + debug!("Writing {} blocks @ {}", blocks.len(), start_block_idx.0); inner.check_init()?; inner.write(blocks, start_block_idx) } @@ -138,7 +148,7 @@ where fn num_blocks(&self) -> Result { let mut inner = self.inner.borrow_mut(); inner.check_init()?; - inner.num_blocks() + inner.card_size_blocks() } } @@ -164,12 +174,7 @@ where >::Error: core::fmt::Debug, { /// Read one or more blocks, starting at the given block index. - fn read( - &mut self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Error> { + fn read(&mut self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Error> { let start_idx = match self.card_type { CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, CardType::Sdhc => start_block_idx.0, @@ -228,9 +233,15 @@ where } /// Determine how many blocks this device can hold. - fn num_blocks(&mut self) -> Result { - let num_bytes = self.card_size_bytes()?; - let num_blocks = (num_bytes / 512) as u32; + fn card_size_blocks(&mut self) -> Result { + let num_blocks = self.with_chip_select(|s| { + let csd = s.read_csd()?; + debug!("CSD: {:?}", csd); + match csd { + Csd::V1(ref contents) => Ok(contents.card_capacity_blocks()), + Csd::V2(ref contents) => Ok(contents.card_capacity_blocks()), + } + })?; Ok(BlockCount(num_blocks)) } @@ -238,6 +249,7 @@ where fn card_size_bytes(&mut self) -> Result { self.with_chip_select(|s| { let csd = s.read_csd()?; + debug!("CSD: {:?}", csd); match csd { Csd::V1(ref contents) => Ok(contents.card_capacity_bytes()), Csd::V2(ref contents) => Ok(contents.card_capacity_bytes()), From 962bb27b1136b35d9540809ae90f0dc8e1d4d508 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 18:14:23 +0100 Subject: [PATCH 17/69] Rename sdmmc as sdcard. Matches the name of the primary type contained within it. --- src/lib.rs | 9 ++++----- src/{sdmmc.rs => sdcard/mod.rs} | 4 +++- src/{sdmmc_proto.rs => sdcard/proto.rs} | 0 3 files changed, 7 insertions(+), 6 deletions(-) rename src/{sdmmc.rs => sdcard/mod.rs} (99%) rename src/{sdmmc_proto.rs => sdcard/proto.rs} (100%) diff --git a/src/lib.rs b/src/lib.rs index 51e09f5..3f801f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ //! # impl std::fmt::Write for DummyUart { fn write_str(&mut self, s: &str) -> std::fmt::Result { Ok(()) } } //! # use std::fmt::Write; //! # use embedded_sdmmc::VolumeManager; -//! # fn main() -> Result<(), embedded_sdmmc::Error> { +//! # fn main() -> Result<(), embedded_sdmmc::Error> { //! # let mut sdmmc_spi = DummySpi; //! # let mut sdmmc_cs = DummyCsPin; //! # let time_source = DummyTimeSource; @@ -90,8 +90,7 @@ mod structure; pub mod blockdevice; pub mod fat; pub mod filesystem; -pub mod sdmmc; -pub mod sdmmc_proto; +pub mod sdcard; pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; pub use crate::fat::FatVolume; @@ -99,8 +98,8 @@ pub use crate::filesystem::{ Attributes, Cluster, DirEntry, Directory, File, FilenameError, Mode, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, }; -pub use crate::sdmmc::Error as SdMmcError; -pub use crate::sdmmc::SdCard; +pub use crate::sdcard::Error as SdCardError; +pub use crate::sdcard::SdCard; mod volume_mgr; pub use volume_mgr::VolumeManager; diff --git a/src/sdmmc.rs b/src/sdcard/mod.rs similarity index 99% rename from src/sdmmc.rs rename to src/sdcard/mod.rs index 17c1dbb..bdb4299 100644 --- a/src/sdmmc.rs +++ b/src/sdcard/mod.rs @@ -5,9 +5,11 @@ //! This is currently optimised for readability and debugability, not //! performance. -use super::sdmmc_proto::*; +pub mod proto; + use super::{Block, BlockCount, BlockDevice, BlockIdx}; use core::cell::RefCell; +use proto::*; // ============================================================================= // Imports diff --git a/src/sdmmc_proto.rs b/src/sdcard/proto.rs similarity index 100% rename from src/sdmmc_proto.rs rename to src/sdcard/proto.rs From 88576b4fae5116025ba04cad5f0198fab04bdbfa Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 19:52:55 +0100 Subject: [PATCH 18/69] Reworked Card Type. Now we store an Option, and the user can unsafely declare a certain type of card to be fitted and initialised. --- src/sdcard/mod.rs | 66 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index bdb4299..73c2dbc 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -72,7 +72,7 @@ where inner: RefCell::new(SdCardInner { spi, cs, - card_type: CardType::Unknown, + card_type: None, options, }), } @@ -85,8 +85,7 @@ where F: FnOnce(&mut SPI) -> T, { let mut inner = self.inner.borrow_mut(); - let result = func(&mut inner.spi); - result + func(&mut inner.spi) } /// Return the usable size of this SD card in bytes. @@ -108,7 +107,24 @@ where /// The next operation will assume the card has been freshly inserted. pub fn mark_card_uninit(&self) { let mut inner = self.inner.borrow_mut(); - inner.card_type = CardType::Unknown; + inner.card_type = None; + } + + /// Get the card type. + pub fn get_card_type(&self) -> Option { + let inner = self.inner.borrow(); + inner.card_type + } + + /// Tell the driver the card has been initialised. + /// + /// # Safety + /// + /// Only do this if the card has actually been initialised and is of the + /// indicated type, otherwise corruption may occur. + pub unsafe fn mark_card_as_init(&self, card_type: CardType) { + let mut inner = self.inner.borrow_mut(); + inner.card_type = Some(card_type); } } @@ -165,7 +181,7 @@ where { spi: SPI, cs: CS, - card_type: CardType, + card_type: Option, options: AcquireOpts, } @@ -178,9 +194,9 @@ where /// Read one or more blocks, starting at the given block index. fn read(&mut self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Error> { let start_idx = match self.card_type { - CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, - CardType::Sdhc => start_block_idx.0, - CardType::Unknown => return Err(Error::CardNotFound), + Some(CardType::SD1 | CardType::SD2) => start_block_idx.0 * 512, + Some(CardType::SDHC) => start_block_idx.0, + None => return Err(Error::CardNotFound), }; self.with_chip_select(|s| { if blocks.len() == 1 { @@ -203,9 +219,9 @@ where /// Write one or more blocks, starting at the given block index. fn write(&mut self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Error> { let start_idx = match self.card_type { - CardType::SD1 | CardType::SD2 => start_block_idx.0 * 512, - CardType::Sdhc => start_block_idx.0, - CardType::Unknown => return Err(Error::CardNotFound), + Some(CardType::SD1 | CardType::SD2) => start_block_idx.0 * 512, + Some(CardType::SDHC) => start_block_idx.0, + None => return Err(Error::CardNotFound), }; self.with_chip_select(|s| { if blocks.len() == 1 { @@ -273,7 +289,7 @@ where /// Read the 'card specific data' block. fn read_csd(&mut self) -> Result { match self.card_type { - CardType::SD1 => { + Some(CardType::SD1) => { let mut csd = CsdV1::new(); if self.card_command(CMD9, 0)? != 0 { return Err(Error::RegisterReadError); @@ -281,7 +297,7 @@ where self.read_data(&mut csd.data)?; Ok(Csd::V1(csd)) } - CardType::SD2 | CardType::Sdhc => { + Some(CardType::SD2 | CardType::SDHC) => { let mut csd = CsdV2::new(); if self.card_command(CMD9, 0)? != 0 { return Err(Error::RegisterReadError); @@ -289,7 +305,7 @@ where self.read_data(&mut csd.data)?; Ok(Csd::V2(csd)) } - CardType::Unknown => Err(Error::CardNotFound), + None => Err(Error::CardNotFound), } } @@ -353,7 +369,7 @@ where /// Check the card is initialised. fn check_init(&mut self) -> Result<(), Error> { - if self.card_type == CardType::Unknown { + if self.card_type.is_none() { // If we don't know what the card type is, try and initialise the // card. This will tell us what type of card it is. self.acquire() @@ -444,14 +460,14 @@ where return Err(Error::Cmd58Error); } if (s.receive()? & 0xC0) == 0xC0 { - card_type = CardType::Sdhc; + card_type = CardType::SDHC; } // Discard other three bytes s.receive()?; s.receive()?; s.receive()?; } - s.card_type = card_type; + s.card_type = Some(card_type); Ok(()) }; let result = f(self); @@ -597,12 +613,20 @@ pub enum Error { /// The different types of card we support. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq)] -enum CardType { - Unknown, +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CardType { + /// An standard-capacity SD Card supporting v1.x of the standard. + /// + /// Uses byte-addressing internally, so limited to 2GiB in size. SD1, + /// An standard-capacity SD Card supporting v2.x of the standard. + /// + /// Uses byte-addressing internally, so limited to 2GiB in size. SD2, - Sdhc, + /// An high-capacity 'SDHC' Card. + /// + /// Uses block-addressing internally to support capacities above 2GiB. + SDHC, } /// A terrible hack for busy-waiting the CPU while we wait for the card to From 79d505c69904b3f4864395dfe6e2675068b38b01 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 19:53:23 +0100 Subject: [PATCH 19/69] Rename card_size_bytes to num_bytes. Matches the num_blocks command in the BlockDevice API. --- README.md | 2 +- src/lib.rs | 4 ++-- src/sdcard/mod.rs | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e2d1f0b..7cdfe7d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ You will need something that implements the `BlockDevice` trait, which can read ```rust // Build an SD Card interface out of an SPI device let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); -write!(uart, "Card size is {} bytes", sdcard.card_size_bytes()?)?; +write!(uart, "Card size is {} bytes", sdcard.num_bytes()?)?; // Now let's look for volumes (also known as partitions) on our block device. let mut cont = embedded_sdmmc::VolumeManager::new(sdcard, time_source); // Try and access Volume 0 (i.e. the first partition) diff --git a/src/lib.rs b/src/lib.rs index 3f801f4..51ab65c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,9 +41,9 @@ //! # let mut sdmmc_cs = DummyCsPin; //! # let time_source = DummyTimeSource; //! let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); -//! println!("Card size {} bytes", sdcard.card_size_bytes()?); +//! 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().card_size_bytes()?); +//! println!("Card size is still {} bytes", volume_mgr.device().num_bytes()?); //! let mut volume0 = volume_mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; //! println!("Volume 0: {:?}", volume0); //! let root_dir = volume_mgr.open_root_dir(&volume0)?; diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 73c2dbc..c2ae917 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -89,10 +89,10 @@ where } /// Return the usable size of this SD card in bytes. - pub fn card_size_bytes(&self) -> Result { + pub fn num_bytes(&self) -> Result { let mut inner = self.inner.borrow_mut(); inner.check_init()?; - inner.card_size_bytes() + inner.num_bytes() } /// Can this card erase single blocks? @@ -166,7 +166,7 @@ where fn num_blocks(&self) -> Result { let mut inner = self.inner.borrow_mut(); inner.check_init()?; - inner.card_size_blocks() + inner.num_blocks() } } @@ -251,7 +251,7 @@ where } /// Determine how many blocks this device can hold. - fn card_size_blocks(&mut self) -> Result { + fn num_blocks(&mut self) -> Result { let num_blocks = self.with_chip_select(|s| { let csd = s.read_csd()?; debug!("CSD: {:?}", csd); @@ -264,7 +264,7 @@ where } /// Return the usable size of this SD card in bytes. - fn card_size_bytes(&mut self) -> Result { + fn num_bytes(&mut self) -> Result { self.with_chip_select(|s| { let csd = s.read_csd()?; debug!("CSD: {:?}", csd); From 3fa093d0141b2572b9dbcc1fc181c235174d5554 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 20:30:35 +0100 Subject: [PATCH 20/69] Move some debug. --- src/sdcard/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index c2ae917..58c8334 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -448,7 +448,6 @@ where } delay.delay(Error::TimeoutCommand(CMD8))?; }; - debug!("Card version: {:?}", card_type); let mut delay = Delay::new(); while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { @@ -467,6 +466,7 @@ where s.receive()?; s.receive()?; } + debug!("Card version: {:?}", card_type); s.card_type = Some(card_type); Ok(()) }; From 7a517e8025488b4f29a7cd9dd3b078632b0bd427 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 20:56:19 +0100 Subject: [PATCH 21/69] Support having no logs at all. --- src/fat/volume.rs | 11 +++-------- src/lib.rs | 30 ++++++++++++++++++++++++++++++ src/sdcard/mod.rs | 22 ++++++---------------- src/volume_mgr.rs | 8 +------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index aae83cf..3afa5a8 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1,18 +1,13 @@ //! FAT volume -#[cfg(feature = "log")] -use log::{debug, trace, warn}; - -#[cfg(feature = "defmt-log")] -use defmt::{debug, trace, warn}; - use crate::{ + debug, fat::{ Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, RESERVED_ENTRIES, }, - Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, DirEntry, Directory, Error, - ShortFileName, TimeSource, VolumeManager, VolumeType, + trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, DirEntry, + Directory, Error, ShortFileName, TimeSource, VolumeManager, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; diff --git a/src/lib.rs b/src/lib.rs index 51ab65c..3a19fec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,36 @@ 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"); + +#[cfg(feature = "log")] +use log::{debug, trace, warn}; + +#[cfg(feature = "defmt-log")] +use defmt::{debug, trace, warn}; + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::debug! but does nothing at all +macro_rules! debug { + ($($arg:tt)+) => {}; +} + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::trace! but does nothing at all +macro_rules! trace { + ($($arg:tt)+) => {}; +} + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::warn! but does nothing at all +macro_rules! warn { + ($($arg:tt)+) => {}; +} + // **************************************************************************** // // Public Types diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 58c8334..05ca02e 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -7,7 +7,7 @@ pub mod proto; -use super::{Block, BlockCount, BlockDevice, BlockIdx}; +use crate::{trace, Block, BlockCount, BlockDevice, BlockIdx}; use core::cell::RefCell; use proto::*; @@ -15,17 +15,7 @@ use proto::*; // Imports // ============================================================================= -#[cfg(feature = "log")] -use log::{debug, trace, warn}; - -#[cfg(feature = "defmt-log")] -use defmt::{debug, trace, warn}; - -#[cfg(all(feature = "defmt-log", feature = "log"))] -compile_error!("Cannot enable both log and defmt-log"); - -#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] -compile_error!("Must enable either log or defmt-log"); +use crate::{debug, warn}; // ============================================================================= // Constants @@ -141,14 +131,14 @@ where &self, blocks: &mut [Block], start_block_idx: BlockIdx, - reason: &str, + _reason: &str, ) -> Result<(), Self::Error> { let mut inner = self.inner.borrow_mut(); debug!( "Read {} blocks @ {} for {}", blocks.len(), start_block_idx.0, - reason + _reason ); inner.check_init()?; inner.read(blocks, start_block_idx) @@ -415,9 +405,9 @@ where Ok(R1_IDLE_STATE) => { break; } - Ok(r) => { + Ok(_r) => { // Try again - warn!("Got response: {:x}, trying again..", r); + warn!("Got response: {:x}, trying again..", _r); } } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index aa66410..9c24913 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -3,18 +3,12 @@ use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; -#[cfg(feature = "log")] -use log::debug; - -#[cfg(feature = "defmt-log")] -use defmt::debug; - use crate::fat::{self, RESERVED_ENTRIES}; use crate::filesystem::{ Attributes, Cluster, DirEntry, Directory, File, Mode, ShortFileName, TimeSource, MAX_FILE_SIZE, }; use crate::{ - Block, BlockCount, BlockDevice, BlockIdx, Error, Volume, VolumeIdx, VolumeType, + 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, }; From da23597c80bfe5b752f9f0699a4f82a114740741 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 21:10:58 +0100 Subject: [PATCH 22/69] Test with no logging enabled. --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 04136fc..c540ac7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - features: ['log', 'defmt-log'] + features: ['log', 'defmt-log', '""'] steps: - uses: actions/checkout@v1 - name: Build From 1e768d7efbb3ce4256c0e46c0a6d04c8f9232203 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 21:31:27 +0100 Subject: [PATCH 23/69] Use the DelayUs trait. --- src/lib.rs | 7 +++++- src/sdcard/mod.rs | 62 +++++++++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3a19fec..70926cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ //! # struct DummyCsPin; //! # struct DummyUart; //! # struct DummyTimeSource; +//! # struct DummyDelayer; //! # impl embedded_hal::blocking::spi::Transfer for DummySpi { //! # type Error = (); //! # fn transfer<'w>(&mut self, data: &'w mut [u8]) -> Result<&'w [u8], ()> { Ok(&[0]) } @@ -33,6 +34,9 @@ //! # impl embedded_sdmmc::TimeSource for DummyTimeSource { //! # fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { embedded_sdmmc::Timestamp::from_fat(0, 0) } //! # } +//! # impl embedded_hal::blocking::delay::DelayUs for DummyDelayer { +//! # fn delay_us(&mut self, us: u8) {} +//! # } //! # impl std::fmt::Write for DummyUart { fn write_str(&mut self, s: &str) -> std::fmt::Result { Ok(()) } } //! # use std::fmt::Write; //! # use embedded_sdmmc::VolumeManager; @@ -40,7 +44,8 @@ //! # let mut sdmmc_spi = DummySpi; //! # let mut sdmmc_cs = DummyCsPin; //! # let time_source = DummyTimeSource; -//! let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); +//! # let delayer = DummyDelayer; +//! let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs, delayer); //! 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()?); diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 05ca02e..e4d9a69 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -34,34 +34,42 @@ const DEFAULT_DELAY_COUNT: u32 = 32_000; /// (which puts the card into SPI mode). /// /// All the APIs take `&self` - mutability is handled using an inner `RefCell`. -pub struct SdCard +pub struct SdCard where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + DELAYER: embedded_hal::blocking::delay::DelayUs, { - inner: RefCell>, + inner: RefCell>, } -impl SdCard +impl SdCard where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + DELAYER: embedded_hal::blocking::delay::DelayUs, { /// Create a new SD/MMC Card driver using a raw SPI interface. /// /// Uses the default options. - pub fn new(spi: SPI, cs: CS) -> SdCard { - Self::new_with_options(spi, cs, AcquireOpts::default()) + 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. - pub fn new_with_options(spi: SPI, cs: CS, options: AcquireOpts) -> SdCard { + pub fn new_with_options( + spi: SPI, + cs: CS, + delayer: DELAYER, + options: AcquireOpts, + ) -> SdCard { SdCard { inner: RefCell::new(SdCardInner { spi, cs, + delayer, card_type: None, options, }), @@ -118,11 +126,12 @@ where } } -impl BlockDevice for SdCard +impl BlockDevice for SdCard where SPI: embedded_hal::blocking::spi::Transfer, >::Error: core::fmt::Debug, CS: embedded_hal::digital::v2::OutputPin, + DELAYER: embedded_hal::blocking::delay::DelayUs, { type Error = Error; @@ -163,23 +172,26 @@ where /// Represents an SD Card on an SPI bus. /// /// All the APIs required `&mut self`. -struct SdCardInner +struct SdCardInner where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + DELAYER: embedded_hal::blocking::delay::DelayUs, { spi: SPI, cs: CS, + delayer: DELAYER, card_type: Option, options: AcquireOpts, } -impl SdCardInner +impl SdCardInner where SPI: embedded_hal::blocking::spi::Transfer, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + DELAYER: embedded_hal::blocking::delay::DelayUs, { /// Read one or more blocks, starting at the given block index. fn read(&mut self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Error> { @@ -309,7 +321,7 @@ where if s != 0xFF { break s; } - delay.delay(Error::TimeoutReadBuffer)?; + delay.delay(&mut self.delayer, Error::TimeoutReadBuffer)?; }; if status != DATA_START_BLOCK { return Err(Error::ReadError); @@ -411,7 +423,7 @@ where } } - delay.delay(Error::TimeoutCommand(CMD0))?; + delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD0))?; } if attempts == 0 { return Err(Error::CardNotFound); @@ -436,12 +448,12 @@ where card_type = CardType::SD2; break 0x4000_0000; } - delay.delay(Error::TimeoutCommand(CMD8))?; + delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD8))?; }; let mut delay = Delay::new(); while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { - delay.delay(Error::TimeoutACommand(ACMD41))?; + delay.delay(&mut s.delayer, Error::TimeoutACommand(ACMD41))?; } if card_type == CardType::SD2 { @@ -547,7 +559,7 @@ where if s == 0xFF { break; } - delay.delay(Error::TimeoutWaitNotBusy)?; + delay.delay(&mut self.delayer, Error::TimeoutWaitNotBusy)?; } Ok(()) } @@ -623,22 +635,26 @@ pub enum CardType { /// sort itself out. /// /// @TODO replace this! -struct Delay(u32); +struct Delay { + count: u32, +} impl Delay { fn new() -> Delay { - Delay(DEFAULT_DELAY_COUNT) + Delay { + count: DEFAULT_DELAY_COUNT, + } } - fn delay(&mut self, err: Error) -> Result<(), Error> { - if self.0 == 0 { + fn delay(&mut self, delayer: &mut T, err: Error) -> Result<(), Error> + where + T: embedded_hal::blocking::delay::DelayUs, + { + if self.count == 0 { Err(err) } else { - let dummy_var: u32 = 0; - for _ in 0..100 { - unsafe { core::ptr::read_volatile(&dummy_var) }; - } - self.0 -= 1; + delayer.delay_us(10); + self.count -= 1; Ok(()) } } From 1943690f0b26b0982bcb1c8635cdc90de55405dc Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Sat, 6 May 2023 21:31:57 +0100 Subject: [PATCH 24/69] Don't loop forever if card gives bad response. Stops it hanging if the card is missing. --- src/sdcard/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index e4d9a69..e492c56 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -420,6 +420,7 @@ where Ok(_r) => { // Try again warn!("Got response: {:x}, trying again..", _r); + attempts -= 1; } } From 7af735d1ac3dce3f4dd5a6eb2038c123a067b9a3 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 17 May 2023 20:31:12 +0100 Subject: [PATCH 25/69] Make CRC checks optional and clean up read/write bytes. 1. Now we try and batch up card byte reads/writes into as few SPI transfer/write calls as possible. 2. The option is now "use_crc" and if not set, we don't even try and enable CRC mode. 3. Note that the 74 bits are not to enable SPI mode but to clear card internal state --- src/sdcard/mod.rs | 218 ++++++++++++++++++++++++++-------------------- 1 file changed, 122 insertions(+), 96 deletions(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index e492c56..658c3c9 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -17,12 +17,6 @@ use proto::*; use crate::{debug, warn}; -// ============================================================================= -// Constants -// ============================================================================= - -const DEFAULT_DELAY_COUNT: u32 = 32_000; - // ============================================================================= // Types and Implementations // ============================================================================= @@ -31,14 +25,15 @@ const DEFAULT_DELAY_COUNT: u32 = 32_000; /// /// Built from an SPI peripheral and a Chip Select pin. We need Chip Select to /// be separate so we can clock out some bytes without Chip Select asserted -/// (which puts the card into SPI mode). +/// (which "flushes the SD cards registers" according to the spec). /// /// All the APIs take `&self` - mutability is handled using an inner `RefCell`. pub struct SdCard where - SPI: embedded_hal::blocking::spi::Transfer, + SPI: embedded_hal::blocking::spi::Transfer + embedded_hal::blocking::spi::Write, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + >::Error: core::fmt::Debug, DELAYER: embedded_hal::blocking::delay::DelayUs, { inner: RefCell>, @@ -46,9 +41,10 @@ where impl SdCard where - SPI: embedded_hal::blocking::spi::Transfer, + SPI: embedded_hal::blocking::spi::Transfer + embedded_hal::blocking::spi::Write, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + >::Error: core::fmt::Debug, DELAYER: embedded_hal::blocking::delay::DelayUs, { /// Create a new SD/MMC Card driver using a raw SPI interface. @@ -128,9 +124,10 @@ where impl BlockDevice for SdCard where - SPI: embedded_hal::blocking::spi::Transfer, - >::Error: core::fmt::Debug, + SPI: embedded_hal::blocking::spi::Transfer + embedded_hal::blocking::spi::Write, CS: embedded_hal::digital::v2::OutputPin, + >::Error: core::fmt::Debug, + >::Error: core::fmt::Debug, DELAYER: embedded_hal::blocking::delay::DelayUs, { type Error = Error; @@ -174,9 +171,10 @@ where /// All the APIs required `&mut self`. struct SdCardInner where - SPI: embedded_hal::blocking::spi::Transfer, + SPI: embedded_hal::blocking::spi::Transfer + embedded_hal::blocking::spi::Write, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + >::Error: core::fmt::Debug, DELAYER: embedded_hal::blocking::delay::DelayUs, { spi: SPI, @@ -188,9 +186,10 @@ where impl SdCardInner where - SPI: embedded_hal::blocking::spi::Transfer, + SPI: embedded_hal::blocking::spi::Transfer + embedded_hal::blocking::spi::Write, CS: embedded_hal::digital::v2::OutputPin, >::Error: core::fmt::Debug, + >::Error: core::fmt::Debug, DELAYER: embedded_hal::blocking::delay::DelayUs, { /// Read one or more blocks, starting at the given block index. @@ -234,7 +233,7 @@ where if s.card_command(CMD13, 0)? != 0x00 { return Err(Error::WriteError); } - if s.receive()? != 0x00 { + if s.read_byte()? != 0x00 { return Err(Error::WriteError); } } else { @@ -246,7 +245,7 @@ where } // Stop the write s.wait_not_busy()?; - s.send(STOP_TRAN_TOKEN)?; + s.write_byte(STOP_TRAN_TOKEN)?; } Ok(()) }) @@ -311,13 +310,14 @@ where } } - /// Read an arbitrary number of bytes from the card. Always fills the - /// given buffer, so make sure it's the right size. + /// Read an arbitrary number of bytes from the card using the SD Card + /// protocol and an optional CRC. Always fills the given buffer, so make + /// sure it's the right size. fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { // Get first non-FF byte. - let mut delay = Delay::new(); + let mut delay = Delay::default(); let status = loop { - let s = self.receive()?; + let s = self.read_byte()?; if s != 0xFF { break s; } @@ -330,30 +330,38 @@ where for b in buffer.iter_mut() { *b = 0xFF; } - self.spi.transfer(buffer).map_err(|_e| Error::Transport)?; - - let mut crc = u16::from(self.receive()?); - crc <<= 8; - crc |= u16::from(self.receive()?); - - let calc_crc = crc16(buffer); - if crc != calc_crc { - return Err(Error::CrcError(crc, calc_crc)); + self.transfer_bytes(buffer)?; + + // These two bytes are always sent. They are either a valid CRC, or + // junk, depending on whether CRC mode was enabled. + let mut crc_bytes = [0xFF; 2]; + self.transfer_bytes(&mut crc_bytes)?; + if self.options.use_crc { + let crc = u16::from_be_bytes(crc_bytes); + let calc_crc = crc16(buffer); + if crc != calc_crc { + return Err(Error::CrcError(crc, calc_crc)); + } } Ok(()) } - /// Write an arbitrary number of bytes to the card. + /// Write an arbitrary number of bytes to the card using the SD protocol and + /// an optional CRC. fn write_data(&mut self, token: u8, buffer: &[u8]) -> Result<(), Error> { - let calc_crc = crc16(buffer); - self.send(token)?; - for &b in buffer.iter() { - self.send(b)?; - } - self.send((calc_crc >> 8) as u8)?; - self.send(calc_crc as u8)?; - let status = self.receive()?; + self.write_byte(token)?; + self.write_bytes(buffer)?; + let crc_bytes = if self.options.use_crc { + crc16(buffer).to_be_bytes() + } else { + [0xFF, 0xFF] + }; + // These two bytes are always sent. They are either a valid CRC, or + // junk, depending on whether CRC mode was enabled. + self.write_bytes(&crc_bytes)?; + + let status = self.read_byte()?; if (status & DATA_RES_MASK) != DATA_RES_ACCEPTED { Err(Error::WriteError) } else { @@ -389,17 +397,13 @@ where trace!("Reset card.."); // Supply minimum of 74 clock cycles without CS asserted. s.cs_high()?; - for _ in 0..10 { - s.send(0xFF)?; - } + s.write_bytes(&[0xFF; 10])?; // Assert CS s.cs_low()?; - // Enter SPI mode - let mut delay = Delay::new(); - let mut attempts = 32; - while attempts > 0 { - trace!("Enter SPI mode, attempt: {}..", 32i32 - attempts); - + // Enter SPI mode. + let mut delay = Delay::default(); + for attempts in 1.. { + trace!("Enter SPI mode, attempt: {}..", attempts); match s.card_command(CMD0, 0) { Err(Error::TimeoutCommand(0)) => { // Try again? @@ -407,9 +411,8 @@ where // Try flushing the card as done here: https://github.com/greiman/SdFat/blob/master/src/SdCard/SdSpiCard.cpp#L170, // https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/65#issuecomment-1270709448 for _ in 0..0xFF { - s.send(0xFF)?; + s.write_byte(0xFF)?; } - attempts -= 1; } Err(e) => { return Err(e); @@ -420,31 +423,28 @@ where Ok(_r) => { // Try again warn!("Got response: {:x}, trying again..", _r); - attempts -= 1; } } - delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD0))?; - } - if attempts == 0 { - return Err(Error::CardNotFound); + delay.delay(&mut s.delayer, Error::CardNotFound)?; } // Enable CRC - debug!("Enable CRC: {}", s.options.require_crc); - if s.card_command(CMD59, 1)? != R1_IDLE_STATE && s.options.require_crc { + debug!("Enable CRC: {}", s.options.use_crc); + // "The SPI interface is initialized in the CRC OFF mode in default" + // -- SD Part 1 Physical Layer Specification v9.00, Section 7.2.2 Bus Transfer Protection + if s.options.use_crc && s.card_command(CMD59, 1)? != R1_IDLE_STATE { return Err(Error::CantEnableCRC); } // Check card version - let mut delay = Delay::new(); + let mut delay = Delay::default(); let arg = loop { if s.card_command(CMD8, 0x1AA)? == (R1_ILLEGAL_COMMAND | R1_IDLE_STATE) { card_type = CardType::SD1; break 0; } - s.receive()?; - s.receive()?; - s.receive()?; - let status = s.receive()?; + let mut buffer = [0xFF; 4]; + s.transfer_bytes(&mut buffer)?; + let status = buffer[3]; if status == 0xAA { card_type = CardType::SD2; break 0x4000_0000; @@ -452,7 +452,7 @@ where delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD8))?; }; - let mut delay = Delay::new(); + let mut delay = Delay::default(); while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { delay.delay(&mut s.delayer, Error::TimeoutACommand(ACMD41))?; } @@ -461,13 +461,12 @@ where if s.card_command(CMD58, 0)? != 0 { return Err(Error::Cmd58Error); } - if (s.receive()? & 0xC0) == 0xC0 { + let mut buffer = [0xFF; 4]; + s.transfer_bytes(&mut buffer)?; + if (buffer[0] & 0xC0) == 0xC0 { card_type = CardType::SDHC; } - // Discard other three bytes - s.receive()?; - s.receive()?; - s.receive()?; + // Ignore the other three bytes } debug!("Card version: {:?}", card_type); s.card_type = Some(card_type); @@ -475,7 +474,7 @@ where }; let result = f(self); self.cs_high()?; - let _ = self.receive(); + let _ = self.read_byte(); result } @@ -513,50 +512,60 @@ where ]; buf[5] = crc7(&buf[0..5]); - for b in buf.iter() { - self.send(*b)?; - } + self.write_bytes(&buf)?; // skip stuff byte for stop read if command == CMD12 { - let _result = self.receive()?; + let _result = self.read_byte()?; } - for _ in 0..512 { - let result = self.receive()?; + let mut delay = Delay::default(); + loop { + let result = self.read_byte()?; if (result & 0x80) == ERROR_OK { return Ok(result); } + delay.delay(&mut self.delayer, Error::TimeoutCommand(command))?; } - - Err(Error::TimeoutCommand(command)) } - /// Receive a byte from the SD card by clocking in an 0xFF byte. - fn receive(&mut self) -> Result { - self.transfer(0xFF) + /// Receive a byte from the SPI bus by clocking out an 0xFF byte. + fn read_byte(&mut self) -> Result { + self.transfer_byte(0xFF) } - /// Send a byte from the SD card. - fn send(&mut self, out: u8) -> Result<(), Error> { - let _ = self.transfer(out)?; + /// Send a byte over the SPI bus and ignore what comes back. + fn write_byte(&mut self, out: u8) -> Result<(), Error> { + let _ = self.transfer_byte(out)?; Ok(()) } - /// Send one byte and receive one byte. - fn transfer(&mut self, out: u8) -> Result { + /// Send one byte and receive one byte over the SPI bus. + fn transfer_byte(&mut self, out: u8) -> Result { self.spi .transfer(&mut [out]) .map(|b| b[0]) .map_err(|_e| Error::Transport) } + /// Send mutiple bytes and ignore what comes back over the SPI bus. + fn write_bytes(&mut self, out: &[u8]) -> Result<(), Error> { + self.spi.write(out).map_err(|_e| Error::Transport)?; + Ok(()) + } + + /// Send multiple bytes and replace them with what comes back over the SPI bus. + fn transfer_bytes(&mut self, in_out: &mut [u8]) -> Result<(), Error> { + self.spi.transfer(in_out).map_err(|_e| Error::Transport)?; + Ok(()) + } + /// Spin until the card returns 0xFF, or we spin too many times and /// timeout. fn wait_not_busy(&mut self) -> Result<(), Error> { - let mut delay = Delay::new(); + let mut delay = Delay::default(); loop { - let s = self.receive()?; + let s = self.read_byte()?; if s == 0xFF { break; } @@ -570,13 +579,19 @@ where #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug)] pub struct AcquireOpts { - /// Some cards don't support CRC mode. At least a 512MiB Transcend one. - pub require_crc: bool, + /// Set to true to enable CRC checking on reading/writing blocks of data. + /// + /// Set to false to disable the CRC. Some cards don't support CRC correctly + /// and this option may be useful in that instance. + /// + /// On by default because without it you might get silent data corruption on + /// your card. + pub use_crc: bool, } impl Default for AcquireOpts { fn default() -> Self { - AcquireOpts { require_crc: true } + AcquireOpts { use_crc: true } } } @@ -632,30 +647,41 @@ pub enum CardType { SDHC, } -/// A terrible hack for busy-waiting the CPU while we wait for the card to -/// sort itself out. +/// This an object you can use to busy-wait with a timeout. /// -/// @TODO replace this! +/// Will let you call `delay` up to `max_retries` times before `delay` returns +/// an error. struct Delay { - count: u32, + retries_left: u32, } impl Delay { - fn new() -> Delay { + /// The default number of retries for a write operation. + /// + /// At 10us each this is 320ms. + pub const DEFAULT_WRITE_RETRIES: u32 = 32000; + + /// Create a new Delay object with the given maximum number of retries. + fn new(max_retries: u32) -> Delay { Delay { - count: DEFAULT_DELAY_COUNT, + retries_left: max_retries, } } + /// Wait for a while. + /// + /// Checks the retry counter first, and if we hit the max retry limit, the + /// value `err` is returned. Otherwise we wait for 10us and then return + /// `Ok(())`. fn delay(&mut self, delayer: &mut T, err: Error) -> Result<(), Error> where T: embedded_hal::blocking::delay::DelayUs, { - if self.count == 0 { + if self.retries_left == 0 { Err(err) } else { delayer.delay_us(10); - self.count -= 1; + self.retries_left -= 1; Ok(()) } } From 37ee056114175d011f09d98b1854798499cad903 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 17 May 2023 20:35:44 +0100 Subject: [PATCH 26/69] Fix example code. Now you need blocking::spi::Write as well as blocking::spi::Transfer --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 70926cc..bb2a86f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,11 @@ //! # struct DummyDelayer; //! # impl embedded_hal::blocking::spi::Transfer for DummySpi { //! # type Error = (); -//! # fn transfer<'w>(&mut self, data: &'w mut [u8]) -> Result<&'w [u8], ()> { Ok(&[0]) } +//! # fn transfer<'w>(&mut self, data: &'w mut [u8]) -> Result<&'w [u8], Self::Error> { Ok(&[0]) } +//! # } +//! # impl embedded_hal::blocking::spi::Write for DummySpi { +//! # type Error = (); +//! # fn write(&mut self, data: &[u8]) -> Result<(), Self::Error> { Ok(()) } //! # } //! # impl embedded_hal::digital::v2::OutputPin for DummyCsPin { //! # type Error = (); From 733e5d8f685140dbbb61859bf27e4e8620ee8f70 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Thu, 18 May 2023 14:20:10 +0100 Subject: [PATCH 27/69] Fixup the README. Also add the code from the README into an example so we can at least check it compiles. --- README.md | 30 +++++++---- examples/readme_test.rs | 115 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 examples/readme_test.rs diff --git a/README.md b/README.md index 7cdfe7d..273c368 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,29 @@ designed for readability and simplicity over performance. You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. ```rust -// Build an SD Card interface out of an SPI device -let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs); -write!(uart, "Card size is {} bytes", sdcard.num_bytes()?)?; +// Build an SD Card interface out of an SPI device, a chip-select pin and a delay object +let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs, delay); +// Get the card size (this also triggers card initialisation because it's not been done yet) +println!("Card size is {} bytes", sdcard.num_bytes()?); // Now let's look for volumes (also known as partitions) on our block device. -let mut cont = embedded_sdmmc::VolumeManager::new(sdcard, time_source); -// Try and access Volume 0 (i.e. the first partition) -let mut volume = cont.get_volume(embedded_sdmmc::VolumeIdx(0))?; -writeln!(uart, "Volume 0: {:?}", v)?; -// Open the root directory +// To do this we need a Volume Manager. It will take ownership of the block device. +let mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); +// Try and access Volume 0 (i.e. the first partition). +// The volume object holds information about the filesystem on that volume. +// 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))?; +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, "MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; + &mut volume0, + &root_dir, + "MY_FILE.TXT", + embedded_sdmmc::Mode::ReadOnly, +)?; // Print the contents of the file while !my_file.eof() { let mut buffer = [0u8; 32]; @@ -33,7 +43,7 @@ while !my_file.eof() { } } volume_mgr.close_file(&volume0, my_file)?; -volume_mgr.close_dir(&volume0, root_dir)?; +volume_mgr.close_dir(&volume0, root_dir); ``` ### Open directories and files diff --git a/examples/readme_test.rs b/examples/readme_test.rs new file mode 100644 index 0000000..bb9ffbc --- /dev/null +++ b/examples/readme_test.rs @@ -0,0 +1,115 @@ +//! This is the code from the README.md file. +//! +//! We add enough stuff to make it compile, but it won't run because our fake +//! SPI doesn't do any replies. + +struct FakeSpi(); + +impl embedded_hal::blocking::spi::Transfer for FakeSpi { + type Error = core::convert::Infallible; + fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> { + Ok(words) + } +} + +impl embedded_hal::blocking::spi::Write for FakeSpi { + type Error = core::convert::Infallible; + fn write<'w>(&mut self, _words: &'w [u8]) -> Result<(), Self::Error> { + Ok(()) + } +} + +struct FakeCs(); + +impl embedded_hal::digital::v2::OutputPin for FakeCs { + type Error = core::convert::Infallible; + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +struct FakeDelayer(); + +impl embedded_hal::blocking::delay::DelayUs for FakeDelayer { + fn delay_us(&mut self, us: u8) { + std::thread::sleep(std::time::Duration::from_micros(u64::from(us))); + } +} + +struct FakeTimesource(); + +impl embedded_sdmmc::TimeSource for FakeTimesource { + fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { + embedded_sdmmc::Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } +} + +#[derive(Debug, Clone)] +enum Error { + Filesystem(embedded_sdmmc::Error), + Disk(embedded_sdmmc::SdCardError), +} + +impl From> for Error { + fn from(value: embedded_sdmmc::Error) -> Error { + Error::Filesystem(value) + } +} + +impl From for Error { + fn from(value: embedded_sdmmc::SdCardError) -> Error { + Error::Disk(value) + } +} + +fn main() -> Result<(), Error> { + let sdmmc_spi = FakeSpi(); + let sdmmc_cs = FakeCs(); + let delay = FakeDelayer(); + let time_source = FakeTimesource(); + // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object + let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, sdmmc_cs, delay); + // Get the card size (this also triggers card initialisation because it's not been done yet) + println!("Card size is {} bytes", sdcard.num_bytes()?); + // Now let's look for volumes (also known as partitions) on our block device. + // To do this we need a Volume Manager. It will take ownership of the block device. + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); + // Try and access Volume 0 (i.e. the first partition). + // The volume object holds information about the filesystem on that volume. + // 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))?; + 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, + "MY_FILE.TXT", + embedded_sdmmc::Mode::ReadOnly, + )?; + // Print the contents of the file + while !my_file.eof() { + 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); + Ok(()) +} From e065c43101f541ee8503e3efd2116dd4495e2b94 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Thu, 18 May 2023 14:20:55 +0100 Subject: [PATCH 28/69] Reformat Cargo.toml. Puts the keys into alphabetical order. --- Cargo.toml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 62b8136..96b5bb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,25 @@ [package] -name = "embedded-sdmmc" -version = "0.4.0" authors = ["Jonathan 'theJPster' Pallant "] +categories = ["embedded", "no-std"] description = "A basic SD/MMC driver for Embedded Rust." +edition = "2021" keywords = ["sdcard", "mmc", "embedded", "fat32"] -categories = ["embedded", "no-std"] license = "MIT OR Apache-2.0" -repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" -edition = "2021" +name = "embedded-sdmmc" readme = "README.md" +repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" +version = "0.4.0" [dependencies] +byteorder = {version = "1", default-features = false} +defmt = {version = "0.3", optional = true} embedded-hal = "0.2.3" -byteorder = { version = "1", default-features = false } -log = { version = "0.4", default-features = false, optional = true } -defmt = { version = "0.3", optional = true } +log = {version = "0.4", default-features = false, optional = true} [dev-dependencies] -hex-literal = "0.3" env_logger = "0.9" +hex-literal = "0.3" [features] -defmt-log = [ "defmt" ] -default = [ "log" ] +default = ["log"] +defmt-log = ["defmt"] From 764e6461488fd8c51b773805fc965d911a0967e8 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 19 May 2023 20:21:37 +0100 Subject: [PATCH 29/69] Set a different delay depending on context. The spec gives some suggested numbers for timeouts for reads and writes. --- src/sdcard/mod.rs | 56 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 658c3c9..350c774 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -229,7 +229,7 @@ where // Start a single-block write s.card_command(CMD24, start_idx)?; s.write_data(DATA_START_BLOCK, &blocks[0].contents)?; - s.wait_not_busy()?; + s.wait_not_busy(Delay::new_write())?; if s.card_command(CMD13, 0)? != 0x00 { return Err(Error::WriteError); } @@ -240,11 +240,11 @@ where // Start a multi-block write s.card_command(CMD25, start_idx)?; for block in blocks.iter() { - s.wait_not_busy()?; + s.wait_not_busy(Delay::new_write())?; s.write_data(WRITE_MULTIPLE_TOKEN, &block.contents)?; } // Stop the write - s.wait_not_busy()?; + s.wait_not_busy(Delay::new_write())?; s.write_byte(STOP_TRAN_TOKEN)?; } Ok(()) @@ -315,7 +315,7 @@ where /// sure it's the right size. fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { // Get first non-FF byte. - let mut delay = Delay::default(); + let mut delay = Delay::new_read(); let status = loop { let s = self.read_byte()?; if s != 0xFF { @@ -401,7 +401,7 @@ where // Assert CS s.cs_low()?; // Enter SPI mode. - let mut delay = Delay::default(); + let mut delay = Delay::new_command(); for attempts in 1.. { trace!("Enter SPI mode, attempt: {}..", attempts); match s.card_command(CMD0, 0) { @@ -436,7 +436,7 @@ where return Err(Error::CantEnableCRC); } // Check card version - let mut delay = Delay::default(); + let mut delay = Delay::new_command(); let arg = loop { if s.card_command(CMD8, 0x1AA)? == (R1_ILLEGAL_COMMAND | R1_IDLE_STATE) { card_type = CardType::SD1; @@ -452,7 +452,7 @@ where delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD8))?; }; - let mut delay = Delay::default(); + let mut delay = Delay::new_command(); while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { delay.delay(&mut s.delayer, Error::TimeoutACommand(ACMD41))?; } @@ -499,7 +499,7 @@ where /// Perform a command. fn card_command(&mut self, command: u8, arg: u32) -> Result { if command != CMD0 && command != CMD12 { - self.wait_not_busy()?; + self.wait_not_busy(Delay::new_command())?; } let mut buf = [ @@ -519,7 +519,7 @@ where let _result = self.read_byte()?; } - let mut delay = Delay::default(); + let mut delay = Delay::new_command(); loop { let result = self.read_byte()?; if (result & 0x80) == ERROR_OK { @@ -562,8 +562,7 @@ where /// Spin until the card returns 0xFF, or we spin too many times and /// timeout. - fn wait_not_busy(&mut self) -> Result<(), Error> { - let mut delay = Delay::default(); + fn wait_not_busy(&mut self, mut delay: Delay) -> Result<(), Error> { loop { let s = self.read_byte()?; if s == 0xFF { @@ -656,10 +655,26 @@ struct Delay { } impl Delay { + /// The default number of retries for a read operation. + /// + /// At ~10us each this is ~100ms. + /// + /// See `Part1_Physical_Layer_Simplified_Specification_Ver9.00-1.pdf` Section 4.6.2.1 + pub const DEFAULT_READ_RETRIES: u32 = 10_000; + /// The default number of retries for a write operation. /// - /// At 10us each this is 320ms. - pub const DEFAULT_WRITE_RETRIES: u32 = 32000; + /// At ~10us each this is ~500ms. + /// + /// See `Part1_Physical_Layer_Simplified_Specification_Ver9.00-1.pdf` Section 4.6.2.2 + pub const DEFAULT_WRITE_RETRIES: u32 = 50_000; + + /// The default number of retries for a control command. + /// + /// At ~10us each this is ~100ms. + /// + /// No value is given in the specification, so we pick the same as the read timeout. + pub const DEFAULT_COMMAND_RETRIES: u32 = 10_000; /// Create a new Delay object with the given maximum number of retries. fn new(max_retries: u32) -> Delay { @@ -668,6 +683,21 @@ impl Delay { } } + /// Create a new Delay object with the maximum number of retries for a read operation. + fn new_read() -> Delay { + Delay::new(Self::DEFAULT_READ_RETRIES) + } + + /// Create a new Delay object with the maximum number of retries for a write operation. + fn new_write() -> Delay { + Delay::new(Self::DEFAULT_WRITE_RETRIES) + } + + /// Create a new Delay object with the maximum number of retries for a command operation. + fn new_command() -> Delay { + Delay::new(Self::DEFAULT_COMMAND_RETRIES) + } + /// Wait for a while. /// /// Checks the retry counter first, and if we hit the max retry limit, the From 6042ac4892359af3e608ad15ca3b278594789bfd Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 20 May 2023 11:18:55 +0100 Subject: [PATCH 30/69] Removed unused field. It was a rustc warning and it seems to work without. If we need it in future, we can add it back. --- src/filesystem/directory.rs | 2 -- src/volume_mgr.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index ea7a9e5..bcd8bd6 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -33,8 +33,6 @@ pub struct DirEntry { pub struct Directory { /// 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, } impl DirEntry { diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 9c24913..c6e5620 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -172,7 +172,6 @@ where self.open_dirs[open_dirs_row] = (volume.idx, Cluster::ROOT_DIR); Ok(Directory { cluster: Cluster::ROOT_DIR, - entry: None, }) } @@ -217,7 +216,6 @@ where self.open_dirs[open_dirs_row] = (volume.idx, dir_entry.cluster); Ok(Directory { cluster: dir_entry.cluster, - entry: Some(dir_entry), }) } From 1cfdab59cadab99e8f07e0f6227e94b746fc437a Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Mon, 17 Oct 2022 17:18:00 -0400 Subject: [PATCH 31/69] Update Controller open_files when allocating file --- src/volume_mgr.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index c6e5620..27c46a1 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -406,7 +406,7 @@ where open_files_row = Some(i); } } - open_files_row.ok_or(Error::TooManyOpenDirs) + open_files_row.ok_or(Error::TooManyOpenFiles) } /// Delete a closed file with the given full path, if exists. @@ -498,6 +498,12 @@ where file.starting_cluster = match &mut volume.volume_type { VolumeType::Fat(fat) => fat.alloc_cluster(self, None, false)?, }; + for f in self.open_files.iter_mut() { + if f.1 == Cluster(0) { + *f = (f.0, file.starting_cluster) + } + } + file.entry.cluster = file.starting_cluster; debug!("Alloc first cluster {:?}", file.starting_cluster); } From a087f64f567e5a89a062f8ac62c56d45fd60ebf3 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Mon, 17 Oct 2022 18:16:22 -0400 Subject: [PATCH 32/69] Use unique IDs for files and directories --- Cargo.toml | 1 + src/filesystem/directory.rs | 4 +- src/filesystem/files.rs | 4 +- src/filesystem/mod.rs | 2 + src/filesystem/search_id.rs | 34 ++++++++++++ src/volume_mgr.rs | 107 ++++++++++++++++++++---------------- 6 files changed, 104 insertions(+), 48 deletions(-) create mode 100644 src/filesystem/search_id.rs diff --git a/Cargo.toml b/Cargo.toml index 86f2bcf..5e2bad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ version = "0.5.0" 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] diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index bcd8bd6..aa223f9 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -2,7 +2,7 @@ use core::convert::TryFrom; use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; -use crate::filesystem::{Attributes, Cluster, ShortFileName, Timestamp}; +use crate::filesystem::{Attributes, Cluster, SearchId, ShortFileName, Timestamp}; /// Represents a directory entry, which tells you about /// other files and directories. @@ -33,6 +33,8 @@ pub struct DirEntry { pub struct Directory { /// The starting point of the directory listing. pub(crate) cluster: Cluster, + /// Search ID for this directory. + pub(crate) search_id: SearchId, } impl DirEntry { diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index a619ed5..2a00a07 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,4 +1,4 @@ -use crate::filesystem::{Cluster, DirEntry}; +use crate::filesystem::{Cluster, DirEntry, SearchId}; /// Represents an open file on disk. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] @@ -16,6 +16,8 @@ pub struct File { pub(crate) mode: Mode, /// DirEntry of this file pub(crate) entry: DirEntry, + /// Search ID for this file + pub(crate) search_id: SearchId, } /// Errors related to file operations diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index c1b2a81..7e7c9b4 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -11,6 +11,7 @@ mod cluster; mod directory; mod filename; mod files; +mod search_id; mod timestamp; pub use self::attributes::Attributes; @@ -18,4 +19,5 @@ pub use self::cluster::Cluster; pub use self::directory::{DirEntry, Directory}; pub use self::filename::{FilenameError, ShortFileName}; pub use self::files::{File, FileError, Mode}; +pub use self::search_id::{IdGenerator, SearchId}; pub use self::timestamp::{TimeSource, Timestamp}; diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs new file mode 100644 index 0000000..3b8a506 --- /dev/null +++ b/src/filesystem/search_id.rs @@ -0,0 +1,34 @@ +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +/// Unique ID used to search for files and directories in the open File/Directory lists +pub struct SearchId(pub(crate) u32); + +impl PartialEq for SearchId { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +/// ID generator intented to be used in a static context. +/// +/// This object will always return a different ID. +pub struct IdGenerator { + next_id: core::sync::atomic::AtomicU32, +} + +impl IdGenerator { + /// Create a new [`IdGenerator`]. + pub const fn new() -> Self { + Self { + next_id: core::sync::atomic::AtomicU32::new(0), + } + } + + /// Generate a new, unique [`SearchId`]. + pub fn next(&self) -> SearchId { + use core::sync::atomic::Ordering; + let id = self.next_id.load(Ordering::Acquire); + self.next_id.store(id + 1, Ordering::Release); + SearchId(id) + } +} diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 27c46a1..f91cebf 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -5,12 +5,16 @@ use core::convert::TryFrom; use crate::fat::{self, RESERVED_ENTRIES}; use crate::filesystem::{ - Attributes, Cluster, DirEntry, Directory, File, Mode, ShortFileName, TimeSource, MAX_FILE_SIZE, + Attributes, Cluster, DirEntry, Directory, File, IdGenerator, Mode, SearchId, ShortFileName, + TimeSource, 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, }; +use heapless::Vec; + +static ID_GENERATOR: IdGenerator = IdGenerator::new(); /// A `VolumeManager` wraps a block device and gives access to the volumes within it. pub struct VolumeManager @@ -21,8 +25,8 @@ where { pub(crate) block_device: D, pub(crate) timesource: T, - open_dirs: [(VolumeIdx, Cluster); MAX_DIRS], - open_files: [(VolumeIdx, Cluster); MAX_FILES], + open_dirs: Vec<(VolumeIdx, Cluster, SearchId), MAX_DIRS>, + open_files: Vec<(VolumeIdx, Cluster, SearchId), MAX_FILES>, } impl VolumeManager @@ -60,8 +64,8 @@ where VolumeManager { block_device, timesource, - open_dirs: [(VolumeIdx(0), Cluster::INVALID); MAX_DIRS], - open_files: [(VolumeIdx(0), Cluster::INVALID); MAX_FILES], + open_dirs: Vec::new(), + open_files: Vec::new(), } } @@ -157,21 +161,20 @@ where // 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) { + for (v, c, _) in self.open_dirs.iter() { + if *v == volume.idx && *c == Cluster::ROOT_DIR { 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)?; + let search_id = ID_GENERATOR.next(); // Remember this open directory - self.open_dirs[open_dirs_row] = (volume.idx, Cluster::ROOT_DIR); + self.open_dirs + .push((volume.idx, Cluster::ROOT_DIR, search_id)) + .map_err(|_| Error::TooManyOpenDirs)?; + Ok(Directory { cluster: Cluster::ROOT_DIR, + search_id, }) } @@ -188,14 +191,9 @@ where 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); - } + if self.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); } - let open_dirs_row = open_dirs_row.ok_or(Error::TooManyOpenDirs)?; // Open the directory let dir_entry = match &volume.volume_type { @@ -207,25 +205,29 @@ where } // 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 dir_table_row in self.open_dirs.iter() { + if dir_table_row.0 == volume.idx && dir_table_row.1 == dir_entry.cluster { return Err(Error::DirAlreadyOpen); } } - // Remember this open directory - self.open_dirs[open_dirs_row] = (volume.idx, dir_entry.cluster); + // Remember this open directory. + let search_id = ID_GENERATOR.next(); + self.open_dirs + .push((volume.idx, dir_entry.cluster, search_id)) + .map_err(|_| Error::TooManyOpenDirs)?; + Ok(Directory { cluster: dir_entry.cluster, + search_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; + for (i, d) in self.open_dirs.iter().enumerate() { + if d.2 == dir.search_id { + self.open_dirs.swap_remove(i); break; } } @@ -265,10 +267,12 @@ where dir_entry: DirEntry, mode: Mode, ) -> Result> { - let open_files_row = self.get_open_files_row()?; + if self.open_files.is_full() { + return Err(Error::TooManyOpenFiles); + } // Check it's not already open for dir_table_row in self.open_files.iter() { - if *dir_table_row == (volume.idx, dir_entry.cluster) { + if dir_table_row.0 == volume.idx && dir_table_row.1 == dir_entry.cluster { return Err(Error::DirAlreadyOpen); } } @@ -280,6 +284,8 @@ where } let mode = solve_mode_variant(mode, true); + let search_id = ID_GENERATOR.next(); + let file = match mode { Mode::ReadOnly => File { starting_cluster: dir_entry.cluster, @@ -288,6 +294,7 @@ where length: dir_entry.size, mode, entry: dir_entry, + search_id, }, Mode::ReadWriteAppend => { let mut file = File { @@ -297,6 +304,7 @@ where length: dir_entry.size, mode, entry: dir_entry, + search_id, }; // seek_from_end with 0 can't fail file.seek_from_end(0).ok(); @@ -310,6 +318,7 @@ where length: dir_entry.size, mode, entry: dir_entry, + search_id, }; match &mut volume.volume_type { VolumeType::Fat(fat) => { @@ -330,7 +339,10 @@ where _ => return Err(Error::Unsupported), }; // Remember this open file - self.open_files[open_files_row] = (volume.idx, file.starting_cluster); + self.open_files + .push((volume.idx, file.starting_cluster, search_id)) + .map_err(|_| Error::TooManyOpenDirs)?; + Ok(file) } @@ -346,7 +358,10 @@ where VolumeType::Fat(fat) => fat.find_directory_entry(self, dir, name), }; - let open_files_row = self.get_open_files_row()?; + if self.open_files.is_full() { + return Err(Error::TooManyOpenFiles); + } + let dir_entry = match dir_entry { Ok(entry) => Some(entry), Err(_) @@ -375,6 +390,8 @@ where } }; + let search_id = ID_GENERATOR.next(); + let file = File { starting_cluster: entry.cluster, current_cluster: (0, entry.cluster), @@ -382,9 +399,14 @@ where length: entry.size, mode, entry, + search_id, }; + // Remember this open file - self.open_files[open_files_row] = (volume.idx, file.starting_cluster); + self.open_files + .push((volume.idx, file.starting_cluster, search_id)) + .map_err(|_| Error::TooManyOpenFiles)?; + Ok(file) } _ => { @@ -428,9 +450,8 @@ where return Err(Error::DeleteDirAsFile); } - let target = (volume.idx, dir_entry.cluster); for d in self.open_files.iter_mut() { - if *d == target { + if d.0 == volume.idx && d.1 == dir_entry.cluster { return Err(Error::FileIsOpen); } } @@ -498,11 +519,6 @@ where file.starting_cluster = match &mut volume.volume_type { VolumeType::Fat(fat) => fat.alloc_cluster(self, None, false)?, }; - for f in self.open_files.iter_mut() { - if f.1 == Cluster(0) { - *f = (f.0, file.starting_cluster) - } - } file.entry.cluster = file.starting_cluster; debug!("Alloc first cluster {:?}", file.starting_cluster); @@ -593,10 +609,9 @@ where /// 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; + for (i, f) in self.open_files.iter().enumerate() { + if f.2 == file.search_id { + self.open_files.swap_remove(i); break; } } @@ -609,7 +624,7 @@ where .open_dirs .iter() .chain(self.open_files.iter()) - .all(|(_, c)| c == &Cluster::INVALID) + .all(|(_, c, _)| c == &Cluster::INVALID) } /// Consume self and return BlockDevice and TimeSource From b54e9e6ced3721ac79052321d0f4e27f3da3731e Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Mon, 17 Oct 2022 18:41:47 -0400 Subject: [PATCH 33/69] Cleanup comparisons, fix has_open_handles --- src/filesystem/search_id.rs | 8 +--- src/volume_mgr.rs | 81 ++++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs index 3b8a506..85c35a4 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -1,14 +1,8 @@ -#[derive(Clone, Copy, Debug)] +#[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 File/Directory lists pub struct SearchId(pub(crate) u32); -impl PartialEq for SearchId { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - /// ID generator intented to be used in a static context. /// /// This object will always return a different ID. diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index f91cebf..51d3e9a 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -16,6 +16,23 @@ use heapless::Vec; static ID_GENERATOR: IdGenerator = IdGenerator::new(); +#[derive(PartialEq, Eq)] +struct UniqueCluster { + volume_idx: VolumeIdx, + cluster: Cluster, + search_id: SearchId, +} + +impl UniqueCluster { + fn compare_volume_and_cluster(&self, volume_idx: VolumeIdx, cluster: Cluster) -> bool { + self.volume_idx == volume_idx && self.cluster == cluster + } + + fn compare_id(&self, search_id: SearchId) -> bool { + self.search_id == search_id + } +} + /// A `VolumeManager` wraps a block device and gives access to the volumes within it. pub struct VolumeManager where @@ -25,8 +42,8 @@ where { pub(crate) block_device: D, pub(crate) timesource: T, - open_dirs: Vec<(VolumeIdx, Cluster, SearchId), MAX_DIRS>, - open_files: Vec<(VolumeIdx, Cluster, SearchId), MAX_FILES>, + open_dirs: Vec, + open_files: Vec, } impl VolumeManager @@ -161,15 +178,19 @@ where // 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. - for (v, c, _) in self.open_dirs.iter() { - if *v == volume.idx && *c == Cluster::ROOT_DIR { + for u in self.open_dirs.iter() { + if u.compare_volume_and_cluster(volume.idx, Cluster::ROOT_DIR) { return Err(Error::DirAlreadyOpen); } } let search_id = ID_GENERATOR.next(); // Remember this open directory self.open_dirs - .push((volume.idx, Cluster::ROOT_DIR, search_id)) + .push(UniqueCluster { + volume_idx: volume.idx, + cluster: Cluster::ROOT_DIR, + search_id, + }) .map_err(|_| Error::TooManyOpenDirs)?; Ok(Directory { @@ -205,15 +226,19 @@ where } // Check it's not already open - for dir_table_row in self.open_dirs.iter() { - if dir_table_row.0 == volume.idx && dir_table_row.1 == dir_entry.cluster { + for d in self.open_dirs.iter() { + if d.compare_volume_and_cluster(volume.idx, dir_entry.cluster) { return Err(Error::DirAlreadyOpen); } } // Remember this open directory. let search_id = ID_GENERATOR.next(); self.open_dirs - .push((volume.idx, dir_entry.cluster, search_id)) + .push(UniqueCluster { + volume_idx: volume.idx, + cluster: dir_entry.cluster, + search_id, + }) .map_err(|_| Error::TooManyOpenDirs)?; Ok(Directory { @@ -226,7 +251,7 @@ where /// and so must close it if you want to do something with it. pub fn close_dir(&mut self, volume: &Volume, dir: Directory) { for (i, d) in self.open_dirs.iter().enumerate() { - if d.2 == dir.search_id { + if d.compare_id(dir.search_id) { self.open_dirs.swap_remove(i); break; } @@ -271,8 +296,8 @@ where return Err(Error::TooManyOpenFiles); } // Check it's not already open - for dir_table_row in self.open_files.iter() { - if dir_table_row.0 == volume.idx && dir_table_row.1 == dir_entry.cluster { + for d in self.open_files.iter() { + if d.compare_volume_and_cluster(volume.idx, dir_entry.cluster) { return Err(Error::DirAlreadyOpen); } } @@ -340,7 +365,11 @@ where }; // Remember this open file self.open_files - .push((volume.idx, file.starting_cluster, search_id)) + .push(UniqueCluster { + volume_idx: volume.idx, + cluster: file.starting_cluster, + search_id, + }) .map_err(|_| Error::TooManyOpenDirs)?; Ok(file) @@ -404,7 +433,11 @@ where // Remember this open file self.open_files - .push((volume.idx, file.starting_cluster, search_id)) + .push(UniqueCluster { + volume_idx: volume.idx, + cluster: file.starting_cluster, + search_id, + }) .map_err(|_| Error::TooManyOpenFiles)?; Ok(file) @@ -419,18 +452,6 @@ where } } - /// 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); - } - } - open_files_row.ok_or(Error::TooManyOpenFiles) - } - /// Delete a closed file with the given full path, if exists. pub fn delete_file_in_dir( &mut self, @@ -451,7 +472,7 @@ where } for d in self.open_files.iter_mut() { - if d.0 == volume.idx && d.1 == dir_entry.cluster { + if d.compare_volume_and_cluster(volume.idx, dir_entry.cluster) { return Err(Error::FileIsOpen); } } @@ -610,7 +631,7 @@ where /// Close a file with the given full path. pub fn close_file(&mut self, volume: &Volume, file: File) -> Result<(), Error> { for (i, f) in self.open_files.iter().enumerate() { - if f.2 == file.search_id { + if f.compare_id(file.search_id) { self.open_files.swap_remove(i); break; } @@ -620,11 +641,7 @@ where /// 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 From 6426e70e689647d770041e954b06de615f79369d Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Mon, 17 Oct 2022 22:03:01 -0400 Subject: [PATCH 34/69] Cleanup ID/volume/cluster search implementation --- src/volume_mgr.rs | 97 +++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 51d3e9a..a7b9fe2 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -17,13 +17,13 @@ use heapless::Vec; static ID_GENERATOR: IdGenerator = IdGenerator::new(); #[derive(PartialEq, Eq)] -struct UniqueCluster { +struct ClusterDescriptor { volume_idx: VolumeIdx, cluster: Cluster, search_id: SearchId, } -impl UniqueCluster { +impl ClusterDescriptor { fn compare_volume_and_cluster(&self, volume_idx: VolumeIdx, cluster: Cluster) -> bool { self.volume_idx == volume_idx && self.cluster == cluster } @@ -42,8 +42,8 @@ where { pub(crate) block_device: D, pub(crate) timesource: T, - open_dirs: Vec, - open_files: Vec, + open_dirs: Vec, + open_files: Vec, } impl VolumeManager @@ -167,7 +167,7 @@ where } } - /// Open a directory. + /// Open the volume's root directory. /// /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. /// @@ -175,18 +175,21 @@ where /// this directory handle is open. In particular, stop this directory /// being unlinked. pub fn open_root_dir(&mut self, volume: &Volume) -> Result> { + if self.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); + } + // 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. - for u in self.open_dirs.iter() { - if u.compare_volume_and_cluster(volume.idx, Cluster::ROOT_DIR) { - return Err(Error::DirAlreadyOpen); - } + if cluster_already_open(&self.open_dirs, volume.idx, Cluster::ROOT_DIR) { + return Err(Error::DirAlreadyOpen); } + let search_id = ID_GENERATOR.next(); // Remember this open directory self.open_dirs - .push(UniqueCluster { + .push(ClusterDescriptor { volume_idx: volume.idx, cluster: Cluster::ROOT_DIR, search_id, @@ -226,15 +229,14 @@ where } // Check it's not already open - for d in self.open_dirs.iter() { - if d.compare_volume_and_cluster(volume.idx, dir_entry.cluster) { - return Err(Error::DirAlreadyOpen); - } + if cluster_already_open(&self.open_dirs, volume.idx, dir_entry.cluster) { + return Err(Error::DirAlreadyOpen); } + // Remember this open directory. let search_id = ID_GENERATOR.next(); self.open_dirs - .push(UniqueCluster { + .push(ClusterDescriptor { volume_idx: volume.idx, cluster: dir_entry.cluster, search_id, @@ -250,12 +252,11 @@ where /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. pub fn close_dir(&mut self, volume: &Volume, dir: Directory) { - for (i, d) in self.open_dirs.iter().enumerate() { - if d.compare_id(dir.search_id) { - self.open_dirs.swap_remove(i); - break; - } - } + // Unwrap, because we should never be in a situation where we're attempting to close a dir + // with an ID which doesn't exist in our open dirs list. + let idx_to_close = cluster_position_by_id(&self.open_dirs, dir.search_id).unwrap(); + self.open_dirs.swap_remove(idx_to_close); + drop(dir); } /// Look in a directory for a named file. @@ -295,12 +296,12 @@ where if self.open_files.is_full() { return Err(Error::TooManyOpenFiles); } + // Check it's not already open - for d in self.open_files.iter() { - if d.compare_volume_and_cluster(volume.idx, dir_entry.cluster) { - return Err(Error::DirAlreadyOpen); - } + if cluster_already_open(&self.open_dirs, volume.idx, dir_entry.cluster) { + return Err(Error::DirAlreadyOpen); } + if dir_entry.attributes.is_directory() { return Err(Error::OpenedDirAsFile); } @@ -363,9 +364,10 @@ where } _ => return Err(Error::Unsupported), }; + // Remember this open file self.open_files - .push(UniqueCluster { + .push(ClusterDescriptor { volume_idx: volume.idx, cluster: file.starting_cluster, search_id, @@ -433,7 +435,7 @@ where // Remember this open file self.open_files - .push(UniqueCluster { + .push(ClusterDescriptor { volume_idx: volume.idx, cluster: file.starting_cluster, search_id, @@ -471,10 +473,8 @@ where return Err(Error::DeleteDirAsFile); } - for d in self.open_files.iter_mut() { - if d.compare_volume_and_cluster(volume.idx, dir_entry.cluster) { - return Err(Error::FileIsOpen); - } + if cluster_already_open(&self.open_files, volume.idx, dir_entry.cluster) { + return Err(Error::FileIsOpen); } match &volume.volume_type { @@ -541,6 +541,16 @@ where VolumeType::Fat(fat) => fat.alloc_cluster(self, None, false)?, }; + // Update the cluster descriptor in our open files list + let cluster_to_update = self + .open_files + .iter_mut() + .find(|d| d.compare_id(file.search_id)); + + if let Some(c) = cluster_to_update { + c.cluster = file.starting_cluster; + } + file.entry.cluster = file.starting_cluster; debug!("Alloc first cluster {:?}", file.starting_cluster); } @@ -630,12 +640,12 @@ where /// Close a file with the given full path. pub fn close_file(&mut self, volume: &Volume, file: File) -> Result<(), Error> { - for (i, f) in self.open_files.iter().enumerate() { - if f.compare_id(file.search_id) { - self.open_files.swap_remove(i); - break; - } - } + // Unwrap, because we should never be in a situation where we're attempting to close a file + // with an ID which doesn't exist in our open files list. + let idx_to_close = cluster_position_by_id(&self.open_files, file.search_id).unwrap(); + self.open_files.swap_remove(idx_to_close); + + drop(file); Ok(()) } @@ -704,6 +714,19 @@ where } } +fn cluster_position_by_id(vec: &[ClusterDescriptor], id_to_find: SearchId) -> Option { + vec.iter().position(|f| f.compare_id(id_to_find)) +} + +fn cluster_already_open( + vec: &[ClusterDescriptor], + volume_idx: VolumeIdx, + cluster: Cluster, +) -> bool { + vec.iter() + .any(|d| d.compare_volume_and_cluster(volume_idx, cluster)) +} + /// Transform mode variants (ReadWriteCreate_Or_Append) to simple modes ReadWriteAppend or /// ReadWriteCreate fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { From f570e56bfac5f61357992c7def76c78942591438 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Tue, 18 Oct 2022 17:13:46 -0400 Subject: [PATCH 35/69] Make IdGenerator non-static --- src/filesystem/search_id.rs | 13 +++---- src/volume_mgr.rs | 69 +++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs index 85c35a4..9e7af81 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -7,22 +7,19 @@ pub struct SearchId(pub(crate) u32); /// /// This object will always return a different ID. pub struct IdGenerator { - next_id: core::sync::atomic::AtomicU32, + next_id: u32, } impl IdGenerator { /// Create a new [`IdGenerator`]. pub const fn new() -> Self { - Self { - next_id: core::sync::atomic::AtomicU32::new(0), - } + Self { next_id: 0 } } /// Generate a new, unique [`SearchId`]. - pub fn next(&self) -> SearchId { - use core::sync::atomic::Ordering; - let id = self.next_id.load(Ordering::Acquire); - self.next_id.store(id + 1, Ordering::Release); + pub fn get(&mut self) -> SearchId { + let id = self.next_id; + self.next_id += 1; SearchId(id) } } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index a7b9fe2..9f3d03f 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -14,8 +14,6 @@ use crate::{ }; use heapless::Vec; -static ID_GENERATOR: IdGenerator = IdGenerator::new(); - #[derive(PartialEq, Eq)] struct ClusterDescriptor { volume_idx: VolumeIdx, @@ -24,6 +22,14 @@ struct ClusterDescriptor { } impl ClusterDescriptor { + fn new(volume_idx: VolumeIdx, cluster: Cluster, search_id: SearchId) -> Self { + Self { + volume_idx, + cluster, + search_id, + } + } + fn compare_volume_and_cluster(&self, volume_idx: VolumeIdx, cluster: Cluster) -> bool { self.volume_idx == volume_idx && self.cluster == cluster } @@ -42,6 +48,7 @@ where { pub(crate) block_device: D, pub(crate) timesource: T, + id_generator: IdGenerator, open_dirs: Vec, open_files: Vec, } @@ -81,6 +88,7 @@ where VolumeManager { block_device, timesource, + id_generator: IdGenerator::new(), open_dirs: Vec::new(), open_files: Vec::new(), } @@ -186,14 +194,14 @@ where return Err(Error::DirAlreadyOpen); } - let search_id = ID_GENERATOR.next(); + let search_id = self.id_generator.get(); // Remember this open directory self.open_dirs - .push(ClusterDescriptor { - volume_idx: volume.idx, - cluster: Cluster::ROOT_DIR, + .push(ClusterDescriptor::new( + volume.idx, + Cluster::ROOT_DIR, search_id, - }) + )) .map_err(|_| Error::TooManyOpenDirs)?; Ok(Directory { @@ -234,13 +242,13 @@ where } // Remember this open directory. - let search_id = ID_GENERATOR.next(); + let search_id = self.id_generator.get(); self.open_dirs - .push(ClusterDescriptor { - volume_idx: volume.idx, - cluster: dir_entry.cluster, + .push(ClusterDescriptor::new( + volume.idx, + dir_entry.cluster, search_id, - }) + )) .map_err(|_| Error::TooManyOpenDirs)?; Ok(Directory { @@ -255,7 +263,7 @@ where // Unwrap, because we should never be in a situation where we're attempting to close a dir // with an ID which doesn't exist in our open dirs list. let idx_to_close = cluster_position_by_id(&self.open_dirs, dir.search_id).unwrap(); - self.open_dirs.swap_remove(idx_to_close); + self.open_dirs.remove(idx_to_close); drop(dir); } @@ -310,7 +318,7 @@ where } let mode = solve_mode_variant(mode, true); - let search_id = ID_GENERATOR.next(); + let search_id = self.id_generator.get(); let file = match mode { Mode::ReadOnly => File { @@ -367,11 +375,11 @@ where // Remember this open file self.open_files - .push(ClusterDescriptor { - volume_idx: volume.idx, - cluster: file.starting_cluster, + .push(ClusterDescriptor::new( + volume.idx, + file.starting_cluster, search_id, - }) + )) .map_err(|_| Error::TooManyOpenDirs)?; Ok(file) @@ -421,7 +429,7 @@ where } }; - let search_id = ID_GENERATOR.next(); + let search_id = self.id_generator.get(); let file = File { starting_cluster: entry.cluster, @@ -435,11 +443,11 @@ where // Remember this open file self.open_files - .push(ClusterDescriptor { - volume_idx: volume.idx, - cluster: file.starting_cluster, + .push(ClusterDescriptor::new( + volume.idx, + file.starting_cluster, search_id, - }) + )) .map_err(|_| Error::TooManyOpenFiles)?; Ok(file) @@ -478,8 +486,19 @@ where } match &volume.volume_type { - VolumeType::Fat(fat) => fat.delete_directory_entry(self, dir, name), + VolumeType::Fat(fat) => fat.delete_directory_entry(self, dir, name)?, } + + // Unwrap, because we should never be in a situation where we're attempting to close a file + // which doesn't exist in our open files list. + let idx_to_remove = self + .open_files + .iter() + .position(|d| d.compare_volume_and_cluster(volume.idx, dir_entry.cluster)) + .unwrap(); + self.open_files.remove(idx_to_remove); + + Ok(()) } /// Read from an open file. @@ -643,7 +662,7 @@ where // Unwrap, because we should never be in a situation where we're attempting to close a file // with an ID which doesn't exist in our open files list. let idx_to_close = cluster_position_by_id(&self.open_files, file.search_id).unwrap(); - self.open_files.swap_remove(idx_to_close); + self.open_files.remove(idx_to_close); drop(file); Ok(()) From 3c176bc2e4685a2ad22190123f7bd0dd516ceb9f Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sat, 10 Jun 2023 17:18:34 -0400 Subject: [PATCH 36/69] Minor refactor --- src/volume_mgr.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 9f3d03f..0e94acd 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -264,7 +264,6 @@ where // with an ID which doesn't exist in our open dirs list. let idx_to_close = cluster_position_by_id(&self.open_dirs, dir.search_id).unwrap(); self.open_dirs.remove(idx_to_close); - drop(dir); } /// Look in a directory for a named file. @@ -664,7 +663,6 @@ where let idx_to_close = cluster_position_by_id(&self.open_files, file.search_id).unwrap(); self.open_files.remove(idx_to_close); - drop(file); Ok(()) } @@ -733,8 +731,8 @@ where } } -fn cluster_position_by_id(vec: &[ClusterDescriptor], id_to_find: SearchId) -> Option { - vec.iter().position(|f| f.compare_id(id_to_find)) +fn cluster_position_by_id(list: &[ClusterDescriptor], id_to_find: SearchId) -> Option { + list.iter().position(|f| f.compare_id(id_to_find)) } fn cluster_already_open( From b10a70cd82511da79148d03efd95824153ce6cd7 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sat, 10 Jun 2023 22:11:30 -0400 Subject: [PATCH 37/69] Wrapping add for IdGenerator --- src/filesystem/search_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs index 9e7af81..593a597 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -19,7 +19,7 @@ impl IdGenerator { /// Generate a new, unique [`SearchId`]. pub fn get(&mut self) -> SearchId { let id = self.next_id; - self.next_id += 1; + self.next_id = self.next_id.wrapping_add(1); SearchId(id) } } From a547575349a3edbb24aeb4c2ef39a4b71eb1ebf2 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sat, 10 Jun 2023 22:24:35 -0400 Subject: [PATCH 38/69] Return error when attempting to open a file multiple times --- src/volume_mgr.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 0e94acd..3ec245f 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -412,6 +412,12 @@ where _ => return Err(Error::FileNotFound), }; + if let Some(d) = &dir_entry { + if cluster_already_open(&self.open_files, volume.idx, d.cluster) { + return Err(Error::FileAlreadyOpen); + } + } + let mode = solve_mode_variant(mode, dir_entry.is_some()); match mode { From 421e68cca5d6d612ad6d3b3f19693f8d4be44252 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sat, 10 Jun 2023 22:06:49 -0400 Subject: [PATCH 39/69] User-selectable number of retries for card acquisition --- src/sdcard/mod.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 350c774..67b64b9 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -401,7 +401,7 @@ where // Assert CS s.cs_low()?; // Enter SPI mode. - let mut delay = Delay::new_command(); + 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) { @@ -586,11 +586,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, + } } } @@ -708,12 +715,13 @@ impl Delay { T: embedded_hal::blocking::delay::DelayUs, { if self.retries_left == 0 { - Err(err) + return Err(err); } else { delayer.delay_us(10); self.retries_left -= 1; - Ok(()) } + + Ok(()) } } From 4ea9fb861d6121ed925e919f4e69808c2bc903ab Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sun, 11 Jun 2023 07:57:53 -0400 Subject: [PATCH 40/69] Minor refactor --- src/filesystem/search_id.rs | 12 ++++++++---- src/volume_mgr.rs | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs index 593a597..87ae856 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -1,3 +1,5 @@ +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 File/Directory lists @@ -7,19 +9,21 @@ pub struct SearchId(pub(crate) u32); /// /// This object will always return a different ID. pub struct IdGenerator { - next_id: u32, + next_id: Wrapping, } impl IdGenerator { /// Create a new [`IdGenerator`]. pub const fn new() -> Self { - Self { next_id: 0 } + Self { + next_id: Wrapping(0), + } } /// Generate a new, unique [`SearchId`]. pub fn get(&mut self) -> SearchId { let id = self.next_id; - self.next_id = self.next_id.wrapping_add(1); - SearchId(id) + self.next_id += 1; + SearchId(id.0) } } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 3ec245f..80c98d4 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -737,16 +737,20 @@ where } } -fn cluster_position_by_id(list: &[ClusterDescriptor], id_to_find: SearchId) -> Option { - list.iter().position(|f| f.compare_id(id_to_find)) +fn cluster_position_by_id( + cluster_list: &[ClusterDescriptor], + id_to_find: SearchId, +) -> Option { + cluster_list.iter().position(|f| f.compare_id(id_to_find)) } fn cluster_already_open( - vec: &[ClusterDescriptor], + cluster_list: &[ClusterDescriptor], volume_idx: VolumeIdx, cluster: Cluster, ) -> bool { - vec.iter() + cluster_list + .iter() .any(|d| d.compare_volume_and_cluster(volume_idx, cluster)) } From d80f39d59e862b8a08dcda32aae4555093c81028 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sun, 11 Jun 2023 08:00:25 -0400 Subject: [PATCH 41/69] Minor refactor --- src/sdcard/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 67b64b9..7cb153e 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -715,13 +715,12 @@ impl Delay { T: embedded_hal::blocking::delay::DelayUs, { if self.retries_left == 0 { - return Err(err); + Err(err) } else { delayer.delay_us(10); self.retries_left -= 1; + Ok(()) } - - Ok(()) } } From 69eef723587d2c21b332f542ab8ef34cf7a996fb Mon Sep 17 00:00:00 2001 From: ftilde Date: Sat, 24 Jun 2023 13:23:15 +0200 Subject: [PATCH 42/69] Add a local on-stack FAT cache for next_cluster For some operations, e.g. seeking in a file, this makes a huge performance difference since we avoid (re)reading the FAT for every node in the linked list. For other operations that call next_cluster only once there are no negative implications since the memory for the FAT block would have been allocated on the stack anyways. --- src/fat/mod.rs | 34 ++++++++++++ src/fat/volume.rs | 131 ++++++++++++++++++++++++++-------------------- src/volume_mgr.rs | 6 ++- 3 files changed, 112 insertions(+), 59 deletions(-) diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 6b74628..d4c23da 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -14,6 +14,38 @@ 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, + volume_mgr: &VolumeManager, + block_idx: BlockIdx, + reason: &str, + ) -> Result<&Block, Error> + where + D: BlockDevice, + T: TimeSource, + { + if Some(block_idx) != self.idx { + self.idx = Some(block_idx); + volume_mgr + .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,6 +56,8 @@ pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; pub use ondiskdirentry::OnDiskDirEntry; pub use volume::{parse_volume, FatVolume, VolumeName}; +use crate::{Block, BlockDevice, BlockIdx, Error, TimeSource, VolumeManager}; + #[cfg(test)] mod test { diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 3afa5a8..64b804d 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -12,6 +12,8 @@ use crate::{ 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)] @@ -179,24 +181,21 @@ impl FatVolume { &self, volume_mgr: &VolumeManager, cluster: Cluster, + 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(&volume_mgr, 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 @@ -216,13 +215,11 @@ 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(&volume_mgr, 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 @@ -341,18 +338,20 @@ impl FatVolume { } } 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, - }; + let mut block_cache = BlockCache::empty(); + current_cluster = + match self.next_cluster(volume_mgr, 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(volume_mgr, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; } else { current_cluster = None; } @@ -399,7 +398,9 @@ impl FatVolume { } } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { + let mut block_cache = BlockCache::empty(); + current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) + { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -445,6 +446,7 @@ impl FatVolume { _ => 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 @@ -467,13 +469,14 @@ impl FatVolume { } } 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, - }; + current_cluster = + match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; } else { current_cluster = None; } @@ -486,6 +489,7 @@ impl FatVolume { _ => 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))) { @@ -508,7 +512,8 @@ impl FatVolume { } } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) + { Ok(n) => Some(n), _ => None, }; @@ -545,6 +550,7 @@ impl FatVolume { _ => BlockCount(u32::from(self.blocks_per_cluster)), }; + let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { match self.find_entry_in_block( @@ -558,13 +564,14 @@ impl FatVolume { } } 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, - }; + current_cluster = + match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; } else { current_cluster = None; } @@ -576,6 +583,7 @@ impl FatVolume { Cluster::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))) { @@ -589,7 +597,8 @@ impl FatVolume { x => return x, } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { + current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) + { Ok(n) => Some(n), _ => None, } @@ -668,13 +677,15 @@ impl FatVolume { } } 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, - }; + let mut block_cache = BlockCache::empty(); + current_cluster = + match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; } else { current_cluster = None; } @@ -694,7 +705,9 @@ impl FatVolume { x => return x, } } - current_cluster = match self.next_cluster(volume_mgr, cluster) { + let mut block_cache = BlockCache::empty(); + current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) + { Ok(n) => Some(n), _ => None, } @@ -920,10 +933,13 @@ impl FatVolume { // 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(volume_mgr, 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 { @@ -934,7 +950,8 @@ impl FatVolume { } self.update_fat(volume_mgr, cluster, Cluster::END_OF_FILE)?; loop { - match self.next_cluster(volume_mgr, next) { + let mut block_cache = BlockCache::empty(); + match self.next_cluster(volume_mgr, next, &mut block_cache) { Ok(n) => { self.update_fat(volume_mgr, next, Cluster::EMPTY)?; next = n; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 80c98d4..9c355c5 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -3,7 +3,8 @@ 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, IdGenerator, Mode, SearchId, ShortFileName, TimeSource, MAX_FILE_SIZE, @@ -697,9 +698,10 @@ where // 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)?, + VolumeType::Fat(fat) => fat.next_cluster(self, start.1, &mut block_cache)?, }; start.0 += bytes_per_cluster; } From 3743f1a2e3ab33f258950acd9190312040658615 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Fri, 15 Sep 2023 19:19:17 +0300 Subject: [PATCH 43/69] fix: on multi-block writes send ACMD23 and wait not busy Signed-off-by: Lachezar Lechev --- src/sdcard/mod.rs | 7 +++++++ src/sdcard/proto.rs | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 350c774..ad36b49 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -237,6 +237,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() { diff --git a/src/sdcard/proto.rs b/src/sdcard/proto.rs index 1d34b0f..b9e4032 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; From 9204826ba55e548c8dd1c4049b9c8b120535f945 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 19:34:15 +0100 Subject: [PATCH 44/69] Clean up warning about unused argument. --- src/volume_mgr.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 9c355c5..754ecbe 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -261,6 +261,12 @@ where /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. pub fn close_dir(&mut self, volume: &Volume, dir: Directory) { + // We don't strictly speaking need the volume in order to close a + // directory, as we don't flush anything to disk at this point. The open + // directory acts more as a lock. However, we take it because it then + // matches the `close_file` API. + let _ = volume; + // Unwrap, because we should never be in a situation where we're attempting to close a dir // with an ID which doesn't exist in our open dirs list. let idx_to_close = cluster_position_by_id(&self.open_dirs, dir.search_id).unwrap(); @@ -665,6 +671,12 @@ where /// Close a file with the given full path. pub fn close_file(&mut self, volume: &Volume, file: File) -> Result<(), Error> { + // We don't strictly speaking need the volume in order to close a + // directory, as we don't flush anything to disk at this point. However, + // we take it in case we change this in the future and closing a file + // does then cause some disk write to occur. + let _ = volume; + // Unwrap, because we should never be in a situation where we're attempting to close a file // with an ID which doesn't exist in our open files list. let idx_to_close = cluster_position_by_id(&self.open_files, file.search_id).unwrap(); From ccebb448f7f036760d2849e7bbfe130d2ba511cb Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 19:45:12 +0100 Subject: [PATCH 45/69] Clippy fixes. The previous "caching" PR introduced some redundant references. --- src/fat/volume.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 64b804d..7a0ece7 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -192,8 +192,7 @@ impl FatVolume { let fat_offset = cluster.0 * 2; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - let block = - fat_block_cache.read(&volume_mgr, this_fat_block_num, "next_cluster")?; + let block = fat_block_cache.read(volume_mgr, 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 { @@ -215,8 +214,7 @@ impl FatVolume { let fat_offset = cluster.0 * 4; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - let block = - fat_block_cache.read(&volume_mgr, this_fat_block_num, "next_cluster")?; + let block = fat_block_cache.read(volume_mgr, this_fat_block_num, "next_cluster")?; let fat_entry = LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) & 0x0FFF_FFFF; From 3aa510f7bc9a434a226bbd8f902306595a2a21ca Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 19:45:52 +0100 Subject: [PATCH 46/69] Clarify what inits that card and what doesn't. Closes #87 Closes #90 --- src/sdcard/mod.rs | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index f0415f7..81e85d6 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -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()?; From 8f4cb9ae26b0c2ce8f6bd51029280c65bbfbc9be Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 19:46:35 +0100 Subject: [PATCH 47/69] Add clarification. --- src/volume_mgr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 754ecbe..29cf2cb 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -40,7 +40,8 @@ impl ClusterDescriptor { } } -/// A `VolumeManager` wraps a block device and gives access to the volumes within it. +/// A `VolumeManager` wraps a block device and gives access to the FAT-formatted +/// volumes within it. pub struct VolumeManager where D: BlockDevice, From 58b3bd695a0efdbcea28a7b3d01b79c4e0d9b28d Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 20:07:02 +0100 Subject: [PATCH 48/69] Clean up comments. --- src/fat/mod.rs | 2 +- src/fat/volume.rs | 6 ++++-- src/filesystem/mod.rs | 2 +- src/lib.rs | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/fat/mod.rs b/src/fat/mod.rs index d4c23da..9d398dc 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. diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 7a0ece7..372123a 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -37,7 +37,7 @@ 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)] pub struct FatVolume { @@ -76,7 +76,9 @@ impl FatVolume { 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(()); diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 7e7c9b4..9783319 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. diff --git a/src/lib.rs b/src/lib.rs index bb2a86f..f79b182 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -234,8 +234,9 @@ 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); From f46ef4dcbd15c5c3c7b2b96a6cc0c2948b0c3b8e Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 20:07:06 +0100 Subject: [PATCH 49/69] Check written files have correct length. This test currently fails. --- examples/write_test.rs | 51 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/examples/write_test.rs b/examples/write_test.rs index 47acfec..f0d572f 100644 --- a/examples/write_test.rs +++ b/examples/write_test.rs @@ -138,6 +138,7 @@ fn main() { .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadOnly) .unwrap(); println!("\nReading from file {}\n", FILE_TO_WRITE); + let mut csum = 0; println!("FILE STARTS:"); while !f.eof() { let mut buffer = [0u8; 32]; @@ -147,9 +148,11 @@ fn main() { print!("\\n"); } print!("{}", *b as char); + csum += u32::from(*b); } } println!("EOF\n"); + let mut file_size = f.length() as usize; volume_mgr.close_file(&volume, f).unwrap(); let mut f = volume_mgr @@ -161,15 +164,20 @@ fn main() { 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); + for b in &buffer1[..] { + csum += u32::from(*b); + } + for b in &buffer[..] { + csum += u32::from(*b); + } println!( - "\tFound {}?: {:?}", - FILE_TO_WRITE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_WRITE) + "Number of bytes appendedß: {}\n", + num_written + num_written1 ); + file_size += num_written; + file_size += num_written1; + + f.seek_from_start(0).unwrap(); println!("\nFILE STARTS:"); while !f.eof() { let mut buffer = [0u8; 32]; @@ -184,6 +192,35 @@ fn main() { println!("EOF"); volume_mgr.close_file(&volume, f).unwrap(); + println!("\tFinding {}...", FILE_TO_WRITE); + let dir_ent = volume_mgr + .find_directory_entry(&volume, &root_dir, FILE_TO_WRITE) + .unwrap(); + println!("\tFound {}?: {:?}", FILE_TO_WRITE, dir_ent); + assert_eq!(dir_ent.size as usize, file_size); + let mut f = volume_mgr + .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) + .unwrap(); + println!("\nReading from file {}\n", FILE_TO_WRITE); + println!("FILE STARTS:"); + let mut csum2 = 0; + 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); + csum2 += u32::from(*b); + } + } + println!("EOF\n"); + assert_eq!(f.length() as usize, file_size); + volume_mgr.close_file(&volume, f).unwrap(); + + assert_eq!(csum, csum2); + println!("\nTruncating file"); let mut f = volume_mgr .open_file_in_dir( From 7a7af7bf65b6978fd4d0caca527037356754f837 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 21:11:26 +0100 Subject: [PATCH 50/69] Flush dir_entry on file close. Requires mutable access to the volume in order to do the write. --- examples/create_test.rs | 4 ++-- examples/delete_test.rs | 2 +- examples/readme_test.rs | 2 +- examples/test_mount.rs | 4 ++-- examples/write_test.rs | 20 +++++++++++--------- src/lib.rs | 2 +- src/volume_mgr.rs | 25 +++++++++++-------------- 7 files changed, 29 insertions(+), 30 deletions(-) diff --git a/examples/create_test.rs b/examples/create_test.rs index 239cded..612a5fc 100644 --- a/examples/create_test.rs +++ b/examples/create_test.rs @@ -167,7 +167,7 @@ fn main() { 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(); + volume_mgr.close_file(&mut volume, f).unwrap(); let mut f = volume_mgr .open_file_in_dir( @@ -198,7 +198,7 @@ fn main() { } } println!("EOF"); - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); } } } diff --git a/examples/delete_test.rs b/examples/delete_test.rs index 189f683..7c73da4 100644 --- a/examples/delete_test.rs +++ b/examples/delete_test.rs @@ -154,7 +154,7 @@ fn main() { Err(error) => println!("\tCannot delete file: {:?}", error), } println!("\tClosing {}...", FILE_TO_DELETE); - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); match volume_mgr.delete_file_in_dir(&volume, &root_dir, FILE_TO_DELETE) { Ok(()) => println!("\tDeleted {}.", FILE_TO_DELETE), diff --git a/examples/readme_test.rs b/examples/readme_test.rs index bb9ffbc..b5fd1b3 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -109,7 +109,7 @@ fn main() -> Result<(), Error> { print!("{}", *b as char); } } - volume_mgr.close_file(&volume0, my_file)?; + volume_mgr.close_file(&mut volume0, my_file)?; volume_mgr.close_dir(&volume0, root_dir); Ok(()) } diff --git a/examples/test_mount.rs b/examples/test_mount.rs index f0eddf4..53f2500 100644 --- a/examples/test_mount.rs +++ b/examples/test_mount.rs @@ -158,7 +158,7 @@ fn main() { 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(); + volume_mgr.close_file(&mut volume, f).unwrap(); let test_dir = volume_mgr.open_dir(&volume, &root_dir, "TEST").unwrap(); // Check we can't open it twice @@ -187,7 +187,7 @@ fn main() { } } println!("Checksum over {} bytes: {}", f.length(), csum); - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); assert!(volume_mgr.open_root_dir(&volume).is_err()); volume_mgr.close_dir(&volume, root_dir); diff --git a/examples/write_test.rs b/examples/write_test.rs index f0d572f..7743517 100644 --- a/examples/write_test.rs +++ b/examples/write_test.rs @@ -153,7 +153,7 @@ fn main() { } println!("EOF\n"); let mut file_size = f.length() as usize; - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); let mut f = volume_mgr .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) @@ -170,10 +170,7 @@ fn main() { for b in &buffer[..] { csum += u32::from(*b); } - println!( - "Number of bytes appendedß: {}\n", - num_written + num_written1 - ); + println!("Number of bytes appended: {}\n", num_written + num_written1); file_size += num_written; file_size += num_written1; @@ -190,7 +187,7 @@ fn main() { } } println!("EOF"); - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); println!("\tFinding {}...", FILE_TO_WRITE); let dir_ent = volume_mgr @@ -201,7 +198,12 @@ fn main() { let mut f = volume_mgr .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) .unwrap(); - println!("\nReading from file {}\n", FILE_TO_WRITE); + println!( + "\nReading from file {}, len {}\n", + FILE_TO_WRITE, + f.length() + ); + f.seek_from_start(0).unwrap(); println!("FILE STARTS:"); let mut csum2 = 0; while !f.eof() { @@ -217,7 +219,7 @@ fn main() { } println!("EOF\n"); assert_eq!(f.length() as usize, file_size); - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); assert_eq!(csum, csum2); @@ -254,7 +256,7 @@ fn main() { } } println!("EOF"); - volume_mgr.close_file(&volume, f).unwrap(); + volume_mgr.close_file(&mut volume, f).unwrap(); } } } diff --git a/src/lib.rs b/src/lib.rs index f79b182..89d1ad4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,7 @@ //! print!("{}", *b as char); //! } //! } -//! volume_mgr.close_file(&volume0, my_file)?; +//! volume_mgr.close_file(&mut volume0, my_file)?; //! volume_mgr.close_dir(&volume0, root_dir); //! # Ok(()) //! # } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 29cf2cb..e7566b7 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -658,25 +658,22 @@ where 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)?; - } - } + // Entry update deferred to file close, for performance. } Ok(written) } /// Close a file with the given full path. - pub fn close_file(&mut self, volume: &Volume, file: File) -> Result<(), Error> { - // We don't strictly speaking need the volume in order to close a - // directory, as we don't flush anything to disk at this point. However, - // we take it in case we change this in the future and closing a file - // does then cause some disk write to occur. - let _ = volume; + pub fn close_file(&mut self, volume: &mut Volume, file: File) -> Result<(), Error> { + match volume.volume_type { + VolumeType::Fat(ref mut fat) => { + debug!("Updating FAT info sector"); + fat.update_info_sector(self)?; + debug!("Updating dir entry {:?}", file.entry); + let fat_type = fat.get_fat_type(); + self.write_entry_to_disk(fat_type, &file.entry)?; + } + }; // Unwrap, because we should never be in a situation where we're attempting to close a file // with an ID which doesn't exist in our open files list. From b132e04757d7d2152da64a90028e35b001204911 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 21:26:43 +0100 Subject: [PATCH 51/69] Don't update direntry on files that didn't change. --- src/filesystem/files.rs | 2 ++ src/volume_mgr.rs | 27 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index 2a00a07..f65b020 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -18,6 +18,8 @@ pub struct File { pub(crate) entry: DirEntry, /// Search ID for this file pub(crate) search_id: SearchId, + /// Did we write to this file? + pub(crate) dirty: bool, } /// Errors related to file operations diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index e7566b7..921e517 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -336,6 +336,7 @@ where mode, entry: dir_entry, search_id, + dirty: false, }, Mode::ReadWriteAppend => { let mut file = File { @@ -346,6 +347,7 @@ where mode, entry: dir_entry, search_id, + dirty: false, }; // seek_from_end with 0 can't fail file.seek_from_end(0).ok(); @@ -360,6 +362,7 @@ where mode, entry: dir_entry, search_id, + dirty: false, }; match &mut volume.volume_type { VolumeType::Fat(fat) => { @@ -452,6 +455,7 @@ where mode, entry, search_id, + dirty: false, }; // Remember this open file @@ -567,6 +571,9 @@ where if file.mode == Mode::ReadOnly { return Err(Error::ReadOnly); } + + file.dirty = true; + if file.starting_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 { @@ -665,15 +672,17 @@ where /// Close a file with the given full path. pub fn close_file(&mut self, volume: &mut Volume, file: File) -> Result<(), Error> { - match volume.volume_type { - VolumeType::Fat(ref mut fat) => { - debug!("Updating FAT info sector"); - fat.update_info_sector(self)?; - debug!("Updating dir entry {:?}", file.entry); - let fat_type = fat.get_fat_type(); - self.write_entry_to_disk(fat_type, &file.entry)?; - } - }; + if file.dirty { + match volume.volume_type { + VolumeType::Fat(ref mut fat) => { + debug!("Updating FAT info sector"); + fat.update_info_sector(self)?; + debug!("Updating dir entry {:?}", file.entry); + let fat_type = fat.get_fat_type(); + self.write_entry_to_disk(fat_type, &file.entry)?; + } + }; + } // Unwrap, because we should never be in a situation where we're attempting to close a file // with an ID which doesn't exist in our open files list. From 098e9e6b5ab9bae9bee04b051eafa69925e980d5 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 21:26:59 +0100 Subject: [PATCH 52/69] Clippy fixes. --- examples/create_test.rs | 6 ++---- examples/delete_test.rs | 2 +- examples/readme_test.rs | 2 +- examples/test_mount.rs | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/create_test.rs b/examples/create_test.rs index 612a5fc..314e343 100644 --- a/examples/create_test.rs +++ b/examples/create_test.rs @@ -16,7 +16,7 @@ extern crate embedded_sdmmc; -const FILE_TO_CREATE: &'static str = "CREATE.TXT"; +const FILE_TO_CREATE: &str = "CREATE.TXT"; use embedded_sdmmc::{ Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, @@ -158,9 +158,7 @@ fn main() { let buffer1 = b"\nFile Appended\n"; let mut buffer: Vec = vec![]; for _ in 0..64 { - for _ in 0..15 { - buffer.push(b'a'); - } + buffer.resize(buffer.len() + 15, b'a'); buffer.push(b'\n'); } println!("\nAppending to file"); diff --git a/examples/delete_test.rs b/examples/delete_test.rs index 7c73da4..b927dd9 100644 --- a/examples/delete_test.rs +++ b/examples/delete_test.rs @@ -16,7 +16,7 @@ extern crate embedded_sdmmc; -const FILE_TO_DELETE: &'static str = "DELETE.TXT"; +const FILE_TO_DELETE: &str = "DELETE.TXT"; use embedded_sdmmc::{ Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, diff --git a/examples/readme_test.rs b/examples/readme_test.rs index b5fd1b3..3ab3d82 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(()) } } diff --git a/examples/test_mount.rs b/examples/test_mount.rs index 53f2500..71d32b2 100644 --- a/examples/test_mount.rs +++ b/examples/test_mount.rs @@ -23,8 +23,8 @@ extern crate embedded_sdmmc; -const FILE_TO_PRINT: &'static str = "README.TXT"; -const FILE_TO_CHECKSUM: &'static str = "64MB.DAT"; +const FILE_TO_PRINT: &str = "README.TXT"; +const FILE_TO_CHECKSUM: &str = "64MB.DAT"; use embedded_sdmmc::{ Block, BlockCount, BlockDevice, BlockIdx, Error, Mode, TimeSource, Timestamp, VolumeIdx, From c40c08c264a60de46e3ff7950dc6f4fc433c9db5 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 21:33:46 +0100 Subject: [PATCH 53/69] Don't make file longer if you're not at the end. Closes #72 --- src/volume_mgr.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 921e517..cf1fab5 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -659,14 +659,18 @@ where .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(); + + let to_copy = to_copy as u32; + let new_offset = file.current_offset + to_copy; + if new_offset > file.length { + // We made it longer + file.update_length(new_offset); + } + file.seek_from_start(new_offset).unwrap(); // Entry update deferred to file close, for performance. } + file.entry.attributes.set_archive(true); + file.entry.mtime = self.timesource.get_timestamp(); Ok(written) } From a596c352f2c9cf7587e0b56d4d1821b605a1999b Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 15 Sep 2023 21:37:01 +0100 Subject: [PATCH 54/69] Mark imported types as doc(inline). Gets you docs at the top level. Saves users a click. Also drops the deprecated Controller import. We need to do a breaking release anyway. --- src/lib.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 89d1ad4..e03a443 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,21 +101,28 @@ pub mod fat; pub mod filesystem; pub mod sdcard; +#[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, }; + +#[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"); From 9dc3ea43af1a182ae0af652d8525fd9e18456d51 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 16 Sep 2023 10:37:37 +0100 Subject: [PATCH 55/69] Update changelog. --- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4823930..f97f332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### 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]). + +[#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 @@ -17,11 +29,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Removed -- None +- __Breaking Change__: `Controller` alias for `VolumeManager` removed. ## [Version 0.5.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.5.0) - 2023-05-20 -### Changes +### Changes in v0.5.0 - __Breaking Change__: Renamed `Controller` to `VolumeManager`, to better describe what it does. - __Breaking Change__: Renamed `SdMmcSpi` to `SdCard` @@ -31,17 +43,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - More robust card intialisation procedure, with added retries - Supports building with neither `defmt` nor `log` logging -### Added +### Added in v0.5.0 - Added `mark_card_as_init` method, if you know the card is initialised and want to skip the initialisation step -### Removed +### Removed in v0.5.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 -### Changes +### Changes in v0.4.0 + - Optionally use [defmt](https://github.com/knurling-rs/defmt) for logging. Controlled by `defmt-log` feature flag. - __Breaking Change__: Use SPI blocking traits instead to ease SPI peripheral sharing. @@ -58,33 +71,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Version 0.3.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.3.0) - 2019-12-16 -### Changes +### Changes in v0.3.0 -* 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 (https://github.com/thejpster/embedded-sdmmc-rs/issues/10) ## [Version 0.2.1](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.2.1) - 2019-02-19 -### Changes +### Changes in v0.2.1 -* 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 -### Changes +### Changes in v0.2.0 -* 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 -### Changes +### Changes in v0.1.0 -* 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. From b85b147a0cbe3cda7133023310fab0ce82814d4f Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 17 Sep 2023 22:36:42 +0100 Subject: [PATCH 56/69] Re-wrote API. Now File and Directory are just integers, and they are joined by Volume. All the info is stored inside the volume manager. The upside is you no longer have to pass the right volume along with your file handle! It also becomes much easier to implement a Drop trait on File if you want (although we don't for the reasons explained in the comments). The downside is the File object no longer has any methods. The upside is that it's much easier to store files. --- README.md | 19 +- examples/create_test.rs | 49 +- examples/delete_test.rs | 28 +- examples/readme_test.rs | 20 +- examples/test_mount.rs | 63 ++- examples/write_test.rs | 78 ++- src/fat/bpb.rs | 6 + src/fat/info.rs | 12 +- src/fat/mod.rs | 22 +- src/fat/ondiskdirentry.rs | 6 + src/fat/volume.rs | 278 +++++----- src/filesystem/attributes.rs | 6 + src/filesystem/cluster.rs | 8 +- src/filesystem/directory.rs | 37 +- src/filesystem/filename.rs | 12 + src/filesystem/files.rs | 68 ++- src/filesystem/mod.rs | 11 +- src/filesystem/search_id.rs | 24 +- src/filesystem/timestamp.rs | 6 + src/lib.rs | 322 ++---------- src/sdcard/mod.rs | 12 +- src/sdcard/proto.rs | 6 + src/volume_mgr.rs | 971 ++++++++++++++++++++++++----------- 23 files changed, 1146 insertions(+), 918 deletions(-) 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/create_test.rs b/examples/create_test.rs index 314e343..04cffcb 100644 --- a/examples/create_test.rs +++ b/examples/create_test.rs @@ -118,34 +118,30 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); + let mut volume_mgr: VolumeManager = + VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); for volume_idx in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); + let volume = volume_mgr.open_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(); + if let Ok(volume) = volume { + let root_dir = volume_mgr.open_root_dir(volume).unwrap(); println!("\tListing root directory:"); volume_mgr - .iterate_dir(&volume, &root_dir, |x| { + .iterate_dir(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, - ) + let f = volume_mgr + .open_file_in_dir(root_dir, FILE_TO_CREATE, Mode::ReadWriteCreate) .unwrap(); println!("\nReading from file"); println!("FILE STARTS:"); - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -162,32 +158,27 @@ fn main() { 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(); + let num_written1 = volume_mgr.write(f, &buffer1[..]).unwrap(); + let num_written = volume_mgr.write(f, &buffer[..]).unwrap(); println!("Number of bytes written: {}\n", num_written + num_written1); - volume_mgr.close_file(&mut volume, f).unwrap(); + volume_mgr.close_file(f).unwrap(); - let mut f = volume_mgr - .open_file_in_dir( - &mut volume, - &root_dir, - FILE_TO_CREATE, - Mode::ReadWriteCreateOrAppend, - ) + let f = volume_mgr + .open_file_in_dir(root_dir, FILE_TO_CREATE, Mode::ReadWriteCreateOrAppend) .unwrap(); - f.seek_from_start(0).unwrap(); + volume_mgr.file_seek_from_start(f, 0).unwrap(); println!("\tFinding {}...", FILE_TO_CREATE); println!( "\tFound {}?: {:?}", FILE_TO_CREATE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_CREATE) + volume_mgr.find_directory_entry(root_dir, FILE_TO_CREATE) ); println!("\nReading from file"); println!("FILE STARTS:"); - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -196,7 +187,7 @@ fn main() { } } println!("EOF"); - volume_mgr.close_file(&mut volume, f).unwrap(); + volume_mgr.close_file(f).unwrap(); } } } diff --git a/examples/delete_test.rs b/examples/delete_test.rs index b927dd9..fcc7812 100644 --- a/examples/delete_test.rs +++ b/examples/delete_test.rs @@ -118,15 +118,16 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); + let mut volume_mgr: VolumeManager = + VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); for volume_idx in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); + let volume = volume_mgr.open_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(); + if let Ok(volume) = volume { + let root_dir = volume_mgr.open_root_dir(volume).unwrap(); println!("\tListing root directory:"); volume_mgr - .iterate_dir(&volume, &root_dir, |x| { + .iterate_dir(root_dir, |x| { println!("\t\tFound: {:?}", x); }) .unwrap(); @@ -134,29 +135,24 @@ fn main() { // 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, - ) + .open_file_in_dir(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) + volume_mgr.find_directory_entry(root_dir, FILE_TO_DELETE) ); - match volume_mgr.delete_file_in_dir(&volume, &root_dir, FILE_TO_DELETE) { + match volume_mgr.delete_file_in_dir(root_dir, FILE_TO_DELETE) { Ok(()) => (), Err(error) => println!("\tCannot delete file: {:?}", error), } println!("\tClosing {}...", FILE_TO_DELETE); - volume_mgr.close_file(&mut volume, f).unwrap(); + volume_mgr.close_file(f).unwrap(); - match volume_mgr.delete_file_in_dir(&volume, &root_dir, FILE_TO_DELETE) { + match volume_mgr.delete_file_in_dir(root_dir, FILE_TO_DELETE) { Ok(()) => println!("\tDeleted {}.", FILE_TO_DELETE), Err(error) => println!("\tCannot delete {}: {:?}", FILE_TO_DELETE, error), } @@ -164,7 +160,7 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_DELETE, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_DELETE) + volume_mgr.find_directory_entry(root_dir, FILE_TO_DELETE) ); } } diff --git a/examples/readme_test.rs b/examples/readme_test.rs index 3ab3d82..79afb1a 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -90,26 +90,22 @@ 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(&mut volume0, my_file)?; - volume_mgr.close_dir(&volume0, root_dir); + volume_mgr.close_file(my_file)?; + volume_mgr.close_dir(root_dir)?; Ok(()) } diff --git a/examples/test_mount.rs b/examples/test_mount.rs index 71d32b2..140b7ee 100644 --- a/examples/test_mount.rs +++ b/examples/test_mount.rs @@ -121,15 +121,16 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); + let mut volume_mgr: VolumeManager = + VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); for i in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(i)); + let volume = volume_mgr.open_volume(VolumeIdx(i)); println!("volume {}: {:#?}", i, volume); - if let Ok(mut volume) = volume { - let root_dir = volume_mgr.open_root_dir(&volume).unwrap(); + if let Ok(volume) = volume { + let root_dir = volume_mgr.open_root_dir(volume).unwrap(); println!("\tListing root directory:"); volume_mgr - .iterate_dir(&volume, &root_dir, |x| { + .iterate_dir(root_dir, |x| { println!("\t\tFound: {:?}", x); }) .unwrap(); @@ -137,15 +138,15 @@ fn main() { println!( "\tFound {}?: {:?}", FILE_TO_PRINT, - volume_mgr.find_directory_entry(&volume, &root_dir, FILE_TO_PRINT) + volume_mgr.find_directory_entry(root_dir, FILE_TO_PRINT) ); - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_PRINT, Mode::ReadOnly) + let f = volume_mgr + .open_file_in_dir(root_dir, FILE_TO_PRINT, Mode::ReadOnly) .unwrap(); println!("FILE STARTS:"); - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -156,42 +157,52 @@ fn main() { println!("EOF"); // Can't open file twice assert!(volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_PRINT, Mode::ReadOnly) + .open_file_in_dir(root_dir, FILE_TO_PRINT, Mode::ReadOnly) .is_err()); - volume_mgr.close_file(&mut volume, f).unwrap(); + volume_mgr.close_file(f).unwrap(); - let test_dir = volume_mgr.open_dir(&volume, &root_dir, "TEST").unwrap(); + let test_dir = volume_mgr.open_dir(root_dir, "TEST").unwrap(); // Check we can't open it twice - assert!(volume_mgr.open_dir(&volume, &root_dir, "TEST").is_err()); + assert!(volume_mgr.open_dir(root_dir, "TEST").is_err()); // Print the contents println!("\tListing TEST directory:"); volume_mgr - .iterate_dir(&volume, &test_dir, |x| { + .iterate_dir(test_dir, |x| { println!("\t\tFound: {:?}", x); }) .unwrap(); - volume_mgr.close_dir(&volume, test_dir); + volume_mgr.close_dir(test_dir).unwrap(); // 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) + let f = volume_mgr + .open_file_in_dir(root_dir, FILE_TO_CHECKSUM, Mode::ReadOnly) .unwrap(); - println!("Checksuming {} bytes of {}", f.length(), FILE_TO_CHECKSUM); + println!( + "Checksuming {} bytes of {}", + volume_mgr.file_length(f).unwrap(), + FILE_TO_CHECKSUM + ); let mut csum = 0u32; - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 2047]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(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(&mut volume, f).unwrap(); + println!( + "\nChecksum over {} bytes: {}", + volume_mgr.file_length(f).unwrap(), + csum + ); + // Should be all zero bytes + assert_eq!(csum, 0); + volume_mgr.close_file(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()); + assert!(volume_mgr.open_root_dir(volume).is_err()); + assert!(volume_mgr.close_dir(root_dir).is_ok()); + assert!(volume_mgr.open_root_dir(volume).is_ok()); } } } diff --git a/examples/write_test.rs b/examples/write_test.rs index 7743517..eab38b7 100644 --- a/examples/write_test.rs +++ b/examples/write_test.rs @@ -118,15 +118,16 @@ fn main() { .map_err(Error::DeviceError) .unwrap(); println!("lbd: {:?}", lbd); - let mut volume_mgr = VolumeManager::new(lbd, Clock); + let mut volume_mgr: VolumeManager = + VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); for volume_idx in 0..=3 { - let volume = volume_mgr.get_volume(VolumeIdx(volume_idx)); + let volume = volume_mgr.open_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(); + if let Ok(volume) = volume { + let root_dir = volume_mgr.open_root_dir(volume).unwrap(); println!("\tListing root directory:"); volume_mgr - .iterate_dir(&volume, &root_dir, |x| { + .iterate_dir(root_dir, |x| { println!("\t\tFound: {:?}", x); }) .unwrap(); @@ -134,15 +135,15 @@ fn main() { // 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) + let f = volume_mgr + .open_file_in_dir(root_dir, FILE_TO_WRITE, Mode::ReadOnly) .unwrap(); println!("\nReading from file {}\n", FILE_TO_WRITE); let mut csum = 0; println!("FILE STARTS:"); - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -152,18 +153,18 @@ fn main() { } } println!("EOF\n"); - let mut file_size = f.length() as usize; - volume_mgr.close_file(&mut volume, f).unwrap(); + let mut file_size = volume_mgr.file_length(f).unwrap() as usize; + volume_mgr.close_file(f).unwrap(); - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) + let f = volume_mgr + .open_file_in_dir(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(); + let num_written1 = volume_mgr.write(f, &buffer1[..]).unwrap(); + let num_written = volume_mgr.write(f, &buffer[..]).unwrap(); for b in &buffer1[..] { csum += u32::from(*b); } @@ -174,11 +175,11 @@ fn main() { file_size += num_written; file_size += num_written1; - f.seek_from_start(0).unwrap(); + volume_mgr.file_seek_from_start(f, 0).unwrap(); println!("\nFILE STARTS:"); - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -187,28 +188,28 @@ fn main() { } } println!("EOF"); - volume_mgr.close_file(&mut volume, f).unwrap(); + volume_mgr.close_file(f).unwrap(); println!("\tFinding {}...", FILE_TO_WRITE); let dir_ent = volume_mgr - .find_directory_entry(&volume, &root_dir, FILE_TO_WRITE) + .find_directory_entry(root_dir, FILE_TO_WRITE) .unwrap(); println!("\tFound {}?: {:?}", FILE_TO_WRITE, dir_ent); assert_eq!(dir_ent.size as usize, file_size); - let mut f = volume_mgr - .open_file_in_dir(&mut volume, &root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) + let f = volume_mgr + .open_file_in_dir(root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) .unwrap(); println!( "\nReading from file {}, len {}\n", FILE_TO_WRITE, - f.length() + volume_mgr.file_length(f).unwrap() ); - f.seek_from_start(0).unwrap(); + volume_mgr.file_seek_from_start(f, 0).unwrap(); println!("FILE STARTS:"); let mut csum2 = 0; - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -218,36 +219,31 @@ fn main() { } } println!("EOF\n"); - assert_eq!(f.length() as usize, file_size); - volume_mgr.close_file(&mut volume, f).unwrap(); + assert_eq!(volume_mgr.file_length(f).unwrap() as usize, file_size); + volume_mgr.close_file(f).unwrap(); assert_eq!(csum, csum2); println!("\nTruncating file"); - let mut f = volume_mgr - .open_file_in_dir( - &mut volume, - &root_dir, - FILE_TO_WRITE, - Mode::ReadWriteTruncate, - ) + let f = volume_mgr + .open_file_in_dir(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(); + let num_written = volume_mgr.write(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) + volume_mgr.find_directory_entry(root_dir, FILE_TO_WRITE) ); - f.seek_from_start(0).unwrap(); + volume_mgr.file_seek_from_start(f, 0).unwrap(); println!("\nFILE STARTS:"); - while !f.eof() { + while !volume_mgr.file_eof(f).unwrap() { let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(&volume, &mut f, &mut buffer).unwrap(); + let num_read = volume_mgr.read(f, &mut buffer).unwrap(); for b in &buffer[0..num_read] { if *b == 10 { print!("\\n"); @@ -256,7 +252,7 @@ fn main() { } } println!("EOF"); - volume_mgr.close_file(&mut volume, f).unwrap(); + volume_mgr.close_file(f).unwrap(); } } } diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index 0cc7be3..71b067c 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -132,3 +132,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..091ab98 100644 --- a/src/fat/info.rs +++ b/src/fat/info.rs @@ -3,7 +3,7 @@ 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,7 +13,7 @@ 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). @@ -24,7 +24,7 @@ pub struct Fat32Info { /// 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) @@ -86,3 +86,9 @@ impl<'a> InfoSector<'a> { } } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 9d398dc..5e119cb 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -25,20 +25,18 @@ impl BlockCache { idx: None, } } - pub(crate) fn read( + pub(crate) fn read( &mut self, - volume_mgr: &VolumeManager, + block_device: &D, block_idx: BlockIdx, reason: &str, ) -> Result<&Block, Error> where D: BlockDevice, - T: TimeSource, { if Some(block_idx) != self.idx { self.idx = Some(block_idx); - volume_mgr - .block_device + block_device .read(core::slice::from_mut(&mut self.block), block_idx, reason) .map_err(Error::DeviceError)?; } @@ -56,7 +54,13 @@ pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; pub use ondiskdirentry::OnDiskDirEntry; pub use volume::{parse_volume, FatVolume, VolumeName}; -use crate::{Block, BlockDevice, BlockIdx, Error, TimeSource, VolumeManager}; +use crate::{Block, BlockDevice, BlockIdx, Error}; + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** #[cfg(test)] mod test { @@ -351,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..c85caf5 100644 --- a/src/fat/ondiskdirentry.rs +++ b/src/fat/ondiskdirentry.rs @@ -168,3 +168,9 @@ impl<'a> OnDiskDirEntry<'a> { result } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 372123a..8ada679 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1,4 +1,4 @@ -//! FAT volume +//! FAT-specific volume support. use crate::{ debug, @@ -7,7 +7,7 @@ use crate::{ RESERVED_ENTRIES, }, trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, DirEntry, - Directory, Error, ShortFileName, TimeSource, VolumeManager, VolumeType, + DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; @@ -16,7 +16,7 @@ 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], } @@ -39,7 +39,7 @@ impl core::fmt::Debug for VolumeName { /// 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, @@ -67,13 +67,9 @@ 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(_) => { @@ -84,8 +80,7 @@ impl FatVolume { 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]; @@ -95,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)?; } @@ -113,15 +107,14 @@ impl FatVolume { } /// Write a new entry in the FAT - fn update_fat( + fn update_fat( &mut self, - volume_mgr: &mut VolumeManager, + block_device: &D, cluster: Cluster, new_value: Cluster, ) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { let mut blocks = [Block::new()]; let this_fat_block_num; @@ -130,8 +123,7 @@ 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)?; let entry = match new_value { @@ -151,8 +143,7 @@ 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 { @@ -171,30 +162,29 @@ 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, + block_device: &D, cluster: Cluster, fat_block_cache: &mut BlockCache, ) -> Result> where D: BlockDevice, - T: TimeSource, { 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; - let block = fat_block_cache.read(volume_mgr, this_fat_block_num, "next_cluster")?; + 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 { @@ -216,7 +206,8 @@ impl FatVolume { let fat_offset = cluster.0 * 4; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - let block = fat_block_cache.read(volume_mgr, this_fat_block_num, "next_cluster")?; + 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; @@ -279,10 +270,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> @@ -308,8 +300,7 @@ impl FatVolume { }; 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 { @@ -318,7 +309,7 @@ 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, @@ -329,8 +320,7 @@ impl FatVolume { ); 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); @@ -340,13 +330,14 @@ impl FatVolume { if cluster != Cluster::ROOT_DIR { let mut block_cache = BlockCache::empty(); current_cluster = - match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + 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(volume_mgr, Some(cluster), true)?; + let c = + self.alloc_cluster(block_device, Some(cluster), true)?; first_dir_block_num = self.cluster_to_block(c); Some(c) } @@ -369,8 +360,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 { @@ -379,7 +369,7 @@ 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, @@ -390,8 +380,7 @@ impl FatVolume { ); 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); @@ -399,19 +388,19 @@ impl FatVolume { } } let mut block_cache = BlockCache::empty(); - current_cluster = match self.next_cluster(volume_mgr, 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(volume_mgr, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + 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) } @@ -420,16 +409,15 @@ 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) => { @@ -449,8 +437,7 @@ impl FatVolume { 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 + block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; for entry in 0..Block::LEN / OnDiskDirEntry::LEN { @@ -470,7 +457,7 @@ impl FatVolume { } if cluster != Cluster::ROOT_DIR { current_cluster = - match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + match self.next_cluster(block_device, cluster, &mut block_cache) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -493,8 +480,7 @@ impl FatVolume { 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 { @@ -512,11 +498,11 @@ impl FatVolume { } } } - current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) - { - Ok(n) => Some(n), - _ => None, - }; + current_cluster = + match self.next_cluster(block_device, cluster, &mut block_cache) { + Ok(n) => Some(n), + _ => None, + }; } Ok(()) } @@ -524,15 +510,14 @@ 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, + block_device: &D, + dir: &DirectoryInfo, name: &str, ) -> Result> where D: BlockDevice, - T: TimeSource, { let match_name = ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; match &self.fat_specific_info { @@ -554,7 +539,7 @@ impl FatVolume { 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, block, @@ -565,7 +550,7 @@ impl FatVolume { } if cluster != Cluster::ROOT_DIR { current_cluster = - match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + match self.next_cluster(block_device, cluster, &mut block_cache) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -588,7 +573,7 @@ impl FatVolume { 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, block, @@ -597,11 +582,11 @@ impl FatVolume { x => return x, } } - current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) - { - 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) } @@ -609,20 +594,18 @@ impl FatVolume { } /// Finds an entry in a given block - fn find_entry_in_block( + 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 { @@ -643,15 +626,14 @@ impl FatVolume { } /// 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, + block_device: &D, + dir: &DirectoryInfo, name: &str, ) -> Result<(), Error> where D: BlockDevice, - T: TimeSource, { let match_name = ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; match &self.fat_specific_info { @@ -671,7 +653,7 @@ impl FatVolume { while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - match self.delete_entry_in_block(volume_mgr, &match_name, block) { + match self.delete_entry_in_block(block_device, &match_name, block) { Err(Error::NotInBlock) => continue, x => return x, } @@ -679,7 +661,7 @@ impl FatVolume { if cluster != Cluster::ROOT_DIR { let mut block_cache = BlockCache::empty(); current_cluster = - match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + match self.next_cluster(block_device, cluster, &mut block_cache) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -700,17 +682,17 @@ impl FatVolume { 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.delete_entry_in_block(volume_mgr, &match_name, block) { + match self.delete_entry_in_block(block_device, &match_name, block) { Err(Error::NotInBlock) => continue, x => return x, } } let mut block_cache = BlockCache::empty(); - current_cluster = match self.next_cluster(volume_mgr, cluster, &mut block_cache) - { - 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) } @@ -718,19 +700,17 @@ impl FatVolume { } /// Deletes an entry in a given block - fn delete_entry_in_block( + 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 { @@ -743,8 +723,7 @@ impl FatVolume { } else if dir_entry.matches(match_name) { let mut blocks = blocks; blocks[0].contents[start] = 0xE5; - volume_mgr - .block_device + block_device .write(&blocks, block) .map_err(Error::DeviceError)?; return Ok(()); @@ -754,15 +733,14 @@ impl FatVolume { } /// 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, + block_device: &D, start_cluster: Cluster, end_cluster: Cluster, ) -> Result> where D: BlockDevice, - T: TimeSource, { let mut blocks = [Block::new()]; let mut current_cluster = start_cluster; @@ -782,8 +760,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)?; @@ -814,8 +791,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)?; @@ -837,15 +813,14 @@ impl FatVolume { } /// Tries to allocate a cluster - pub(crate) fn alloc_cluster( + pub(crate) fn alloc_cluster( &mut self, - volume_mgr: &mut VolumeManager, + block_device: &D, prev_cluster: Option, zero: bool, ) -> Result> where D: BlockDevice, - T: TimeSource, { debug!("Allocating new cluster, prev_cluster={:?}", prev_cluster); let end_cluster = Cluster(self.cluster_count + RESERVED_ENTRIES); @@ -858,27 +833,31 @@ impl FatVolume { 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 {:?}..={:?}", + Cluster(RESERVED_ENTRIES), + end_cluster + ); + self.find_next_free_cluster( + block_device, + Cluster(RESERVED_ENTRIES), + end_cluster, + )? + } + Err(e) => return Err(e), + }; + self.update_fat(block_device, new_cluster, Cluster::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 {:?}..={:?}", @@ -886,11 +865,11 @@ 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, + block_device, Cluster(RESERVED_ENTRIES), end_cluster, ) { @@ -909,8 +888,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)?; } @@ -920,14 +898,13 @@ 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, + block_device: &D, cluster: Cluster, ) -> 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 @@ -935,7 +912,7 @@ impl FatVolume { } let mut next = { let mut block_cache = BlockCache::empty(); - match self.next_cluster(volume_mgr, cluster, &mut block_cache) { + match self.next_cluster(block_device, cluster, &mut block_cache) { Ok(n) => n, Err(Error::EndOfFile) => return Ok(()), Err(e) => return Err(e), @@ -948,16 +925,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, Cluster::END_OF_FILE)?; loop { let mut block_cache = BlockCache::empty(); - match self.next_cluster(volume_mgr, next, &mut block_cache) { + 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, Cluster::EMPTY)?; next = n; } Err(Error::EndOfFile) => { - self.update_fat(volume_mgr, next, Cluster::EMPTY)?; + self.update_fat(block_device, next, Cluster::EMPTY)?; break; } Err(e) => return Err(e), @@ -972,19 +949,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]; @@ -1028,8 +1003,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, @@ -1060,3 +1034,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..e0bac87 100644 --- a/src/filesystem/cluster.rs +++ b/src/filesystem/cluster.rs @@ -1,6 +1,6 @@ /// Represents a cluster on disk. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct Cluster(pub(crate) u32); impl Cluster { @@ -42,3 +42,9 @@ impl core::ops::AddAssign for Cluster { self.0 += rhs.0; } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index aa223f9..6b38e34 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -3,6 +3,7 @@ use core::convert::TryFrom; use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; use crate::filesystem::{Attributes, Cluster, SearchId, ShortFileName, Timestamp}; +use crate::Volume; /// Represents a directory entry, which tells you about /// other files and directories. @@ -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, - /// Search ID for this directory. - pub(crate) search_id: SearchId, } impl DirEntry { @@ -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..96c48d2 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -196,6 +196,12 @@ impl core::fmt::Debug for ShortFileName { } } +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + #[cfg(test)] mod test { use super::*; @@ -276,3 +282,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 f65b020..fed246f 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,23 +1,44 @@ -use crate::filesystem::{Cluster, DirEntry, SearchId}; +use crate::{ + filesystem::{Cluster, 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), /// 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, - /// Search ID for this file - pub(crate) search_id: SearchId, /// Did we write to this file? pub(crate) dirty: bool, } @@ -48,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 { @@ -75,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 { @@ -90,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 { @@ -100,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 9783319..5abee5e 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -19,5 +19,14 @@ pub use self::cluster::Cluster; pub use self::directory::{DirEntry, Directory}; pub use self::filename::{FilenameError, ShortFileName}; pub use self::files::{File, FileError, Mode}; -pub use self::search_id::{IdGenerator, SearchId}; +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 index 87ae856..f4be611 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -2,21 +2,25 @@ 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 File/Directory lists +/// Unique ID used to search for files and directories in the open Volume/File/Directory lists pub struct SearchId(pub(crate) u32); -/// ID generator intented to be used in a static context. +/// A Search ID generator. /// /// This object will always return a different ID. -pub struct IdGenerator { +/// +/// 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. +pub struct SearchIdGenerator { next_id: Wrapping, } -impl IdGenerator { - /// Create a new [`IdGenerator`]. - pub const fn new() -> Self { +impl SearchIdGenerator { + /// Create a new generator of Search IDs. + pub const fn new(offset: u32) -> Self { Self { - next_id: Wrapping(0), + next_id: Wrapping(offset), } } @@ -27,3 +31,9 @@ impl IdGenerator { 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 e03a443..99f1acc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,20 +53,20 @@ //! 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(&mut volume0, my_file)?; -//! volume_mgr.close_dir(&volume0, root_dir); +//! volume_mgr.close_file(my_file)?; +//! volume_mgr.close_dir(root_dir)?; //! # Ok(()) //! # } //! ``` @@ -101,6 +101,8 @@ pub mod fat; pub mod filesystem; pub mod sdcard; +use filesystem::SearchId; + #[doc(inline)] pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; @@ -113,6 +115,8 @@ pub use crate::filesystem::{ Timestamp, MAX_FILE_SIZE, }; +use filesystem::DirectoryInfo; + #[doc(inline)] pub use crate::sdcard::Error as SdCardError; @@ -174,10 +178,14 @@ 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 @@ -186,6 +194,8 @@ where 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 @@ -212,6 +222,8 @@ where BadBlockSize(u16), /// Entry not found in the block NotInBlock, + /// Bad offset given when seeking + InvalidOffset, } impl From for Error @@ -225,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, } @@ -248,20 +269,6 @@ pub enum VolumeType { #[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; @@ -280,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 81e85d6..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. /// @@ -443,8 +443,8 @@ where s.cs_low()?; // Enter SPI mode. let mut delay = Delay::new(s.options.acquire_retries); - for attempts in 1.. { - trace!("Enter SPI mode, attempt: {}..", attempts); + for _attempts in 1.. { + trace!("Enter SPI mode, attempt: {}..", _attempts); match s.card_command(CMD0, 0) { Err(Error::TimeoutCommand(0)) => { // Try again? diff --git a/src/sdcard/proto.rs b/src/sdcard/proto.rs index b9e4032..6418574 100644 --- a/src/sdcard/proto.rs +++ b/src/sdcard/proto.rs @@ -241,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 cf1fab5..0420b33 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -1,4 +1,6 @@ -//! 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; @@ -6,53 +8,35 @@ use core::convert::TryFrom; use crate::fat::{self, BlockCache, RESERVED_ENTRIES}; use crate::filesystem::{ - Attributes, Cluster, DirEntry, Directory, File, IdGenerator, Mode, SearchId, ShortFileName, - TimeSource, MAX_FILE_SIZE, + Attributes, Cluster, DirEntry, Directory, DirectoryInfo, File, FileInfo, Mode, + SearchIdGenerator, ShortFileName, TimeSource, 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; -#[derive(PartialEq, Eq)] -struct ClusterDescriptor { - volume_idx: VolumeIdx, - cluster: Cluster, - search_id: SearchId, -} - -impl ClusterDescriptor { - fn new(volume_idx: VolumeIdx, cluster: Cluster, search_id: SearchId) -> Self { - Self { - volume_idx, - cluster, - search_id, - } - } - - fn compare_volume_and_cluster(&self, volume_idx: VolumeIdx, cluster: Cluster) -> bool { - self.volume_idx == volume_idx && self.cluster == cluster - } - - fn compare_id(&self, search_id: SearchId) -> bool { - self.search_id == search_id - } -} - /// A `VolumeManager` wraps a block device and gives access to the FAT-formatted /// volumes within it. -pub struct VolumeManager -where +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, - id_generator: IdGenerator, - open_dirs: Vec, - open_files: Vec, + pub(crate) time_source: T, + id_generator: SearchIdGenerator, + open_volumes: Vec, + open_dirs: Vec, + open_files: Vec, } impl VolumeManager @@ -66,14 +50,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, @@ -82,15 +69,21 @@ 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, - id_generator: IdGenerator::new(), + time_source, + id_generator: SearchIdGenerator::new(id_offset), + open_volumes: Vec::new(), open_dirs: Vec::new(), open_files: Vec::new(), } @@ -105,7 +98,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; @@ -118,6 +111,10 @@ 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); + } + let (part_type, lba_start, num_blocks) = { let mut blocks = [Block::new()]; self.block_device @@ -167,11 +164,16 @@ 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")), } @@ -179,37 +181,27 @@ where /// Open the volume's root directory. /// - /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. - /// - /// 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> { - if self.open_dirs.is_full() { - return Err(Error::TooManyOpenDirs); + /// 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 == Cluster::ROOT_DIR && dir.volume_id == volume { + return Err(Error::DirAlreadyOpen); + } } - // 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. - if cluster_already_open(&self.open_dirs, volume.idx, Cluster::ROOT_DIR) { - return Err(Error::DirAlreadyOpen); - } + let directory_id = Directory(self.id_generator.get()); + let dir_info = DirectoryInfo { + volume_id: volume, + cluster: Cluster::ROOT_DIR, + directory_id, + }; - let search_id = self.id_generator.get(); - // Remember this open directory self.open_dirs - .push(ClusterDescriptor::new( - volume.idx, - Cluster::ROOT_DIR, - search_id, - )) + .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; - Ok(Directory { - cluster: Cluster::ROOT_DIR, - search_id, - }) + Ok(directory_id) } /// Open a directory. @@ -221,90 +213,110 @@ where /// being unlinked. pub fn open_dir( &mut self, - volume: &Volume, - parent_dir: &Directory, + parent_dir: Directory, name: &str, ) -> Result> { if self.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); } + // Find dir by ID + let parent_dir_idx = self.get_dir_by_id(parent_dir)?; + let volume_idx = self.get_volume_by_id(self.open_dirs[parent_dir_idx].volume_id)?; + // 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, name)? + } }; if !dir_entry.attributes.is_directory() { - return Err(Error::OpenedDirAsFile); + return Err(Error::OpenedFileAsDir); } // Check it's not already open - if cluster_already_open(&self.open_dirs, volume.idx, dir_entry.cluster) { - return Err(Error::DirAlreadyOpen); + 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. - let search_id = self.id_generator.get(); + 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, + }; + self.open_dirs - .push(ClusterDescriptor::new( - volume.idx, - dir_entry.cluster, - search_id, - )) + .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; - Ok(Directory { - cluster: dir_entry.cluster, - search_id, - }) + 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) { - // We don't strictly speaking need the volume in order to close a - // directory, as we don't flush anything to disk at this point. The open - // directory acts more as a lock. However, we take it because it then - // matches the `close_file` API. - let _ = volume; + pub fn close_dir(&mut self, directory: Directory) -> Result<(), Error> { + let mut found_idx = None; + for (idx, info) in self.open_dirs.iter().enumerate() { + if directory == info.directory_id { + found_idx = Some(idx); + break; + } + } - // Unwrap, because we should never be in a situation where we're attempting to close a dir - // with an ID which doesn't exist in our open dirs list. - let idx_to_close = cluster_position_by_id(&self.open_dirs, dir.search_id).unwrap(); - self.open_dirs.remove(idx_to_close); + match found_idx { + None => Err(Error::BadHandle), + Some(idx) => { + self.open_dirs.swap_remove(idx); + Ok(()) + } + } } /// Look in a directory for a named file. pub fn find_directory_entry( &mut self, - volume: &Volume, - dir: &Directory, + directory: Directory, name: &str, ) -> Result> { - match &volume.volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(self, dir, name), + 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.find_directory_entry(&self.block_device, &self.open_dirs[directory_idx], name) + } } } /// 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. + /// Open a file from DirEntry. This is obtained by calling iterate_dir. + /// + /// A file can only be opened once. + /// + /// Do not drop the returned file handle - pass it to [`VolumeManager::close_file`]. pub fn open_dir_entry( &mut self, - volume: &mut Volume, + volume: Volume, dir_entry: DirEntry, mode: Mode, ) -> Result> { @@ -312,41 +324,42 @@ where return Err(Error::TooManyOpenFiles); } - // Check it's not already open - if cluster_already_open(&self.open_dirs, volume.idx, dir_entry.cluster) { - return Err(Error::DirAlreadyOpen); + 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 + for f in self.open_files.iter() { + if f.volume_id == volume && f.entry.cluster == dir_entry.cluster { + return Err(Error::DirAlreadyOpen); + } } let mode = solve_mode_variant(mode, true); - let search_id = self.id_generator.get(); + 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, - search_id, 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, - search_id, dirty: false, }; // seek_from_end with 0 can't fail @@ -354,25 +367,25 @@ where 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, - search_id, 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)?; } @@ -383,49 +396,63 @@ where _ => return Err(Error::Unsupported), }; - // Remember this open file - self.open_files - .push(ClusterDescriptor::new( - volume.idx, - file.starting_cluster, - search_id, - )) - .map_err(|_| Error::TooManyOpenDirs)?; + // Remember this open file - can't be full as we checked already + unsafe { + self.open_files.push_unchecked(file); + } - Ok(file) + Ok(file_id) } /// Open a file with the given full path. A file can only be opened once. pub fn open_file_in_dir( &mut self, - volume: &mut Volume, - dir: &Directory, + directory: Directory, name: &str, mode: Mode, ) -> Result> { - let dir_entry = match &volume.volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(self, dir, name), - }; - 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 dir_entry = match &volume_info.volume_type { + VolumeType::Fat(fat) => { + fat.find_directory_entry(&self.block_device, directory_info, name) + } + }; + 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); + } }; - if let Some(d) = &dir_entry { - if cluster_already_open(&self.open_files, volume.idx, d.cluster) { - return Err(Error::FileAlreadyOpen); + // Check if it's open already + if let Some(dir_entry) = &dir_entry { + for f in self.open_files.iter() { + if f.volume_id == volume_info.volume_id && f.entry.cluster == dir_entry.cluster { + return Err(Error::FileAlreadyOpen); + } } } @@ -439,177 +466,173 @@ where 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, + file_name, + att, + )?, }; - let search_id = self.id_generator.get(); + let file_id = File(self.id_generator.get()); - let file = File { - starting_cluster: entry.cluster, + let file = FileInfo { + file_id, + volume_id, current_cluster: (0, entry.cluster), current_offset: 0, - length: entry.size, mode, entry, - search_id, dirty: false, }; // Remember this open file - self.open_files - .push(ClusterDescriptor::new( - volume.idx, - file.starting_cluster, - search_id, - )) - .map_err(|_| Error::TooManyOpenFiles)?; - - Ok(file) + 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) + self.open_dir_entry(volume_id, dir_entry, mode) } } } - /// Delete a closed file with the given full path, if exists. + /// Delete a closed file with the given filename, if it exists. pub fn delete_file_in_dir( &mut self, - volume: &Volume, - dir: &Directory, + directory: 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), + debug!("delete_file(directory={:?}, filename={:?}", directory, name); + + 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 dir_entry = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry(&self.block_device, dir_info, name), }?; if dir_entry.attributes.is_directory() { return Err(Error::DeleteDirAsFile); } - if cluster_already_open(&self.open_files, volume.idx, dir_entry.cluster) { - return Err(Error::FileIsOpen); + for f in self.open_files.iter() { + if f.entry.cluster == dir_entry.cluster && f.volume_id == dir_info.volume_id { + return Err(Error::FileIsOpen); + } } - match &volume.volume_type { - VolumeType::Fat(fat) => fat.delete_directory_entry(self, dir, name)?, + 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, name)? + } } - // Unwrap, because we should never be in a situation where we're attempting to close a file - // which doesn't exist in our open files list. - let idx_to_remove = self - .open_files - .iter() - .position(|d| d.compare_volume_and_cluster(volume.idx, dir_entry.cluster)) - .unwrap(); - self.open_files.remove(idx_to_remove); - Ok(()) } /// 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); } - file.dirty = true; + self.open_files[file_idx].dirty = true; - if file.starting_cluster.0 < RESERVED_ENTRIES { + 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)?, - }; - - // Update the cluster descriptor in our open files list - let cluster_to_update = self - .open_files - .iter_mut() - .find(|d| d.compare_id(file.search_id)); + 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 let Some(c) = cluster_to_update { - c.cluster = file.starting_cluster; - } + // Clone this so we can touch our other structures. + let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; - file.entry.cluster = file.starting_cluster; - debug!("Alloc first cluster {:?}", file.starting_cluster); - } - if (file.current_cluster.1).0 < file.starting_cluster.0 { + 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={}", @@ -619,10 +642,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); @@ -630,9 +657,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); @@ -658,41 +685,54 @@ where .write(&blocks, block_idx) .map_err(Error::DeviceError)?; written += to_copy; - file.current_cluster = current_cluster; + self.open_files[file_idx].current_cluster = current_cluster; let to_copy = to_copy as u32; - let new_offset = file.current_offset + to_copy; - if new_offset > file.length { + 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 - file.update_length(new_offset); + self.open_files[file_idx].update_length(new_offset); } - file.seek_from_start(new_offset).unwrap(); + self.open_files[file_idx] + .seek_from_start(new_offset) + .unwrap(); // Entry update deferred to file close, for performance. } - file.entry.attributes.set_archive(true); - file.entry.mtime = self.timesource.get_timestamp(); + 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: &mut Volume, file: File) -> Result<(), Error> { - if file.dirty { - match volume.volume_type { + 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)?; - debug!("Updating dir entry {:?}", file.entry); + 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.entry)?; + self.write_entry_to_disk(fat_type, &file_info.entry)?; } }; } - // Unwrap, because we should never be in a situation where we're attempting to close a file - // with an ID which doesn't exist in our open files list. - let idx_to_close = cluster_position_by_id(&self.open_files, file.search_id).unwrap(); - self.open_files.remove(idx_to_close); - + self.open_files.swap_remove(file_idx); Ok(()) } @@ -703,19 +743,89 @@ where /// 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()) + } + + 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, + &self, + volume_idx: usize, start: &mut (u32, Cluster), 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? @@ -723,8 +833,10 @@ where 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, &mut block_cache)?, + 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; } @@ -732,7 +844,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; @@ -742,7 +854,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> { @@ -762,23 +874,6 @@ where } } -fn cluster_position_by_id( - cluster_list: &[ClusterDescriptor], - id_to_find: SearchId, -) -> Option { - cluster_list.iter().position(|f| f.compare_id(id_to_find)) -} - -fn cluster_already_open( - cluster_list: &[ClusterDescriptor], - volume_idx: VolumeIdx, - cluster: Cluster, -) -> bool { - cluster_list - .iter() - .any(|d| d.compare_volume_and_cluster(volume_idx, cluster)) -} - /// Transform mode variants (ReadWriteCreate_Or_Append) to simple modes ReadWriteAppend or /// ReadWriteCreate fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { @@ -798,3 +893,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: Cluster(2), + info_location: BlockIdx(1) + BlockCount(1), + }) + }) + } + ); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** From fa56d67857c5b161e2223ba9363318d16385f4d5 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Thu, 21 Sep 2023 12:00:26 +0100 Subject: [PATCH 57/69] Add more tests. Now the tests unpack the gzipped disk automatically into RAM. Need to add some write tests and get the tarpaulin coverage figure up a bit higher. --- .github/workflows/rust.yml | 15 +-- Cargo.toml | 2 + tests/directories.rs | 213 +++++++++++++++++++++++++++++++ disk.img.gz => tests/disk.img.gz | Bin 705847 -> 703296 bytes tests/read_file.rs | 169 ++++++++++++++++++++++++ tests/utils/mod.rs | 180 ++++++++++++++++++++++++++ 6 files changed, 565 insertions(+), 14 deletions(-) create mode 100644 tests/directories.rs rename disk.img.gz => tests/disk.img.gz (72%) create mode 100644 tests/read_file.rs create mode 100644 tests/utils/mod.rs 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/Cargo.toml b/Cargo.toml index 5e2bad1..e1ef0c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ 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" [features] default = ["log"] diff --git a/tests/directories.rs b/tests/directories.rs new file mode 100644 index 0000000..4e871ca --- /dev/null +++ b/tests/directories.rs @@ -0,0 +1,213 @@ +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(); + + volume_mgr + .iterate_dir(test_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 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 + ); + } +} 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 9d2e00e798d96c5ac37f063257e134bcf67ef132..8df362bcc05eb233a964c8d5252a1ef7450309fc 100644 GIT binary patch literal 703296 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�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 diff --git a/tests/read_file.rs b/tests/read_file.rs new file mode 100644 index 0000000..e0f1a76 --- /dev/null +++ b/tests/read_file.rs @@ -0,0 +1,169 @@ +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); +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..24787ea --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,180 @@ +//! 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, + }, + } +} From 6078e8cfcc646a35494c1fe2df9abf603fdd0982 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Thu, 21 Sep 2023 14:48:52 +0100 Subject: [PATCH 58/69] Reworked the examples. Volume Manager API also now takes things that are "convertible to a short file name", so you can use a ShortFileName, or a &str. --- CHANGELOG.md | 4 +- Cargo.toml | 1 + examples/append_file.rs | 49 +++++++ examples/create_file.rs | 52 ++++++++ examples/create_test.rs | 193 --------------------------- examples/delete_file.rs | 51 ++++++++ examples/delete_test.rs | 167 ------------------------ examples/linux/mod.rs | 94 ++++++++++++++ examples/list_dir.rs | 106 +++++++++++++++ examples/read_file.rs | 85 ++++++++++++ examples/test_mount.rs | 208 ------------------------------ examples/write_test.rs | 258 ------------------------------------- src/fat/volume.rs | 14 +- src/filesystem/filename.rs | 54 ++++++-- src/filesystem/mod.rs | 2 +- src/lib.rs | 4 +- src/volume_mgr.rs | 64 +++++---- 17 files changed, 537 insertions(+), 869 deletions(-) create mode 100644 examples/append_file.rs create mode 100644 examples/create_file.rs delete mode 100644 examples/create_test.rs create mode 100644 examples/delete_file.rs delete mode 100644 examples/delete_test.rs create mode 100644 examples/linux/mod.rs create mode 100644 examples/list_dir.rs create mode 100644 examples/read_file.rs delete mode 100644 examples/test_mount.rs delete mode 100644 examples/write_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f97f332..c9a7b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,11 +25,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- None +- New examples, `append_file`, `create_file`, `delete_file`, `list_dir` +- New test cases `tests/directories.rs`, `tests/read_file.rs` ### Removed - __Breaking Change__: `Controller` alias for `VolumeManager` removed. +- 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 diff --git a/Cargo.toml b/Cargo.toml index e1ef0c8..27c5ac6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ env_logger = "0.9" hex-literal = "0.3" flate2 = "1.0" sha256 = "1.4" +chrono = "0.4" [features] default = ["log"] 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 04cffcb..0000000 --- a/examples/create_test.rs +++ /dev/null @@ -1,193 +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: &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 = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - for volume_idx in 0..=3 { - let volume = volume_mgr.open_volume(VolumeIdx(volume_idx)); - println!("volume {}: {:#?}", volume_idx, volume); - if let Ok(volume) = volume { - let root_dir = volume_mgr.open_root_dir(volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(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 f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_CREATE, Mode::ReadWriteCreate) - .unwrap(); - println!("\nReading from file"); - println!("FILE STARTS:"); - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(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 { - buffer.resize(buffer.len() + 15, b'a'); - buffer.push(b'\n'); - } - println!("\nAppending to file"); - let num_written1 = volume_mgr.write(f, &buffer1[..]).unwrap(); - let num_written = volume_mgr.write(f, &buffer[..]).unwrap(); - println!("Number of bytes written: {}\n", num_written + num_written1); - volume_mgr.close_file(f).unwrap(); - - let f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_CREATE, Mode::ReadWriteCreateOrAppend) - .unwrap(); - volume_mgr.file_seek_from_start(f, 0).unwrap(); - - println!("\tFinding {}...", FILE_TO_CREATE); - println!( - "\tFound {}?: {:?}", - FILE_TO_CREATE, - volume_mgr.find_directory_entry(root_dir, FILE_TO_CREATE) - ); - println!("\nReading from file"); - println!("FILE STARTS:"); - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(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(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 fcc7812..0000000 --- a/examples/delete_test.rs +++ /dev/null @@ -1,167 +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: &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 = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - for volume_idx in 0..=3 { - let volume = volume_mgr.open_volume(VolumeIdx(volume_idx)); - println!("volume {}: {:#?}", volume_idx, volume); - if let Ok(volume) = volume { - let root_dir = volume_mgr.open_root_dir(volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(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(root_dir, FILE_TO_DELETE, Mode::ReadWriteCreate) - .unwrap(); - - println!("\tFinding {}...", FILE_TO_DELETE); - println!( - "\tFound {}?: {:?}", - FILE_TO_DELETE, - volume_mgr.find_directory_entry(root_dir, FILE_TO_DELETE) - ); - - match volume_mgr.delete_file_in_dir(root_dir, FILE_TO_DELETE) { - Ok(()) => (), - Err(error) => println!("\tCannot delete file: {:?}", error), - } - println!("\tClosing {}...", FILE_TO_DELETE); - volume_mgr.close_file(f).unwrap(); - - match volume_mgr.delete_file_in_dir(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(root_dir, FILE_TO_DELETE) - ); - } - } -} diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs new file mode 100644 index 0000000..8879548 --- /dev/null +++ b/examples/linux/mod.rs @@ -0,0 +1,94 @@ +//! 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)) + } +} + +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/test_mount.rs b/examples/test_mount.rs deleted file mode 100644 index 140b7ee..0000000 --- a/examples/test_mount.rs +++ /dev/null @@ -1,208 +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: &str = "README.TXT"; -const FILE_TO_CHECKSUM: &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 = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - for i in 0..=3 { - let volume = volume_mgr.open_volume(VolumeIdx(i)); - println!("volume {}: {:#?}", i, volume); - if let Ok(volume) = volume { - let root_dir = volume_mgr.open_root_dir(volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(root_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - println!("\tFinding {}...", FILE_TO_PRINT); - println!( - "\tFound {}?: {:?}", - FILE_TO_PRINT, - volume_mgr.find_directory_entry(root_dir, FILE_TO_PRINT) - ); - let f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_PRINT, Mode::ReadOnly) - .unwrap(); - println!("FILE STARTS:"); - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(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(root_dir, FILE_TO_PRINT, Mode::ReadOnly) - .is_err()); - volume_mgr.close_file(f).unwrap(); - - let test_dir = volume_mgr.open_dir(root_dir, "TEST").unwrap(); - // Check we can't open it twice - assert!(volume_mgr.open_dir(root_dir, "TEST").is_err()); - // Print the contents - println!("\tListing TEST directory:"); - volume_mgr - .iterate_dir(test_dir, |x| { - println!("\t\tFound: {:?}", x); - }) - .unwrap(); - volume_mgr.close_dir(test_dir).unwrap(); - - // 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 f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_CHECKSUM, Mode::ReadOnly) - .unwrap(); - println!( - "Checksuming {} bytes of {}", - volume_mgr.file_length(f).unwrap(), - FILE_TO_CHECKSUM - ); - let mut csum = 0u32; - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 2047]; - let num_read = volume_mgr.read(f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - csum += u32::from(*b); - } - } - println!( - "\nChecksum over {} bytes: {}", - volume_mgr.file_length(f).unwrap(), - csum - ); - // Should be all zero bytes - assert_eq!(csum, 0); - volume_mgr.close_file(f).unwrap(); - - assert!(volume_mgr.open_root_dir(volume).is_err()); - assert!(volume_mgr.close_dir(root_dir).is_ok()); - 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 eab38b7..0000000 --- a/examples/write_test.rs +++ /dev/null @@ -1,258 +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 = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - for volume_idx in 0..=3 { - let volume = volume_mgr.open_volume(VolumeIdx(volume_idx)); - println!("volume {}: {:#?}", volume_idx, volume); - if let Ok(volume) = volume { - let root_dir = volume_mgr.open_root_dir(volume).unwrap(); - println!("\tListing root directory:"); - volume_mgr - .iterate_dir(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 f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_WRITE, Mode::ReadOnly) - .unwrap(); - println!("\nReading from file {}\n", FILE_TO_WRITE); - let mut csum = 0; - println!("FILE STARTS:"); - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - csum += u32::from(*b); - } - } - println!("EOF\n"); - let mut file_size = volume_mgr.file_length(f).unwrap() as usize; - volume_mgr.close_file(f).unwrap(); - - let f = volume_mgr - .open_file_in_dir(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(f, &buffer1[..]).unwrap(); - let num_written = volume_mgr.write(f, &buffer[..]).unwrap(); - for b in &buffer1[..] { - csum += u32::from(*b); - } - for b in &buffer[..] { - csum += u32::from(*b); - } - println!("Number of bytes appended: {}\n", num_written + num_written1); - file_size += num_written; - file_size += num_written1; - - volume_mgr.file_seek_from_start(f, 0).unwrap(); - println!("\nFILE STARTS:"); - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(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(f).unwrap(); - - println!("\tFinding {}...", FILE_TO_WRITE); - let dir_ent = volume_mgr - .find_directory_entry(root_dir, FILE_TO_WRITE) - .unwrap(); - println!("\tFound {}?: {:?}", FILE_TO_WRITE, dir_ent); - assert_eq!(dir_ent.size as usize, file_size); - let f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_WRITE, Mode::ReadWriteAppend) - .unwrap(); - println!( - "\nReading from file {}, len {}\n", - FILE_TO_WRITE, - volume_mgr.file_length(f).unwrap() - ); - volume_mgr.file_seek_from_start(f, 0).unwrap(); - println!("FILE STARTS:"); - let mut csum2 = 0; - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(f, &mut buffer).unwrap(); - for b in &buffer[0..num_read] { - if *b == 10 { - print!("\\n"); - } - print!("{}", *b as char); - csum2 += u32::from(*b); - } - } - println!("EOF\n"); - assert_eq!(volume_mgr.file_length(f).unwrap() as usize, file_size); - volume_mgr.close_file(f).unwrap(); - - assert_eq!(csum, csum2); - - println!("\nTruncating file"); - let f = volume_mgr - .open_file_in_dir(root_dir, FILE_TO_WRITE, Mode::ReadWriteTruncate) - .unwrap(); - - let buffer = b"Hello\n"; - let num_written = volume_mgr.write(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(root_dir, FILE_TO_WRITE) - ); - volume_mgr.file_seek_from_start(f, 0).unwrap(); - println!("\nFILE STARTS:"); - while !volume_mgr.file_eof(f).unwrap() { - let mut buffer = [0u8; 32]; - let num_read = volume_mgr.read(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(f).unwrap(); - } - } -} diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 8ada679..1918ce8 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -514,12 +514,11 @@ impl FatVolume { &self, block_device: &D, dir: &DirectoryInfo, - name: &str, + match_name: &ShortFileName, ) -> Result> where D: BlockDevice, { - let match_name = ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { let mut current_cluster = Some(dir.cluster); @@ -541,7 +540,7 @@ impl FatVolume { match self.find_entry_in_block( block_device, FatType::Fat16, - &match_name, + match_name, block, ) { Err(Error::NotInBlock) => continue, @@ -575,7 +574,7 @@ impl FatVolume { match self.find_entry_in_block( block_device, FatType::Fat32, - &match_name, + match_name, block, ) { Err(Error::NotInBlock) => continue, @@ -630,12 +629,11 @@ impl FatVolume { &self, block_device: &D, dir: &DirectoryInfo, - name: &str, + match_name: &ShortFileName, ) -> Result<(), Error> where D: BlockDevice, { - let match_name = ShortFileName::create_from_str(name).map_err(Error::FilenameError)?; match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { let mut current_cluster = Some(dir.cluster); @@ -653,7 +651,7 @@ impl FatVolume { while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { - match self.delete_entry_in_block(block_device, &match_name, block) { + match self.delete_entry_in_block(block_device, match_name, block) { Err(Error::NotInBlock) => continue, x => return x, } @@ -682,7 +680,7 @@ impl FatVolume { 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.delete_entry_in_block(block_device, &match_name, block) { + match self.delete_entry_in_block(block_device, match_name, block) { Err(Error::NotInBlock) => continue, x => return x, } diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 96c48d2..096e99c 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]) diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 5abee5e..32df4ed 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -17,7 +17,7 @@ mod timestamp; pub use self::attributes::Attributes; pub use self::cluster::Cluster; 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}; diff --git a/src/lib.rs b/src/lib.rs index 99f1acc..c1c5ed1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,12 +73,12 @@ //! //! ## 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)] diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 0420b33..373c557 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -9,7 +9,7 @@ use crate::fat::{self, BlockCache, RESERVED_ENTRIES}; use crate::filesystem::{ Attributes, Cluster, DirEntry, Directory, DirectoryInfo, File, FileInfo, Mode, - SearchIdGenerator, ShortFileName, TimeSource, MAX_FILE_SIZE, + SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ debug, Block, BlockCount, BlockDevice, BlockIdx, Error, Volume, VolumeIdx, VolumeInfo, @@ -211,11 +211,14 @@ 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, parent_dir: Directory, - name: &str, - ) -> Result> { + name: N, + ) -> Result> + where + N: ToShortFileName, + { if self.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); } @@ -223,12 +226,13 @@ where // 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 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, name)? + fat.find_directory_entry(&self.block_device, parent_dir_info, &short_file_name)? } }; @@ -281,16 +285,20 @@ where } /// Look in a directory for a named file. - pub fn find_directory_entry( + pub fn find_directory_entry( &mut self, directory: Directory, - name: &str, - ) -> Result> { + 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) => { - fat.find_directory_entry(&self.block_device, &self.open_dirs[directory_idx], name) + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + fat.find_directory_entry(&self.block_device, &self.open_dirs[directory_idx], &sfn) } } } @@ -405,12 +413,15 @@ where } /// 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, directory: Directory, - name: &str, + name: N, mode: Mode, - ) -> Result> { + ) -> Result> + where + N: ToShortFileName, + { if self.open_files.is_full() { return Err(Error::TooManyOpenFiles); } @@ -420,10 +431,11 @@ where 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, name) + fat.find_directory_entry(&self.block_device, directory_info, &sfn) } }; @@ -463,8 +475,6 @@ 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 volume_idx = self.get_volume_by_id(volume_id)?; let entry = match &mut self.open_volumes[volume_idx].volume_type { @@ -472,7 +482,7 @@ where &self.block_device, &self.time_source, directory_info, - file_name, + sfn, att, )?, }; @@ -505,19 +515,21 @@ where } /// Delete a closed file with the given filename, if it exists. - pub fn delete_file_in_dir( + pub fn delete_file_in_dir( &mut self, directory: Directory, - name: &str, - ) -> Result<(), Error> { - debug!("delete_file(directory={:?}, filename={:?}", directory, name); - + 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, name), + VolumeType::Fat(fat) => fat.find_directory_entry(&self.block_device, dir_info, &sfn), }?; if dir_entry.attributes.is_directory() { @@ -533,7 +545,7 @@ where 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, name)? + fat.delete_directory_entry(&self.block_device, dir_info, &sfn)? } } @@ -789,6 +801,12 @@ where 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 { From 33e93d789e973d06c99441bde92b6281f6bfc4e6 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Thu, 21 Sep 2023 15:23:11 +0100 Subject: [PATCH 59/69] Add missing blocks and comments. --- examples/linux/mod.rs | 6 +++++- examples/readme_test.rs | 6 ++++++ tests/directories.rs | 8 ++++++++ tests/read_file.rs | 8 ++++++++ tests/utils/mod.rs | 6 ++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs index 8879548..1a1b190 100644 --- a/examples/linux/mod.rs +++ b/examples/linux/mod.rs @@ -91,4 +91,8 @@ impl TimeSource for Clock { } } -// End of file +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/readme_test.rs b/examples/readme_test.rs index 79afb1a..beba096 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -109,3 +109,9 @@ fn main() -> Result<(), Error> { volume_mgr.close_dir(root_dir)?; Ok(()) } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/directories.rs b/tests/directories.rs index 4e871ca..1bf7e4b 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -1,3 +1,5 @@ +//! Directory related tests + mod utils; #[derive(Debug, Clone)] @@ -211,3 +213,9 @@ fn fat32_root_directory_listing() { ); } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/tests/read_file.rs b/tests/read_file.rs index e0f1a76..0a6bffb 100644 --- a/tests/read_file.rs +++ b/tests/read_file.rs @@ -1,3 +1,5 @@ +//! Reading related tests + mod utils; static TEST_DAT_SHA256_SUM: &str = @@ -167,3 +169,9 @@ fn read_file_backwards() { 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 index 24787ea..b74db45 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -178,3 +178,9 @@ pub fn make_time_source() -> TestTimeSource { }, } } + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** From c63688f02c68454944672f9acea2573d4d0b0d90 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Thu, 21 Sep 2023 17:14:40 +0100 Subject: [PATCH 60/69] Can now close volumes. Test coverage at 56.2%. --- src/lib.rs | 4 ++ src/volume_mgr.rs | 28 +++++++++++++ tests/directories.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++ tests/open_files.rs | 92 ++++++++++++++++++++++++++++++++++++++++++ tests/volume.rs | 79 ++++++++++++++++++++++++++++++++++++ 5 files changed, 299 insertions(+) create mode 100644 tests/open_files.rs create mode 100644 tests/volume.rs diff --git a/src/lib.rs b/src/lib.rs index c1c5ed1..9aa00cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,10 @@ where 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 diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 373c557..274c64e 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -115,6 +115,12 @@ where 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 @@ -284,6 +290,28 @@ where } } + /// Close a volume + /// + /// You can't close it if there are any files or directories open on it. + pub fn close_volume(&mut self, volume: 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( &mut self, diff --git a/tests/directories.rs b/tests/directories.rs index 1bf7e4b..f6d49cc 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -214,6 +214,102 @@ fn fat32_root_directory_listing() { } } +#[test] +fn open_dir_twice() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let 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/tests/open_files.rs b/tests/open_files.rs new file mode 100644 index 0000000..e8e570c --- /dev/null +++ b/tests/open_files.rs @@ -0,0 +1,92 @@ +//! 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.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 5 bytes in it + assert_eq!(volume_mgr.file_length(f3).expect("file length"), 5); + 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/volume.rs b/tests/volume.rs new file mode 100644 index 0000000..81a7d05 --- /dev/null +++ b/tests/volume.rs @@ -0,0 +1,79 @@ +//! 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); + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** From 457de8186fa916c991e42d020231945ff91cb19d Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 22 Sep 2023 16:40:36 +0100 Subject: [PATCH 61/69] Add more tests, and more changes. - Cluster is now ClusterId - Helper method for turning bytes to a block count - Removed unsafe open_dir_entry API --- CHANGELOG.md | 6 +- src/blockdevice.rs | 20 ++- src/fat/bpb.rs | 15 ++- src/fat/info.rs | 8 +- src/fat/mod.rs | 14 +-- src/fat/ondiskdirentry.rs | 10 +- src/fat/volume.rs | 240 ++++++++++++++++++++++-------------- src/filesystem/cluster.rs | 42 +++---- src/filesystem/directory.rs | 8 +- src/filesystem/files.rs | 4 +- src/filesystem/mod.rs | 2 +- src/lib.rs | 14 +-- src/volume_mgr.rs | 52 ++++---- tests/directories.rs | 9 ++ tests/open_files.rs | 5 +- tests/volume.rs | 30 +++++ tests/write_file.rs | 60 +++++++++ 17 files changed, 351 insertions(+), 188 deletions(-) create mode 100644 tests/write_file.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a7b5a..d9bcb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - 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 @@ -31,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Removed - __Breaking Change__: `Controller` alias for `VolumeManager` removed. +- __Breaking Change__: `VolumeManager::open_dir_entry` removed, as it was unsafe to the user to randomly pick a starting cluster. - Old examples `create_test`, `test_mount`, `write_test`, `delete_test` ## [Version 0.5.0](https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.5.0) - 2023-05-20 @@ -60,7 +62,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Optionally use [defmt](https://github.com/knurling-rs/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. @@ -81,7 +83,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - 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) +- 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 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 71b067c..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"); diff --git a/src/fat/info.rs b/src/fat/info.rs index 091ab98..f9f8e2c 100644 --- a/src/fat/info.rs +++ b/src/fat/info.rs @@ -1,4 +1,4 @@ -use crate::{BlockCount, BlockIdx, Cluster}; +use crate::{BlockCount, BlockIdx, ClusterId}; use byteorder::{ByteOrder, LittleEndian}; /// Indentifies the supported types of FAT format @@ -17,7 +17,7 @@ pub enum FatSpecificInfo { 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, } @@ -78,11 +78,11 @@ 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)), } } } diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 5e119cb..35641cb 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -66,7 +66,7 @@ use crate::{Block, BlockDevice, BlockIdx, Error}; 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(); @@ -143,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, @@ -161,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, @@ -186,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, @@ -203,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, @@ -228,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, @@ -253,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, diff --git a/src/fat/ondiskdirentry.rs b/src/fat/ondiskdirentry.rs index c85caf5..8ce3f63 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 diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 1918ce8..4c6fe99 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -6,7 +6,7 @@ use crate::{ Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, RESERVED_ENTRIES, }, - trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, Cluster, DirEntry, + trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, ClusterId, DirEntry, DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; @@ -58,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 @@ -110,8 +110,8 @@ impl FatVolume { fn update_fat( &mut self, block_device: &D, - cluster: Cluster, - new_value: Cluster, + cluster: ClusterId, + new_value: ClusterId, ) -> Result<(), Error> where D: BlockDevice, @@ -127,10 +127,10 @@ impl FatVolume { .read(&mut blocks, this_fat_block_num, "read_fat") .map_err(Error::DeviceError)?; 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,9 +147,9 @@ impl FatVolume { .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( @@ -172,9 +172,9 @@ impl FatVolume { pub(crate) fn next_cluster( &self, block_device: &D, - cluster: Cluster, + cluster: ClusterId, fat_block_cache: &mut BlockCache, - ) -> Result> + ) -> Result> where D: BlockDevice, { @@ -198,7 +198,7 @@ impl FatVolume { } f => { // Seems legit - Ok(Cluster(u32::from(f))) + Ok(ClusterId(u32::from(f))) } } } @@ -214,7 +214,7 @@ impl FatVolume { match fat_entry { 0x0000_0000 => { // Jumped to free space - Err(Error::JumpedFree) + Err(Error::UnterminatedFatChain) } 0x0FFF_FFF7 => { // Bad cluster @@ -226,7 +226,7 @@ impl FatVolume { } f => { // Seems legit - Ok(Cluster(f)) + Ok(ClusterId(f)) } } } @@ -241,12 +241,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)); @@ -257,7 +257,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; @@ -284,26 +284,33 @@ 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) { 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]); @@ -313,7 +320,7 @@ impl FatVolume { let entry = DirEntry::new( name, attributes, - Cluster(0), + ClusterId(0), ctime, block, start as u32, @@ -327,7 +334,7 @@ impl FatVolume { } } } - if cluster != Cluster::ROOT_DIR { + if cluster != ClusterId::ROOT_DIR { let mut block_cache = BlockCache::empty(); current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { @@ -350,8 +357,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); @@ -373,7 +382,7 @@ impl FatVolume { let entry = DirEntry::new( name, attributes, - Cluster(0), + ClusterId(0), ctime, block, start as u32, @@ -421,41 +430,44 @@ 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 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) { - 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 { + if cluster != ClusterId::ROOT_DIR { current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { Ok(n) => { @@ -471,8 +483,10 @@ 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()]; @@ -521,16 +535,21 @@ 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 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)), }; @@ -543,11 +562,11 @@ impl FatVolume { match_name, block, ) { - Err(Error::NotInBlock) => continue, + Err(Error::FileNotFound) => continue, x => return x, } } - if cluster != Cluster::ROOT_DIR { + if cluster != ClusterId::ROOT_DIR { current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { Ok(n) => { @@ -564,7 +583,7 @@ 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(); @@ -577,7 +596,7 @@ impl FatVolume { match_name, block, ) { - Err(Error::NotInBlock) => continue, + Err(Error::FileNotFound) => continue, x => return x, } } @@ -592,7 +611,7 @@ impl FatVolume { } } - /// Finds an entry in a given block + /// Finds an entry in a given block of directory entries. fn find_entry_in_block( &self, block_device: &D, @@ -613,7 +632,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 @@ -621,7 +640,7 @@ 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 @@ -636,27 +655,41 @@ 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 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(block_device, match_name, block) { - Err(Error::NotInBlock) => continue, - x => return x, + 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 { + // 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) { @@ -670,21 +703,33 @@ impl FatVolume { 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(block_device, match_name, block) { - Err(Error::NotInBlock) => continue, - x => return x, + Err(Error::FileNotFound) => { + // Carry on + continue; + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } } } + // Find the next cluster let mut block_cache = BlockCache::empty(); current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { @@ -692,12 +737,18 @@ impl FatVolume { _ => 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 + /// Deletes a directory entry from a block of directory entries. + /// + /// Entries are marked as deleted by setting the first byte of the file name + /// to a special value. fn delete_entry_in_block( &self, block_device: &D, @@ -717,26 +768,25 @@ 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; - 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( &self, block_device: &D, - start_cluster: Cluster, - end_cluster: Cluster, - ) -> Result> + start_cluster: ClusterId, + end_cluster: ClusterId, + ) -> Result> where D: BlockDevice, { @@ -814,17 +864,17 @@ impl FatVolume { pub(crate) fn alloc_cluster( &mut self, block_device: &D, - prev_cluster: Option, + prev_cluster: Option, zero: bool, - ) -> Result> + ) -> Result> where D: BlockDevice, { 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 {:?}..={:?}", @@ -837,18 +887,18 @@ impl FatVolume { Err(_) if start_cluster.0 > RESERVED_ENTRIES => { debug!( "Retrying, finding next free between {:?}..={:?}", - Cluster(RESERVED_ENTRIES), + ClusterId(RESERVED_ENTRIES), end_cluster ); self.find_next_free_cluster( block_device, - Cluster(RESERVED_ENTRIES), + ClusterId(RESERVED_ENTRIES), end_cluster, )? } Err(e) => return Err(e), }; - self.update_fat(block_device, new_cluster, Cluster::END_OF_FILE)?; + self.update_fat(block_device, new_cluster, ClusterId::END_OF_FILE)?; if let Some(cluster) = prev_cluster { trace!( "Updating old cluster {:?} to {:?} in FAT", @@ -868,7 +918,7 @@ impl FatVolume { Err(_) if new_cluster.0 > RESERVED_ENTRIES => { match self.find_next_free_cluster( block_device, - Cluster(RESERVED_ENTRIES), + ClusterId(RESERVED_ENTRIES), end_cluster, ) { Ok(cluster) => Some(cluster), @@ -899,7 +949,7 @@ impl FatVolume { pub(crate) fn truncate_cluster_chain( &mut self, block_device: &D, - cluster: Cluster, + cluster: ClusterId, ) -> Result<(), Error> where D: BlockDevice, @@ -923,16 +973,16 @@ impl FatVolume { } else { self.next_free_cluster = Some(next); } - self.update_fat(block_device, cluster, Cluster::END_OF_FILE)?; + self.update_fat(block_device, cluster, ClusterId::END_OF_FILE)?; loop { let mut block_cache = BlockCache::empty(); match self.next_cluster(block_device, next, &mut block_cache) { Ok(n) => { - self.update_fat(block_device, next, Cluster::EMPTY)?; + self.update_fat(block_device, next, ClusterId::EMPTY)?; next = n; } Err(Error::EndOfFile) => { - self.update_fat(block_device, next, Cluster::EMPTY)?; + self.update_fat(block_device, next, ClusterId::EMPTY)?; break; } Err(e) => return Err(e), @@ -1024,7 +1074,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()); diff --git a/src/filesystem/cluster.rs b/src/filesystem/cluster.rs index e0bac87..34d8590 100644 --- a/src/filesystem/cluster.rs +++ b/src/filesystem/cluster.rs @@ -1,48 +1,38 @@ -/// 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, Ord, PartialOrd)] -pub struct Cluster(pub(crate) u32); +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 6b38e34..744e12d 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -2,7 +2,7 @@ use core::convert::TryFrom; use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; -use crate::filesystem::{Attributes, Cluster, SearchId, ShortFileName, Timestamp}; +use crate::filesystem::{Attributes, ClusterId, SearchId, ShortFileName, Timestamp}; use crate::Volume; /// Represents a directory entry, which tells you about @@ -19,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 @@ -57,7 +57,7 @@ pub(crate) struct DirectoryInfo { /// 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, + pub(crate) cluster: ClusterId, } impl DirEntry { @@ -92,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, diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index fed246f..6d66125 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,5 +1,5 @@ use crate::{ - filesystem::{Cluster, DirEntry, SearchId}, + filesystem::{ClusterId, DirEntry, SearchId}, Volume, }; @@ -32,7 +32,7 @@ pub(crate) struct FileInfo { /// 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, /// What mode the file was opened in diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 32df4ed..69076f1 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -15,7 +15,7 @@ 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, ToShortFileName}; pub use self::files::{File, FileError, Mode}; diff --git a/src/lib.rs b/src/lib.rs index 9aa00cc..61070fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,8 +111,8 @@ 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; @@ -188,7 +188,7 @@ where 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, @@ -198,8 +198,6 @@ where 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 @@ -216,16 +214,14 @@ 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, } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 274c64e..22e0118 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -8,7 +8,7 @@ use core::convert::TryFrom; use crate::fat::{self, BlockCache, RESERVED_ENTRIES}; use crate::filesystem::{ - Attributes, Cluster, DirEntry, Directory, DirectoryInfo, File, FileInfo, Mode, + Attributes, ClusterId, DirEntry, Directory, DirectoryInfo, File, FileInfo, Mode, SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ @@ -191,7 +191,7 @@ where /// 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 == Cluster::ROOT_DIR && dir.volume_id == volume { + if dir.cluster == ClusterId::ROOT_DIR && dir.volume_id == volume { return Err(Error::DirAlreadyOpen); } } @@ -199,7 +199,7 @@ where let directory_id = Directory(self.id_generator.get()); let dir_info = DirectoryInfo { volume_id: volume, - cluster: Cluster::ROOT_DIR, + cluster: ClusterId::ROOT_DIR, directory_id, }; @@ -345,12 +345,13 @@ where } } - /// Open a file from DirEntry. This is obtained by calling iterate_dir. + /// Open a file from a DirEntry. This is obtained by calling iterate_dir. /// - /// A file can only be opened once. + /// # Safety /// - /// Do not drop the returned file handle - pass it to [`VolumeManager::close_file`]. - pub fn open_dir_entry( + /// The DirEntry must be a valid DirEntry read from disk, and not just + /// random numbers. + unsafe fn open_dir_entry( &mut self, volume: Volume, dir_entry: DirEntry, @@ -369,10 +370,8 @@ where } // Check it's not already open - for f in self.open_files.iter() { - if f.volume_id == volume && f.entry.cluster == dir_entry.cluster { - return Err(Error::DirAlreadyOpen); - } + if self.file_is_open(volume, dir_entry.cluster) { + return Err(Error::FileAlreadyOpen); } let mode = solve_mode_variant(mode, true); @@ -489,10 +488,8 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - for f in self.open_files.iter() { - if f.volume_id == volume_info.volume_id && f.entry.cluster == dir_entry.cluster { - return Err(Error::FileAlreadyOpen); - } + if self.file_is_open(volume_info.volume_id, dir_entry.cluster) { + return Err(Error::FileAlreadyOpen); } } @@ -537,7 +534,8 @@ where _ => { // Safe to unwrap, since we actually have an entry if we got here let dir_entry = dir_entry.unwrap(); - self.open_dir_entry(volume_id, dir_entry, mode) + // Safety: We read this dir entry off disk and didn't change it + unsafe { self.open_dir_entry(volume_id, dir_entry, mode) } } } } @@ -564,10 +562,8 @@ where return Err(Error::DeleteDirAsFile); } - for f in self.open_files.iter() { - if f.entry.cluster == dir_entry.cluster && f.volume_id == dir_info.volume_id { - return Err(Error::FileIsOpen); - } + if self.file_is_open(dir_info.volume_id, dir_entry.cluster) { + return Err(Error::FileAlreadyOpen); } let volume_idx = self.get_volume_by_id(dir_info.volume_id)?; @@ -580,6 +576,18 @@ where Ok(()) } + /// Check if a file is open + /// + /// Returns `true` if it's open, `false`, otherwise. + fn file_is_open(&self, volume: Volume, starting_cluster: ClusterId) -> bool { + for f in self.open_files.iter() { + if f.volume_id == volume && f.entry.cluster == starting_cluster { + return true; + } + } + false + } + /// Read from an open file. pub fn read(&mut self, file: File, buffer: &mut [u8]) -> Result> { let file_idx = self.get_file_by_id(file)?; @@ -868,7 +876,7 @@ where fn find_data_on_disk( &self, volume_idx: usize, - start: &mut (u32, Cluster), + start: &mut (u32, ClusterId), desired_offset: u32, ) -> Result<(BlockIdx, usize, usize), Error> { let bytes_per_cluster = match &self.open_volumes[volume_idx].volume_type { @@ -1211,7 +1219,7 @@ mod tests { next_free_cluster: None, cluster_count: 965_788, fat_specific_info: fat::FatSpecificInfo::Fat32(fat::Fat32Info { - first_root_dir_cluster: Cluster(2), + first_root_dir_cluster: ClusterId(2), info_location: BlockIdx(1) + BlockCount(1), }) }) diff --git a/tests/directories.rs b/tests/directories.rs index f6d49cc..12abb85 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -1,5 +1,7 @@ //! Directory related tests +use embedded_sdmmc::ShortFileName; + mod utils; #[derive(Debug, Clone)] @@ -135,9 +137,16 @@ fn fat16_sub_directory_listing() { ]; 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"); diff --git a/tests/open_files.rs b/tests/open_files.rs index e8e570c..0e07d0c 100644 --- a/tests/open_files.rs +++ b/tests/open_files.rs @@ -71,14 +71,15 @@ fn open_files() { .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 5 bytes in it - assert_eq!(volume_mgr.file_length(f3).expect("file length"), 5); + // 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"); diff --git a/tests/volume.rs b/tests/volume.rs index 81a7d05..a1a0455 100644 --- a/tests/volume.rs +++ b/tests/volume.rs @@ -72,6 +72,36 @@ fn open_all_volumes() { }; } +#[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 +// +// **************************************************************************** From 21aaa88d730c791dedf30dfdea61c14fc6bfd578 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 22 Sep 2023 16:54:17 +0100 Subject: [PATCH 62/69] Check file match by dir entry not by starting cluster. This is because some files have no starting cluster and thus look like the same file when they are not. Fixes #75 --- src/fat/volume.rs | 2 +- src/volume_mgr.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 4c6fe99..e9ec2da 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -320,7 +320,7 @@ impl FatVolume { let entry = DirEntry::new( name, attributes, - ClusterId(0), + ClusterId::EMPTY, ctime, block, start as u32, diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 22e0118..a813ee2 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -370,7 +370,7 @@ where } // Check it's not already open - if self.file_is_open(volume, dir_entry.cluster) { + if self.file_is_open(volume, &dir_entry) { return Err(Error::FileAlreadyOpen); } @@ -488,7 +488,7 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - if self.file_is_open(volume_info.volume_id, dir_entry.cluster) { + if self.file_is_open(volume_info.volume_id, &dir_entry) { return Err(Error::FileAlreadyOpen); } } @@ -562,7 +562,7 @@ where return Err(Error::DeleteDirAsFile); } - if self.file_is_open(dir_info.volume_id, dir_entry.cluster) { + if self.file_is_open(dir_info.volume_id, &dir_entry) { return Err(Error::FileAlreadyOpen); } @@ -579,9 +579,12 @@ where /// Check if a file is open /// /// Returns `true` if it's open, `false`, otherwise. - fn file_is_open(&self, volume: Volume, starting_cluster: ClusterId) -> bool { + 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.cluster == starting_cluster { + if f.volume_id == volume + && f.entry.entry_block == dir_entry.entry_block + && f.entry.entry_offset == dir_entry.entry_offset + { return true; } } From cf32701f7f2340e945c799c8e0aa19dbb3dff8ac Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Fri, 22 Sep 2023 19:43:44 +0100 Subject: [PATCH 63/69] Some clean-ups. Co-authored-by: Ryan Summers --- src/fat/volume.rs | 1 + src/volume_mgr.rs | 16 +++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index e9ec2da..c946ec8 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -126,6 +126,7 @@ impl FatVolume { block_device .read(&mut blocks, this_fat_block_num, "read_fat") .map_err(Error::DeviceError)?; + // See let entry = match new_value { ClusterId::INVALID => 0xFFF6, ClusterId::BAD => 0xFFF7, diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index a813ee2..4cafafa 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -273,21 +273,13 @@ where /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. pub fn close_dir(&mut self, directory: Directory) -> Result<(), Error> { - let mut found_idx = None; for (idx, info) in self.open_dirs.iter().enumerate() { if directory == info.directory_id { - found_idx = Some(idx); - break; - } - } - - match found_idx { - None => Err(Error::BadHandle), - Some(idx) => { self.open_dirs.swap_remove(idx); - Ok(()) + return Ok(()); } } + Err(Error::BadHandle) } /// Close a volume @@ -357,6 +349,7 @@ where dir_entry: DirEntry, mode: Mode, ) -> Result> { + // This check is load-bearing - we do an unchecked push later. if self.open_files.is_full() { return Err(Error::TooManyOpenFiles); } @@ -449,6 +442,7 @@ where where N: ToShortFileName, { + // This check is load-bearing - we do an unchecked push later. if self.open_files.is_full() { return Err(Error::TooManyOpenFiles); } @@ -524,7 +518,7 @@ where dirty: false, }; - // Remember this open file + // Remember this open file - can't be full as we checked already unsafe { self.open_files.push_unchecked(file); } From 1a8cafd396cf580408d448f233632bd48341df97 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 23 Sep 2023 21:50:56 +0100 Subject: [PATCH 64/69] Add a little shell example. You can walk around a disk image and do directory listings. I had to make some more things 'Debug' so I could print the filesystem state. Also our handling of ".." was not right so I fixed that. --- examples/linux/mod.rs | 1 + examples/shell.rs | 151 ++++++++++++++++++++++++++++++++++++ src/fat/ondiskdirentry.rs | 19 +++-- src/filesystem/filename.rs | 6 ++ src/filesystem/search_id.rs | 1 + src/volume_mgr.rs | 5 +- 6 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 examples/shell.rs diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs index 1a1b190..5abb99f 100644 --- a/examples/linux/mod.rs +++ b/examples/linux/mod.rs @@ -74,6 +74,7 @@ impl BlockDevice for LinuxBlockDevice { } } +#[derive(Debug)] pub struct Clock; impl TimeSource for Clock { diff --git a/examples/shell.rs b/examples/shell.rs new file mode 100644 index 0000000..4612bae --- /dev/null +++ b/examples/shell.rs @@ -0,0 +1,151 @@ +//! 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 State { + directory: Directory, + volume: Volume, +} + +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 mut volume_mgr: VolumeManager = + VolumeManager::new_with_limits(lbd, Clock, 100); + let stdin = std::io::stdin(); + let mut volumes: [Option; 4] = [None, None, None, None]; + + let mut current_volume = None; + for volume_no in 0..4 { + match volume_mgr.open_volume(VolumeIdx(volume_no)) { + Ok(volume) => { + println!("Volume # {}: found", volume_no,); + match volume_mgr.open_root_dir(volume) { + Ok(root_dir) => { + volumes[volume_no] = Some(State { + directory: root_dir, + volume, + }); + if current_volume.is_none() { + current_volume = Some(volume_no); + } + } + Err(e) => { + println!("Failed to open root directory: {e:?}"); + volume_mgr.close_volume(volume).expect("close volume"); + } + } + } + Err(e) => { + println!("Failed to open volume {volume_no}: {e:?}"); + } + } + } + + let Some(mut current_volume) = current_volume else { + println!("No volumes found in file. Sorry."); + return Ok(()); + }; + + loop { + print!("{}:> ", current_volume); + std::io::stdout().flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + log::info!("Got command: {line:?}"); + if line == "quit" { + break; + } else if line == "help" { + println!("Commands:"); + println!("\thelp -> this help text"); + println!("\t: -> change volume/partition"); + println!("\tdir -> do a directory listing"); + println!("\tquit -> exits the program"); + } else if line == "0:" { + current_volume = 0; + } else if line == "1:" { + current_volume = 1; + } else if line == "2:" { + current_volume = 2; + } else if line == "3:" { + current_volume = 3; + } else if line == "stat" { + println!("Status:\n{volume_mgr:#?}"); + } else if line == "dir" { + let Some(s) = &volumes[current_volume] else { + println!("That volume isn't available"); + continue; + }; + let r = volume_mgr.iterate_dir(s.directory, |entry| { + println!( + "{:12} {:9} {} {}", + entry.name, + entry.size, + entry.mtime, + if entry.attributes.is_directory() { + "

" + } else { + "" + } + ); + }); + handle("iterating directory", r); + } else if let Some(arg) = line.strip_prefix("cd ") { + let Some(s) = &mut volumes[current_volume] else { + println!("This volume isn't available"); + continue; + }; + match volume_mgr.open_dir(s.directory, arg) { + Ok(d) => { + let r = volume_mgr.close_dir(s.directory); + handle("closing old directory", r); + s.directory = d; + } + Err(e) => { + handle("changing directory", Err(e)); + } + } + } else { + println!("Unknown command {line:?} - try 'help' for help"); + } + } + + for (idx, s) in volumes.into_iter().enumerate() { + if let Some(state) = s { + println!("Unmounting {idx}..."); + let r = volume_mgr.close_dir(state.directory); + handle("closing directory", r); + let r = volume_mgr.close_volume(state.volume); + handle("closing volume", r); + println!("Unmounted {idx}!"); + } + } + + println!("Bye!"); + Ok(()) +} + +fn handle(operation: &str, r: Result<(), Error>) { + if let Err(e) = r { + println!("Error {operation}: {e:?}"); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/src/fat/ondiskdirentry.rs b/src/fat/ondiskdirentry.rs index 8ce3f63..14be74c 100644 --- a/src/fat/ondiskdirentry.rs +++ b/src/fat/ondiskdirentry.rs @@ -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, diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 096e99c..120eaa5 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -85,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() { diff --git a/src/filesystem/search_id.rs b/src/filesystem/search_id.rs index f4be611..30c1018 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/search_id.rs @@ -12,6 +12,7 @@ pub struct SearchId(pub(crate) u32); /// 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, } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 4cafafa..5abfcbb 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -20,6 +20,7 @@ use heapless::Vec; /// A `VolumeManager` wraps a block device and gives access to the FAT-formatted /// volumes within it. +#[derive(Debug)] pub struct VolumeManager< D, T, @@ -242,6 +243,8 @@ where } }; + debug!("Found dir entry: {:?}", dir_entry); + if !dir_entry.attributes.is_directory() { return Err(Error::OpenedFileAsDir); } @@ -482,7 +485,7 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - if self.file_is_open(volume_info.volume_id, &dir_entry) { + if self.file_is_open(volume_info.volume_id, dir_entry) { return Err(Error::FileAlreadyOpen); } } From 9b5ddf76a26bbf2a8c818d74810ee9d4a4ef02e7 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 3 Oct 2023 21:19:36 +0100 Subject: [PATCH 65/69] Remove log line from shell example. Fixes the build. --- examples/shell.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/shell.rs b/examples/shell.rs index 4612bae..8ecf75e 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -65,7 +65,6 @@ fn main() -> Result<(), Error> { let mut line = String::new(); stdin.read_line(&mut line)?; let line = line.trim(); - log::info!("Got command: {line:?}"); if line == "quit" { break; } else if line == "help" { From 0578d4e5d649ae60ec3dac6c6930e3843fbedf9d Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 3 Oct 2023 21:22:23 +0100 Subject: [PATCH 66/69] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9bcb2d..a8aa658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- New examples, `append_file`, `create_file`, `delete_file`, `list_dir` +- New examples, `append_file`, `create_file`, `delete_file`, `list_dir`, `shell` - New test cases `tests/directories.rs`, `tests/read_file.rs` ### Removed From a041eee5648e425c9fb52fc161a72cd2495288b6 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 3 Oct 2023 22:10:22 +0100 Subject: [PATCH 67/69] Added hexdump and cat. Also moved the code into a method so we can use ?. Although ? is a pain when your objects cannot safely be dropped. --- examples/shell.rs | 261 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 185 insertions(+), 76 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index 8ecf75e..8ec1d81 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -10,9 +10,158 @@ use crate::linux::{Clock, LinuxBlockDevice}; mod linux; -struct State { +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, + if entry.attributes.is_directory() { + "" + } else { + "" + } + ); + })?; + } 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> { @@ -22,21 +171,25 @@ fn main() -> Result<(), Error> { 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 mut volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 100); let stdin = std::io::stdin(); - let mut volumes: [Option; 4] = [None, None, None, None]; + + 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 volume_mgr.open_volume(VolumeIdx(volume_no)) { + match ctx.volume_mgr.open_volume(VolumeIdx(volume_no)) { Ok(volume) => { println!("Volume # {}: found", volume_no,); - match volume_mgr.open_root_dir(volume) { + match ctx.volume_mgr.open_root_dir(volume) { Ok(root_dir) => { - volumes[volume_no] = Some(State { + ctx.volumes[volume_no] = Some(VolumeState { directory: root_dir, volume, + path: vec![], }); if current_volume.is_none() { current_volume = Some(volume_no); @@ -44,7 +197,7 @@ fn main() -> Result<(), Error> { } Err(e) => { println!("Failed to open root directory: {e:?}"); - volume_mgr.close_volume(volume).expect("close volume"); + ctx.volume_mgr.close_volume(volume).expect("close volume"); } } } @@ -54,82 +207,44 @@ fn main() -> Result<(), Error> { } } - let Some(mut current_volume) = current_volume else { - println!("No volumes found in file. Sorry."); - return Ok(()); + 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!("{}:> ", current_volume); + 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 line == "help" { - println!("Commands:"); - println!("\thelp -> this help text"); - println!("\t: -> change volume/partition"); - println!("\tdir -> do a directory listing"); - println!("\tquit -> exits the program"); - } else if line == "0:" { - current_volume = 0; - } else if line == "1:" { - current_volume = 1; - } else if line == "2:" { - current_volume = 2; - } else if line == "3:" { - current_volume = 3; - } else if line == "stat" { - println!("Status:\n{volume_mgr:#?}"); - } else if line == "dir" { - let Some(s) = &volumes[current_volume] else { - println!("That volume isn't available"); - continue; - }; - let r = volume_mgr.iterate_dir(s.directory, |entry| { - println!( - "{:12} {:9} {} {}", - entry.name, - entry.size, - entry.mtime, - if entry.attributes.is_directory() { - "" - } else { - "" - } - ); - }); - handle("iterating directory", r); - } else if let Some(arg) = line.strip_prefix("cd ") { - let Some(s) = &mut volumes[current_volume] else { - println!("This volume isn't available"); - continue; - }; - match volume_mgr.open_dir(s.directory, arg) { - Ok(d) => { - let r = volume_mgr.close_dir(s.directory); - handle("closing old directory", r); - s.directory = d; - } - Err(e) => { - handle("changing directory", Err(e)); - } - } - } else { - println!("Unknown command {line:?} - try 'help' for help"); + } else if let Err(e) = ctx.process_line(line) { + println!("Error: {:?}", e); } } - for (idx, s) in volumes.into_iter().enumerate() { + 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 = volume_mgr.close_dir(state.directory); - handle("closing directory", r); - let r = volume_mgr.close_volume(state.volume); - handle("closing volume", r); - println!("Unmounted {idx}!"); + let r = ctx.volume_mgr.close_volume(state.volume); + if let Err(e) = r { + println!("Error closing volume: {e:?}"); + } } } @@ -137,12 +252,6 @@ fn main() -> Result<(), Error> { Ok(()) } -fn handle(operation: &str, r: Result<(), Error>) { - if let Err(e) = r { - println!("Error {operation}: {e:?}"); - } -} - // **************************************************************************** // // End Of File From 52e77c9cb3ee5b2a7677ae1056df5f6dd09fb207 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sat, 7 Oct 2023 20:49:28 +0100 Subject: [PATCH 68/69] Shell prints all attributes in DIR. --- examples/shell.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index 8ec1d81..35e9e13 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -59,15 +59,8 @@ impl Context { }; self.volume_mgr.iterate_dir(s.directory, |entry| { println!( - "{:12} {:9} {} {}", - entry.name, - entry.size, - entry.mtime, - if entry.attributes.is_directory() { - "" - } else { - "" - } + "{:12} {:9} {} {:?}", + entry.name, entry.size, entry.mtime, entry.attributes ); })?; } else if let Some(arg) = line.strip_prefix("cd ") { From 2d2c8897f0c4ac44e9718ccc30c6e5f189f5208a Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 20 Oct 2023 16:54:45 +0100 Subject: [PATCH 69/69] Bump to v0.6.0 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++------------------ Cargo.toml | 2 +- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8aa658..f08a337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,13 @@ 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 @@ -35,9 +38,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - __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 in v0.5.0 +### Changed - __Breaking Change__: Renamed `Controller` to `VolumeManager`, to better describe what it does. - __Breaking Change__: Renamed `SdMmcSpi` to `SdCard` @@ -47,19 +50,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - More robust card intialisation procedure, with added retries - Supports building with neither `defmt` nor `log` logging -### Added in v0.5.0 +### Added - Added `mark_card_as_init` method, if you know the card is initialised and want to skip the initialisation step -### Removed in v0.5.0 +### Removed - __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 -### Changes in v0.4.0 +### Changed -- 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: @@ -73,9 +76,9 @@ 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 in v0.3.0 +### Changed - Updated to `v2` embedded-hal traits. - Added open support for all modes. @@ -85,23 +88,34 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - 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 in v0.2.1 +### Changed - 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 in v0.2.0 +### Changed - 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 in v0.1.0 +### Changed - Can read blocks from an SD Card using an `embedded_hal::SPI` device and a `embedded_hal::OutputPin` for Chip Select. - Can read partition tables and open a FAT32 or FAT16 formatted partition. - Can open and iterate the root directory of a FAT16 formatted partition. + +[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: http://semver.org/spec/v2.0.0.html +[Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.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 27c5ac6..8bd40d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ 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}