From 5a1efd823a9d9c3fc377a02323ac6c04dfd77e17 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Fri, 22 Aug 2025 03:06:18 -0500 Subject: [PATCH 1/9] :arrow_up: chore(dependencies): Update `libogc` bindings --- ogc-sys/src/ogc.rs | 255 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 229 insertions(+), 26 deletions(-) diff --git a/ogc-sys/src/ogc.rs b/ogc-sys/src/ogc.rs index 86751d4..45fb306 100644 --- a/ogc-sys/src/ogc.rs +++ b/ogc-sys/src/ogc.rs @@ -77,13 +77,13 @@ pub const true_: u32 = 1; pub const false_: u32 = 0; pub const __bool_true_false_are_defined: u32 = 1; pub const _NEWLIB_VERSION_H__: u32 = 1; -pub const _NEWLIB_VERSION: &[u8; 6] = b"4.4.0\0"; +pub const _NEWLIB_VERSION: &[u8; 6] = b"4.5.0\0"; pub const __NEWLIB__: u32 = 4; -pub const __NEWLIB_MINOR__: u32 = 4; +pub const __NEWLIB_MINOR__: u32 = 5; pub const __NEWLIB_PATCHLEVEL__: u32 = 0; pub const _DEFAULT_SOURCE: u32 = 1; pub const _POSIX_SOURCE: u32 = 1; -pub const _POSIX_C_SOURCE: u32 = 200809; +pub const _POSIX_C_SOURCE: u32 = 202405; pub const _ATFILE_SOURCE: u32 = 1; pub const __ATFILE_VISIBLE: u32 = 1; pub const __BSD_VISIBLE: u32 = 1; @@ -91,7 +91,7 @@ pub const __GNU_VISIBLE: u32 = 0; pub const __ISO_C_VISIBLE: u32 = 2011; pub const __LARGEFILE_VISIBLE: u32 = 0; pub const __MISC_VISIBLE: u32 = 1; -pub const __POSIX_VISIBLE: u32 = 200809; +pub const __POSIX_VISIBLE: u32 = 202405; pub const __SVID_VISIBLE: u32 = 1; pub const __XSI_VISIBLE: u32 = 0; pub const __SSP_FORTIFY_LEVEL: u32 = 0; @@ -242,6 +242,14 @@ pub const COLOR_MONEYGREEN: u32 = 3279471478; pub const COLOR_SKYBLUE: u32 = 3096885357; pub const COLOR_CREAM: u32 = 3900434563; pub const COLOR_MEDGRAY: u32 = 2592250496; +pub const CONSOLE_COLOR_BLACK: u32 = 0; +pub const CONSOLE_COLOR_RED: u32 = 1; +pub const CONSOLE_COLOR_GREEN: u32 = 2; +pub const CONSOLE_COLOR_YELLOW: u32 = 3; +pub const CONSOLE_COLOR_BLUE: u32 = 4; +pub const CONSOLE_COLOR_MAGENTA: u32 = 5; +pub const CONSOLE_COLOR_CYAN: u32 = 6; +pub const CONSOLE_COLOR_WHITE: u32 = 7; pub const FEATURE_MEDIUM_CANREAD: u32 = 1; pub const FEATURE_MEDIUM_CANWRITE: u32 = 2; pub const FEATURE_GAMECUBE_SLOTA: u32 = 16; @@ -1129,6 +1137,7 @@ pub const _REENT_CHECK_VERIFY: u32 = 1; pub const _UNBUF_STREAM_OPT: u32 = 1; pub const _WANT_IO_C99_FORMATS: u32 = 1; pub const _WANT_IO_LONG_LONG: u32 = 1; +pub const _WANT_IO_POS_ARGS: u32 = 1; pub const _WANT_REGISTER_FINI: u32 = 1; pub const _WANT_USE_GDTOA: u32 = 1; pub const _WIDE_ORIENT: u32 = 1; @@ -1243,6 +1252,8 @@ pub const CLOCK_DISABLED: u32 = 0; pub const CLOCK_ALLOWED: u32 = 1; pub const CLOCK_DISALLOWED: u32 = 0; pub const TIMER_ABSTIME: u32 = 4; +pub const CLOCK_REALTIME: u32 = 1; +pub const CLOCK_MONOTONIC: u32 = 4; pub const SYS_BASE_CACHED: u32 = 2147483648; pub const SYS_BASE_UNCACHED: u32 = 3221225472; pub const SYS_WD_NULL: u32 = 4294967295; @@ -1297,6 +1308,8 @@ pub const VI_MAX_WIDTH_MPAL: u32 = 720; pub const VI_MAX_HEIGHT_MPAL: u32 = 480; pub const VI_MAX_WIDTH_EURGB60: u32 = 720; pub const VI_MAX_HEIGHT_EURGB60: u32 = 480; +pub const VI_MAX_WIDTH_DEBUG: u32 = 720; +pub const VI_MAX_HEIGHT_DEBUG: u32 = 480; pub const HW_IPC_PPCBASE: u32 = 3439329280; pub const HW_IPC_PPC_SEND: u32 = 1; pub const HW_IPC_PPC_MSG_ACK: u32 = 2; @@ -1326,8 +1339,11 @@ pub const ES_SIG_ECDSA: u32 = 65538; pub const ES_CERT_RSA4096: u32 = 0; pub const ES_CERT_RSA2048: u32 = 1; pub const ES_CERT_ECDSA: u32 = 2; +pub const ES_KEY_NANDFS: u32 = 2; pub const ES_KEY_COMMON: u32 = 4; +pub const ES_KEY_BACKUP: u32 = 5; pub const ES_KEY_SDCARD: u32 = 6; +pub const ES_KEY_KOREAN: u32 = 11; pub const MAX_NUM_TMD_CONTENTS: u32 = 512; pub const STM_EVENT_RESET: u32 = 131072; pub const STM_EVENT_POWER: u32 = 2048; @@ -2654,6 +2670,110 @@ extern "C" { #[doc = "CON_EnableGecko(int channel, int safe)\n Enable or disable the USB gecko console.\n\n # Arguments\n\n* `channel` (direction in) - EXI channel, or -1 �to disable the gecko console\n * `safe` (direction in) - If true, use safe mode (wait for peer)\n\n # Returns\n\nnone"] pub fn CON_EnableGecko(channel: ::libc::c_int, safe: ::libc::c_int); } +#[doc = "A callback for printing a character."] +pub type ConsolePrint = ::core::option::Option< + unsafe extern "C" fn(con: *mut ::libc::c_void, c: ::libc::c_int) -> bool, +>; +#[doc = "A font struct for the console."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ConsoleFont { + #[doc = "< A pointer to the font graphics"] + pub gfx: *mut u8_, + #[doc = "< Offset to the first valid character in the font table"] + pub asciiOffset: u16_, + #[doc = "< Number of characters in the font graphics"] + pub numChars: u16_, +} +#[doc = "Console structure used to store the state of a console render context.\n\n Default values from consoleGetDefault();\n PrintConsole defaultConsole =\n {\n \t//Font:\n \t{\n \t\t(u8*)default_font_bin, //font gfx\n \t\t0, //first ascii character in the set\n \t\t128, //number of characters in the font set\n\t},\n\t0,0, //cursorX cursorY\n\t0,0, //prevcursorX prevcursorY\n\t40, //console width\n\t30, //console height\n\t0, //window x\n\t0, //window y\n\t32, //window width\n\t24, //window height\n\t3, //tab size\n\t0, //font character offset\n\t0, //print callback\n\tfalse //console initialized\n };\n "] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PrintConsole { + #[doc = "< Font of the console"] + pub font: ConsoleFont, + #[doc = "< Framebuffer address"] + pub destbuffer: *mut ::libc::c_void, + pub con_xres: ::libc::c_int, + pub con_yres: ::libc::c_int, + pub con_stride: ::libc::c_int, + pub target_x: ::libc::c_int, + pub target_y: ::libc::c_int, + pub tgt_stride: ::libc::c_int, + #[doc = "< Current X location of the cursor (as a tile offset by default)"] + pub cursorX: ::libc::c_int, + #[doc = "< Current Y location of the cursor (as a tile offset by default)"] + pub cursorY: ::libc::c_int, + #[doc = "< Internal state"] + pub prevCursorX: ::libc::c_int, + #[doc = "< Internal state"] + pub prevCursorY: ::libc::c_int, + #[doc = "< Width of the console hardware layer in characters"] + pub con_cols: ::libc::c_int, + #[doc = "< Height of the console hardware layer in characters"] + pub con_rows: ::libc::c_int, + #[doc = "< Window X location in characters (not implemented)"] + pub windowX: ::libc::c_int, + #[doc = "< Window Y location in characters (not implemented)"] + pub windowY: ::libc::c_int, + #[doc = "< Window width in characters (not implemented)"] + pub windowWidth: ::libc::c_int, + #[doc = "< Window height in characters (not implemented)"] + pub windowHeight: ::libc::c_int, + #[doc = "< Size of a tab"] + pub tabSize: ::libc::c_int, + #[doc = "< Foreground color"] + pub fg: ::libc::c_uint, + #[doc = "< Background color"] + pub bg: ::libc::c_uint, + #[doc = "< Attribute flags"] + pub flags: ::libc::c_uint, + #[doc = "< Callback for printing a character. Should return true if it has handled rendering the graphics (else the print engine will attempt to render via tiles)."] + pub PrintChar: ConsolePrint, + #[doc = "< True if the console is initialized"] + pub consoleInitialised: bool, +} +#[doc = "< Swallows prints to stderr"] +pub const debugDevice_NULL: debugDevice = 0; +#[doc = "< Outputs stderr debug statements using exi uart"] +pub const debugDevice_EXI: debugDevice = 1; +#[doc = "< Directs stderr debug statements to console window"] +pub const debugDevice_CONSOLE: debugDevice = 2; +#[doc = "Console debug devices supported by libogc."] +pub type debugDevice = ::libc::c_uint; +extern "C" { + #[doc = "Loads the font into the console.\n # Arguments\n\n* `console` - Pointer to the console to update, if NULL it will update the current console.\n * `font` - The font to load."] + pub fn consoleSetFont(console: *mut PrintConsole, font: *mut ConsoleFont); +} +extern "C" { + #[doc = "Sets the print window.\n # Arguments\n\n* `console` - Console to set, if NULL it will set the current console window.\n * `x` - X location of the window.\n * `y` - Y location of the window.\n * `width` - Width of the window.\n * `height` - Height of the window."] + pub fn consoleSetWindow( + console: *mut PrintConsole, + x: ::libc::c_uint, + y: ::libc::c_uint, + width: ::libc::c_uint, + height: ::libc::c_uint, + ); +} +extern "C" { + #[doc = "Gets a pointer to the console with the default values.\n This should only be used when using a single console or without changing the console that is returned, otherwise use consoleInit().\n # Returns\n\nA pointer to the console with the default values."] + pub fn consoleGetDefault() -> *mut PrintConsole; +} +extern "C" { + #[doc = "Make the specified console the render target.\n # Arguments\n\n* `console` - A pointer to the console struct (must have been initialized with consoleInit(PrintConsole* console)).\n # Returns\n\nA pointer to the previous console."] + pub fn consoleSelect(console: *mut PrintConsole) -> *mut PrintConsole; +} +extern "C" { + #[doc = "Initialise the console.\n # Arguments\n\n* `console` - A pointer to the console data to initialize (if it's NULL, the default console will be used).\n # Returns\n\nA pointer to the current console."] + pub fn consoleInit(console: *mut PrintConsole) -> *mut PrintConsole; +} +extern "C" { + #[doc = "Initializes debug console output on stderr to the specified device.\n # Arguments\n\n* `device` - The debug device (or devices) to output debug print statements to."] + pub fn consoleDebugInit(device: debugDevice); +} +extern "C" { + #[doc = "Clears the screen by using iprintf(\""] + pub fn consoleClear(); +} pub type sec_t = u32; pub type FN_MEDIUM_STARTUP = ::core::option::Option bool>; pub type FN_MEDIUM_ISINSERTED = ::core::option::Option bool>; @@ -4678,6 +4798,8 @@ extern "C" { extern "C" { pub fn TPL_CloseTPLFile(tdf: *mut TPLFile); } +pub type __gnuc_va_list = __builtin_va_list; +pub type va_list = __gnuc_va_list; pub type wchar_t = ::libc::c_int; #[repr(C)] #[repr(align(16))] @@ -4773,13 +4895,6 @@ pub type _flock_t = _LOCK_RECURSIVE_T; pub struct __locale_t { _unused: [u8; 0], } -extern "C" { - pub fn memset( - arg1: *mut ::libc::c_void, - arg2: ::libc::c_int, - arg3: ::libc::c_ulong, - ) -> *mut ::libc::c_void; -} #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct _Bigint { @@ -4922,7 +5037,7 @@ pub struct _reent { #[derive(Debug, Copy, Clone)] pub struct _reent__bindgen_ty_1 { pub _reent: __BindgenUnionField<_reent__bindgen_ty_1__bindgen_ty_1>, - pub bindgen_union_field: [u64; 25usize], + pub bindgen_union_field: [u64; 29usize], } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -4945,6 +5060,7 @@ pub struct _reent__bindgen_ty_1__bindgen_ty_1 { pub _wcrtomb_state: _mbstate_t, pub _wcsrtombs_state: _mbstate_t, pub _h_errno: ::libc::c_int, + pub _getlocalename_l_buf: [::libc::c_char; 32usize], } extern "C" { pub static mut _impure_ptr: *mut _reent; @@ -5616,7 +5732,15 @@ extern "C" { extern "C" { pub fn SYS_Time() -> u64_; } +#[doc = "Printing callback used by dietPrint.\n> **Note:** The incoming `buf` may be NULL, in which case the callback is\nexpected to output the number of space characters given by `size.`"] +pub type DietPrintFn = + ::core::option::Option; extern "C" { + #[doc = "! Prints the specified formatted text (works like vprintf)."] + pub fn dietPrintV(fmt: *const ::libc::c_char, va: *mut [u32; 3usize]); +} +extern "C" { + #[doc = "! Prints the specified formatted text (works like printf)."] pub fn kprintf(str_: *const ::libc::c_char, ...); } extern "C" { @@ -5761,6 +5885,16 @@ extern "C" { extern "C" { pub static mut TVEurgb60Hz480ProgAa: GXRModeObj; } +extern "C" { + #[doc = "< Video and render mode configuration for 480 lines,progressive,singlefield RGB mode"] + pub static mut TVRgb480Prog: GXRModeObj; +} +extern "C" { + pub static mut TVRgb480ProgSoft: GXRModeObj; +} +extern "C" { + pub static mut TVRgb480ProgAa: GXRModeObj; +} #[doc = "void (*VIRetraceCallback)(u32 retraceCnt)\n function pointer typedef for the user's retrace callback\n # Arguments\n\n* `retraceCnt` (direction in) - current retrace count"] pub type VIRetraceCallback = ::core::option::Option; pub type VIPositionCallback = ::core::option::Option; @@ -6393,13 +6527,37 @@ extern "C" { pub fn ES_GetTitleContentsCount(titleID: u64_, num: *mut u32_) -> s32; } extern "C" { - pub fn ES_GetTitleContents(titleID: u64_, data: *mut u8_, size: u32_) -> s32; + pub fn ES_GetTitleContents(titleID: u64_, contents: *mut u32_, num: u32_) -> s32; } extern "C" { pub fn ES_GetTMDViewSize(titleID: u64_, size: *mut u32_) -> s32; } extern "C" { - pub fn ES_GetTMDView(titleID: u64_, data: *mut u8_, size: u32_) -> s32; + pub fn ES_GetTMDView(titleID: u64_, view: *mut tmd_view, size: u32_) -> s32; +} +extern "C" { + pub fn ES_GetConsumptionCount(ticketID: u64_, n_limits: *mut u32_) -> s32; +} +extern "C" { + pub fn ES_GetConsumption(ticketID: u64_, limits: *mut tiklimit, n_limits: u32_) -> s32; +} +extern "C" { + pub fn ES_DiGetTMDViewSize( + s_tmd: *const signed_blob, + tmd_size: u32_, + view_size: *mut u32_, + ) -> s32; +} +extern "C" { + pub fn ES_DiGetTMDView( + s_tmd: *const signed_blob, + tmd_size: u32_, + view: *mut tmd_view, + view_size: u32_, + ) -> s32; +} +extern "C" { + pub fn ES_DiGetTicketView(s_tik: *const signed_blob, view: *mut tikview) -> s32; } extern "C" { pub fn ES_GetNumSharedContents(cnt: *mut u32_) -> s32; @@ -6424,6 +6582,16 @@ extern "C" { keyid: *mut u32_, ) -> s32; } +extern "C" { + pub fn ES_DiVerifyWithTicketView( + certificates: *const signed_blob, + certificates_size: u32_, + s_tmd: *const signed_blob, + tmd_size: u32_, + ticket_view: *const tikview, + keynum: *mut u32_, + ) -> s32; +} extern "C" { pub fn ES_AddTicket( tik: *const signed_blob, @@ -6438,7 +6606,22 @@ extern "C" { pub fn ES_DeleteTicket(view: *const tikview) -> s32; } extern "C" { - pub fn ES_AddTitleTMD(tmd: *const signed_blob, tmd_size: u32_) -> s32; + pub fn ES_ExportTitleInit(titleID: u64_, tmd_out: *mut signed_blob, tmd_size: u32_) -> s32; +} +extern "C" { + pub fn ES_ExportContentBegin(titleID: u64_, cid: u32_) -> s32; +} +extern "C" { + pub fn ES_ExportContentData(cfd: s32, data: *mut ::libc::c_void, data_size: u32_) -> s32; +} +extern "C" { + pub fn ES_ExportContentEnd(cfd: s32) -> s32; +} +extern "C" { + pub fn ES_ExportTitleDone() -> s32; +} +extern "C" { + pub fn ES_ReimportTitleInit(tmd: *const signed_blob, tmd_size: u32_) -> s32; } extern "C" { pub fn ES_AddTitleStart( @@ -6454,7 +6637,7 @@ extern "C" { pub fn ES_AddContentStart(titleID: u64_, cid: u32_) -> s32; } extern "C" { - pub fn ES_AddContentData(cid: s32, data: *mut u8_, data_size: u32_) -> s32; + pub fn ES_AddContentData(cid: s32, data: *const ::libc::c_void, data_size: u32_) -> s32; } extern "C" { pub fn ES_AddContentFinish(cid: u32_) -> s32; @@ -6483,10 +6666,10 @@ extern "C" { pub fn ES_OpenContent(index: u16_) -> s32; } extern "C" { - pub fn ES_OpenTitleContent(titleID: u64_, views: *mut tikview, index: u16_) -> s32; + pub fn ES_OpenTitleContent(titleID: u64_, views: *const tikview, index: u16_) -> s32; } extern "C" { - pub fn ES_ReadContent(cfd: s32, data: *mut u8_, data_size: u32_) -> s32; + pub fn ES_ReadContent(cfd: s32, data: *mut ::libc::c_void, data_size: u32_) -> s32; } extern "C" { pub fn ES_SeekContent(cfd: s32, where_: s32, whence: s32) -> s32; @@ -6503,23 +6686,37 @@ extern "C" { extern "C" { pub fn ES_Encrypt( keynum: u32_, - iv: *mut u8_, - source: *mut u8_, + iv: *mut u32_, + source: *const ::libc::c_void, size: u32_, - dest: *mut u8_, + dest: *mut ::libc::c_void, ) -> s32; } extern "C" { pub fn ES_Decrypt( keynum: u32_, - iv: *mut u8_, - source: *mut u8_, + iv: *mut u32_, + source: *const ::libc::c_void, size: u32_, - dest: *mut u8_, + dest: *mut ::libc::c_void, ) -> s32; } extern "C" { - pub fn ES_Sign(source: *mut u8_, size: u32_, sig: *mut u8_, certs: *mut u8_) -> s32; + pub fn ES_Sign( + source: *const ::libc::c_void, + size: u32_, + ap_signature: *mut u8_, + ap_certificate: *mut signed_blob, + ) -> s32; +} +extern "C" { + pub fn ES_VerifySign( + source: *const ::libc::c_void, + size: u32_, + ap_signature: *const u8_, + certificates: *const signed_blob, + certificates_size: u32_, + ) -> s32; } extern "C" { pub fn ES_GetDeviceCert(outbuf: *mut u8_) -> s32; @@ -6531,7 +6728,10 @@ extern "C" { pub fn ES_GetBoot2Version(version: *mut u32_) -> s32; } extern "C" { - pub fn ES_NextCert(certs: *const signed_blob) -> *mut signed_blob; + pub fn ES_CheckHasKoreanKey() -> s32; +} +extern "C" { + pub fn ES_NextCert(certs: *const signed_blob) -> *const signed_blob; } pub type stmcallback = ::core::option::Option; extern "C" { @@ -7431,6 +7631,9 @@ extern "C" { extern "C" { pub static mut __io_usbstorage: DISC_INTERFACE; } +extern "C" { + pub static mut __io_usbstorage_sector_size: u32_; +} extern "C" { pub fn WII_Initialize() -> s32; } From 7e9f1bc22fb2f6ce0567f1a1fc34a90b623654bc Mon Sep 17 00:00:00 2001 From: ProfElements Date: Sun, 28 Sep 2025 02:02:52 -0500 Subject: [PATCH 2/9] test (#3) Reviewed-on: https://git.profelements.xyz/profelements/ogc-rs/pulls/3 Co-authored-by: ProfElements Co-committed-by: ProfElements --- .forgejo/workflows/workflow.yml | 44 +++++++++++++++++++++++++ .github/workflows/rust.yml | 57 --------------------------------- powerpc-unknown-eabi.json | 2 +- 3 files changed, 45 insertions(+), 58 deletions(-) create mode 100644 .forgejo/workflows/workflow.yml delete mode 100644 .github/workflows/rust.yml diff --git a/.forgejo/workflows/workflow.yml b/.forgejo/workflows/workflow.yml new file mode 100644 index 0000000..e8c70a0 --- /dev/null +++ b/.forgejo/workflows/workflow.yml @@ -0,0 +1,44 @@ +name: CI +on: push +jobs: + check: + runs-on: docker + container: + image: "devkitpro/devkitppc" + steps: + - name: Install required packages + run: | + apt-get update + apt-get install -y gcc libc6-dev nodejs clang + - name: Checkout source + uses: actions/checkout@v5 + - name: Install nightly toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y + . $HOME/.cargo/env + rustup toolchain install nightly --profile minimal --component rust-src + - name: Cargo check + run: | + . $HOME/.cargo/env + cargo check + clippy: + runs-on: docker + container: + image: "devkitpro/devkitppc" + steps: + - name: Install required packages + run: | + apt-get update + apt-get install -y gcc libc6-dev nodejs clang + - name: Checkout source + uses: actions/checkout@v5 + - name: Install nightly toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y + . $HOME/.cargo/env + rustup toolchain install nightly --profile minimal --component rust-src + rustup component add clippy + - name: Cargo clippy + run: | + . $HOME/.cargo/env + cargo clippy diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 1efa0f0..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Rust CI - -on: [push, pull_request] - -name: Rust CI - -jobs: - check: - name: Check - runs-on: ubuntu-latest - container: - image: "devkitpro/devkitppc" - steps: - - name: Install required packages - run: | - sudo apt-get update - sudo apt-get install -y gcc libc6-dev nodejs clang - - - name: Checkout sources - uses: actions/checkout@v2 - - - name: Install nightly toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - components: rust-src - - - name: Run cargo check - uses: actions-rs/cargo@v1 - with: - command: check - - lints: - name: Lints - runs-on: ubuntu-latest - container: - image: "devkitpro/devkitppc" - steps: - - name: Install required packages - run: | - sudo apt-get update - sudo apt-get install -y gcc libc6-dev nodejs clang - - - name: Checkout sources - uses: actions/checkout@v2 - - - name: Install nightly toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - components: clippy, rust-src - - - name: Run cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy diff --git a/powerpc-unknown-eabi.json b/powerpc-unknown-eabi.json index e28c9fd..cc2c32a 100644 --- a/powerpc-unknown-eabi.json +++ b/powerpc-unknown-eabi.json @@ -28,6 +28,6 @@ "target-family": "unix", "target-mcount": "_mcount", "target-c-int-width": 32, - "target-pointer-width": "32", + "target-pointer-width": 32, "vendor": "nintendo" } From 1b7dbfa3a88be38d99f5c1b18dcc9328589bf016 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Fri, 22 Aug 2025 13:55:51 -0500 Subject: [PATCH 3/9] :sparkles: feat(ios/sdio): Add `/dev/sdio/slot0` device helper functions. --- src/ios.rs | 6 ++- src/ios/sdio.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/ios/sdio.rs diff --git a/src/ios.rs b/src/ios.rs index 4921688..7e2f08f 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -17,9 +17,13 @@ pub mod fs; /// E-Ticket System IOS Device /// -/// `/dev/es` device hellper functions. +/// `/dev/es` device helper functions. pub mod es; +/// SDIO IOS Device +/// `/dev/sdio/slot0` device helper functions +pub mod sdio; + #[repr(u32)] /// Interprocess Control / IOS File Mode pub enum Mode { diff --git a/src/ios/sdio.rs b/src/ios/sdio.rs new file mode 100644 index 0000000..dc3a34c --- /dev/null +++ b/src/ios/sdio.rs @@ -0,0 +1,100 @@ +use crate::ios; + +/// SDIO supported Ioctls +pub enum Ioctl { + /// Write to a SD host controller register + WriteHostControllerRegister, + /// Read from a SD host controller register + ReadHostControllerRegister, + /// Reset SD card + ResetSDCard, + /// Set SD card clock + SetClock, + /// Send SDIO command + SendCommand, + /// Get SD card status + GetStatus, + /// Get operating conditions register + GetOperatingConditionsRegister, +} + +impl From for i32 { + fn from(value: Ioctl) -> Self { + match value { + Ioctl::WriteHostControllerRegister => 1, + Ioctl::ReadHostControllerRegister => 2, + Ioctl::ResetSDCard => 4, + Ioctl::SetClock => 6, + Ioctl::SendCommand => 7, + Ioctl::GetStatus => 11, + Ioctl::GetOperatingConditionsRegister => 12, + } + } +} +/// Try from Ioctl Error +/// This happens when you don't provide a proper i32 to map to an [`Ioctl`] +pub struct TryFromIoctlError; +impl TryFrom for Ioctl { + type Error = TryFromIoctlError; + fn try_from(value: i32) -> Result { + match value { + 1 => Ok(Self::WriteHostControllerRegister), + 2 => Ok(Self::ReadHostControllerRegister), + 4 => Ok(Self::ResetSDCard), + 6 => Ok(Self::SetClock), + 7 => Ok(Self::SendCommand), + 11 => Ok(Self::GetStatus), + 12 => Ok(Self::GetOperatingConditionsRegister), + _ => Err(TryFromIoctlError), + } + } +} + +/// Write to a SD host controller register +pub fn write_to_host_controller_register( + register: u8, + size: u8, + data: u32, +) -> Result<(), ios::Error> { + let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::None)?; + + let mut buffer = [0u8; 24]; + buffer[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); + buffer[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + buffer[16..20].copy_from_slice(&data.to_be_bytes()); + + ios::ioctl(sdio, Ioctl::WriteHostControllerRegister, &buffer, &mut [])?; + + let _ = ios::close(sdio); + Ok(()) +} + +/// Read from a SD host controller register +pub fn read_from_host_controller_register(register: u8, size: u8) -> Result { + let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::None)?; + + let mut value = [0u8; 4]; + let mut query = [0u8; 24]; + query[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); + query[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + + ios::ioctl(sdio, Ioctl::ReadHostControllerRegister, &query, &mut value)?; + + let _ = ios::close(sdio); + Ok(u32::from_be_bytes(value)) +} +/// Read SD card status returning the relative card address +pub fn get_sdcard_status() -> Result { + let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::None)?; + + let mut buffer = [0u8; 4]; + ios::ioctl(sdio, Ioctl::GetStatus, &[], &mut buffer)?; + + let _ = ios::close(sdio); + Ok(u16::from_be_bytes( + buffer[0..2].try_into().map_err(|_| ios::Error::Invalid)?, + )) +} + +// /// Set SD card clock +//pub fn set_clock From 20b8a074e15f954cfb1329158ae152c5ec1a2b95 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Sun, 28 Sep 2025 00:47:55 -0500 Subject: [PATCH 4/9] stuff --- examples/ios/src/main.rs | 313 ++++++++++++++++++++++++++- src/ios/sdio.rs | 441 +++++++++++++++++++++++++++++++++++---- 2 files changed, 716 insertions(+), 38 deletions(-) diff --git a/examples/ios/src/main.rs b/examples/ios/src/main.rs index 394579b..377d13e 100644 --- a/examples/ios/src/main.rs +++ b/examples/ios/src/main.rs @@ -3,7 +3,11 @@ use alloc::vec; use ogc_rs::{ - ios::{self, Mode, SeekMode}, + ios::{ + self, + sdio::{Device, Request}, + Mode, SeekMode, + }, print, println, }; extern crate alloc; @@ -26,7 +30,314 @@ extern "C" fn main() { let _ = ios::close(fd); } } + + let (is_sdhc, mut device) = try_init_sd(); + + let mut block = [0u8; 512]; + + //read_sectors(blocks: &mut [[0u8; 512]], offset: usize]) -> Result<(), ios::Error>; + device + .read_sectors(core::slice::from_mut(&mut block), 0) + .unwrap(); + + let bpb = BPB::from_bytes(block[0x00B..].try_into().unwrap()); + + let mut info_block = [0u8; 512]; + device + .read_sectors( + core::slice::from_mut(&mut info_block), + bpb.fs_info_sector as usize, + ) + .unwrap(); + + let info = FSInfo::from_bytes(&info_block); + + println!("{:?}", bpb); + println!("{:?}", info); + loop { core::hint::spin_loop(); } } + +pub struct SDCard { + rca: u32, + is_sdhc: bool, + device: Device, +} + +impl SDCard { + pub fn init() -> Option { + let (is_sdhc, mut device) = try_init_sd(); + + let resp_rca = device.send_command(&Request::SEND_RCA).ok()?; + let rca = resp_rca.rsp_field0; + + Some(Self { + rca, + is_sdhc, + device, + }) + } +} + +impl DeviceExt for Device { + fn read_sectors( + &mut self, + sectors: &mut [[u8; 512]], + offset: usize, + ) -> Result<(), ogc_rs::ios::Error> { + let resp_rca = self.send_command(&Request::SEND_RCA)?; + let rca = resp_rca.rsp_field0; + + self.send_command(&Request::select(rca))?; + + const SDIO_CMD_READMULTIBLOCK: u32 = 0x12; + const SDIO_CMD_TYPE_AC: u32 = 3; + const SDIO_RESPONSE_TYPE_R1: u32 = 1; + + // SDIO requires 32 byte alignment + // On hardware this probably needs to be in the IPC memory space :shrug: + let mut aligned_buffer = ogc_rs::utils::alloc_aligned_buffer(sectors.as_flattened_mut()); + + //let req = Request::read_multiblock(offset, sectors, &mut aligned_buffer); + + self.send_command(&Request::new( + SDIO_CMD_READMULTIBLOCK, + SDIO_CMD_TYPE_AC, + SDIO_RESPONSE_TYPE_R1, + offset as u32, + sectors.len() as u32, + 512, + aligned_buffer.as_mut_ptr(), + ))?; + + self.send_command(&Request::DE_SELECT)?; + + sectors + .as_flattened_mut() + .copy_from_slice(&mut aligned_buffer); + + Ok(()) + } +} + +trait DeviceExt { + fn read_sectors( + &mut self, + sectors: &mut [[u8; 512]], + offset: usize, + ) -> Result<(), ogc_rs::ios::Error>; +} + +pub fn try_init_sd() -> (bool, Device) { + const SD_STATUS_INSERTED: u32 = 0b1; + const SD_STATUS_INITALIZED: u32 = 0b1_0000_0000_0000_0000; + const SD_STATUS_SDHC: u32 = 0x100000; + + const HOST_CTRL_4BIT: u32 = 2; + + const HOST_CONTROLLER_REG_SOFT_RESET: u8 = 47; + const HOST_CONTROLLER_REG_PWR_CTRL: u8 = 41; + const HOST_CONTROLLER_REG_HOST_CTRL: u8 = 40; + const HOST_CONTROLLER_REG_CLK_CTRL: u8 = 44; + const HOST_CONTROLLER_REG_TIMEOUT_CTRL: u8 = 46; + // + // const SDIO_CMD_APPCMD: u32 = 55; + // const SDIO_CMD_SELECT: u32 = 7; + // const SDIO_CMD_SEND_CID: u32 = 2; + // const SDIO_CMD_SEND_RCA: u32 = 3; + // const SDIO_APPCMD_SENDOPCOND: u32 = 41; + // const SDIO_APPCMD_SET_BUS_WIDTH: u32 = 6; + // const SDIO_CMD_SET_BLOCK_LENGTH: u32 = 16; + // + // const SDIO_CMD_TYPE_AC: u32 = 3; + // + // const SDIO_RESPONSE_TYPE_R1: u32 = 1; + // const SDIO_RESPONSE_TYPE_R2: u32 = 3; + // const SDIO_RESPONSE_TYPE_R5: u32 = 6; + // const SDIO_RESPONSE_TYPE_R1B: u32 = 2; + // const SDIO_RESPONSE_TYPE_R3: u32 = 4; + // + let mut device = Device::open().unwrap(); + + // Reset + let mut rca = device.reset().unwrap(); + let status = device.get_status().unwrap(); + let is_sdhc = status & SD_STATUS_SDHC == SD_STATUS_SDHC; + + if status & SD_STATUS_INSERTED != SD_STATUS_INSERTED { + println!("SD Card not found"); + panic!(); + } + + if status & SD_STATUS_INITALIZED != SD_STATUS_INITALIZED { + // Drop and reopen the device + drop(device); + let mut device = Device::open().unwrap(); + + // Reset host controller using device + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_SOFT_RESET, 1, 7) + .unwrap(); + + let software_reset = 7; + // Wait until properly reset + while software_reset + == device + .read_from_host_controller_register(HOST_CONTROLLER_REG_SOFT_RESET, 1) + .unwrap() + { + core::hint::spin_loop(); + } + + let _ = device.write_to_host_controller_register(0x34, 4, 0x13f00c3); + let _ = device.write_to_host_controller_register(0x38, 4, 0x13f00c3); + + // Set power + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_PWR_CTRL, 1, 14) + .unwrap(); + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_PWR_CTRL, 1, 15) + .unwrap(); + + // Clock + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0) + .unwrap(); + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0x101) + .unwrap(); + + let clk_ctrl = 0x101; + + while clk_ctrl + == device + .read_from_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2) + .unwrap() + { + core::hint::spin_loop(); + } + + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0x107) + .unwrap(); + + // Timeout + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_TIMEOUT_CTRL, 1, 14) + .unwrap(); + let _ = device.send_command(&Request::GO_IDLE).unwrap(); + let resp = device.send_command(&Request::SEND_IF_COND).unwrap(); + + if resp.rsp_field0 & 0xFF != 0xAA { + println!("Response from IF_COND: {}", resp.rsp_field0); + } + + let is_sdhc = loop { + let _ = device.send_command(&Request::APP_CMD).unwrap(); + let resp = device.send_command(&Request::SEND_OP_COND).unwrap(); + if resp.rsp_field0 & 1 << 31 == 1 << 31 { + break resp.rsp_field0 & 1 << 30 == 1 << 30; + } + }; + + let resp_cid = device.send_command(&Request::send_cid(rca)).unwrap(); + println!("{:?}", resp_cid); + + let resp_rca = device.send_command(&Request::SEND_RCA).unwrap(); + println!("{:?}", resp_rca); + rca = resp_rca.rsp_field0; + + let mut host_ctrl = device + .read_from_host_controller_register(HOST_CONTROLLER_REG_HOST_CTRL, 1) + .unwrap(); + host_ctrl &= 0xff; + host_ctrl &= !HOST_CTRL_4BIT; + host_ctrl |= HOST_CTRL_4BIT; + device + .write_to_host_controller_register(HOST_CONTROLLER_REG_HOST_CTRL, 1, host_ctrl) + .unwrap(); + + device.enable_clock(true).unwrap(); + + device.send_command(&Request::select(rca)).unwrap(); + { + device + .send_command(&Request::set_block_length(512)) + .unwrap(); + device.send_command(&Request::appcmd_with_rca(rca)).unwrap(); + device.send_command(&Request::set_bus_width(4)).unwrap(); + } + device.send_command(&Request::DE_SELECT).unwrap(); + + println!("END OF INIT"); + + return (is_sdhc, device); + } + + return (is_sdhc, device); +} + +#[derive(Debug)] +pub struct BPB { + bytes_per_sector: u16, + sectors_per_cluster: u8, + reserved_sector_count: u16, + fat_count: u8, + fat16_max_root_dir_entry_count: u16, + sector_count: u16, + media_type: u8, + sectors_per_fat_count: u16, + sectors_per_track: u16, + head_count: u16, + hidden_sector_count: u32, + sector_count_fat32: u32, + sectors_per_fat: u32, + drive_flags: u16, + version: u16, + cluster_root_dir_start: u32, + fs_info_sector: u16, + backup_sector: u16, +} + +impl BPB { + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { + bytes_per_sector: u16::from_le_bytes(bytes[0..2].try_into().unwrap()), + sectors_per_cluster: bytes[2], + reserved_sector_count: u16::from_le_bytes(bytes[3..5].try_into().unwrap()), + fat_count: bytes[5], + fat16_max_root_dir_entry_count: u16::from_le_bytes(bytes[6..8].try_into().unwrap()), + sector_count: u16::from_le_bytes(bytes[8..10].try_into().unwrap()), + media_type: bytes[10], + sectors_per_fat_count: u16::from_le_bytes(bytes[11..13].try_into().unwrap()), + sectors_per_track: u16::from_le_bytes(bytes[13..15].try_into().unwrap()), + head_count: u16::from_le_bytes(bytes[15..17].try_into().unwrap()), + hidden_sector_count: u32::from_le_bytes(bytes[17..21].try_into().unwrap()), + sector_count_fat32: u32::from_le_bytes(bytes[21..25].try_into().unwrap()), + sectors_per_fat: u32::from_le_bytes(bytes[25..29].try_into().unwrap()), + drive_flags: u16::from_le_bytes(bytes[29..31].try_into().unwrap()), + version: u16::from_le_bytes(bytes[31..33].try_into().unwrap()), + cluster_root_dir_start: u32::from_le_bytes(bytes[33..37].try_into().unwrap()), + fs_info_sector: u16::from_le_bytes(bytes[37..39].try_into().unwrap()), + backup_sector: u16::from_le_bytes(bytes[39..41].try_into().unwrap()), + } + } +} + +#[derive(Debug)] +pub struct FSInfo { + free_cluster_count: u32, + recent_cluster: u32, +} + +impl FSInfo { + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { + free_cluster_count: u32::from_le_bytes(bytes[488..492].try_into().unwrap()), + recent_cluster: u32::from_le_bytes(bytes[492..496].try_into().unwrap()), + } + } +} diff --git a/src/ios/sdio.rs b/src/ios/sdio.rs index dc3a34c..07f1bdc 100644 --- a/src/ios/sdio.rs +++ b/src/ios/sdio.rs @@ -49,52 +49,419 @@ impl TryFrom for Ioctl { } } } +//#[repr(C, align(32))] +/// SDIO request +pub struct Request { + command: u32, + command_type: u32, + response_type: u32, + arg: u32, + block_count: u32, + block_size: u32, + dma_addr: *mut u8, + is_dma: u32, + pad0: u32, +} -/// Write to a SD host controller register -pub fn write_to_host_controller_register( - register: u8, - size: u8, - data: u32, -) -> Result<(), ios::Error> { - let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::None)?; +impl Request { + const SDIO_CMD_GO_IDLE: u32 = 0; + const SDIO_RESPONSE_TYPE_R6: u32 = 7; + const SDIO_CMD_SEND_IF_COND: u32 = 8; - let mut buffer = [0u8; 24]; - buffer[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); - buffer[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); - buffer[16..20].copy_from_slice(&data.to_be_bytes()); + const SDIO_CMD_APPCMD: u32 = 55; + const SDIO_CMD_SELECT: u32 = 7; + const SDIO_CMD_SEND_CID: u32 = 2; + const SDIO_CMD_SEND_RCA: u32 = 3; + const SDIO_APPCMD_SENDOPCOND: u32 = 41; + const SDIO_APPCMD_SET_BUS_WIDTH: u32 = 6; + const SDIO_CMD_SET_BLOCK_LENGTH: u32 = 16; - ios::ioctl(sdio, Ioctl::WriteHostControllerRegister, &buffer, &mut [])?; + const SDIO_CMD_TYPE_AC: u32 = 3; - let _ = ios::close(sdio); - Ok(()) -} + const SDIO_RESPONSE_TYPE_R1: u32 = 1; + const SDIO_RESPONSE_TYPE_R2: u32 = 3; + const SDIO_RESPONSE_TYPE_R5: u32 = 6; + const SDIO_RESPONSE_TYPE_R1B: u32 = 2; + const SDIO_RESPONSE_TYPE_R3: u32 = 4; -/// Read from a SD host controller register -pub fn read_from_host_controller_register(register: u8, size: u8) -> Result { - let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::None)?; + /// SDIO_CMD_GO_IDLE + pub const GO_IDLE: Request = + Request::new(Self::SDIO_CMD_GO_IDLE, 0, 0, 0, 0, 0, core::ptr::null_mut()); - let mut value = [0u8; 4]; - let mut query = [0u8; 24]; - query[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); - query[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + /// SDIO_CMD_SEND_IF_COND + pub const SEND_IF_COND: Request = Request::new( + Self::SDIO_CMD_SEND_IF_COND, + 0, + Self::SDIO_RESPONSE_TYPE_R6, + 0x1AA, + 0, + 0, + core::ptr::null_mut(), + ); - ios::ioctl(sdio, Ioctl::ReadHostControllerRegister, &query, &mut value)?; + /// SDIO_CMD_APPCMD + pub const APP_CMD: Request = Request::new( + Self::SDIO_CMD_APPCMD, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + 0, + 0, + 0, + core::ptr::null_mut(), + ); - let _ = ios::close(sdio); - Ok(u32::from_be_bytes(value)) -} -/// Read SD card status returning the relative card address -pub fn get_sdcard_status() -> Result { - let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::None)?; + /// SDIO_CMD_APPCMD_SEND_OP_COND + pub const SEND_OP_COND: Request = Request::new( + Self::SDIO_APPCMD_SENDOPCOND, + 0, + Self::SDIO_RESPONSE_TYPE_R3, + 0x40300000, + 0, + 0, + core::ptr::null_mut(), + ); + + /// SDIO_CMD_DESELECT + pub const DE_SELECT: Request = Request::new( + Self::SDIO_CMD_SELECT, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1B, + 0, + 0, + 0, + core::ptr::null_mut(), + ); + + /// SDIO_CMD_SENDRCA + pub const SEND_RCA: Request = Request::new( + Self::SDIO_CMD_SEND_RCA, + 0, + Self::SDIO_RESPONSE_TYPE_R5, + 0, + 0, + 0, + core::ptr::null_mut(), + ); + + /// SDIO_CMD_APPCMD_SET_BUS_WIDTH + pub const fn set_bus_width(width: u32) -> Request { + Request::new( + Self::SDIO_APPCMD_SET_BUS_WIDTH, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + width, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// SDIO_CMD_SEND_CID + pub const fn send_cid(rca: u32) -> Request { + Request::new( + Self::SDIO_CMD_SEND_CID, + 0, + Self::SDIO_RESPONSE_TYPE_R2, + rca, + 0, + 0, + core::ptr::null_mut(), + ) + } - let mut buffer = [0u8; 4]; - ios::ioctl(sdio, Ioctl::GetStatus, &[], &mut buffer)?; + /// SDIO_CMD_SET_BLOCK_LENGTH + pub const fn set_block_length(length: u32) -> Request { + Request::new( + Self::SDIO_CMD_SET_BLOCK_LENGTH, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + length, + 0, + 0, + core::ptr::null_mut(), + ) + } + pub const fn select(rca: u32) -> Request { + Request::new( + Self::SDIO_CMD_SELECT, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1B, + rca, + 0, + 0, + core::ptr::null_mut(), + ) + } - let _ = ios::close(sdio); - Ok(u16::from_be_bytes( - buffer[0..2].try_into().map_err(|_| ios::Error::Invalid)?, - )) + pub const fn appcmd_with_rca(rca: u32) -> Request { + Request::new( + Self::SDIO_CMD_APPCMD, + Self::SDIO_CMD_TYPE_AC, + Self::SDIO_RESPONSE_TYPE_R1, + rca, + 0, + 0, + core::ptr::null_mut(), + ) + } + + /// Create a new `Request` for the SDIO device + pub const fn new( + command: u32, + command_type: u32, + response_type: u32, + arg: u32, + block_count: u32, + block_size: u32, + dma_addr: *mut u8, + ) -> Self { + let is_dma = !dma_addr.is_null() as u32; + Self { + command, + command_type, + response_type, + arg, + block_count, + block_size, + dma_addr, + is_dma, + pad0: 0, + } + } +} + +//#[repr(C, align(32))] +/// SDIO response +#[derive(Debug)] +pub struct Response { + pub rsp_field0: u32, + pub rsp_field1: u32, + pub rsp_field2: u32, + pub acmd12_response: u32, } -// /// Set SD card clock -//pub fn set_clock +pub use dev::Device; +mod dev { + use core::mem::ManuallyDrop; + + use crate::ios::{ + self, + sdio::{Ioctl, Request, Response}, + FileDescriptor, + }; + + type RawFd = core::ffi::c_int; + + struct ValidRawFd { + fd: RawFd, + } + impl ValidRawFd { + pub fn new(fd: RawFd) -> Option { + if fd.is_positive() || fd == 0 { + Some(ValidRawFd { fd }) + } else { + None + } + } + + pub fn as_raw_fd(&self) -> RawFd { + self.fd + } + + pub fn into_raw_fd(self) -> RawFd { + self.fd + } + } + + struct OwnedFd { + fd: ValidRawFd, + } + + impl OwnedFd { + pub unsafe fn from_raw_fd(fd: RawFd) -> Option { + ValidRawFd::new(fd).map(|fd| OwnedFd { fd }) + } + + pub fn into_raw_fd(self) -> RawFd { + ManuallyDrop::new(self).fd.as_raw_fd() + } + + pub fn as_file_descriptor(&self) -> FileDescriptor { + FileDescriptor(self.fd.as_raw_fd()) + } + } + + impl Drop for OwnedFd { + fn drop(&mut self) { + let _ = ios::close(ios::FileDescriptor(self.fd.as_raw_fd())); + } + } + + /// `/dev/sdio/slot0` Device + pub struct Device { + fd: OwnedFd, + } + + impl Device { + /// Try to open `/dev/sdio/slot0` + pub fn open() -> Result { + let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::Read)?; + let fd = unsafe { OwnedFd::from_raw_fd(sdio.0) }.ok_or(ios::Error::Invalid)?; + Ok(Self { fd }) + } + + /// Write to a SD host controller register + pub fn write_to_host_controller_register( + &mut self, + register: u8, + size: u8, + data: u32, + ) -> Result<(), ios::Error> { + let mut buffer = [0u8; 24]; + buffer[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); + buffer[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + buffer[16..20].copy_from_slice(&data.to_be_bytes()); + + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::WriteHostControllerRegister, + &buffer, + &mut [], + )?; + + Ok(()) + } + + /// Read from a SD host controller register + pub fn read_from_host_controller_register( + &mut self, + register: u8, + size: u8, + ) -> Result { + let mut value = [0u8; 4]; + let mut query = [0u8; 24]; + query[0..4].copy_from_slice(&u32::from(register).to_be_bytes()); + query[12..16].copy_from_slice(&u32::from(size).to_be_bytes()); + + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::ReadHostControllerRegister, + &query, + &mut value, + )?; + + Ok(u32::from_be_bytes(value)) + } + /// Reset SD card + pub fn reset(&mut self) -> Result { + let mut buffer = [0u8; 4]; + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::ResetSDCard, + &[], + &mut buffer, + )?; + + Ok(u32::from_be_bytes(buffer)) + } + + /// Enable SD card clock + pub fn enable_clock(&mut self, enable: bool) -> Result<(), ios::Error> { + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::SetClock, + &u32::from(enable).to_be_bytes(), + &mut [], + )?; + + Ok(()) + } + + /// Send SDIO command + pub fn send_command(&mut self, request: &Request) -> Result { + let mut in_buf: [u8; _] = [0u8; core::mem::size_of::()]; + in_buf[0..4].copy_from_slice(&request.command.to_be_bytes()); + in_buf[4..8].copy_from_slice(&request.command_type.to_be_bytes()); + in_buf[8..12].copy_from_slice(&request.response_type.to_be_bytes()); + in_buf[12..16].copy_from_slice(&request.arg.to_be_bytes()); + in_buf[16..20].copy_from_slice(&request.block_count.to_be_bytes()); + in_buf[20..24].copy_from_slice(&request.block_size.to_be_bytes()); + in_buf[24..28].copy_from_slice(&request.dma_addr.expose_provenance().to_be_bytes()); + in_buf[28..32].copy_from_slice(&request.is_dma.to_be_bytes()); + //in_buf[32..36].copy_from_slice(&request.pad0.to_be_bytes()); + + let mut out_buf: [u8; _] = [0u8; core::mem::size_of::()]; + + if !request.dma_addr.is_null() && request.is_dma != 0 { + let dma_bytes = unsafe { + core::slice::from_raw_parts( + request.dma_addr, + usize::try_from(request.block_count * request.block_size) + .map_err(|_| ios::Error::Invalid)?, + ) + }; + + ios::ioctlv::<2, 1, 3>( + self.fd.as_file_descriptor(), + Ioctl::SendCommand, + &[&in_buf, dma_bytes], + &mut [&mut out_buf], + )?; + } else { + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::SendCommand, + &in_buf, + &mut out_buf, + )?; + } + + let resp = Response { + rsp_field0: u32::from_be_bytes( + out_buf[0..4].try_into().map_err(|_| ios::Error::Invalid)?, + ), + rsp_field1: u32::from_be_bytes( + out_buf[4..8].try_into().map_err(|_| ios::Error::Invalid)?, + ), + rsp_field2: u32::from_be_bytes( + out_buf[8..12].try_into().map_err(|_| ios::Error::Invalid)?, + ), + acmd12_response: u32::from_be_bytes( + out_buf[12..16] + .try_into() + .map_err(|_| ios::Error::Invalid)?, + ), + }; + + Ok(resp) + } + + /// Read SD card status returning the relative card address + pub fn get_status(&mut self) -> Result { + let mut buffer = [0u8; 4]; + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::GetStatus, + &[], + &mut buffer, + )?; + + Ok(u32::from_be_bytes( + buffer[0..4].try_into().map_err(|_| ios::Error::Invalid)?, + )) + } + + /// Get operation conditions register + pub fn get_operating_conditions_register(&mut self) -> Result { + let mut buffer = [0u8; 4]; + ios::ioctl( + self.fd.as_file_descriptor(), + Ioctl::GetOperatingConditionsRegister, + &[], + &mut buffer, + )?; + + Ok(u32::from_be_bytes(buffer)) + } + } +} From 0d9868ec6278e2840fa2d2504ebaff418c18fd22 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Mon, 29 Sep 2025 19:50:52 -0500 Subject: [PATCH 5/9] :wip: chore(examples/ios): Add embedded-sdmmc --- examples/ios/Cargo.lock | 64 ++++++++++++++++++++++++++++++++++++++--- examples/ios/Cargo.toml | 2 ++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/examples/ios/Cargo.lock b/examples/ios/Cargo.lock index 9484399..ae20198 100644 --- a/examples/ios/Cargo.lock +++ b/examples/ios/Cargo.lock @@ -23,7 +23,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools", @@ -54,9 +54,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cexpr" @@ -99,6 +105,29 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-sdmmc" +version = "0.9.0" +dependencies = [ + "byteorder", + "embedded-hal", + "embedded-io", + "heapless", + "log", +] + [[package]] name = "errno" version = "0.3.9" @@ -115,6 +144,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "home" version = "0.5.9" @@ -387,7 +435,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", @@ -406,6 +454,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "syn" version = "1.0.109" @@ -432,6 +486,8 @@ dependencies = [ name = "template" version = "0.1.0" dependencies = [ + "bitflags 2.9.4", + "embedded-sdmmc", "ogc-rs", ] diff --git a/examples/ios/Cargo.toml b/examples/ios/Cargo.toml index b677eb6..e7b9d1b 100644 --- a/examples/ios/Cargo.toml +++ b/examples/ios/Cargo.toml @@ -9,4 +9,6 @@ dev = { panic = "abort" } release = { panic = "abort", lto = true, codegen-units = 1, strip = "symbols", opt-level = "s" } [dependencies] +bitflags = "2.9.4" +embedded-sdmmc = {path = "./embedded-sdmmc" } ogc-rs = { path = "../../" } From 83cbc8dbe3406f491fdf8c0f03dbb30c77f38345 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Mon, 29 Sep 2025 19:52:57 -0500 Subject: [PATCH 6/9] :wip: :shrug: --- .../ios/embedded-sdmmc/.cargo-checksum.json | 1 + .../ios/embedded-sdmmc/.cargo_vcs_info.json | 6 + .../embedded-sdmmc/.github/workflows/rust.yml | 33 + examples/ios/embedded-sdmmc/CHANGELOG.md | 204 +++ examples/ios/embedded-sdmmc/Cargo.lock | 745 ++++++++ examples/ios/embedded-sdmmc/Cargo.toml | 141 ++ examples/ios/embedded-sdmmc/Cargo.toml.orig | 35 + examples/ios/embedded-sdmmc/LICENSE-APACHE | 201 +++ examples/ios/embedded-sdmmc/LICENSE-MIT | 26 + examples/ios/embedded-sdmmc/NOTICE | 6 + examples/ios/embedded-sdmmc/README.md | 111 ++ .../embedded-sdmmc/examples/append_file.rs | 45 + .../ios/embedded-sdmmc/examples/big_dir.rs | 53 + .../embedded-sdmmc/examples/create_file.rs | 48 + .../embedded-sdmmc/examples/delete_file.rs | 48 + .../ios/embedded-sdmmc/examples/linux/mod.rs | 91 + .../ios/embedded-sdmmc/examples/list_dir.rs | 99 ++ .../ios/embedded-sdmmc/examples/read_file.rs | 84 + .../embedded-sdmmc/examples/readme_test.rs | 157 ++ examples/ios/embedded-sdmmc/examples/shell.rs | 609 +++++++ .../ios/embedded-sdmmc/src/blockdevice.rs | 315 ++++ examples/ios/embedded-sdmmc/src/fat/bpb.rs | 140 ++ examples/ios/embedded-sdmmc/src/fat/info.rs | 94 + examples/ios/embedded-sdmmc/src/fat/mod.rs | 368 ++++ .../embedded-sdmmc/src/fat/ondiskdirentry.rs | 166 ++ examples/ios/embedded-sdmmc/src/fat/volume.rs | 1447 +++++++++++++++ .../src/filesystem/attributes.rs | 108 ++ .../embedded-sdmmc/src/filesystem/cluster.rs | 68 + .../src/filesystem/directory.rs | 336 ++++ .../embedded-sdmmc/src/filesystem/filename.rs | 506 ++++++ .../embedded-sdmmc/src/filesystem/files.rs | 359 ++++ .../embedded-sdmmc/src/filesystem/handles.rs | 48 + .../ios/embedded-sdmmc/src/filesystem/mod.rs | 32 + .../src/filesystem/timestamp.rs | 141 ++ examples/ios/embedded-sdmmc/src/lib.rs | 461 +++++ examples/ios/embedded-sdmmc/src/sdcard/mod.rs | 720 ++++++++ .../ios/embedded-sdmmc/src/sdcard/proto.rs | 739 ++++++++ examples/ios/embedded-sdmmc/src/structure.rs | 64 + examples/ios/embedded-sdmmc/src/volume_mgr.rs | 1550 +++++++++++++++++ .../ios/embedded-sdmmc/tests/directories.rs | 599 +++++++ examples/ios/embedded-sdmmc/tests/disk.img.gz | Bin 0 -> 707976 bytes .../ios/embedded-sdmmc/tests/open_files.rs | 145 ++ .../ios/embedded-sdmmc/tests/read_file.rs | 212 +++ .../ios/embedded-sdmmc/tests/utils/mod.rs | 190 ++ examples/ios/embedded-sdmmc/tests/volume.rs | 115 ++ .../ios/embedded-sdmmc/tests/write_file.rs | 168 ++ examples/ios/src/main.rs | 323 +++- src/ios/sdio.rs | 3 +- 48 files changed, 12117 insertions(+), 43 deletions(-) create mode 100644 examples/ios/embedded-sdmmc/.cargo-checksum.json create mode 100644 examples/ios/embedded-sdmmc/.cargo_vcs_info.json create mode 100644 examples/ios/embedded-sdmmc/.github/workflows/rust.yml create mode 100644 examples/ios/embedded-sdmmc/CHANGELOG.md create mode 100644 examples/ios/embedded-sdmmc/Cargo.lock create mode 100644 examples/ios/embedded-sdmmc/Cargo.toml create mode 100644 examples/ios/embedded-sdmmc/Cargo.toml.orig create mode 100644 examples/ios/embedded-sdmmc/LICENSE-APACHE create mode 100644 examples/ios/embedded-sdmmc/LICENSE-MIT create mode 100644 examples/ios/embedded-sdmmc/NOTICE create mode 100644 examples/ios/embedded-sdmmc/README.md create mode 100644 examples/ios/embedded-sdmmc/examples/append_file.rs create mode 100644 examples/ios/embedded-sdmmc/examples/big_dir.rs create mode 100644 examples/ios/embedded-sdmmc/examples/create_file.rs create mode 100644 examples/ios/embedded-sdmmc/examples/delete_file.rs create mode 100644 examples/ios/embedded-sdmmc/examples/linux/mod.rs create mode 100644 examples/ios/embedded-sdmmc/examples/list_dir.rs create mode 100644 examples/ios/embedded-sdmmc/examples/read_file.rs create mode 100644 examples/ios/embedded-sdmmc/examples/readme_test.rs create mode 100644 examples/ios/embedded-sdmmc/examples/shell.rs create mode 100644 examples/ios/embedded-sdmmc/src/blockdevice.rs create mode 100644 examples/ios/embedded-sdmmc/src/fat/bpb.rs create mode 100644 examples/ios/embedded-sdmmc/src/fat/info.rs create mode 100644 examples/ios/embedded-sdmmc/src/fat/mod.rs create mode 100644 examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs create mode 100644 examples/ios/embedded-sdmmc/src/fat/volume.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/attributes.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/cluster.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/directory.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/filename.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/files.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/handles.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/mod.rs create mode 100644 examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs create mode 100644 examples/ios/embedded-sdmmc/src/lib.rs create mode 100644 examples/ios/embedded-sdmmc/src/sdcard/mod.rs create mode 100644 examples/ios/embedded-sdmmc/src/sdcard/proto.rs create mode 100644 examples/ios/embedded-sdmmc/src/structure.rs create mode 100644 examples/ios/embedded-sdmmc/src/volume_mgr.rs create mode 100644 examples/ios/embedded-sdmmc/tests/directories.rs create mode 100644 examples/ios/embedded-sdmmc/tests/disk.img.gz create mode 100644 examples/ios/embedded-sdmmc/tests/open_files.rs create mode 100644 examples/ios/embedded-sdmmc/tests/read_file.rs create mode 100644 examples/ios/embedded-sdmmc/tests/utils/mod.rs create mode 100644 examples/ios/embedded-sdmmc/tests/volume.rs create mode 100644 examples/ios/embedded-sdmmc/tests/write_file.rs diff --git a/examples/ios/embedded-sdmmc/.cargo-checksum.json b/examples/ios/embedded-sdmmc/.cargo-checksum.json new file mode 100644 index 0000000..5a6c91a --- /dev/null +++ b/examples/ios/embedded-sdmmc/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{".cargo_vcs_info.json":"fdc51c58c1ab93d419b5391479a1ddf7953637ff600d45d2c01ee0e9b8eff899",".github/workflows/rust.yml":"ef823091e9e8806c9e799bf62d9c00d4a58a33f3dbaf81541391df276a737266","CHANGELOG.md":"2b57b31c1fdf26061a1c9fa33a1778a92cf8546bd53cd9424ed335d5e0c7404a","Cargo.lock":"41951573de00887244b6b260a2233c6e578f95542fb59ed5ed842207378d5b90","Cargo.toml":"e30ed8c552a22ecc01669fc0f7f5faed7a41241af9455f769137337faf4898bf","Cargo.toml.orig":"dbe63ac0424e16f2b2b366e4cecca627baf8635f5a48d629fb1570520f962d0b","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"bbb6fa175b21c44b9f5860afb8bf455f1df8a18cb2940f60d4e0d12810c1d1bf","NOTICE":"895d2a6a539cb8006b2f73260dbfdf039645121672ab82c6fc06eb7337c618ed","README.md":"629253caf1349f3cca5580120a866075380eaf71bcf4b807ff8b5f7afe95686d","examples/append_file.rs":"bc218b225aa5de4d6777b3c5b21135e1e95a73683a4e9ad7c3b7b939f2e436d8","examples/big_dir.rs":"621973ce7e3fe8791c338b4310515fc59452a1a337264afc6dc00d0c6cea410b","examples/create_file.rs":"fffe091cc2d42b1773336479ae1ade9deb255c4cf31b35d23ab315e414799f3c","examples/delete_file.rs":"7fba86d44efe35ff9e0da51a9e3b83e0efce62695f8ef64952be68e161f23a7c","examples/linux/mod.rs":"05d87ba9556a8cbb1747d2e30f0652766c72e60b2de71d57af9e1e126bb81751","examples/list_dir.rs":"02fee78deb0acd9fcdbcfa5853c706f044b1eec30c338265333818e68ba09077","examples/read_file.rs":"801dec1e763582ba87fc3de26af2f0219872b44527f686e384a4f8c6e93103e7","examples/readme_test.rs":"ea342c196cc2acf3426f830611abeeb72dcd50f13951946b019fd64e0ed2decc","examples/shell.rs":"d560cfccdfcf5bbe9d8108c395b76e6c5603d01b63e809f6652ba2ae25210474","src/blockdevice.rs":"ca8d62683a0f3f6e7a9c3779ab97b93cd8dd82cadbf1ee373b14c9f79f906d8c","src/fat/bpb.rs":"eba33b298ec91491cf165285ad2dc7f6ed4d3d7142b57ded924412c7fdb74252","src/fat/info.rs":"b51732d3a7b9e5f7811d6576cd8d92f4c39ac2d6e458017e4adf48851720c81d","src/fat/mod.rs":"10f162b31ed9008d641d7ac84891838ca6868d4397abf7d34423c63ac3d6cc69","src/fat/ondiskdirentry.rs":"314891e467111087b5bb60bfe46145593348ffbebf4e0f399a9ae20db65ad5ff","src/fat/volume.rs":"750dc7984968a6002347181fa4f9c4a8dcdb9e321366f5650259463bc3d42ddd","src/filesystem/attributes.rs":"ee450054e6e72e78faab66cf8d0deb60b0763c76f2a16a01925e95241124997e","src/filesystem/cluster.rs":"598f504a3451bd2e6872a9d4e90a4f29e887eb84d6d3211409757405914ab597","src/filesystem/directory.rs":"a9c4efed2d33f5d0b17f7b99b2c534475e9d847b03fb1ee4b09e5ce237069b46","src/filesystem/filename.rs":"0496389ec26c3195366829b5668d601fd26e7e9af2cfa0435bef55b1d2d487b1","src/filesystem/files.rs":"df40efac53e52d255e9af061ebf5a4291a0e66c621c8a3f1d6b1700bb725ab82","src/filesystem/handles.rs":"c6aa8bf344ad6701cb1f9d741c6277f85b2c1ff84bb35250d8ea92d22cbedeb6","src/filesystem/mod.rs":"96a6b7a87aeb20dfda1cfe486f6a6d39ff578d95e349a107d6e193dcb8dc5605","src/filesystem/timestamp.rs":"a20dffee9a839736d4bcd6987da31037f69393cf398f40305f09842b6eb86076","src/lib.rs":"33c9133ff73e3f2054aa08bcc0c52953b1c1097c95b83b25d7cc1376665f5075","src/sdcard/mod.rs":"edfcbef8079138c5ea94e185616ce84e809dc0d1a29cc6dccfdfd599417e6c42","src/sdcard/proto.rs":"b6501505261dce7beb8e4661bfea057e970cb8a126a6fd471527b3d68f5d17aa","src/structure.rs":"4cdd9759b11cc3a4c59dcb46bbadf0cf86fab1b0a02b8fc04d4d17898c4b9f14","src/volume_mgr.rs":"53cb76773ce0b172c0a438ee010f2a292d1691b8640b9f346a9625e5ef1a3128","tests/directories.rs":"2d6dbbe4bcdfa1986417e7b8c0fdf89fb76eaee388927b2853fef5cb68caa2b6","tests/disk.img.gz":"2456473f6f3c10a0177c2a050a99ffbd509181e8738b238178908bcf6cc8e2af","tests/open_files.rs":"265b15519fa8d688deb6aab8eac768aaf8601d81d8a785e43ce7224c3432c3bd","tests/read_file.rs":"9106bd0bf46e65d53a865fce2e3421b19d20d2d3ca336e00397e89fa106aab0c","tests/utils/mod.rs":"f14cf0f49c0166ff3151a784b801e2c9fded88e33ef5e7afd2605b1621fe2f31","tests/volume.rs":"7d20a44e79142f64a568b39221f323c5de5d0511e59e30b9a8775583038b8dd7","tests/write_file.rs":"f57607729cfae12489a2127cd5393caf9d0270291c81c6d26c1a32fda371a00b"},"package":"ce3c7f9ea039eeafc4a49597b7bd5ae3a1c8e51b2803a381cb0f29ce90fe1ec6"} \ No newline at end of file diff --git a/examples/ios/embedded-sdmmc/.cargo_vcs_info.json b/examples/ios/embedded-sdmmc/.cargo_vcs_info.json new file mode 100644 index 0000000..d520715 --- /dev/null +++ b/examples/ios/embedded-sdmmc/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "a53635929839a8ddaaf7b0a4c689c17e680ace8d" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/examples/ios/embedded-sdmmc/.github/workflows/rust.yml b/examples/ios/embedded-sdmmc/.github/workflows/rust.yml new file mode 100644 index 0000000..45b6e5d --- /dev/null +++ b/examples/ios/embedded-sdmmc/.github/workflows/rust.yml @@ -0,0 +1,33 @@ +name: Rust + +on: [push, pull_request] + +jobs: + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check formatting + run: cargo fmt -- --check + + build-test: + runs-on: ubuntu-latest + strategy: + matrix: + # Always run MSRV too! + rust: ["stable", "1.76"] + features: ['log', 'defmt-log', '""'] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Build + run: cargo build --no-default-features --features ${{matrix.features}} --verbose + env: + DEFMT_LOG: debug + - name: Run Tests + run: cargo test --no-default-features --features ${{matrix.features}} --verbose diff --git a/examples/ios/embedded-sdmmc/CHANGELOG.md b/examples/ios/embedded-sdmmc/CHANGELOG.md new file mode 100644 index 0000000..8ff7a2f --- /dev/null +++ b/examples/ios/embedded-sdmmc/CHANGELOG.md @@ -0,0 +1,204 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning]. + +## [Unreleased] + +## [Version 0.9.0] - 2025-06-08 + +### Changed + +- __Breaking Change__: `VolumeManager` now uses interior-mutability (with a `RefCell`) and so most methods are now `&self`. This also makes it easier to open multiple `File`, `Directory` or `Volume` objects at once. +- __Breaking Change__: The `VolumeManager`, `File`, `Directory` and `Volume` no longer implement `Send` or `Sync`. +- `VolumeManager` uses an interior block cache of 512 bytes, increasing its size by about 520 bytes but hugely reducing stack space required at run-time. +- __Breaking Change__: The `VolumeManager::device` method now takes a callback rather than giving you a reference to the underlying `BlockDevice` +- __Breaking Change__: `Error:LockError` variant added. +- __Breaking Change__: `SearchId` was renamed to `Handle` +- Fixed writing at block start mid-file (previously overwrote subsequent file data with zeros up to the end of the block) + +### Added + +- `File` now implements the `embedded-io` `Read`, `Write` and `Seek` traits. +- New `iterate_dir_lfn` method on `VolumeManager` and `Directory` - provides decoded Long File Names as `Option<&str>` + +### Removed + +- __Breaking Change__: Removed the `reason: &str` argument from `BlockDevice` + +## [Version 0.8.2] - 2025-06-07 + +### Changed + +* Fixed writing at block start mid-file (previously overwrote subsequent file data with zeros up to the end of the block) + +## [Version 0.8.1] - 2024-11-03 + +### Changed + +* Second FAT is now updated, if it is present +* When creating a directory `..` now points at the root directory correctly +* The info block containing the free cluster count is now updated when unmounting a FAT32 volume. + +## [Version 0.8.0] - 2024-07-12 + +### Changed + +- Fixed a bug when seeking backwards through files. +- Updated to `heapless-0.8` and `embedded-hal-bus-0.2`. +- No longer panics if the close fails when a `Volume` is dropped - the failure is instead ignored. + +### Added + +- `File` now has a `flush()` method. +- `File` now has a `close()` method. + +### Removed + +- __Breaking Change__: Removed `CS` type-param on `SdCard` - now we use the `SpiDevice` chip-select (closing [#126]) +- __Breaking Change__: Removed the 74 clock cycle 'init' sequence - now applications must do this + +## [Version 0.7.0] - 2024-02-04 + +### Changed + +- __Breaking Change__: `Volume`, `Directory` and `File` are now smart! They hold references to the thing they were made from, and will clean themselves up when dropped. The trade-off is you can can't open multiple volumes, directories or files at the same time. +- __Breaking Change__: Renamed the old types to `RawVolume`, `RawDirectory` and `RawFile` +- __Breaking Change__: Renamed `Error::FileNotFound` to `Error::NotFound` +- Fixed long-standing bug that caused an integer overflow when a FAT32 directory was longer than one cluster ([#74]) +- You can now open directories multiple times without error +- Updated to [embedded-hal] 1.0 + +### Added + +- `RawVolume`, `RawDirectory` and `RawFile` types (like the old `Volume`, `Directory` and `File` types) +- New method `make_dir_in_dir` +- Empty strings and `"."` convert to `ShortFileName::this_dir()` +- New API `change_dir` which changes a directory to point to some child directory (or the parent) without opening a new directory. +- Updated 'shell' example to support `mkdir`, `tree` and relative/absolute paths + +### Removed + +- None + +[#126]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/126 +[#74]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/74 +[embedded-hal]: https://crates.io/crates/embedded-hal + +## [Version 0.6.0] - 2023-10-20 + +### Changed + +- Writing to a file no longer flushes file metadata to the Directory Entry. + Instead closing a file now flushes file metadata to the Directory Entry. + Requires mutable access to the Volume ([#94]). +- Files now have the correct length when modified, not appended ([#72]). +- Calling `SdCard::get_card_type` will now perform card initialisation ([#87] and [#90]). +- Removed warning about unused arguments. +- Types are now documented at the top level ([#86]). +- Renamed `Cluster` to `ClusterId` and stopped you adding two together + +[#72]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/72 +[#86]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/86 +[#87]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/87 +[#90]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/90 +[#94]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/94 + +### Added + +- New examples, `append_file`, `create_file`, `delete_file`, `list_dir`, `shell` +- New test cases `tests/directories.rs`, `tests/read_file.rs` + +### Removed + +- __Breaking Change__: `Controller` alias for `VolumeManager` removed. +- __Breaking Change__: `VolumeManager::open_dir_entry` removed, as it was unsafe to the user to randomly pick a starting cluster. +- Old examples `create_test`, `test_mount`, `write_test`, `delete_test` + +## [Version 0.5.0] - 2023-05-20 + +### Changed + +- __Breaking Change__: Renamed `Controller` to `VolumeManager`, to better describe what it does. +- __Breaking Change__: Renamed `SdMmcSpi` to `SdCard` +- __Breaking Change__: `AcquireOpts` now has `use_crc` (which makes it ask for CRCs to be enabled) instead of `require_crc` (which simply allowed the enable-CRC command to fail) +- __Breaking Change__: `SdCard::new` now requires an object that implements the embedded-hal `DelayUs` trait +- __Breaking Change__: Renamed `card_size_bytes` to `num_bytes`, to match `num_blocks` +- More robust card intialisation procedure, with added retries +- Supports building with neither `defmt` nor `log` logging + +### Added + +- Added `mark_card_as_init` method, if you know the card is initialised and want to skip the initialisation step + +### Removed + +- __Breaking Change__: Removed `BlockSpi` type - card initialisation now handled as an internal state variable + +## [Version 0.4.0] - 2023-01-18 + +### Changed + +- Optionally use [defmt] s/defmt) for logging. + Controlled by `defmt-log` feature flag. +- __Breaking Change__: Use SPI blocking traits instead to ease SPI peripheral sharing. + See: +- Added `Controller::has_open_handles` and `Controller::free` methods. +- __Breaking Change__: Changed interface to enforce correct SD state at compile time. +- __Breaking Change__: Added custom error type for `File` operations. +- Fix `env_logger` pulling in the `std` feature in `log` in library builds. +- Raise the minimum supported Rust version to 1.56.0. +- Code tidy-ups and more documentation. +- Add `MAX_DIRS` and `MAX_FILES` generics to `Controller` to allow an arbitrary numbers of concurrent open directories and files. +- Add new constructor method `Controller::new_with_limits(block_device: D, timesource: T) -> Controller` + to create a `Controller` with custom limits. + +## [Version 0.3.0] - 2019-12-16 + +### Changed + +- Updated to `v2` embedded-hal traits. +- Added open support for all modes. +- Added write support for files. +- Added `Info_Sector` tracking for FAT32. +- Change directory iteration to look in all the directory's clusters. +- Added `write_test` and `create_test`. +- De-duplicated FAT16 and FAT32 code () + +## [Version 0.2.1] - 2019-02-19 + +### Changed + +- Added `readme=README.md` to `Cargo.toml` + +## [Version 0.2.0] - 2019-01-24 + +### Changed + +- Reduce delay waiting for response. Big speed improvements. + +## [Version 0.1.1] - 2018-12-23 + +### Changed + +- Can read blocks from an SD Card using an `embedded_hal::SPI` device and a + `embedded_hal::OutputPin` for Chip Select. +- Can read partition tables and open a FAT32 or FAT16 formatted partition. +- Can open and iterate the root directory of a FAT16 formatted partition. + +[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: http://semver.org/spec/v2.0.0.html +[Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.9.0...develop +[Version 0.9.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.2...v0.9.0 +[Version 0.8.2]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.1...v0.8.2 +[Version 0.8.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.0...v0.8.1 +[Version 0.8.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.7.0...v0.8.0 +[Version 0.7.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.6.0...v0.7.0 +[Version 0.6.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.5.0...v0.6.0 +[Version 0.5.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.4.0...v0.5.0 +[Version 0.4.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.3.0...v0.4.0 +[Version 0.3.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.1...v0.3.0 +[Version 0.2.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.2.0...v0.2.1 +[Version 0.2.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.1.1...v0.2.0 +[Version 0.1.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/releases/tag/v0.1.1 diff --git a/examples/ios/embedded-sdmmc/Cargo.lock b/examples/ios/embedded-sdmmc/Cargo.lock new file mode 100644 index 0000000..0d76a75 --- /dev/null +++ b/examples/ios/embedded-sdmmc/Cargo.lock @@ -0,0 +1,745 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-bus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3980bf28e8577db59fe2bdb3df868a419469d2cecb363644eea2b6f7797669" +dependencies = [ + "critical-section", + "embedded-hal", + "portable-atomic", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-sdmmc" +version = "0.9.0" +dependencies = [ + "byteorder", + "chrono", + "defmt 0.3.100", + "embedded-hal", + "embedded-hal-bus", + "embedded-io", + "env_logger", + "flate2", + "heapless", + "hex-literal", + "log", + "sha2", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.173" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/examples/ios/embedded-sdmmc/Cargo.toml b/examples/ios/embedded-sdmmc/Cargo.toml new file mode 100644 index 0000000..4938a65 --- /dev/null +++ b/examples/ios/embedded-sdmmc/Cargo.toml @@ -0,0 +1,141 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.76" +name = "embedded-sdmmc" +version = "0.9.0" +authors = [ + "Jonathan 'theJPster' Pallant ", + "Rust Embedded Community Developers", +] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A basic SD/MMC driver for Embedded Rust." +readme = "README.md" +keywords = [ + "sdcard", + "mmc", + "embedded", + "fat32", +] +categories = [ + "embedded", + "no-std", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" + +[features] +default = ["log"] +defmt-log = ["dep:defmt"] +log = ["dep:log"] + +[lib] +name = "embedded_sdmmc" +path = "src/lib.rs" + +[[example]] +name = "append_file" +path = "examples/append_file.rs" + +[[example]] +name = "big_dir" +path = "examples/big_dir.rs" + +[[example]] +name = "create_file" +path = "examples/create_file.rs" + +[[example]] +name = "delete_file" +path = "examples/delete_file.rs" + +[[example]] +name = "list_dir" +path = "examples/list_dir.rs" + +[[example]] +name = "read_file" +path = "examples/read_file.rs" + +[[example]] +name = "readme_test" +path = "examples/readme_test.rs" + +[[example]] +name = "shell" +path = "examples/shell.rs" + +[[test]] +name = "directories" +path = "tests/directories.rs" + +[[test]] +name = "open_files" +path = "tests/open_files.rs" + +[[test]] +name = "read_file" +path = "tests/read_file.rs" + +[[test]] +name = "volume" +path = "tests/volume.rs" + +[[test]] +name = "write_file" +path = "tests/write_file.rs" + +[dependencies.byteorder] +version = "1" +default-features = false + +[dependencies.defmt] +version = "0.3" +optional = true + +[dependencies.embedded-hal] +version = "1.0.0" + +[dependencies.embedded-io] +version = "0.6.1" + +[dependencies.heapless] +version = "^0.8" + +[dependencies.log] +version = "0.4" +optional = true +default-features = false + +[dev-dependencies.chrono] +version = "0.4" + +[dev-dependencies.embedded-hal-bus] +version = "0.2.0" + +[dev-dependencies.env_logger] +version = "0.10.0" + +[dev-dependencies.flate2] +version = "1.0" + +[dev-dependencies.hex-literal] +version = "0.4.1" + +[dev-dependencies.sha2] +version = "0.10" diff --git a/examples/ios/embedded-sdmmc/Cargo.toml.orig b/examples/ios/embedded-sdmmc/Cargo.toml.orig new file mode 100644 index 0000000..11b6ff6 --- /dev/null +++ b/examples/ios/embedded-sdmmc/Cargo.toml.orig @@ -0,0 +1,35 @@ +[package] +authors = ["Jonathan 'theJPster' Pallant ", "Rust Embedded Community Developers"] +categories = ["embedded", "no-std"] +description = "A basic SD/MMC driver for Embedded Rust." +edition = "2021" +keywords = ["sdcard", "mmc", "embedded", "fat32"] +license = "MIT OR Apache-2.0" +name = "embedded-sdmmc" +readme = "README.md" +repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" +version = "0.9.0" + +# Make sure to update the CI too! +rust-version = "1.76" + +[dependencies] +byteorder = {version = "1", default-features = false} +defmt = {version = "0.3", optional = true} +embedded-hal = "1.0.0" +embedded-io = "0.6.1" +heapless = "^0.8" +log = {version = "0.4", default-features = false, optional = true} + +[dev-dependencies] +chrono = "0.4" +embedded-hal-bus = "0.2.0" +env_logger = "0.10.0" +flate2 = "1.0" +hex-literal = "0.4.1" +sha2 = "0.10" + +[features] +default = ["log"] +defmt-log = ["dep:defmt"] +log = ["dep:log"] diff --git a/examples/ios/embedded-sdmmc/LICENSE-APACHE b/examples/ios/embedded-sdmmc/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/examples/ios/embedded-sdmmc/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/examples/ios/embedded-sdmmc/LICENSE-MIT b/examples/ios/embedded-sdmmc/LICENSE-MIT new file mode 100644 index 0000000..b59c97c --- /dev/null +++ b/examples/ios/embedded-sdmmc/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright (c) 2018-2024 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers +Copyright (c) 2011-2018 Bill Greiman + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/examples/ios/embedded-sdmmc/NOTICE b/examples/ios/embedded-sdmmc/NOTICE new file mode 100644 index 0000000..5dc1a0e --- /dev/null +++ b/examples/ios/embedded-sdmmc/NOTICE @@ -0,0 +1,6 @@ +# Copyright Notices + +This is a copyright notices file, as described by the Apache-2.0 license. + +Copyright (c) 2018-2024 Jonathan 'theJPster' Pallant and the Rust Embedded Community developers +Copyright (c) 2011-2018 Bill Greiman diff --git a/examples/ios/embedded-sdmmc/README.md b/examples/ios/embedded-sdmmc/README.md new file mode 100644 index 0000000..9bd06b1 --- /dev/null +++ b/examples/ios/embedded-sdmmc/README.md @@ -0,0 +1,111 @@ +# Embedded SD/MMC [![crates.io](https://img.shields.io/crates/v/embedded-sdmmc.svg)](https://crates.io/crates/embedded-sdmmc) [![Documentation](https://docs.rs/embedded-sdmmc/badge.svg)](https://docs.rs/embedded-sdmmc) + +This crate is intended to allow you to read/write files on a FAT formatted SD +card on your Rust Embedded device, as easily as using the `SdFat` Arduino +library. It is written in pure-Rust, is `#![no_std]` and does not use `alloc` +or `collections` to keep the memory footprint low. In the first instance it is +designed for readability and simplicity over performance. + +## Using the crate + +You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. + +```rust +use embedded_sdmmc::{SdCard, VolumeManager, Mode, VolumeIdx}; +// Build an SD Card interface out of an SPI device, a chip-select pin and the delay object +let sdcard = SdCard::new(sdmmc_spi, delay); +// Get the card size (this also triggers card initialisation because it's not been done yet) +println!("Card size is {} bytes", sdcard.num_bytes()?); +// Now let's look for volumes (also known as partitions) on our block device. +// To do this we need a Volume Manager. It will take ownership of the block device. +let volume_mgr = VolumeManager::new(sdcard, time_source); +// Try and access Volume 0 (i.e. the first partition). +// The volume object holds information about the filesystem on that volume. +let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; +println!("Volume 0: {:?}", volume0); +// Open the root directory (mutably borrows from the volume). +let root_dir = volume0.open_root_dir()?; +// Open a file called "MY_FILE.TXT" in the root directory +// This mutably borrows the directory. +let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; +// Print the contents of the file, assuming it's in ISO-8859-1 encoding +while !my_file.is_eof() { + let mut buffer = [0u8; 32]; + let num_read = my_file.read(&mut buffer)?; + for b in &buffer[0..num_read] { + print!("{}", *b as char); + } +} +``` + +For writing files: + +```rust +let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; +my_other_file.write(b"Timestamp,Signal,Value\n")?; +my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; + +// Don't forget to flush the file so that the directory entry is updated +my_other_file.flush()?; +``` + +### Open directories and files + +By default the `VolumeManager` will initialize with a maximum number of `4` open directories, files and volumes. This can be customized by specifying the `MAX_DIR`, `MAX_FILES` and `MAX_VOLUMES` generic consts of the `VolumeManager`: + +```rust +// Create a volume manager with a maximum of 6 open directories, 12 open files, and 4 volumes (or partitions) +let cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); +``` + +## Supported features + +* Open files in all supported methods from an open directory +* Open an arbitrary number of directories and files +* Read data from open files +* Write data to open files +* Close files +* Delete files +* Iterate root directory +* Iterate sub-directories +* Log over defmt or the common log interface (feature flags). + +## No-std usage + +This repository houses no examples for no-std usage, however you can check out the following examples: + +* [Pi Pico](https://github.com/rp-rs/rp-hal-boards/blob/main/boards/rp-pico/examples/pico_spi_sd_card.rs) +* [STM32H7XX](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/sdmmc_fat.rs) +* [atsamd(pygamer)](https://github.com/atsamd-rs/atsamd/blob/master/boards/pygamer/examples/sd_card.rs) + +## Todo List (PRs welcome!) + +* Create new dirs +* Delete (empty) directories +* Handle MS-DOS `/path/foo/bar.txt` style paths. + +## Changelog + +The changelog has moved to [CHANGELOG.md](/CHANGELOG.md) + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) + +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +Copyright notices are stored in the [NOTICE](./NOTICE) file. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/examples/ios/embedded-sdmmc/examples/append_file.rs b/examples/ios/embedded-sdmmc/examples/append_file.rs new file mode 100644 index 0000000..54b7577 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/append_file.rs @@ -0,0 +1,45 @@ +//! Append File Example. +//! +//! ```bash +//! $ cargo run --example append_file -- ./disk.img +//! $ cargo run --example append_file -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example append_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_APPEND: &str = "README.TXT"; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("\nCreating file {}...", FILE_TO_APPEND); + let f = root_dir.open_file_in_dir(FILE_TO_APPEND, Mode::ReadWriteAppend)?; + f.write(b"\r\n\r\nThis has been added to your file.\r\n")?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/big_dir.rs b/examples/ios/embedded-sdmmc/examples/big_dir.rs new file mode 100644 index 0000000..bfc7e83 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/big_dir.rs @@ -0,0 +1,53 @@ +//! Big Directory Example. +//! +//! Attempts to create an infinite number of files in the root directory of the +//! first volume of the given block device. This is basically to see what +//! happens when the root directory runs out of space. +//! +//! ```bash +//! $ cargo run --example big_dir -- ./disk.img +//! $ cargo run --example big_dir -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example big_dir -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0)).unwrap(); + println!("Volume: {:?}", volume); + let root_dir = volume.open_root_dir().unwrap(); + + let mut file_num = 0; + loop { + file_num += 1; + let file_name = format!("{}.da", file_num); + println!("opening file {file_name} for writing"); + let file = root_dir + .open_file_in_dir(file_name.as_str(), Mode::ReadWriteCreateOrTruncate) + .unwrap(); + let buf = b"hello world, from rust"; + println!("writing to file"); + file.write(&buf[..]).unwrap(); + println!("closing file"); + drop(file); + } +} diff --git a/examples/ios/embedded-sdmmc/examples/create_file.rs b/examples/ios/embedded-sdmmc/examples/create_file.rs new file mode 100644 index 0000000..7f3cfb4 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/create_file.rs @@ -0,0 +1,48 @@ +//! Create File Example. +//! +//! ```bash +//! $ cargo run --example create_file -- ./disk.img +//! $ cargo run --example create_file -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example create_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_CREATE: &str = "CREATE.TXT"; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("\nCreating file {}...", FILE_TO_CREATE); + // This will panic if the file already exists: use ReadWriteCreateOrAppend + // or ReadWriteCreateOrTruncate instead if you want to modify an existing + // file. + let f = root_dir.open_file_in_dir(FILE_TO_CREATE, Mode::ReadWriteCreate)?; + f.write(b"Hello, this is a new file on disk\r\n")?; + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/delete_file.rs b/examples/ios/embedded-sdmmc/examples/delete_file.rs new file mode 100644 index 0000000..3df1978 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/delete_file.rs @@ -0,0 +1,48 @@ +//! Delete File Example. +//! +//! ```bash +//! $ cargo run --example delete_file -- ./disk.img +//! $ cargo run --example delete_file -- /dev/mmcblk0 +//! ``` +//! +//! NOTE: THIS EXAMPLE DELETES A FILE CALLED README.TXT. IF YOU DO NOT WANT THAT +//! FILE DELETED FROM YOUR DISK IMAGE, DO NOT RUN THIS EXAMPLE. +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example delete_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_DELETE: &str = "README.TXT"; + +use embedded_sdmmc::{Error, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("Deleting file {}...", FILE_TO_DELETE); + root_dir.delete_file_in_dir(FILE_TO_DELETE)?; + println!("Deleted!"); + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/linux/mod.rs b/examples/ios/embedded-sdmmc/examples/linux/mod.rs new file mode 100644 index 0000000..6eefe23 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/linux/mod.rs @@ -0,0 +1,91 @@ +//! Helpers for using embedded-sdmmc on Linux + +use chrono::Timelike; +use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx, TimeSource, Timestamp}; +use std::cell::RefCell; +use std::fs::{File, OpenOptions}; +use std::io::prelude::*; +use std::io::SeekFrom; +use std::path::Path; + +#[derive(Debug)] +pub struct LinuxBlockDevice { + file: RefCell, + print_blocks: bool, +} + +impl LinuxBlockDevice { + pub fn new

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

+//! 64MB.DAT 67108864 2018-12-09 19:21:38 +//! FSEVEN~1 0 2023-09-21 11:32:04 +//! Listing /TEST +//! . 0 2018-12-09 19:21:02 +//! .. 0 2018-12-09 19:21:02 +//! TEST.DAT 3500 2018-12-09 19:22:12 +//! Listing /FSEVEN~1 +//! . 0 2023-09-21 11:32:22 +//! .. 0 2023-09-21 11:32:04 +//! FSEVEN~1 36 2023-09-21 11:32:04 +//! $ +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example list_dir -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +use embedded_sdmmc::{ShortFileName, VolumeIdx}; + +type Error = embedded_sdmmc::Error; + +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 4, 4>; +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + list_dir(root_dir, "/")?; + Ok(()) +} + +/// Recursively print a directory listing for the open directory given. +/// +/// The path is for display purposes only. +fn list_dir(directory: Directory<'_>, path: &str) -> Result<(), Error> { + println!("Listing {}", path); + let mut children = Vec::new(); + directory.iterate_dir(|entry| { + println!( + "{:12} {:9} {} {}", + entry.name, + entry.size, + entry.mtime, + if entry.attributes.is_directory() { + "" + } else { + "" + } + ); + if entry.attributes.is_directory() + && entry.name != ShortFileName::parent_dir() + && entry.name != ShortFileName::this_dir() + { + children.push(entry.name.clone()); + } + })?; + for child_name in children { + let child_dir = directory.open_dir(&child_name)?; + let child_path = if path == "/" { + format!("/{}", child_name) + } else { + format!("{}/{}", path, child_name) + }; + list_dir(child_dir, &child_path)?; + } + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/read_file.rs b/examples/ios/embedded-sdmmc/examples/read_file.rs new file mode 100644 index 0000000..0800de9 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/read_file.rs @@ -0,0 +1,84 @@ +//! Read File Example. +//! +//! ```bash +//! $ cargo run --example read_file -- ./disk.img +//! Reading file README.TXT... +//! 00000000 [54, 68, 69, 73, 20, 69, 73, 20, 61, 20, 46, 41, 54, 31, 36, 20] |This.is.a.FAT16.| +//! 00000010 [70, 61, 74, 69, 74, 69, 6f, 6e, 2e, 20, 49, 74, 20, 63, 6f, 6e] |patition..It.con| +//! 00000020 [74, 61, 69, 6e, 73, 20, 66, 6f, 75, 72, 20, 66, 69, 6c, 65, 73] |tains.four.files| +//! 00000030 [20, 61, 6e, 64, 20, 61, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |.and.a.directory| +//! 00000040 [2e, 0a, 0a, 2a, 20, 54, 68, 69, 73, 20, 66, 69, 6c, 65, 20, 28] |...*.This.file.(| +//! 00000050 [52, 45, 41, 44, 4d, 45, 2e, 54, 58, 54, 29, 0a, 2a, 20, 41, 20] |README.TXT).*.A.| +//! 00000060 [36, 34, 20, 4d, 69, 42, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c] |64.MiB.file.full| +//! 00000070 [20, 6f, 66, 20, 7a, 65, 72, 6f, 73, 20, 28, 36, 34, 4d, 42, 2e] |.of.zeros.(64MB.| +//! 00000080 [44, 41, 54, 29, 2e, 0a, 2a, 20, 41, 20, 33, 35, 30, 30, 20, 62] |DAT)..*.A.3500.b| +//! 00000090 [79, 74, 65, 20, 66, 69, 6c, 65, 20, 66, 75, 6c, 6c, 20, 6f, 66] |yte.file.full.of| +//! 000000a0 [20, 72, 61, 6e, 64, 6f, 6d, 20, 64, 61, 74, 61, 2e, 0a, 2a, 20] |.random.data..*.| +//! 000000b0 [41, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79, 20, 63, 61, 6c, 6c] |A.directory.call| +//! 000000c0 [65, 64, 20, 54, 45, 53, 54, 0a, 2a, 20, 41, 20, 7a, 65, 72, 6f] |ed.TEST.*.A.zero| +//! 000000d0 [20, 62, 79, 74, 65, 20, 66, 69, 6c, 65, 20, 69, 6e, 20, 74, 68] |.byte.file.in.th| +//! 000000e0 [65, 20, 54, 45, 53, 54, 20, 64, 69, 72, 65, 63, 74, 6f, 72, 79] |e.TEST.directory| +//! 000000f0 [20, 63, 61, 6c, 6c, 65, 64, 20, 45, 4d, 50, 54, 59, 2e, 44, 41] |.called.EMPTY.DA| +//! 00000100 [54, 0a, 0d] |T...............| +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example read_file -- ./disk.img +//! ``` + +mod linux; +use linux::*; + +const FILE_TO_READ: &str = "README.TXT"; + +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; + println!("\nReading file {}...", FILE_TO_READ); + let f = root_dir.open_file_in_dir(FILE_TO_READ, Mode::ReadOnly)?; + // Proves we can open two files at once now (or try to - this file doesn't exist) + let f2 = root_dir.open_file_in_dir("MISSING.DAT", Mode::ReadOnly); + assert!(f2.is_err()); + while !f.is_eof() { + let mut buffer = [0u8; 16]; + let offset = f.offset(); + let mut len = f.read(&mut buffer)?; + print!("{:08x} {:02x?}", offset, &buffer[0..len]); + while len < buffer.len() { + print!(" "); + len += 1; + } + print!(" |"); + for b in buffer.iter() { + let ch = char::from(*b); + if ch.is_ascii_graphic() { + print!("{}", ch); + } else { + print!("."); + } + } + println!("|"); + } + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/readme_test.rs b/examples/ios/embedded-sdmmc/examples/readme_test.rs new file mode 100644 index 0000000..0d63d80 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/readme_test.rs @@ -0,0 +1,157 @@ +//! This is the code from the README.md file. +//! +//! We add enough stuff to make it compile, but it won't run because our fake +//! SPI doesn't do any replies. + +#![allow(dead_code)] + +use core::cell::RefCell; + +use embedded_sdmmc::{Error, SdCardError, TimeSource, Timestamp}; + +pub struct DummyCsPin; + +impl embedded_hal::digital::ErrorType for DummyCsPin { + type Error = core::convert::Infallible; +} + +impl embedded_hal::digital::OutputPin for DummyCsPin { + #[inline(always)] + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + #[inline(always)] + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +struct FakeSpiBus(); + +impl embedded_hal::spi::ErrorType for FakeSpiBus { + type Error = core::convert::Infallible; +} + +impl embedded_hal::spi::SpiBus for FakeSpiBus { + fn read(&mut self, _: &mut [u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn write(&mut self, _: &[u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn transfer(&mut self, _: &mut [u8], _: &[u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn transfer_in_place(&mut self, _: &mut [u8]) -> Result<(), Self::Error> { + Ok(()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +struct FakeCs(); + +impl embedded_hal::digital::ErrorType for FakeCs { + type Error = core::convert::Infallible; +} + +impl embedded_hal::digital::OutputPin for FakeCs { + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Clone, Copy)] +struct FakeDelayer(); + +impl embedded_hal::delay::DelayNs for FakeDelayer { + fn delay_ns(&mut self, ns: u32) { + std::thread::sleep(std::time::Duration::from_nanos(u64::from(ns))); + } +} + +struct FakeTimesource(); + +impl TimeSource for FakeTimesource { + fn get_timestamp(&self) -> Timestamp { + Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } +} + +#[derive(Debug, Clone)] +enum MyError { + Filesystem(Error), + Disk(SdCardError), +} + +impl From> for MyError { + fn from(value: Error) -> MyError { + MyError::Filesystem(value) + } +} + +impl From for MyError { + fn from(value: SdCardError) -> MyError { + MyError::Disk(value) + } +} + +fn main() -> Result<(), MyError> { + // BEGIN Fake stuff that will be replaced with real peripherals + let spi_bus = RefCell::new(FakeSpiBus()); + let delay = FakeDelayer(); + let sdmmc_spi = embedded_hal_bus::spi::RefCellDevice::new(&spi_bus, DummyCsPin, delay).unwrap(); + let time_source = FakeTimesource(); + // END Fake stuff that will be replaced with real peripherals + + use embedded_sdmmc::{Mode, SdCard, VolumeIdx, VolumeManager}; + // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object + let sdcard = SdCard::new(sdmmc_spi, delay); + // Get the card size (this also triggers card initialisation because it's not been done yet) + println!("Card size is {} bytes", sdcard.num_bytes()?); + // Now let's look for volumes (also known as partitions) on our block device. + // To do this we need a Volume Manager. It will take ownership of the block device. + let volume_mgr = VolumeManager::new(sdcard, time_source); + // Try and access Volume 0 (i.e. the first partition). + // The volume object holds information about the filesystem on that volume. + let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; + println!("Volume 0: {:?}", volume0); + // Open the root directory (mutably borrows from the volume). + let root_dir = volume0.open_root_dir()?; + // Open a file called "MY_FILE.TXT" in the root directory + // This mutably borrows the directory. + let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; + // Print the contents of the file, assuming it's in ISO-8859-1 encoding + while !my_file.is_eof() { + let mut buffer = [0u8; 32]; + let num_read = my_file.read(&mut buffer)?; + for b in &buffer[0..num_read] { + print!("{}", *b as char); + } + } + + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/examples/shell.rs b/examples/ios/embedded-sdmmc/examples/shell.rs new file mode 100644 index 0000000..4268276 --- /dev/null +++ b/examples/ios/embedded-sdmmc/examples/shell.rs @@ -0,0 +1,609 @@ +//! A simple shell demo for embedded-sdmmc +//! +//! Presents a basic command prompt which implements some basic MS-DOS style +//! shell commands. +//! +//! ```bash +//! $ cargo run --example shell -- ./disk.img +//! $ cargo run --example shell -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example shell -- ./disk.img +//! ``` +//! +//! Note that `embedded_sdmmc` itself does not care about 'paths' - only +//! accessing files and directories on on disk, relative to some previously +//! opened directory. A 'path' is an operating-system level construct, and can +//! vary greatly (see MS-DOS paths vs POSIX paths). This example, however, +//! implements an MS-DOS style Path API over the top of embedded-sdmmc. Feel +//! free to copy it if it suits your particular application. +//! +//! The four primary partitions are scanned on the given disk image on start-up. +//! Any valid FAT16 or FAT32 volumes are mounted, and given volume labels from +//! `A:` to `D:`, like MS-DOS. Also like MS-DOS, file and directory names use +//! the `8.3` format, like `FILENAME.TXT`. Long filenames are not supported. +//! +//! Unlike MS-DOS, this application uses the POSIX `/` as the directory +//! separator. +//! +//! Every volume has its own *current working directory*. The shell has one +//! *current volume* selected but it remembers the *current working directory* +//! for the unselected volumes. +//! +//! A path comprises: +//! +//! * An optional volume specifier, like `A:` +//! * If the volume specifier is not given, the current volume is used. +//! * An optional `/` to indicate this is an absolute path, not a relative path +//! * If this is a relative path, traversal starts at the Current Working +//! Directory for the volume +//! * An optional sequence of directory names, each followed by a `/` +//! * An optional final filename +//! * If this is missing, then `.` is the default (which selects the +//! containing directory) +//! +//! An *expanded path* has all optional components, and works independently of +//! whichever volume is currently selected, or the current working directory +//! within that volume. The empty path (`""`) is invalid, but commands may +//! assume that in the absence of a path argument they are to use the current +//! working directory on the current volume. +//! +//! As an example, imagine that volume `A:` is the current volume, and we have +//! these current working directories: +//! +//! * `A:` has a CWD of `/CATS` +//! * `B:` has a CWD of `/DOGS` +//! +//! The following path expansions would occur: +//! +//! | Given Path | Volume | Absolute | Directory Names | Final Filename | Expanded Path | +//! | --------------------------- | ------- | -------- | ------------------ | -------------- | ------------------------------ | +//! | `NAMES.CSV` | Current | No | `[]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `./NAMES.CSV` | Current | No | `[.]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `BACKUP.000/` | Current | No | `[BACKUP.000]` | None | `A:/CATS/BACKUP.000/.` | +//! | `BACKUP.000/NAMES.CSV` | Current | No | `[BACKUP.000]` | `NAMES.CSV` | `A:/CATS/BACKUP.000/NAMES.CSV` | +//! | `/BACKUP.000/NAMES.CSV` | Current | Yes | `[BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `../BACKUP.000/NAMES.CSV` | Current | No | `[.., BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `A:NAMES.CSV` | `A:` | No | `[]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `A:./NAMES.CSV` | `A:` | No | `[.]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `A:BACKUP.000/` | `A:` | No | `[BACKUP.000]` | None | `A:/CATS/BACKUP.000/.` | +//! | `A:BACKUP.000/NAMES.CSV` | `A:` | No | `[BACKUP.000]` | `NAMES.CSV` | `A:/CATS/BACKUP.000/NAMES.CSV` | +//! | `A:/BACKUP.000/NAMES.CSV` | `A:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `A:../BACKUP.000/NAMES.CSV` | `A:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `B:NAMES.CSV` | `B:` | No | `[]` | `NAMES.CSV` | `B:/DOGS/NAMES.CSV` | +//! | `B:./NAMES.CSV` | `B:` | No | `[.]` | `NAMES.CSV` | `B:/DOGS/NAMES.CSV` | +//! | `B:BACKUP.000/` | `B:` | No | `[BACKUP.000]` | None | `B:/DOGS/BACKUP.000/.` | +//! | `B:BACKUP.000/NAMES.CSV` | `B:` | No | `[BACKUP.000]` | `NAMES.CSV` | `B:/DOGS/BACKUP.000/NAMES.CSV` | +//! | `B:/BACKUP.000/NAMES.CSV` | `B:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | +//! | `B:../BACKUP.000/NAMES.CSV` | `B:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | + +use std::{cell::RefCell, io::prelude::*}; + +use embedded_sdmmc::{ + Error as EsError, LfnBuffer, Mode, RawDirectory, RawVolume, ShortFileName, VolumeIdx, +}; + +type VolumeManager = embedded_sdmmc::VolumeManager; +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 8, 4>; + +use crate::linux::{Clock, LinuxBlockDevice}; + +type Error = EsError; + +mod linux; + +/// Represents a path on a volume within `embedded_sdmmc`. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +struct Path(str); + +impl std::ops::Deref for Path { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Path { + /// Create a new Path from a string slice. + /// + /// The `Path` borrows the string slice. No validation is performed on the + /// path. + fn new + ?Sized>(s: &S) -> &Path { + unsafe { &*(s.as_ref() as *const str as *const Path) } + } + + /// Does this path specify a volume? + fn volume(&self) -> Option { + let mut char_iter = self.chars(); + match (char_iter.next(), char_iter.next()) { + (Some(volume), Some(':')) => Some(volume), + _ => None, + } + } + + /// Is this an absolute path? + fn is_absolute(&self) -> bool { + let tail = self.without_volume(); + tail.starts_with('/') + } + + /// Iterate through the directory components. + /// + /// This will exclude the final path component (i.e. it will not include the + /// 'basename'). + fn iterate_dirs(&self) -> impl Iterator { + let path = self.without_volume(); + let path = path.strip_prefix('/').unwrap_or(path); + if let Some((directories, _basename)) = path.rsplit_once('/') { + directories.split('/') + } else { + "".split('/') + } + } + + /// Iterate through all the components. + /// + /// This will include the final path component (i.e. it will include the + /// 'basename'). + fn iterate_components(&self) -> impl Iterator { + let path = self.without_volume(); + let path = path.strip_prefix('/').unwrap_or(path); + path.split('/') + } + + /// Get the final component of this path (the 'basename'). + fn basename(&self) -> Option<&str> { + if let Some((_, basename)) = self.rsplit_once('/') { + if basename.is_empty() { + None + } else { + Some(basename) + } + } else { + let path = self.without_volume(); + Some(path) + } + } + + /// Return this [`Path`], but without a leading volume. + fn without_volume(&self) -> &Path { + if let Some((volume, tail)) = self.split_once(':') { + // only support single char drive letters + if volume.chars().count() == 1 { + return Path::new(tail); + } + } + self + } +} + +impl PartialEq for Path { + fn eq(&self, other: &str) -> bool { + let s: &str = self; + s == other + } +} + +struct VolumeState { + directory: RawDirectory, + volume: RawVolume, + path: Vec, +} + +struct Context { + volume_mgr: VolumeManager, + volumes: RefCell<[Option; 4]>, + current_volume: usize, +} + +impl Context { + fn current_path(&self) -> Vec { + let Some(s) = &self.volumes.borrow()[self.current_volume] else { + return vec![]; + }; + s.path.clone() + } + + /// Print some help text + fn help(&self) -> Result<(), Error> { + println!("Commands:"); + println!("\thelp -> this help text"); + println!("\t: -> change volume/partition"); + println!("\tstat -> print volume manager status"); + println!("\tdir [] -> do a directory listing"); + println!("\ttree [] -> do a recursive directory listing"); + println!("\tcd .. -> go up a level"); + println!("\tcd -> change into directory "); + println!("\tcat -> print a text file"); + println!("\thexdump -> print a binary file"); + println!("\tmkdir -> create an empty directory"); + println!("\tquit -> exits the program"); + println!(); + println!("Paths can be:"); + println!(); + println!("\t* Bare names, like `FILE.DAT`"); + println!("\t* Relative, like `../SOMEDIR/FILE.DAT` or `./FILE.DAT`"); + println!("\t* Absolute, like `B:/SOMEDIR/FILE.DAT`"); + Ok(()) + } + + /// Print volume manager status + fn stat(&self) -> Result<(), Error> { + println!("Status:\n{:#?}", self.volume_mgr); + Ok(()) + } + + /// Print a directory listing + fn dir(&self, path: &Path) -> Result<(), Error> { + println!("Directory listing of {:?}", path); + let dir = self.resolve_existing_directory(path)?; + let mut storage = [0u8; 128]; + let mut lfn_buffer = LfnBuffer::new(&mut storage); + dir.iterate_dir_lfn(&mut lfn_buffer, |entry, lfn| { + if !entry.attributes.is_volume() { + print!( + "{:12} {:9} {} {} {:08X?} {:5?}", + entry.name, + entry.size, + entry.ctime, + entry.mtime, + entry.cluster, + entry.attributes, + ); + if let Some(lfn) = lfn { + println!(" {:?}", lfn); + } else { + println!(); + } + } + })?; + Ok(()) + } + + /// Print a recursive directory listing for the given path + fn tree(&self, path: &Path) -> Result<(), Error> { + println!("Directory listing of {:?}", path); + let dir = self.resolve_existing_directory(path)?; + // tree_dir will close this directory, always + Self::tree_dir(dir) + } + + /// Print a recursive directory listing for the given open directory. + /// + /// Will close the given directory. + fn tree_dir(dir: Directory) -> Result<(), Error> { + let mut children = Vec::new(); + dir.iterate_dir(|entry| { + println!( + "{:12} {:9} {} {} {:08X?} {:?}", + entry.name, entry.size, entry.ctime, entry.mtime, entry.cluster, entry.attributes + ); + if entry.attributes.is_directory() + && entry.name != ShortFileName::this_dir() + && entry.name != ShortFileName::parent_dir() + { + children.push(entry.name.clone()); + } + })?; + for child in children { + println!("Entering {}", child); + let child_dir = dir.open_dir(&child)?; + Self::tree_dir(child_dir)?; + println!("Returning from {}", child); + } + Ok(()) + } + + /// Change into `` + /// + /// * An arg of `..` goes up one level + /// * A relative arg like `../FOO` goes up a level and then into the `FOO` + /// sub-folder, starting from the current directory on the current volume + /// * An absolute path like `B:/FOO` changes the CWD on Volume 1 to path + /// `/FOO` + fn cd(&self, full_path: &Path) -> Result<(), Error> { + let volume_idx = self.resolve_volume(full_path)?; + let (mut d, fragment) = self.resolve_filename(full_path)?; + d.change_dir(fragment)?; + let Some(s) = &mut self.volumes.borrow_mut()[volume_idx] else { + return Err(Error::NoSuchVolume); + }; + self.volume_mgr + .close_dir(s.directory) + .expect("close open dir"); + s.directory = d.to_raw_directory(); + if full_path.is_absolute() { + s.path.clear(); + } + for fragment in full_path.iterate_components().filter(|s| !s.is_empty()) { + if fragment == ".." { + s.path.pop(); + } else if fragment == "." { + // do nothing + } else { + s.path.push(fragment.to_owned()); + } + } + Ok(()) + } + + /// print a text file + fn cat(&self, filename: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(filename)?; + let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?; + let mut data = Vec::new(); + while !f.is_eof() { + let mut buffer = vec![0u8; 65536]; + let n = f.read(&mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + if let Ok(s) = std::str::from_utf8(&data) { + println!("{}", s); + } else { + println!("I'm afraid that file isn't UTF-8 encoded"); + } + Ok(()) + } + + /// print a binary file + fn hexdump(&self, filename: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(filename)?; + let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?; + let mut data = Vec::new(); + while !f.is_eof() { + let mut buffer = vec![0u8; 65536]; + let n = f.read(&mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + for (idx, chunk) in data.chunks(16).enumerate() { + print!("{:08x} | ", idx * 16); + for b in chunk { + print!("{:02x} ", b); + } + for _padding in 0..(16 - chunk.len()) { + print!(" "); + } + print!("| "); + for b in chunk { + print!( + "{}", + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + ); + } + println!(); + } + Ok(()) + } + + /// create a directory + fn mkdir(&self, dir_name: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(dir_name)?; + dir.make_dir_in_dir(filename) + } + + fn process_line(&mut self, line: &str) -> Result<(), Error> { + if line == "help" { + self.help()?; + } else if line == "A:" || line == "a:" { + self.current_volume = 0; + } else if line == "B:" || line == "b:" { + self.current_volume = 1; + } else if line == "C:" || line == "c:" { + self.current_volume = 2; + } else if line == "D:" || line == "d:" { + self.current_volume = 3; + } else if line == "dir" { + self.dir(Path::new("."))?; + } else if let Some(path) = line.strip_prefix("dir ") { + self.dir(Path::new(path.trim()))?; + } else if line == "tree" { + self.tree(Path::new("."))?; + } else if let Some(path) = line.strip_prefix("tree ") { + self.tree(Path::new(path.trim()))?; + } else if line == "stat" { + self.stat()?; + } else if let Some(path) = line.strip_prefix("cd ") { + self.cd(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("cat ") { + self.cat(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("hexdump ") { + self.hexdump(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("mkdir ") { + self.mkdir(Path::new(path.trim()))?; + } else { + println!("Unknown command {line:?} - try 'help' for help"); + } + Ok(()) + } + + /// Resolves an existing directory. + /// + /// Converts a string path into a directory handle. + /// + /// * Bare names (no leading `.`, `/` or `N:/`) are mapped to the current + /// directory in the current volume. + /// * Relative names, like `../SOMEDIR` or `./SOMEDIR`, traverse + /// starting at the current volume and directory. + /// * Absolute, like `B:/SOMEDIR/OTHERDIR` start at the given volume. + fn resolve_existing_directory<'a>(&'a self, full_path: &Path) -> Result, Error> { + let (mut dir, fragment) = self.resolve_filename(full_path)?; + dir.change_dir(fragment)?; + Ok(dir) + } + + /// Either get the volume from the path, or pick the current volume. + fn resolve_volume(&self, path: &Path) -> Result { + match path.volume() { + None => Ok(self.current_volume), + Some('A' | 'a') => Ok(0), + Some('B' | 'b') => Ok(1), + Some('C' | 'c') => Ok(2), + Some('D' | 'd') => Ok(3), + Some(_) => Err(Error::NoSuchVolume), + } + } + + /// Resolves a filename. + /// + /// Converts a string path into a directory handle and a name within that + /// directory (that may or may not exist). + /// + /// * Bare names (no leading `.`, `/` or `N:/`) are mapped to the current + /// directory in the current volume. + /// * Relative names, like `../SOMEDIR/SOMEFILE` or `./SOMEDIR/SOMEFILE`, traverse + /// starting at the current volume and directory. + /// * Absolute, like `B:/SOMEDIR/SOMEFILE` start at the given volume. + fn resolve_filename<'a, 'path>( + &'a self, + full_path: &'path Path, + ) -> Result<(Directory<'a>, &'path str), Error> { + let volume_idx = self.resolve_volume(full_path)?; + let Some(s) = &self.volumes.borrow()[volume_idx] else { + return Err(Error::NoSuchVolume); + }; + let mut work_dir = if full_path.is_absolute() { + // relative to root + self.volume_mgr + .open_root_dir(s.volume)? + .to_directory(&self.volume_mgr) + } else { + // relative to CWD + self.volume_mgr + .open_dir(s.directory, ".")? + .to_directory(&self.volume_mgr) + }; + + for fragment in full_path.iterate_dirs() { + work_dir.change_dir(fragment)?; + } + Ok((work_dir, full_path.basename().unwrap_or("."))) + } + + /// Convert a volume index to a letter + fn volume_to_letter(volume: usize) -> char { + match volume { + 0 => 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + _ => panic!("Invalid volume ID"), + } + } +} + +impl Drop for Context { + fn drop(&mut self) { + for v in self.volumes.borrow_mut().iter_mut() { + if let Some(v) = v { + println!("Closing directory {:?}", v.directory); + self.volume_mgr + .close_dir(v.directory) + .expect("Closing directory"); + println!("Closing volume {:?}", v.volume); + self.volume_mgr + .close_volume(v.volume) + .expect("Closing volume"); + } + *v = None; + } + } +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut args = std::env::args().skip(1); + let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); + let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + println!("Opening '{filename}'..."); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; + let stdin = std::io::stdin(); + + let mut ctx = Context { + volume_mgr: VolumeManager::new_with_limits(lbd, Clock, 100), + volumes: RefCell::new([None, None, None, None]), + current_volume: 0, + }; + + let mut current_volume = None; + for volume_no in 0..4 { + match ctx.volume_mgr.open_raw_volume(VolumeIdx(volume_no)) { + Ok(volume) => { + println!( + "Volume # {}: found, label: {:?}", + Context::volume_to_letter(volume_no), + ctx.volume_mgr.get_root_volume_label(volume)? + ); + match ctx.volume_mgr.open_root_dir(volume) { + Ok(root_dir) => { + ctx.volumes.borrow_mut()[volume_no] = Some(VolumeState { + directory: root_dir, + volume, + path: vec![], + }); + if current_volume.is_none() { + current_volume = Some(volume_no); + } + } + Err(e) => { + println!("Failed to open root directory: {e:?}"); + ctx.volume_mgr.close_volume(volume).expect("close volume"); + } + } + } + Err(e) => { + println!("Failed to open volume {volume_no}: {e:?}"); + } + } + } + + match current_volume { + Some(n) => { + // Default to the first valid partition + ctx.current_volume = n; + } + None => { + println!("No volumes found in file. Sorry."); + return Ok(()); + } + }; + + loop { + print!("{}:/", Context::volume_to_letter(ctx.current_volume)); + print!("{}", ctx.current_path().join("/")); + print!("> "); + std::io::stdout().flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + if line == "quit" { + break; + } else if let Err(e) = ctx.process_line(line) { + println!("Error: {:?}", e); + } + } + + println!("Bye!"); + Ok(()) +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/blockdevice.rs b/examples/ios/embedded-sdmmc/src/blockdevice.rs new file mode 100644 index 0000000..674ae55 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/blockdevice.rs @@ -0,0 +1,315 @@ +//! Traits and types for working with Block Devices. +//! +//! Generic code for handling block devices, such as types for identifying +//! a particular block on a block device by its index. + +/// A standard 512 byte block (also known as a sector). +/// +/// IBM PC formatted 5.25" and 3.5" floppy disks, IDE/SATA Hard Drives up to +/// about 2 TiB, and almost all SD/MMC cards have 512 byte blocks. +/// +/// This library does not support devices with a block size other than 512 +/// bytes. +#[derive(Clone)] +pub struct Block { + /// The 512 bytes in this block (or sector). + pub contents: [u8; Block::LEN], +} + +impl Block { + /// All our blocks are a fixed length of 512 bytes. We do not support + /// 'Advanced Format' Hard Drives with 4 KiB blocks, nor weird old + /// pre-3.5-inch floppy disk formats. + pub const LEN: usize = 512; + + /// Sometimes we want `LEN` as a `u32` and the casts don't look nice. + pub const LEN_U32: u32 = 512; + + /// Create a new block full of zeros. + pub fn new() -> Block { + Block { + contents: [0u8; Self::LEN], + } + } +} + +impl core::ops::Deref for Block { + type Target = [u8; 512]; + fn deref(&self) -> &[u8; 512] { + &self.contents + } +} + +impl core::ops::DerefMut for Block { + fn deref_mut(&mut self) -> &mut [u8; 512] { + &mut self.contents + } +} + +impl core::fmt::Debug for Block { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { + writeln!(fmt, "Block:")?; + for line in self.contents.chunks(32) { + for b in line { + write!(fmt, "{:02x}", b)?; + } + write!(fmt, " ")?; + for &b in line { + if (0x20..=0x7F).contains(&b) { + write!(fmt, "{}", b as char)?; + } else { + write!(fmt, ".")?; + } + } + writeln!(fmt)?; + } + Ok(()) + } +} + +impl Default for Block { + fn default() -> Self { + Self::new() + } +} + +/// A block device - a device which can read and write blocks (or +/// sectors). Only supports devices which are <= 2 TiB in size. +pub trait BlockDevice { + /// The errors that the `BlockDevice` can return. Must be debug formattable. + type Error: core::fmt::Debug; + /// Read one or more blocks, starting at the given block index. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; + /// Write one or more blocks, starting at the given block index. + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result; +} + +/// A caching layer for block devices +/// +/// Caches a single block. +#[derive(Debug)] +pub struct BlockCache { + block_device: D, + block: [Block; 1], + block_idx: Option, +} + +impl BlockCache +where + D: BlockDevice, +{ + /// Create a new block cache + pub fn new(block_device: D) -> BlockCache { + BlockCache { + block_device, + block: [Block::new()], + block_idx: None, + } + } + + /// Read a block, and return a reference to it. + pub fn read(&mut self, block_idx: BlockIdx) -> Result<&Block, D::Error> { + if self.block_idx != Some(block_idx) { + self.block_idx = None; + self.block_device.read(&mut self.block, block_idx)?; + self.block_idx = Some(block_idx); + } + Ok(&self.block[0]) + } + + /// Read a block, and return a reference to it. + pub fn read_mut(&mut self, block_idx: BlockIdx) -> Result<&mut Block, D::Error> { + if self.block_idx != Some(block_idx) { + self.block_idx = None; + self.block_device.read(&mut self.block, block_idx)?; + self.block_idx = Some(block_idx); + } + Ok(&mut self.block[0]) + } + + /// Write back a block you read with [`Self::read_mut`] and then modified. + pub fn write_back(&mut self) -> Result<(), D::Error> { + self.block_device.write( + &self.block, + self.block_idx.expect("write_back with no read"), + ) + } + + /// Write back a block you read with [`Self::read_mut`] and then modified, but to two locations. + /// + /// This is useful for updating two File Allocation Tables. + pub fn write_back_with_duplicate(&mut self, duplicate: BlockIdx) -> Result<(), D::Error> { + self.block_device.write( + &self.block, + self.block_idx.expect("write_back with no read"), + )?; + self.block_device.write(&self.block, duplicate)?; + Ok(()) + } + + /// Access a blank sector + pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block { + self.block_idx = Some(block_idx); + self.block[0].fill(0); + &mut self.block[0] + } + + /// Access the block device + pub fn block_device(&mut self) -> &mut D { + // invalidate the cache + self.block_idx = None; + // give them the block device + &mut self.block_device + } + + /// Get the block device back + pub fn free(self) -> D { + self.block_device + } +} + +/// The linear numeric address of a block (or sector). +/// +/// The first block on a disk gets `BlockIdx(0)` (which usually contains the +/// Master Boot Record). +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockIdx(pub u32); + +impl BlockIdx { + /// Convert a block index into a 64-bit byte offset from the start of the + /// volume. Useful if your underlying block device actually works in + /// bytes, like `open("/dev/mmcblk0")` does on Linux. + pub fn into_bytes(self) -> u64 { + (u64::from(self.0)) * (Block::LEN as u64) + } + + /// Create an iterator from the current `BlockIdx` through the given + /// number of blocks. + pub fn range(self, num: BlockCount) -> BlockIter { + BlockIter::new(self, self + BlockCount(num.0)) + } +} + +impl core::ops::Add for BlockIdx { + type Output = BlockIdx; + fn add(self, rhs: BlockCount) -> BlockIdx { + BlockIdx(self.0 + rhs.0) + } +} + +impl core::ops::AddAssign for BlockIdx { + fn add_assign(&mut self, rhs: BlockCount) { + self.0 += rhs.0 + } +} + +impl core::ops::Sub for BlockIdx { + type Output = BlockIdx; + fn sub(self, rhs: BlockCount) -> BlockIdx { + BlockIdx(self.0 - rhs.0) + } +} + +impl core::ops::SubAssign for BlockIdx { + fn sub_assign(&mut self, rhs: BlockCount) { + self.0 -= rhs.0 + } +} + +/// The a number of blocks (or sectors). +/// +/// Add this to a `BlockIdx` to get an actual address on disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockCount(pub u32); + +impl core::ops::Add for BlockCount { + type Output = BlockCount; + fn add(self, rhs: BlockCount) -> BlockCount { + BlockCount(self.0 + rhs.0) + } +} + +impl core::ops::AddAssign for BlockCount { + fn add_assign(&mut self, rhs: BlockCount) { + self.0 += rhs.0 + } +} + +impl core::ops::Sub for BlockCount { + type Output = BlockCount; + fn sub(self, rhs: BlockCount) -> BlockCount { + BlockCount(self.0 - rhs.0) + } +} + +impl core::ops::SubAssign for BlockCount { + fn sub_assign(&mut self, rhs: BlockCount) { + self.0 -= rhs.0 + } +} + +impl BlockCount { + /// How many blocks are required to hold this many bytes. + /// + /// ``` + /// # use embedded_sdmmc::BlockCount; + /// assert_eq!(BlockCount::from_bytes(511), BlockCount(1)); + /// assert_eq!(BlockCount::from_bytes(512), BlockCount(1)); + /// assert_eq!(BlockCount::from_bytes(513), BlockCount(2)); + /// assert_eq!(BlockCount::from_bytes(1024), BlockCount(2)); + /// assert_eq!(BlockCount::from_bytes(1025), BlockCount(3)); + /// ``` + pub const fn from_bytes(byte_count: u32) -> BlockCount { + let mut count = byte_count / Block::LEN_U32; + if (count * Block::LEN_U32) != byte_count { + count += 1; + } + BlockCount(count) + } + + /// Take a number of blocks and increment by the integer number of blocks + /// required to get to the block that holds the byte at the given offset. + pub fn offset_bytes(self, offset: u32) -> Self { + BlockCount(self.0 + (offset / Block::LEN_U32)) + } +} + +/// An iterator returned from `Block::range`. +pub struct BlockIter { + inclusive_end: BlockIdx, + current: BlockIdx, +} + +impl BlockIter { + /// Create a new `BlockIter`, from the given start block, through (and + /// including) the given end block. + pub const fn new(start: BlockIdx, inclusive_end: BlockIdx) -> BlockIter { + BlockIter { + inclusive_end, + current: start, + } + } +} + +impl core::iter::Iterator for BlockIter { + type Item = BlockIdx; + fn next(&mut self) -> Option { + if self.current.0 >= self.inclusive_end.0 { + None + } else { + let this = self.current; + self.current += BlockCount(1); + Some(this) + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/bpb.rs b/examples/ios/embedded-sdmmc/src/fat/bpb.rs new file mode 100644 index 0000000..c7e83b6 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/bpb.rs @@ -0,0 +1,140 @@ +//! Boot Parameter Block + +use crate::{ + blockdevice::BlockCount, + fat::{FatType, OnDiskDirEntry}, +}; +use byteorder::{ByteOrder, LittleEndian}; + +/// A Boot Parameter Block. +/// +/// This is the first sector of a FAT formatted partition, and it describes +/// various properties of the FAT filesystem. +pub struct Bpb<'a> { + data: &'a [u8; 512], + pub(crate) fat_type: FatType, + cluster_count: u32, +} + +impl<'a> Bpb<'a> { + pub(crate) const FOOTER_VALUE: u16 = 0xAA55; + + /// Attempt to parse a Boot Parameter Block from a 512 byte sector. + pub fn create_from_bytes(data: &[u8; 512]) -> Result { + let mut bpb = Bpb { + data, + fat_type: FatType::Fat16, + cluster_count: 0, + }; + if bpb.footer() != Self::FOOTER_VALUE { + return Err("Bad BPB footer"); + } + + let root_dir_blocks = + BlockCount::from_bytes(u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32).0; + let non_data_blocks = u32::from(bpb.reserved_block_count()) + + (u32::from(bpb.num_fats()) * bpb.fat_size()) + + root_dir_blocks; + let data_blocks = bpb.total_blocks() - non_data_blocks; + bpb.cluster_count = data_blocks / u32::from(bpb.blocks_per_cluster()); + if bpb.cluster_count < 4085 { + return Err("FAT12 is unsupported"); + } else if bpb.cluster_count < 65525 { + bpb.fat_type = FatType::Fat16; + } else { + bpb.fat_type = FatType::Fat32; + } + + match bpb.fat_type { + FatType::Fat16 => Ok(bpb), + FatType::Fat32 if bpb.fs_ver() == 0 => { + // Only support FAT32 version 0.0 + Ok(bpb) + } + _ => Err("Invalid FAT format"), + } + } + + // FAT16/FAT32 + define_field!(bytes_per_block, u16, 11); + define_field!(blocks_per_cluster, u8, 13); + define_field!(reserved_block_count, u16, 14); + define_field!(num_fats, u8, 16); + define_field!(root_entries_count, u16, 17); + define_field!(total_blocks16, u16, 19); + define_field!(media, u8, 21); + define_field!(fat_size16, u16, 22); + define_field!(blocks_per_track, u16, 24); + define_field!(num_heads, u16, 26); + define_field!(hidden_blocks, u32, 28); + define_field!(total_blocks32, u32, 32); + define_field!(footer, u16, 510); + + // FAT32 only + define_field!(fat_size32, u32, 36); + define_field!(fs_ver, u16, 42); + define_field!(first_root_dir_cluster, u32, 44); + define_field!(fs_info, u16, 48); + define_field!(backup_boot_block, u16, 50); + + /// Get the OEM name string for this volume + pub fn oem_name(&self) -> &[u8] { + &self.data[3..11] + } + + // FAT16/FAT32 functions + + /// Get the Volume Label string for this volume + pub fn volume_label(&self) -> [u8; 11] { + let mut result = [0u8; 11]; + match self.fat_type { + FatType::Fat16 => result.copy_from_slice(&self.data[43..=53]), + FatType::Fat32 => result.copy_from_slice(&self.data[71..=81]), + } + result + } + + // FAT32 only functions + + /// On a FAT32 volume, return the free block count from the Info Block. On + /// a FAT16 volume, returns None. + pub fn fs_info_block(&self) -> Option { + match self.fat_type { + FatType::Fat16 => None, + FatType::Fat32 => Some(BlockCount(u32::from(self.fs_info()))), + } + } + + // Magic functions that get the right FAT16/FAT32 result + + /// Get the size of the File Allocation Table in blocks. + pub fn fat_size(&self) -> u32 { + let result = u32::from(self.fat_size16()); + if result != 0 { + result + } else { + self.fat_size32() + } + } + + /// Get the total number of blocks in this filesystem. + pub fn total_blocks(&self) -> u32 { + let result = u32::from(self.total_blocks16()); + if result != 0 { + result + } else { + self.total_blocks32() + } + } + + /// Get the total number of clusters in this filesystem. + pub fn total_clusters(&self) -> u32 { + self.cluster_count + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/info.rs b/examples/ios/embedded-sdmmc/src/fat/info.rs new file mode 100644 index 0000000..f9f8e2c --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/info.rs @@ -0,0 +1,94 @@ +use crate::{BlockCount, BlockIdx, ClusterId}; +use byteorder::{ByteOrder, LittleEndian}; + +/// Indentifies the supported types of FAT format +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum FatSpecificInfo { + /// Fat16 Format + Fat16(Fat16Info), + /// Fat32 Format + Fat32(Fat32Info), +} + +/// FAT32 specific data +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Fat32Info { + /// The root directory does not have a reserved area in FAT32. This is the + /// cluster it starts in (nominally 2). + pub(crate) first_root_dir_cluster: ClusterId, + /// Block idx of the info sector + pub(crate) info_location: BlockIdx, +} + +/// FAT16 specific data +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Fat16Info { + /// The block the root directory starts in. Relative to start of partition + /// (so add `self.lba_offset` before passing to volume manager) + pub(crate) first_root_dir_block: BlockCount, + /// Number of entries in root directory (it's reserved and not in the FAT) + pub(crate) root_entries_count: u16, +} + +/// File System Information structure is only present on FAT32 partitions. It +/// may contain a valid number of free clusters and the number of the next +/// free cluster. The information contained in the structure must be +/// considered as advisory only. File system driver implementations are not +/// required to ensure that information within the structure is kept +/// consistent. +pub struct InfoSector<'a> { + data: &'a [u8; 512], +} + +impl<'a> InfoSector<'a> { + const LEAD_SIG: u32 = 0x4161_5252; + const STRUC_SIG: u32 = 0x6141_7272; + const TRAIL_SIG: u32 = 0xAA55_0000; + + /// Try and create a new Info Sector from a block. + pub fn create_from_bytes(data: &[u8; 512]) -> Result { + let info = InfoSector { data }; + if info.lead_sig() != Self::LEAD_SIG { + return Err("Bad lead signature on InfoSector"); + } + if info.struc_sig() != Self::STRUC_SIG { + return Err("Bad struc signature on InfoSector"); + } + if info.trail_sig() != Self::TRAIL_SIG { + return Err("Bad trail signature on InfoSector"); + } + Ok(info) + } + + define_field!(lead_sig, u32, 0); + define_field!(struc_sig, u32, 484); + define_field!(free_count, u32, 488); + define_field!(next_free, u32, 492); + define_field!(trail_sig, u32, 508); + + /// Return how many free clusters are left in this volume, if known. + pub fn free_clusters_count(&self) -> Option { + match self.free_count() { + 0xFFFF_FFFF => None, + n => Some(n), + } + } + + /// Return the number of the next free cluster, if known. + pub fn next_free_cluster(&self) -> Option { + match self.next_free() { + // 0 and 1 are reserved clusters + 0xFFFF_FFFF | 0 | 1 => None, + n => Some(ClusterId(n)), + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/mod.rs b/examples/ios/embedded-sdmmc/src/fat/mod.rs new file mode 100644 index 0000000..c27a8cd --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/mod.rs @@ -0,0 +1,368 @@ +//! FAT16/FAT32 file system implementation +//! +//! Implements the File Allocation Table file system. Supports FAT16 and FAT32 volumes. + +/// Number of entries reserved at the start of a File Allocation Table +pub const RESERVED_ENTRIES: u32 = 2; + +/// Indentifies the supported types of FAT format +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FatType { + /// FAT16 Format + Fat16, + /// FAT32 Format + Fat32, +} + +mod bpb; +mod info; +mod ondiskdirentry; +mod volume; + +pub use bpb::Bpb; +pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; +pub use ondiskdirentry::OnDiskDirEntry; +pub use volume::{parse_volume, FatVolume, VolumeName}; + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod test { + + use super::*; + use crate::{Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; + + fn parse(input: &str) -> Vec { + let mut output = Vec::new(); + for line in input.lines() { + let line = line.trim(); + if !line.is_empty() { + // 32 bytes per line + for index in 0..32 { + let start = index * 2; + let end = start + 1; + let piece = &line[start..=end]; + let value = u8::from_str_radix(piece, 16).unwrap(); + output.push(value); + } + } + } + output + } + + /// This is the first block of this directory listing. + /// total 19880 + /// -rw-r--r-- 1 jonathan jonathan 10841 2016-03-01 19:56:36.000000000 +0000 bcm2708-rpi-b.dtb + /// -rw-r--r-- 1 jonathan jonathan 11120 2016-03-01 19:56:34.000000000 +0000 bcm2708-rpi-b-plus.dtb + /// -rw-r--r-- 1 jonathan jonathan 10871 2016-03-01 19:56:36.000000000 +0000 bcm2708-rpi-cm.dtb + /// -rw-r--r-- 1 jonathan jonathan 12108 2016-03-01 19:56:36.000000000 +0000 bcm2709-rpi-2-b.dtb + /// -rw-r--r-- 1 jonathan jonathan 12575 2016-03-01 19:56:36.000000000 +0000 bcm2710-rpi-3-b.dtb + /// -rw-r--r-- 1 jonathan jonathan 17920 2016-03-01 19:56:38.000000000 +0000 bootcode.bin + /// -rw-r--r-- 1 jonathan jonathan 136 2015-11-21 20:28:30.000000000 +0000 cmdline.txt + /// -rw-r--r-- 1 jonathan jonathan 1635 2015-11-21 20:28:30.000000000 +0000 config.txt + /// -rw-r--r-- 1 jonathan jonathan 18693 2016-03-01 19:56:30.000000000 +0000 COPYING.linux + /// -rw-r--r-- 1 jonathan jonathan 2505 2016-03-01 19:56:38.000000000 +0000 fixup_cd.dat + /// -rw-r--r-- 1 jonathan jonathan 6481 2016-03-01 19:56:38.000000000 +0000 fixup.dat + /// -rw-r--r-- 1 jonathan jonathan 9722 2016-03-01 19:56:38.000000000 +0000 fixup_db.dat + /// -rw-r--r-- 1 jonathan jonathan 9724 2016-03-01 19:56:38.000000000 +0000 fixup_x.dat + /// -rw-r--r-- 1 jonathan jonathan 110 2015-11-21 21:32:06.000000000 +0000 issue.txt + /// -rw-r--r-- 1 jonathan jonathan 4046732 2016-03-01 19:56:40.000000000 +0000 kernel7.img + /// -rw-r--r-- 1 jonathan jonathan 3963140 2016-03-01 19:56:38.000000000 +0000 kernel.img + /// -rw-r--r-- 1 jonathan jonathan 1494 2016-03-01 19:56:34.000000000 +0000 LICENCE.broadcom + /// -rw-r--r-- 1 jonathan jonathan 18974 2015-11-21 21:32:06.000000000 +0000 LICENSE.oracle + /// drwxr-xr-x 2 jonathan jonathan 8192 2016-03-01 19:56:54.000000000 +0000 overlays + /// -rw-r--r-- 1 jonathan jonathan 612472 2016-03-01 19:56:40.000000000 +0000 start_cd.elf + /// -rw-r--r-- 1 jonathan jonathan 4888200 2016-03-01 19:56:42.000000000 +0000 start_db.elf + /// -rw-r--r-- 1 jonathan jonathan 2739672 2016-03-01 19:56:40.000000000 +0000 start.elf + /// -rw-r--r-- 1 jonathan jonathan 3840328 2016-03-01 19:56:44.000000000 +0000 start_x.elf + /// drwxr-xr-x 2 jonathan jonathan 8192 2015-12-05 21:55:06.000000000 +0000 'System Volume Information' + #[test] + fn test_dir_entries() { + #[derive(Debug)] + enum Expected { + Lfn(bool, u8, u8, [u16; 13]), + Short(DirEntry), + } + let raw_data = r#" + 626f6f7420202020202020080000699c754775470000699c7547000000000000 boot ...i.uGuG..i.uG...... + 416f007600650072006c000f00476100790073000000ffffffff0000ffffffff Ao.v.e.r.l...Ga.y.s............. + 4f5645524c4159532020201000001b9f6148614800001b9f6148030000000000 OVERLAYS .....aHaH....aH...... + 422d0070006c00750073000f00792e006400740062000000ffff0000ffffffff B-.p.l.u.s...y..d.t.b........... + 01620063006d00320037000f0079300038002d0072007000690000002d006200 .b.c.m.2.7...y0.8.-.r.p.i...-.b. + 42434d3237307e31445442200064119f614861480000119f61480900702b0000 BCM270~1DTB .d..aHaH....aH..p+.. + 4143004f005000590049000f00124e0047002e006c0069006e00000075007800 AC.O.P.Y.I....N.G...l.i.n...u.x. + 434f5059494e7e314c494e2000000f9f6148614800000f9f6148050005490000 COPYIN~1LIN ....aHaH....aH...I.. + 4263006f006d000000ffff0f0067ffffffffffffffffffffffff0000ffffffff Bc.o.m.......g.................. + 014c004900430045004e000f0067430045002e00620072006f00000061006400 .L.I.C.E.N...gC.E...b.r.o...a.d. + 4c4943454e437e3142524f200000119f614861480000119f61480800d6050000 LICENC~1BRO ....aHaH....aH...... + 422d0062002e00640074000f001962000000ffffffffffffffff0000ffffffff B-.b...d.t....b................. + 01620063006d00320037000f0019300039002d0072007000690000002d003200 .b.c.m.2.7....0.9.-.r.p.i...-.2. + 42434d3237307e34445442200064129f614861480000129f61480f004c2f0000 BCM270~4DTB .d..aHaH....aH..L/.. + 422e0064007400620000000f0059ffffffffffffffffffffffff0000ffffffff B..d.t.b.....Y.................. + 01620063006d00320037000f0059300038002d0072007000690000002d006200 .b.c.m.2.7...Y0.8.-.r.p.i...-.b. + "#; + + let results = [ + Expected::Short(DirEntry { + name: unsafe { + VolumeName::create_from_str("boot") + .unwrap() + .to_short_filename() + }, + mtime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), + ctime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), + attributes: Attributes::create_from_fat(Attributes::VOLUME), + cluster: ClusterId(0), + size: 0, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 1, + 0x47, + [ + 'o' as u16, 'v' as u16, 'e' as u16, 'r' as u16, 'l' as u16, 'a' as u16, + 'y' as u16, 's' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("OVERLAYS").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 54).unwrap(), + attributes: Attributes::create_from_fat(Attributes::DIRECTORY), + cluster: ClusterId(3), + size: 0, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x79, + [ + '-' as u16, 'p' as u16, 'l' as u16, 'u' as u16, 's' as u16, '.' as u16, + 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x79, + [ + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + 'b' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("BCM270~1.DTB").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(9), + size: 11120, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 1, + 0x12, + [ + 'C' as u16, 'O' as u16, 'P' as u16, 'Y' as u16, 'I' as u16, 'N' as u16, + 'G' as u16, '.' as u16, 'l' as u16, 'i' as u16, 'n' as u16, 'u' as u16, + 'x' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("COPYIN~1.LIN").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 30).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(5), + size: 18693, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x67, + [ + 'c' as u16, + 'o' as u16, + 'm' as u16, + '\u{0}' as u16, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x67, + [ + 'L' as u16, 'I' as u16, 'C' as u16, 'E' as u16, 'N' as u16, 'C' as u16, + 'E' as u16, '.' as u16, 'b' as u16, 'r' as u16, 'o' as u16, 'a' as u16, + 'd' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("LICENC~1.BRO").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 34).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(8), + size: 1494, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x19, + [ + '-' as u16, 'b' as u16, '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x19, + [ + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '9' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + '2' as u16, + ], + ), + Expected::Short(DirEntry { + name: ShortFileName::create_from_str("BCM270~4.DTB").unwrap(), + mtime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), + ctime: Timestamp::from_calendar(2016, 3, 1, 19, 56, 36).unwrap(), + attributes: Attributes::create_from_fat(Attributes::ARCHIVE), + cluster: ClusterId(15), + size: 12108, + entry_block: BlockIdx(0), + entry_offset: 0, + }), + Expected::Lfn( + true, + 2, + 0x59, + [ + '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + ], + ), + Expected::Lfn( + false, + 1, + 0x59, + [ + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + 'b' as u16, + ], + ), + ]; + + let data = parse(raw_data); + for (part, expected) in data.chunks(OnDiskDirEntry::LEN).zip(results.iter()) { + let on_disk_entry = OnDiskDirEntry::new(part); + match expected { + Expected::Lfn(start, index, csum, contents) if on_disk_entry.is_lfn() => { + let (calc_start, calc_index, calc_csum, calc_contents) = + on_disk_entry.lfn_contents().unwrap(); + assert_eq!(*start, calc_start); + assert_eq!(*index, calc_index); + assert_eq!(*contents, calc_contents); + assert_eq!(*csum, calc_csum); + } + Expected::Short(expected_entry) if !on_disk_entry.is_lfn() => { + let parsed_entry = on_disk_entry.get_entry(FatType::Fat32, BlockIdx(0), 0); + assert_eq!(*expected_entry, parsed_entry); + } + _ => { + panic!( + "Bad dir entry, expected:\n{:#?}\nhad\n{:#?}", + expected, on_disk_entry + ); + } + } + } + } + + #[test] + fn test_bpb() { + // Taken from a Raspberry Pi bootable SD-Card + const BPB_EXAMPLE: [u8; 512] = hex!( + "EB 3C 90 6D 6B 66 73 2E 66 61 74 00 02 10 01 00 + 02 00 02 00 00 F8 20 00 3F 00 FF 00 00 00 00 00 + 00 E0 01 00 80 01 29 BB B0 71 77 62 6F 6F 74 20 + 20 20 20 20 20 20 46 41 54 31 36 20 20 20 0E 1F + BE 5B 7C AC 22 C0 74 0B 56 B4 0E BB 07 00 CD 10 + 5E EB F0 32 E4 CD 16 CD 19 EB FE 54 68 69 73 20 + 69 73 20 6E 6F 74 20 61 20 62 6F 6F 74 61 62 6C + 65 20 64 69 73 6B 2E 20 20 50 6C 65 61 73 65 20 + 69 6E 73 65 72 74 20 61 20 62 6F 6F 74 61 62 6C + 65 20 66 6C 6F 70 70 79 20 61 6E 64 0D 0A 70 72 + 65 73 73 20 61 6E 79 20 6B 65 79 20 74 6F 20 74 + 72 79 20 61 67 61 69 6E 20 2E 2E 2E 20 0D 0A 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" + ); + let bpb = Bpb::create_from_bytes(&BPB_EXAMPLE).unwrap(); + assert_eq!(bpb.footer(), Bpb::FOOTER_VALUE); + assert_eq!(bpb.oem_name(), b"mkfs.fat"); + assert_eq!(bpb.bytes_per_block(), 512); + assert_eq!(bpb.blocks_per_cluster(), 16); + assert_eq!(bpb.reserved_block_count(), 1); + assert_eq!(bpb.num_fats(), 2); + assert_eq!(bpb.root_entries_count(), 512); + assert_eq!(bpb.total_blocks16(), 0); + assert_eq!(bpb.fat_size16(), 32); + assert_eq!(bpb.total_blocks32(), 122_880); + assert_eq!(bpb.footer(), 0xAA55); + assert_eq!(bpb.volume_label(), *b"boot "); + assert_eq!(bpb.fat_size(), 32); + assert_eq!(bpb.total_blocks(), 122_880); + assert_eq!(bpb.fat_type, FatType::Fat16); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs b/examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs new file mode 100644 index 0000000..83707e4 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/ondiskdirentry.rs @@ -0,0 +1,166 @@ +//! Directory Entry as stored on-disk + +use crate::{fat::FatType, Attributes, BlockIdx, ClusterId, DirEntry, ShortFileName, Timestamp}; +use byteorder::{ByteOrder, LittleEndian}; + +/// A 32-byte directory entry as stored on-disk in a directory file. +/// +/// This is the same for FAT16 and FAT32 (except FAT16 doesn't use +/// first_cluster_hi). +pub struct OnDiskDirEntry<'a> { + data: &'a [u8], +} + +impl<'a> core::fmt::Debug for OnDiskDirEntry<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "OnDiskDirEntry<")?; + write!(f, "raw_attr = {}", self.raw_attr())?; + write!(f, ", create_time = {}", self.create_time())?; + write!(f, ", create_date = {}", self.create_date())?; + write!(f, ", last_access_data = {}", self.last_access_data())?; + write!(f, ", first_cluster_hi = {}", self.first_cluster_hi())?; + write!(f, ", write_time = {}", self.write_time())?; + write!(f, ", write_date = {}", self.write_date())?; + write!(f, ", first_cluster_lo = {}", self.first_cluster_lo())?; + write!(f, ", file_size = {}", self.file_size())?; + write!(f, ", is_end = {}", self.is_end())?; + write!(f, ", is_valid = {}", self.is_valid())?; + write!(f, ", is_lfn = {}", self.is_lfn())?; + write!( + f, + ", first_cluster_fat32 = {:?}", + self.first_cluster_fat32() + )?; + write!( + f, + ", first_cluster_fat16 = {:?}", + self.first_cluster_fat16() + )?; + write!(f, ">")?; + Ok(()) + } +} + +impl<'a> OnDiskDirEntry<'a> { + pub(crate) const LEN: usize = 32; + pub(crate) const LEN_U32: u32 = 32; + + define_field!(raw_attr, u8, 11); + define_field!(create_time, u16, 14); + define_field!(create_date, u16, 16); + define_field!(last_access_data, u16, 18); + define_field!(first_cluster_hi, u16, 20); + define_field!(write_time, u16, 22); + define_field!(write_date, u16, 24); + define_field!(first_cluster_lo, u16, 26); + define_field!(file_size, u32, 28); + + /// Create a new on-disk directory entry from a block of 32 bytes read + /// from a directory file. + pub fn new(data: &[u8]) -> OnDiskDirEntry { + OnDiskDirEntry { data } + } + + /// Is this the last entry in the directory? + pub fn is_end(&self) -> bool { + self.data[0] == 0x00 + } + + /// Is this a valid entry? + pub fn is_valid(&self) -> bool { + !self.is_end() && (self.data[0] != 0xE5) + } + + /// Is this a Long Filename entry? + pub fn is_lfn(&self) -> bool { + let attributes = Attributes::create_from_fat(self.raw_attr()); + attributes.is_lfn() + } + + /// If this is an LFN, get the contents so we can re-assemble the filename. + pub fn lfn_contents(&self) -> Option<(bool, u8, u8, [u16; 13])> { + if self.is_lfn() { + let is_start = (self.data[0] & 0x40) != 0; + let sequence = self.data[0] & 0x1F; + let csum = self.data[13]; + let buffer = [ + LittleEndian::read_u16(&self.data[1..=2]), + LittleEndian::read_u16(&self.data[3..=4]), + LittleEndian::read_u16(&self.data[5..=6]), + LittleEndian::read_u16(&self.data[7..=8]), + LittleEndian::read_u16(&self.data[9..=10]), + LittleEndian::read_u16(&self.data[14..=15]), + LittleEndian::read_u16(&self.data[16..=17]), + LittleEndian::read_u16(&self.data[18..=19]), + LittleEndian::read_u16(&self.data[20..=21]), + LittleEndian::read_u16(&self.data[22..=23]), + LittleEndian::read_u16(&self.data[24..=25]), + LittleEndian::read_u16(&self.data[28..=29]), + LittleEndian::read_u16(&self.data[30..=31]), + ]; + Some((is_start, sequence, csum, buffer)) + } else { + None + } + } + + /// Does this on-disk entry match the given filename? + pub fn matches(&self, sfn: &ShortFileName) -> bool { + self.data[0..11] == sfn.contents + } + + /// Which cluster, if any, does this file start at? Assumes this is from a FAT32 volume. + pub fn first_cluster_fat32(&self) -> ClusterId { + let cluster_no = + (u32::from(self.first_cluster_hi()) << 16) | u32::from(self.first_cluster_lo()); + ClusterId(cluster_no) + } + + /// Which cluster, if any, does this file start at? Assumes this is from a FAT16 volume. + fn first_cluster_fat16(&self) -> ClusterId { + let cluster_no = u32::from(self.first_cluster_lo()); + ClusterId(cluster_no) + } + + /// Convert the on-disk format into a DirEntry + pub fn get_entry( + &self, + fat_type: FatType, + entry_block: BlockIdx, + entry_offset: u32, + ) -> DirEntry { + let attributes = Attributes::create_from_fat(self.raw_attr()); + let mut result = DirEntry { + name: ShortFileName { + contents: [0u8; 11], + }, + mtime: Timestamp::from_fat(self.write_date(), self.write_time()), + ctime: Timestamp::from_fat(self.create_date(), self.create_time()), + attributes, + cluster: { + let cluster = if fat_type == FatType::Fat32 { + self.first_cluster_fat32() + } else { + self.first_cluster_fat16() + }; + if cluster == ClusterId::EMPTY && attributes.is_directory() { + // FAT16/FAT32 uses a cluster ID of `0` in the ".." entry to mean 'root directory' + ClusterId::ROOT_DIR + } else { + cluster + } + }, + size: self.file_size(), + entry_block, + entry_offset, + }; + result.name.contents.copy_from_slice(&self.data[0..11]); + result + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/fat/volume.rs b/examples/ios/embedded-sdmmc/src/fat/volume.rs new file mode 100644 index 0000000..4344e75 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/fat/volume.rs @@ -0,0 +1,1447 @@ +//! FAT-specific volume support. + +use crate::{ + debug, + fat::{ + Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, + RESERVED_ENTRIES, + }, + filesystem::FilenameError, + trace, warn, Attributes, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, ClusterId, + DirEntry, DirectoryInfo, Error, LfnBuffer, ShortFileName, TimeSource, VolumeType, +}; +use byteorder::{ByteOrder, LittleEndian}; +use core::convert::TryFrom; + +/// An MS-DOS 11 character volume label. +/// +/// ISO-8859-1 encoding is assumed. Trailing spaces are trimmed. Reserved +/// characters are not allowed. There is no file extension, unlike with a +/// filename. +/// +/// Volume labels can be found in the BIOS Parameter Block, and in a root +/// directory entry with the 'Volume Label' bit set. Both places should have the +/// same contents, but they can get out of sync. +/// +/// MS-DOS FDISK would show you the one in the BPB, but DIR would show you the +/// one in the root directory. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(PartialEq, Eq, Clone)] +pub struct VolumeName { + pub(crate) contents: [u8; Self::TOTAL_LEN], +} + +impl VolumeName { + const TOTAL_LEN: usize = 11; + + /// Get name + pub fn name(&self) -> &[u8] { + let mut bytes = &self.contents[..]; + while let [rest @ .., last] = bytes { + if last.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes + } + + /// Create a new MS-DOS volume label. + pub fn create_from_str(name: &str) -> Result { + let mut sfn = VolumeName { + contents: [b' '; Self::TOTAL_LEN], + }; + + let mut idx = 0; + for ch in name.chars() { + match ch { + // Microsoft say these are the invalid characters + '\u{0000}'..='\u{001F}' + | '"' + | '*' + | '+' + | ',' + | '/' + | ':' + | ';' + | '<' + | '=' + | '>' + | '?' + | '[' + | '\\' + | ']' + | '.' + | '|' => { + return Err(FilenameError::InvalidCharacter); + } + x if x > '\u{00FF}' => { + // We only handle ISO-8859-1 which is Unicode Code Points + // \U+0000 to \U+00FF. This is above that. + return Err(FilenameError::InvalidCharacter); + } + _ => { + let b = ch as u8; + if idx < Self::TOTAL_LEN { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + idx += 1; + } + } + } + if idx == 0 { + return Err(FilenameError::FilenameEmpty); + } + Ok(sfn) + } + + /// Convert to a Short File Name + /// + /// # Safety + /// + /// Volume Labels can contain things that Short File Names cannot, so only + /// do this conversion if you are creating the name of a directory entry + /// with the 'Volume Label' attribute. + pub unsafe fn to_short_filename(self) -> ShortFileName { + ShortFileName { + contents: self.contents, + } + } +} + +impl core::fmt::Display for VolumeName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let mut printed = 0; + for &c in self.name().iter() { + // converting a byte to a codepoint means you are assuming + // ISO-8859-1 encoding, because that's how Unicode was designed. + write!(f, "{}", c as char)?; + printed += 1; + } + if let Some(mut width) = f.width() { + if width > printed { + width -= printed; + for _ in 0..width { + write!(f, "{}", f.fill())?; + } + } + } + Ok(()) + } +} + +impl core::fmt::Debug for VolumeName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "VolumeName(\"{}\")", self) + } +} + +/// Identifies a FAT16 or FAT32 Volume on the disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq)] +pub struct FatVolume { + /// The block number of the start of the partition. All other BlockIdx values are relative to this. + pub(crate) lba_start: BlockIdx, + /// The number of blocks in this volume + pub(crate) num_blocks: BlockCount, + /// The name of this volume + pub(crate) name: VolumeName, + /// Number of 512 byte blocks (or Blocks) in a cluster + pub(crate) blocks_per_cluster: u8, + /// The block the data starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) first_data_block: BlockCount, + /// The block the FAT starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) fat_start: BlockCount, + /// The block the second FAT starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) second_fat_start: Option, + /// Expected number of free clusters + pub(crate) free_clusters_count: Option, + /// Number of the next expected free cluster + pub(crate) next_free_cluster: Option, + /// Total number of clusters + pub(crate) cluster_count: u32, + /// Type of FAT + pub(crate) fat_specific_info: FatSpecificInfo, +} + +impl FatVolume { + /// Write a new entry in the FAT + pub fn update_info_sector( + &mut self, + block_cache: &mut BlockCache, + ) -> Result<(), Error> + where + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_) => { + // FAT16 volumes don't have an info sector + } + FatSpecificInfo::Fat32(fat32_info) => { + if self.free_clusters_count.is_none() && self.next_free_cluster.is_none() { + return Ok(()); + } + trace!("Reading info sector"); + let block = block_cache + .read_mut(fat32_info.info_location) + .map_err(Error::DeviceError)?; + if let Some(count) = self.free_clusters_count { + block[488..492].copy_from_slice(&count.to_le_bytes()); + } + if let Some(next_free_cluster) = self.next_free_cluster { + block[492..496].copy_from_slice(&next_free_cluster.0.to_le_bytes()); + } + trace!("Writing info sector"); + block_cache.write_back()?; + } + } + Ok(()) + } + + /// Get the type of FAT this volume is + pub(crate) fn get_fat_type(&self) -> FatType { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_) => FatType::Fat16, + FatSpecificInfo::Fat32(_) => FatType::Fat32, + } + } + + /// Write a new entry in the FAT + fn update_fat( + &mut self, + block_cache: &mut BlockCache, + cluster: ClusterId, + new_value: ClusterId, + ) -> Result<(), Error> + where + D: BlockDevice, + { + let mut second_fat_block_num = None; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_fat16_info) => { + let fat_offset = cluster.0 * 2; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + if let Some(second_fat_start) = self.second_fat_start { + second_fat_block_num = + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); + } + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Reading FAT for update"); + let block = block_cache + .read_mut(this_fat_block_num) + .map_err(Error::DeviceError)?; + // See + let entry = match new_value { + ClusterId::INVALID => 0xFFF6, + ClusterId::BAD => 0xFFF7, + ClusterId::EMPTY => 0x0000, + ClusterId::END_OF_FILE => 0xFFFF, + _ => new_value.0 as u16, + }; + LittleEndian::write_u16( + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 1], + entry, + ); + } + FatSpecificInfo::Fat32(_fat32_info) => { + // FAT32 => 4 bytes per entry + let fat_offset = cluster.0 * 4; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + if let Some(second_fat_start) = self.second_fat_start { + second_fat_block_num = + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); + } + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Reading FAT for update"); + let block = block_cache + .read_mut(this_fat_block_num) + .map_err(Error::DeviceError)?; + let entry = match new_value { + ClusterId::INVALID => 0x0FFF_FFF6, + ClusterId::BAD => 0x0FFF_FFF7, + ClusterId::EMPTY => 0x0000_0000, + _ => new_value.0, + }; + let existing = + LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]); + let new = (existing & 0xF000_0000) | (entry & 0x0FFF_FFFF); + LittleEndian::write_u32( + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 3], + new, + ); + } + } + trace!("Updating FAT"); + if let Some(duplicate) = second_fat_block_num { + block_cache.write_back_with_duplicate(duplicate)?; + } else { + block_cache.write_back()?; + } + Ok(()) + } + + /// Look in the FAT to see which cluster comes next. + pub(crate) fn next_cluster( + &self, + block_cache: &mut BlockCache, + cluster: ClusterId, + ) -> Result> + where + D: BlockDevice, + { + if cluster.0 > (u32::MAX / 4) { + panic!("next_cluster called on invalid cluster {:x?}", cluster); + } + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_fat16_info) => { + let fat_offset = cluster.0 * 2; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Walking FAT"); + let block = block_cache.read(this_fat_block_num)?; + let fat_entry = + LittleEndian::read_u16(&block[this_fat_ent_offset..=this_fat_ent_offset + 1]); + match fat_entry { + 0xFFF7 => { + // Bad cluster + Err(Error::BadCluster) + } + 0xFFF8..=0xFFFF => { + // There is no next cluster + Err(Error::EndOfFile) + } + f => { + // Seems legit + Ok(ClusterId(u32::from(f))) + } + } + } + FatSpecificInfo::Fat32(_fat32_info) => { + let fat_offset = cluster.0 * 4; + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Walking FAT"); + let block = block_cache.read(this_fat_block_num)?; + let fat_entry = + LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) + & 0x0FFF_FFFF; + match fat_entry { + 0x0000_0000 => { + // Jumped to free space + Err(Error::UnterminatedFatChain) + } + 0x0FFF_FFF7 => { + // Bad cluster + Err(Error::BadCluster) + } + 0x0000_0001 | 0x0FFF_FFF8..=0x0FFF_FFFF => { + // There is no next cluster + Err(Error::EndOfFile) + } + f => { + // Seems legit + Ok(ClusterId(f)) + } + } + } + } + } + + /// Number of bytes in a cluster. + pub(crate) fn bytes_per_cluster(&self) -> u32 { + u32::from(self.blocks_per_cluster) * Block::LEN_U32 + } + + /// Converts a cluster number (or `Cluster`) to a block number (or + /// `BlockIdx`). Gives an absolute `BlockIdx` you can pass to the + /// volume manager. + pub(crate) fn cluster_to_block(&self, cluster: ClusterId) -> BlockIdx { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + let block_num = match cluster { + ClusterId::ROOT_DIR => fat16_info.first_root_dir_block, + ClusterId(c) => { + // FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector; + let first_block_of_cluster = + BlockCount((c - 2) * u32::from(self.blocks_per_cluster)); + self.first_data_block + first_block_of_cluster + } + }; + self.lba_start + block_num + } + FatSpecificInfo::Fat32(fat32_info) => { + let cluster_num = match cluster { + ClusterId::ROOT_DIR => fat32_info.first_root_dir_cluster.0, + c => c.0, + }; + // FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector; + let first_block_of_cluster = + BlockCount((cluster_num - 2) * u32::from(self.blocks_per_cluster)); + self.lba_start + self.first_data_block + first_block_of_cluster + } + } + } + + /// Finds a empty entry space and writes the new entry to it, allocates a new cluster if it's + /// needed + pub(crate) fn write_new_directory_entry( + &mut self, + block_cache: &mut BlockCache, + time_source: &T, + dir_cluster: ClusterId, + name: ShortFileName, + attributes: Attributes, + ) -> Result> + where + D: BlockDevice, + T: TimeSource, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_cluster); + let mut first_dir_block_num = match dir_cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_cluster), + }; + let dir_size = match dir_cluster { + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + // Walk the directory + while let Some(cluster) = current_cluster { + for block_idx in first_dir_block_num.range(dir_size) { + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in + block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + // 0x00 or 0xE5 represents a free entry + if !dir_entry.is_valid() { + let ctime = time_source.get_timestamp(); + let entry = DirEntry::new( + name, + attributes, + ClusterId::EMPTY, + ctime, + block_idx, + (i * OnDiskDirEntry::LEN) as u32, + ); + dir_entry_bytes + .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); + trace!("Updating directory"); + block_cache.write_back()?; + return Ok(entry); + } + } + } + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + Err(Error::EndOfFile) => { + let c = self.alloc_cluster(block_cache, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + Err(Error::NotEnoughSpace) + } + FatSpecificInfo::Fat32(fat32_info) => { + // All directories on FAT32 have a cluster chain but the root + // dir starts in a specified cluster. + let mut current_cluster = match dir_cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_cluster), + }; + let mut first_dir_block_num = self.cluster_to_block(dir_cluster); + + let dir_size = BlockCount(u32::from(self.blocks_per_cluster)); + // Walk the cluster chain until we run out of clusters + while let Some(cluster) = current_cluster { + // Loop through the blocks in the cluster + for block_idx in first_dir_block_num.range(dir_size) { + // Read a block of directory entries + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)?; + // Are any entries in the block we just loaded blank? If so + // we can use them. + for (i, dir_entry_bytes) in + block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + // 0x00 or 0xE5 represents a free entry + if !dir_entry.is_valid() { + let ctime = time_source.get_timestamp(); + let entry = DirEntry::new( + name, + attributes, + ClusterId(0), + ctime, + block_idx, + (i * OnDiskDirEntry::LEN) as u32, + ); + dir_entry_bytes + .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); + trace!("Updating directory"); + block_cache.write_back()?; + return Ok(entry); + } + } + } + // Well none of the blocks in that cluster had any space in + // them, let's fetch another one. + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + Err(Error::EndOfFile) => { + let c = self.alloc_cluster(block_cache, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; + } + // We ran out of clusters in the chain, and apparently we weren't + // able to make the chain longer, so the disk must be full. + Err(Error::NotEnoughSpace) + } + } + } + + /// Calls callback `func` with every valid entry in the given directory. + /// Useful for performing directory listings. + pub(crate) fn iterate_dir( + &self, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry), + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, _| func(de)) + } + FatSpecificInfo::Fat32(fat32_info) => { + self.iterate_fat32(dir_info, fat32_info, block_cache, |de, _| func(de)) + } + } + } + + /// Calls callback `func` with every valid entry in the given directory, + /// including the Long File Name. + /// + /// Useful for performing directory listings. + pub(crate) fn iterate_dir_lfn( + &self, + block_cache: &mut BlockCache, + lfn_buffer: &mut LfnBuffer<'_>, + dir_info: &DirectoryInfo, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + D: BlockDevice, + { + #[derive(Clone, Copy)] + enum SeqState { + Waiting, + Remaining { csum: u8, next: u8 }, + Complete { csum: u8 }, + } + + impl SeqState { + fn update( + self, + lfn_buffer: &mut LfnBuffer<'_>, + start: bool, + sequence: u8, + csum: u8, + buffer: [u16; 13], + ) -> Self { + #[cfg(feature = "log")] + debug!("LFN Contents {start} {sequence} {csum:02x} {buffer:04x?}"); + #[cfg(feature = "defmt-log")] + debug!( + "LFN Contents {=bool} {=u8} {=u8:02x} {=[?; 13]:#04x}", + start, sequence, csum, buffer + ); + match (start, sequence, self) { + (true, 0x01, _) => { + lfn_buffer.clear(); + lfn_buffer.push(&buffer); + SeqState::Complete { csum } + } + (true, sequence, _) if sequence >= 0x02 && sequence < 0x14 => { + lfn_buffer.clear(); + lfn_buffer.push(&buffer); + SeqState::Remaining { + csum, + next: sequence - 1, + } + } + (false, 0x01, SeqState::Remaining { csum, next }) if next == sequence => { + lfn_buffer.push(&buffer); + SeqState::Complete { csum } + } + (false, sequence, SeqState::Remaining { csum, next }) + if sequence >= 0x01 && sequence < 0x13 && next == sequence => + { + lfn_buffer.push(&buffer); + SeqState::Remaining { + csum, + next: sequence - 1, + } + } + _ => { + // this seems wrong + lfn_buffer.clear(); + SeqState::Waiting + } + } + } + } + + let mut seq_state = SeqState::Waiting; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, odde| { + if let Some((start, this_seqno, csum, buffer)) = odde.lfn_contents() { + seq_state = seq_state.update(lfn_buffer, start, this_seqno, csum, buffer); + } else if let SeqState::Complete { csum } = seq_state { + if csum == de.name.csum() { + // Checksum is good, and all the pieces are there + func(de, Some(lfn_buffer.as_str())) + } else { + // Checksum was bad + func(de, None) + } + } else { + func(de, None) + } + }) + } + FatSpecificInfo::Fat32(fat32_info) => { + self.iterate_fat32(dir_info, fat32_info, block_cache, |de, odde| { + if let Some((start, this_seqno, csum, buffer)) = odde.lfn_contents() { + seq_state = seq_state.update(lfn_buffer, start, this_seqno, csum, buffer); + } else if let SeqState::Complete { csum } = seq_state { + if csum == de.name.csum() { + // Checksum is good, and all the pieces are there + func(de, Some(lfn_buffer.as_str())) + } else { + // Checksum was bad + func(de, None) + } + } else { + func(de, None) + } + }) + } + } + } + + fn iterate_fat16( + &self, + dir_info: &DirectoryInfo, + fat16_info: &Fat16Info, + block_cache: &mut BlockCache, + mut func: F, + ) -> Result<(), Error> + where + F: for<'odde> FnMut(&DirEntry, &OnDiskDirEntry<'odde>), + D: BlockDevice, + { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_info.cluster), + }; + let dir_size = match dir_info.cluster { + ClusterId::ROOT_DIR => { + let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + while let Some(cluster) = current_cluster { + for block_idx in first_dir_block_num.range(dir_size) { + trace!("Reading FAT"); + let block = block_cache.read(block_idx)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + return Ok(()); + } else if dir_entry.is_valid() { + // Safe, since Block::LEN always fits on a u32 + let start = (i * OnDiskDirEntry::LEN) as u32; + let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); + func(&entry, &dir_entry); + } + } + } + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + Ok(()) + } + + fn iterate_fat32( + &self, + dir_info: &DirectoryInfo, + fat32_info: &Fat32Info, + block_cache: &mut BlockCache, + mut func: F, + ) -> Result<(), Error> + where + F: for<'odde> FnMut(&DirEntry, &OnDiskDirEntry<'odde>), + D: BlockDevice, + { + // All directories on FAT32 have a cluster chain but the root + // dir starts in a specified cluster. + let mut current_cluster = match dir_info.cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_info.cluster), + }; + while let Some(cluster) = current_cluster { + let start_block_idx = self.cluster_to_block(cluster); + for block_idx in start_block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { + trace!("Reading FAT"); + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + return Ok(()); + } else if dir_entry.is_valid() { + // Safe, since Block::LEN always fits on a u32 + let start = (i * OnDiskDirEntry::LEN) as u32; + let entry = dir_entry.get_entry(FatType::Fat32, block_idx, start); + func(&entry, &dir_entry); + } + } + } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + }; + } + Ok(()) + } + + /// Get an entry from the given directory + pub(crate) fn find_directory_entry( + &self, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + match_name: &ShortFileName, + ) -> Result> + where + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_info.cluster), + }; + let dir_size = match dir_info.cluster { + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + while let Some(cluster) = current_cluster { + for block in first_dir_block_num.range(dir_size) { + match self.find_entry_in_block( + block_cache, + FatType::Fat16, + match_name, + block, + ) { + Err(Error::NotFound) => continue, + x => return x, + } + } + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + Err(Error::NotFound) + } + FatSpecificInfo::Fat32(fat32_info) => { + let mut current_cluster = match dir_info.cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_info.cluster), + }; + while let Some(cluster) = current_cluster { + let block_idx = self.cluster_to_block(cluster); + for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { + match self.find_entry_in_block( + block_cache, + FatType::Fat32, + match_name, + block, + ) { + Err(Error::NotFound) => continue, + x => return x, + } + } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } + } + Err(Error::NotFound) + } + } + } + + /// Finds an entry in a given block of directory entries. + fn find_entry_in_block( + &self, + block_cache: &mut BlockCache, + fat_type: FatType, + match_name: &ShortFileName, + block_idx: BlockIdx, + ) -> Result> + where + D: BlockDevice, + { + trace!("Reading directory"); + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + break; + } else if dir_entry.matches(match_name) { + // Found it + // Block::LEN always fits on a u32 + let start = (i * OnDiskDirEntry::LEN) as u32; + return Ok(dir_entry.get_entry(fat_type, block_idx, start)); + } + } + Err(Error::NotFound) + } + + /// Delete an entry from the given directory + pub(crate) fn delete_directory_entry( + &self, + block_cache: &mut BlockCache, + dir_info: &DirectoryInfo, + match_name: &ShortFileName, + ) -> Result<(), Error> + where + D: BlockDevice, + { + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + // Root directories on FAT16 have a fixed size, because they use + // a specially reserved space on disk (see + // `first_root_dir_block`). Other directories can have any size + // as they are made of regular clusters. + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { + ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, + _ => self.cluster_to_block(dir_info.cluster), + }; + let dir_size = match dir_info.cluster { + ClusterId::ROOT_DIR => { + let len_bytes = + u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; + BlockCount::from_bytes(len_bytes) + } + _ => BlockCount(u32::from(self.blocks_per_cluster)), + }; + + // Walk the directory + while let Some(cluster) = current_cluster { + // Scan the cluster / root dir a block at a time + for block_idx in first_dir_block_num.range(dir_size) { + match self.delete_entry_in_block(block_cache, match_name, block_idx) { + Err(Error::NotFound) => { + // Carry on + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } + } + } + // if it's not the root dir, find the next cluster so we can keep looking + if cluster != ClusterId::ROOT_DIR { + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; + } else { + current_cluster = None; + } + } + // Ok, give up + } + FatSpecificInfo::Fat32(fat32_info) => { + // Root directories on FAT32 start at a specified cluster, but + // they can have any length. + let mut current_cluster = match dir_info.cluster { + ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), + _ => Some(dir_info.cluster), + }; + // Walk the directory + while let Some(cluster) = current_cluster { + // Scan the cluster a block at a time + let start_block_idx = self.cluster_to_block(cluster); + for block_idx in + start_block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) + { + match self.delete_entry_in_block(block_cache, match_name, block_idx) { + Err(Error::NotFound) => { + // Carry on + continue; + } + x => { + // Either we deleted it OK, or there was some + // catastrophic error reading/writing the disk. + return x; + } + } + } + // Find the next cluster + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } + } + // Ok, give up + } + } + // If we get here we never found the right entry in any of the + // blocks that made up the directory + Err(Error::NotFound) + } + + /// Deletes a directory entry from a block of directory entries. + /// + /// Entries are marked as deleted by setting the first byte of the file name + /// to a special value. + fn delete_entry_in_block( + &self, + block_cache: &mut BlockCache, + match_name: &ShortFileName, + block_idx: BlockIdx, + ) -> Result<(), Error> + where + D: BlockDevice, + { + trace!("Reading directory"); + let block = block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); + if dir_entry.is_end() { + // Can quit early + break; + } else if dir_entry.matches(match_name) { + let start = i * OnDiskDirEntry::LEN; + // set first byte to the 'unused' marker + block[start] = 0xE5; + trace!("Updating directory"); + return block_cache.write_back().map_err(Error::DeviceError); + } + } + Err(Error::NotFound) + } + + /// Finds the next free cluster after the start_cluster and before end_cluster + pub(crate) fn find_next_free_cluster( + &self, + block_cache: &mut BlockCache, + start_cluster: ClusterId, + end_cluster: ClusterId, + ) -> Result> + where + D: BlockDevice, + { + let mut current_cluster = start_cluster; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(_fat16_info) => { + while current_cluster.0 < end_cluster.0 { + trace!( + "current_cluster={:?}, end_cluster={:?}", + current_cluster, + end_cluster + ); + let fat_offset = current_cluster.0 * 2; + trace!("fat_offset = {:?}", fat_offset); + let this_fat_block_num = + self.lba_start + self.fat_start.offset_bytes(fat_offset); + trace!("this_fat_block_num = {:?}", this_fat_block_num); + let mut this_fat_ent_offset = usize::try_from(fat_offset % Block::LEN_U32) + .map_err(|_| Error::ConversionError)?; + trace!("Reading block {:?}", this_fat_block_num); + let block = block_cache + .read(this_fat_block_num) + .map_err(Error::DeviceError)?; + while this_fat_ent_offset <= Block::LEN - 2 { + let fat_entry = LittleEndian::read_u16( + &block[this_fat_ent_offset..=this_fat_ent_offset + 1], + ); + if fat_entry == 0 { + return Ok(current_cluster); + } + this_fat_ent_offset += 2; + current_cluster += 1; + } + } + } + FatSpecificInfo::Fat32(_fat32_info) => { + while current_cluster.0 < end_cluster.0 { + trace!( + "current_cluster={:?}, end_cluster={:?}", + current_cluster, + end_cluster + ); + let fat_offset = current_cluster.0 * 4; + trace!("fat_offset = {:?}", fat_offset); + let this_fat_block_num = + self.lba_start + self.fat_start.offset_bytes(fat_offset); + trace!("this_fat_block_num = {:?}", this_fat_block_num); + let mut this_fat_ent_offset = usize::try_from(fat_offset % Block::LEN_U32) + .map_err(|_| Error::ConversionError)?; + trace!("Reading block {:?}", this_fat_block_num); + let block = block_cache + .read(this_fat_block_num) + .map_err(Error::DeviceError)?; + while this_fat_ent_offset <= Block::LEN - 4 { + let fat_entry = LittleEndian::read_u32( + &block[this_fat_ent_offset..=this_fat_ent_offset + 3], + ) & 0x0FFF_FFFF; + if fat_entry == 0 { + return Ok(current_cluster); + } + this_fat_ent_offset += 4; + current_cluster += 1; + } + } + } + } + warn!("Out of space..."); + Err(Error::NotEnoughSpace) + } + + /// Tries to allocate a cluster + pub(crate) fn alloc_cluster( + &mut self, + block_cache: &mut BlockCache, + prev_cluster: Option, + zero: bool, + ) -> Result> + where + D: BlockDevice, + { + debug!("Allocating new cluster, prev_cluster={:?}", prev_cluster); + let end_cluster = ClusterId(self.cluster_count + RESERVED_ENTRIES); + let start_cluster = match self.next_free_cluster { + Some(cluster) if cluster.0 < end_cluster.0 => cluster, + _ => ClusterId(RESERVED_ENTRIES), + }; + trace!( + "Finding next free between {:?}..={:?}", + start_cluster, + end_cluster + ); + let new_cluster = match self.find_next_free_cluster(block_cache, start_cluster, end_cluster) + { + Ok(cluster) => cluster, + Err(_) if start_cluster.0 > RESERVED_ENTRIES => { + debug!( + "Retrying, finding next free between {:?}..={:?}", + ClusterId(RESERVED_ENTRIES), + end_cluster + ); + self.find_next_free_cluster(block_cache, ClusterId(RESERVED_ENTRIES), end_cluster)? + } + Err(e) => return Err(e), + }; + // This new cluster is the end of the file's chain + self.update_fat(block_cache, new_cluster, ClusterId::END_OF_FILE)?; + // If there's something before this new one, update the FAT to point it at us + if let Some(cluster) = prev_cluster { + trace!( + "Updating old cluster {:?} to {:?} in FAT", + cluster, + new_cluster + ); + self.update_fat(block_cache, cluster, new_cluster)?; + } + trace!( + "Finding next free between {:?}..={:?}", + new_cluster, + end_cluster + ); + self.next_free_cluster = + match self.find_next_free_cluster(block_cache, new_cluster, end_cluster) { + Ok(cluster) => Some(cluster), + Err(_) if new_cluster.0 > RESERVED_ENTRIES => { + match self.find_next_free_cluster( + block_cache, + ClusterId(RESERVED_ENTRIES), + end_cluster, + ) { + Ok(cluster) => Some(cluster), + Err(e) => return Err(e), + } + } + Err(e) => return Err(e), + }; + debug!("Next free cluster is {:?}", self.next_free_cluster); + // Record that we've allocated a cluster + if let Some(ref mut number_free_cluster) = self.free_clusters_count { + *number_free_cluster -= 1; + }; + if zero { + let start_block_idx = self.cluster_to_block(new_cluster); + let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); + for block_idx in start_block_idx.range(num_blocks) { + trace!("Zeroing cluster {:?}", block_idx); + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; + } + } + debug!("All done, returning {:?}", new_cluster); + Ok(new_cluster) + } + + /// Marks the input cluster as an EOF and all the subsequent clusters in the chain as free + pub(crate) fn truncate_cluster_chain( + &mut self, + block_cache: &mut BlockCache, + cluster: ClusterId, + ) -> Result<(), Error> + where + D: BlockDevice, + { + if cluster.0 < RESERVED_ENTRIES { + // file doesn't have any valid cluster allocated, there is nothing to do + return Ok(()); + } + let mut next = { + match self.next_cluster(block_cache, cluster) { + Ok(n) => n, + Err(Error::EndOfFile) => return Ok(()), + Err(e) => return Err(e), + } + }; + if let Some(ref mut next_free_cluster) = self.next_free_cluster { + if next_free_cluster.0 > next.0 { + *next_free_cluster = next; + } + } else { + self.next_free_cluster = Some(next); + } + self.update_fat(block_cache, cluster, ClusterId::END_OF_FILE)?; + loop { + match self.next_cluster(block_cache, next) { + Ok(n) => { + self.update_fat(block_cache, next, ClusterId::EMPTY)?; + next = n; + } + Err(Error::EndOfFile) => { + self.update_fat(block_cache, next, ClusterId::EMPTY)?; + break; + } + Err(e) => return Err(e), + } + if let Some(ref mut number_free_cluster) = self.free_clusters_count { + *number_free_cluster += 1; + }; + } + Ok(()) + } + + /// Writes a Directory Entry to the disk + pub(crate) fn write_entry_to_disk( + &self, + block_cache: &mut BlockCache, + entry: &DirEntry, + ) -> Result<(), Error> + where + D: BlockDevice, + { + let fat_type = match self.fat_specific_info { + FatSpecificInfo::Fat16(_) => FatType::Fat16, + FatSpecificInfo::Fat32(_) => FatType::Fat32, + }; + trace!("Reading directory for update"); + let block = block_cache + .read_mut(entry.entry_block) + .map_err(Error::DeviceError)?; + + let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; + block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); + + trace!("Updating directory"); + block_cache.write_back().map_err(Error::DeviceError)?; + Ok(()) + } + + /// Create a new directory. + /// + /// 1) Creates the directory entry in the parent + /// 2) Allocates a new cluster to hold the new directory + /// 3) Writes out the `.` and `..` entries in the new directory + pub(crate) fn make_dir( + &mut self, + block_cache: &mut BlockCache, + time_source: &T, + parent: ClusterId, + sfn: ShortFileName, + att: Attributes, + ) -> Result<(), Error> + where + D: BlockDevice, + T: TimeSource, + { + let mut new_dir_entry_in_parent = + self.write_new_directory_entry(block_cache, time_source, parent, sfn, att)?; + if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { + new_dir_entry_in_parent.cluster = self.alloc_cluster(block_cache, None, false)?; + // update the parent dir with the cluster of the new dir + self.write_entry_to_disk(block_cache, &new_dir_entry_in_parent)?; + } + let new_dir_start_block = self.cluster_to_block(new_dir_entry_in_parent.cluster); + debug!("Made new dir entry {:?}", new_dir_entry_in_parent); + let now = time_source.get_timestamp(); + let fat_type = self.get_fat_type(); + // A blank block + let block = block_cache.blank_mut(new_dir_start_block); + // make the "." entry + let dot_entry_in_child = DirEntry { + name: crate::ShortFileName::this_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at ourselves + cluster: new_dir_entry_in_parent.cluster, + size: 0, + entry_block: new_dir_start_block, + entry_offset: 0, + }; + debug!("New dir has {:?}", dot_entry_in_child); + let mut offset = 0; + block[offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); + offset += OnDiskDirEntry::LEN; + // make the ".." entry + let dot_dot_entry_in_child = DirEntry { + name: crate::ShortFileName::parent_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at our parent + cluster: if parent == ClusterId::ROOT_DIR { + // indicate parent is root using Cluster(0) + ClusterId::EMPTY + } else { + parent + }, + size: 0, + entry_block: new_dir_start_block, + entry_offset: OnDiskDirEntry::LEN_U32, + }; + debug!("New dir has {:?}", dot_dot_entry_in_child); + block[offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); + + block_cache.write_back()?; + + for block_idx in new_dir_start_block + .range(BlockCount(u32::from(self.blocks_per_cluster))) + .skip(1) + { + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; + } + + Ok(()) + } +} + +/// Load the boot parameter block from the start of the given partition and +/// determine if the partition contains a valid FAT16 or FAT32 file system. +pub fn parse_volume( + block_cache: &mut BlockCache, + lba_start: BlockIdx, + num_blocks: BlockCount, +) -> Result> +where + D: BlockDevice, + D::Error: core::fmt::Debug, +{ + trace!("Reading BPB"); + let block = block_cache.read(lba_start).map_err(Error::DeviceError)?; + let bpb = Bpb::create_from_bytes(&block.contents).map_err(Error::FormatError)?; + let fat_start = BlockCount(u32::from(bpb.reserved_block_count())); + let second_fat_start = if bpb.num_fats() == 2 { + Some(fat_start + BlockCount(bpb.fat_size())) + } else { + None + }; + match bpb.fat_type { + FatType::Fat16 => { + if bpb.bytes_per_block() as usize != Block::LEN { + return Err(Error::BadBlockSize(bpb.bytes_per_block())); + } + // FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz) + RootDirSectors; + let root_dir_blocks = ((u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32) + + (Block::LEN_U32 - 1)) + / Block::LEN_U32; + let first_root_dir_block = + fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); + let first_data_block = first_root_dir_block + BlockCount(root_dir_blocks); + let volume = FatVolume { + lba_start, + num_blocks, + name: VolumeName { + contents: bpb.volume_label(), + }, + blocks_per_cluster: bpb.blocks_per_cluster(), + first_data_block, + fat_start, + second_fat_start, + free_clusters_count: None, + next_free_cluster: None, + cluster_count: bpb.total_clusters(), + fat_specific_info: FatSpecificInfo::Fat16(Fat16Info { + root_entries_count: bpb.root_entries_count(), + first_root_dir_block, + }), + }; + Ok(VolumeType::Fat(volume)) + } + FatType::Fat32 => { + // FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz); + let first_data_block = + fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); + // Safe to unwrap since this is a Fat32 Type + let info_location = bpb.fs_info_block().unwrap(); + let mut volume = FatVolume { + lba_start, + num_blocks, + name: VolumeName { + contents: bpb.volume_label(), + }, + blocks_per_cluster: bpb.blocks_per_cluster(), + first_data_block, + fat_start, + second_fat_start, + free_clusters_count: None, + next_free_cluster: None, + cluster_count: bpb.total_clusters(), + fat_specific_info: FatSpecificInfo::Fat32(Fat32Info { + info_location: lba_start + info_location, + first_root_dir_cluster: ClusterId(bpb.first_root_dir_cluster()), + }), + }; + + // Now we don't need the BPB, update the volume with data from the info sector + trace!("Reading info block"); + let info_block = block_cache + .read(lba_start + info_location) + .map_err(Error::DeviceError)?; + let info_sector = + InfoSector::create_from_bytes(info_block).map_err(Error::FormatError)?; + volume.free_clusters_count = info_sector.free_clusters_count(); + volume.next_free_cluster = info_sector.next_free_cluster(); + + Ok(VolumeType::Fat(volume)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn volume_name() { + let sfn = VolumeName { + contents: *b"Hello \xA399 ", + }; + assert_eq!(sfn, VolumeName::create_from_str("Hello £99").unwrap()) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/attributes.rs b/examples/ios/embedded-sdmmc/src/filesystem/attributes.rs new file mode 100644 index 0000000..e22dcd1 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/attributes.rs @@ -0,0 +1,108 @@ +/// Indicates whether a directory entry is read-only, a directory, a volume +/// label, etc. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct Attributes(pub(crate) u8); + +impl Attributes { + /// Indicates this file cannot be written. + pub const READ_ONLY: u8 = 0x01; + /// Indicates the file is hidden. + pub const HIDDEN: u8 = 0x02; + /// Indicates this is a system file. + pub const SYSTEM: u8 = 0x04; + /// Indicates this is a volume label. + pub const VOLUME: u8 = 0x08; + /// Indicates this is a directory. + pub const DIRECTORY: u8 = 0x10; + /// Indicates this file needs archiving (i.e. has been modified since last + /// archived). + pub const ARCHIVE: u8 = 0x20; + /// This set of flags indicates the file is actually a long file name + /// fragment. + pub const LFN: u8 = Self::READ_ONLY | Self::HIDDEN | Self::SYSTEM | Self::VOLUME; + + /// Create a `Attributes` value from the `u8` stored in a FAT16/FAT32 + /// Directory Entry. + pub(crate) fn create_from_fat(value: u8) -> Attributes { + Attributes(value) + } + + pub(crate) fn set_archive(&mut self, flag: bool) { + let archive = if flag { 0x20 } else { 0x00 }; + self.0 |= archive; + } + + /// Does this file has the read-only attribute set? + pub fn is_read_only(self) -> bool { + (self.0 & Self::READ_ONLY) == Self::READ_ONLY + } + + /// Does this file has the hidden attribute set? + pub fn is_hidden(self) -> bool { + (self.0 & Self::HIDDEN) == Self::HIDDEN + } + + /// Does this file has the system attribute set? + pub fn is_system(self) -> bool { + (self.0 & Self::SYSTEM) == Self::SYSTEM + } + + /// Does this file has the volume attribute set? + pub fn is_volume(self) -> bool { + (self.0 & Self::VOLUME) == Self::VOLUME + } + + /// Does this entry point at a directory? + pub fn is_directory(self) -> bool { + (self.0 & Self::DIRECTORY) == Self::DIRECTORY + } + + /// Does this need archiving? + pub fn is_archive(self) -> bool { + (self.0 & Self::ARCHIVE) == Self::ARCHIVE + } + + /// Is this a long file name fragment? + pub fn is_lfn(self) -> bool { + (self.0 & Self::LFN) == Self::LFN + } +} + +impl core::fmt::Debug for Attributes { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + // Worst case is "DRHSVA" + let mut output = heapless::String::<7>::new(); + if self.is_lfn() { + output.push_str("LFN").unwrap(); + } else { + if self.is_directory() { + output.push_str("D").unwrap(); + } else { + output.push_str("F").unwrap(); + } + if self.is_read_only() { + output.push_str("R").unwrap(); + } + if self.is_hidden() { + output.push_str("H").unwrap(); + } + if self.is_system() { + output.push_str("S").unwrap(); + } + if self.is_volume() { + output.push_str("V").unwrap(); + } + if self.is_archive() { + output.push_str("A").unwrap(); + } + } + f.pad(&output) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/cluster.rs b/examples/ios/embedded-sdmmc/src/filesystem/cluster.rs new file mode 100644 index 0000000..14f1126 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/cluster.rs @@ -0,0 +1,68 @@ +/// Identifies a cluster on disk. +/// +/// A cluster is a consecutive group of blocks. Each cluster has a a numeric ID. +/// Some numeric IDs are reserved for special purposes. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct ClusterId(pub(crate) u32); + +impl ClusterId { + /// Magic value indicating an invalid cluster value. + pub const INVALID: ClusterId = ClusterId(0xFFFF_FFF6); + /// Magic value indicating a bad cluster. + pub const BAD: ClusterId = ClusterId(0xFFFF_FFF7); + /// Magic value indicating a empty cluster. + pub const EMPTY: ClusterId = ClusterId(0x0000_0000); + /// Magic value indicating the cluster holding the root directory (which + /// doesn't have a number in FAT16 as there's a reserved region). + pub const ROOT_DIR: ClusterId = ClusterId(0xFFFF_FFFC); + /// Magic value indicating that the cluster is allocated and is the final cluster for the file + pub const END_OF_FILE: ClusterId = ClusterId(0xFFFF_FFFF); +} + +impl core::ops::Add for ClusterId { + type Output = ClusterId; + fn add(self, rhs: u32) -> ClusterId { + ClusterId(self.0 + rhs) + } +} + +impl core::ops::AddAssign for ClusterId { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + +impl core::fmt::Debug for ClusterId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "ClusterId(")?; + match *self { + Self::INVALID => { + write!(f, "{:08}", "INVALID")?; + } + Self::BAD => { + write!(f, "{:08}", "BAD")?; + } + Self::EMPTY => { + write!(f, "{:08}", "EMPTY")?; + } + Self::ROOT_DIR => { + write!(f, "{:08}", "ROOT")?; + } + Self::END_OF_FILE => { + write!(f, "{:08}", "EOF")?; + } + ClusterId(value) => { + write!(f, "{:08x}", value)?; + } + } + write!(f, ")")?; + Ok(()) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/directory.rs b/examples/ios/embedded-sdmmc/src/filesystem/directory.rs new file mode 100644 index 0000000..527807b --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/directory.rs @@ -0,0 +1,336 @@ +use crate::blockdevice::BlockIdx; +use crate::fat::{FatType, OnDiskDirEntry}; +use crate::filesystem::{Attributes, ClusterId, Handle, LfnBuffer, ShortFileName, Timestamp}; +use crate::{Error, RawVolume, VolumeManager}; + +use super::ToShortFileName; + +/// A directory entry, which tells you about other files and directories. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DirEntry { + /// The name of the file + pub name: ShortFileName, + /// When the file was last modified + pub mtime: Timestamp, + /// When the file was first created + pub ctime: Timestamp, + /// The file attributes (Read Only, Archive, etc) + pub attributes: Attributes, + /// The starting cluster of the file. The FAT tells us the following Clusters. + pub cluster: ClusterId, + /// The size of the file in bytes. + pub size: u32, + /// The disk block of this entry + pub entry_block: BlockIdx, + /// The offset on its block (in bytes) + pub entry_offset: u32, +} + +/// A handle for an open directory on disk. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and if you drop it, the VolumeManager will think you +/// still have the directory open, and it won't let you open the directory +/// again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_dir`] to close it +/// cleanly. +/// +/// If you want your directories to close themselves on drop, create your own +/// `Directory` type that wraps this one and also holds a `VolumeManager` +/// reference. You'll then also need to put your `VolumeManager` in some kind of +/// Mutex or RefCell, and deal with the fact you can't put them both in the same +/// struct any more because one refers to the other. Basically, it's complicated +/// and there's a reason we did it this way. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RawDirectory(pub(crate) Handle); + +impl RawDirectory { + /// Convert a raw directory into a droppable [`Directory`] + pub fn to_directory< + D, + T, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + >( + self, + volume_mgr: &VolumeManager, + ) -> Directory + where + D: crate::BlockDevice, + T: crate::TimeSource, + { + Directory::new(self, volume_mgr) + } +} + +/// A handle for an open directory on disk, which closes on drop. +/// +/// In contrast to a `RawDirectory`, a `Directory` holds a mutable reference to +/// its parent `VolumeManager`, which restricts which operations you can perform. +/// +/// If you drop a value of this type, it closes the directory automatically, but +/// any error that may occur will be ignored. To handle potential errors, use +/// the [`Directory::close`] method. +pub struct Directory< + 'a, + D, + T, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, +> where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + raw_directory: RawDirectory, + volume_mgr: &'a VolumeManager, +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + /// Create a new `Directory` from a `RawDirectory` + pub fn new( + raw_directory: RawDirectory, + volume_mgr: &'a VolumeManager, + ) -> Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { + Directory { + raw_directory, + volume_mgr, + } + } + + /// Open a directory. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + pub fn open_dir( + &self, + name: N, + ) -> Result, Error> + where + N: ToShortFileName, + { + let d = self.volume_mgr.open_dir(self.raw_directory, name)?; + Ok(d.to_directory(self.volume_mgr)) + } + + /// Change to a directory, mutating this object. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + pub fn change_dir(&mut self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + let d = self.volume_mgr.open_dir(self.raw_directory, name)?; + self.volume_mgr.close_dir(self.raw_directory).unwrap(); + self.raw_directory = d; + Ok(()) + } + + /// Look in a directory for a named file. + pub fn find_directory_entry(&self, name: N) -> Result> + where + N: ToShortFileName, + { + self.volume_mgr + .find_directory_entry(self.raw_directory, name) + } + + /// Call a callback function for each directory entry in a directory. + /// + /// Long File Names will be ignored. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir(&self, func: F) -> Result<(), Error> + where + F: FnMut(&DirEntry), + { + self.volume_mgr.iterate_dir(self.raw_directory, func) + } + + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. + /// + /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the + /// Long File Name. If you pass one that isn't large enough, any Long File + /// Names that don't fit will be ignored and presented as if they only had a + /// Short File Name. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir_lfn( + &self, + lfn_buffer: &mut LfnBuffer<'_>, + func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + { + self.volume_mgr + .iterate_dir_lfn(self.raw_directory, lfn_buffer, func) + } + + /// Open a file with the given full path. A file can only be opened once. + pub fn open_file_in_dir( + &self, + name: N, + mode: crate::Mode, + ) -> Result, crate::Error> + where + N: super::ToShortFileName, + { + let f = self + .volume_mgr + .open_file_in_dir(self.raw_directory, name, mode)?; + Ok(f.to_file(self.volume_mgr)) + } + + /// Delete a closed file with the given filename, if it exists. + pub fn delete_file_in_dir(&self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + self.volume_mgr.delete_file_in_dir(self.raw_directory, name) + } + + /// Make a directory inside this directory + pub fn make_dir_in_dir(&self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + self.volume_mgr.make_dir_in_dir(self.raw_directory, name) + } + + /// Convert back to a raw directory + pub fn to_raw_directory(self) -> RawDirectory { + let d = self.raw_directory; + core::mem::forget(self); + d + } + + /// Consume the `Directory` handle and close it. The behavior of this is similar + /// to using [`core::mem::drop`] or letting the `Directory` go out of scope, + /// except this lets the user handle any errors that may occur in the process, + /// whereas when using drop, any errors will be discarded silently. + pub fn close(self) -> Result<(), Error> { + let result = self.volume_mgr.close_dir(self.raw_directory); + core::mem::forget(self); + result + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop + for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn drop(&mut self) { + _ = self.volume_mgr.close_dir(self.raw_directory) + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + core::fmt::Debug for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Directory({})", self.raw_directory.0 .0) + } +} + +#[cfg(feature = "defmt-log")] +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + defmt::Format for Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "Directory({})", self.raw_directory.0 .0) + } +} + +/// Holds information about an open file on disk +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub(crate) struct DirectoryInfo { + /// The handle for this directory. + pub(crate) raw_directory: RawDirectory, + /// The handle for the volume this directory is on + pub(crate) raw_volume: RawVolume, + /// The starting point of the directory listing. + pub(crate) cluster: ClusterId, +} + +impl DirEntry { + pub(crate) fn serialize(&self, fat_type: FatType) -> [u8; OnDiskDirEntry::LEN] { + let mut data = [0u8; OnDiskDirEntry::LEN]; + data[0..11].copy_from_slice(&self.name.contents); + data[11] = self.attributes.0; + // 12: Reserved. Must be set to zero + // 13: CrtTimeTenth, not supported, set to zero + data[14..18].copy_from_slice(&self.ctime.serialize_to_fat()[..]); + // 0 + 18: LastAccDate, not supported, set to zero + let cluster_number = self.cluster.0; + let cluster_hi = if fat_type == FatType::Fat16 { + [0u8; 2] + } else { + // Safe due to the AND operation + (((cluster_number >> 16) & 0x0000_FFFF) as u16).to_le_bytes() + }; + data[20..22].copy_from_slice(&cluster_hi[..]); + data[22..26].copy_from_slice(&self.mtime.serialize_to_fat()[..]); + // Safe due to the AND operation + let cluster_lo = ((cluster_number & 0x0000_FFFF) as u16).to_le_bytes(); + data[26..28].copy_from_slice(&cluster_lo[..]); + data[28..32].copy_from_slice(&self.size.to_le_bytes()[..]); + data + } + + pub(crate) fn new( + name: ShortFileName, + attributes: Attributes, + cluster: ClusterId, + ctime: Timestamp, + entry_block: BlockIdx, + entry_offset: u32, + ) -> Self { + Self { + name, + mtime: ctime, + ctime, + attributes, + cluster, + size: 0, + entry_block, + entry_offset, + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/filename.rs b/examples/ios/embedded-sdmmc/src/filesystem/filename.rs new file mode 100644 index 0000000..31d2854 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/filename.rs @@ -0,0 +1,506 @@ +//! Filename related types + +use crate::fat::VolumeName; +use crate::trace; + +/// Various filename related errors that can occur. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub enum FilenameError { + /// Tried to create a file with an invalid character. + InvalidCharacter, + /// Tried to create a file with no file name. + FilenameEmpty, + /// Given name was too long (we are limited to 8.3). + NameTooLong, + /// Can't start a file with a period, or after 8 characters. + MisplacedPeriod, + /// Can't extract utf8 from file name + Utf8Error, +} + +/// Describes things we can convert to short 8.3 filenames +pub trait ToShortFileName { + /// Try and convert this value into a [`ShortFileName`]. + fn to_short_filename(self) -> Result; +} + +impl ToShortFileName for ShortFileName { + fn to_short_filename(self) -> Result { + Ok(self) + } +} + +impl ToShortFileName for &ShortFileName { + fn to_short_filename(self) -> Result { + Ok(self.clone()) + } +} + +impl ToShortFileName for &str { + fn to_short_filename(self) -> Result { + ShortFileName::create_from_str(self) + } +} + +/// An MS-DOS 8.3 filename. +/// +/// ISO-8859-1 encoding is assumed. All lower-case is converted to upper-case by +/// default. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(PartialEq, Eq, Clone)] +pub struct ShortFileName { + pub(crate) contents: [u8; Self::TOTAL_LEN], +} + +impl ShortFileName { + const BASE_LEN: usize = 8; + const TOTAL_LEN: usize = 11; + + /// Get a short file name containing "..", which means "parent directory". + pub const fn parent_dir() -> Self { + Self { + contents: *b".. ", + } + } + + /// Get a short file name containing ".", which means "this directory". + pub const fn this_dir() -> Self { + Self { + contents: *b". ", + } + } + + /// Get base name (without extension) of the file. + pub fn base_name(&self) -> &[u8] { + Self::bytes_before_space(&self.contents[..Self::BASE_LEN]) + } + + /// Get extension of the file (without base name). + pub fn extension(&self) -> &[u8] { + Self::bytes_before_space(&self.contents[Self::BASE_LEN..]) + } + + fn bytes_before_space(bytes: &[u8]) -> &[u8] { + bytes.split(|b| *b == b' ').next().unwrap_or(&[]) + } + + /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry. + /// + /// The output uses ISO-8859-1 encoding. + pub fn create_from_str(name: &str) -> Result { + let mut sfn = ShortFileName { + contents: [b' '; Self::TOTAL_LEN], + }; + + // Special case `..`, which means "parent directory". + if name == ".." { + return Ok(ShortFileName::parent_dir()); + } + + // Special case `.` (or blank), which means "this directory". + if name.is_empty() || name == "." { + return Ok(ShortFileName::this_dir()); + } + + let mut idx = 0; + let mut seen_dot = false; + for ch in name.chars() { + match ch { + // Microsoft say these are the invalid characters + '\u{0000}'..='\u{001F}' + | '"' + | '*' + | '+' + | ',' + | '/' + | ':' + | ';' + | '<' + | '=' + | '>' + | '?' + | '[' + | '\\' + | ']' + | ' ' + | '|' => { + return Err(FilenameError::InvalidCharacter); + } + x if x > '\u{00FF}' => { + // We only handle ISO-8859-1 which is Unicode Code Points + // \U+0000 to \U+00FF. This is above that. + return Err(FilenameError::InvalidCharacter); + } + '.' => { + // Denotes the start of the file extension + if (1..=Self::BASE_LEN).contains(&idx) { + idx = Self::BASE_LEN; + seen_dot = true; + } else { + return Err(FilenameError::MisplacedPeriod); + } + } + _ => { + let b = ch.to_ascii_uppercase() as u8; + if seen_dot { + if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + } else if idx < Self::BASE_LEN { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + idx += 1; + } + } + } + if idx == 0 { + return Err(FilenameError::FilenameEmpty); + } + Ok(sfn) + } + + /// Convert a Short File Name to a Volume Label. + /// + /// # Safety + /// + /// Volume Labels can contain things that Short File Names cannot, so only + /// do this conversion if you have the name of a directory entry with the + /// 'Volume Label' attribute. + pub unsafe fn to_volume_label(self) -> VolumeName { + VolumeName { + contents: self.contents, + } + } + + /// Get the LFN checksum for this short filename + pub fn csum(&self) -> u8 { + let mut result = 0u8; + for b in self.contents.iter() { + result = result.rotate_right(1).wrapping_add(*b); + } + result + } +} + +impl core::fmt::Display for ShortFileName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let mut printed = 0; + for (i, &c) in self.contents.iter().enumerate() { + if c != b' ' { + if i == Self::BASE_LEN { + write!(f, ".")?; + printed += 1; + } + // converting a byte to a codepoint means you are assuming + // ISO-8859-1 encoding, because that's how Unicode was designed. + write!(f, "{}", c as char)?; + printed += 1; + } + } + if let Some(mut width) = f.width() { + if width > printed { + width -= printed; + for _ in 0..width { + write!(f, "{}", f.fill())?; + } + } + } + Ok(()) + } +} + +impl core::fmt::Debug for ShortFileName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "ShortFileName(\"{}\")", self) + } +} + +/// Used to store a Long File Name +#[derive(Debug)] +pub struct LfnBuffer<'a> { + /// We fill this buffer in from the back + inner: &'a mut [u8], + /// How many bytes are free. + /// + /// This is also the byte index the string starts from. + free: usize, + /// Did we overflow? + overflow: bool, + /// If a surrogate-pair is split over two directory entries, remember half of it here. + unpaired_surrogate: Option, +} + +impl<'a> LfnBuffer<'a> { + /// Create a new, empty, LFN Buffer using the given mutable slice as its storage. + pub fn new(storage: &'a mut [u8]) -> LfnBuffer<'a> { + let len = storage.len(); + LfnBuffer { + inner: storage, + free: len, + overflow: false, + unpaired_surrogate: None, + } + } + + /// Empty out this buffer + pub fn clear(&mut self) { + self.free = self.inner.len(); + self.overflow = false; + self.unpaired_surrogate = None; + } + + /// Push the 13 UTF-16 codepoints into this string. + /// + /// We assume they are pushed last-chunk-first, as you would find + /// them on disk. + /// + /// Any chunk starting with a half of a surrogate pair has that saved for the next call. + /// + /// ```text + /// [de00, 002e, 0074, 0078, 0074, 0000, ffff, ffff, ffff, ffff, ffff, ffff, ffff] + /// [0041, 0042, 0030, 0031, 0032, 0033, 0034, 0035, 0036, 0037, 0038, 0039, d83d] + /// + /// Would map to + /// + /// 0041 0042 0030 0031 0032 0033 0034 0035 0036 0037 0038 0039 1f600 002e 0074 0078 0074, or + /// + /// "AB0123456789😀.txt" + /// ``` + pub fn push(&mut self, buffer: &[u16; 13]) { + // find the first null, if any + let null_idx = buffer + .iter() + .position(|&b| b == 0x0000) + .unwrap_or(buffer.len()); + // take all the wide chars, up to the null (or go to the end) + let buffer = &buffer[0..null_idx]; + + // This next part will convert the 16-bit values into chars, noting that + // chars outside the Basic Multilingual Plane will require two 16-bit + // values to encode (see UTF-16 Surrogate Pairs). + // + // We cache the decoded chars into this array so we can iterate them + // backwards. It's 60 bytes, but it'll have to do. + let mut char_vec: heapless::Vec = heapless::Vec::new(); + // Now do the decode, including the unpaired surrogate (if any) from + // last time (maybe it has a pair now!) + let mut is_first = true; + for ch in char::decode_utf16( + buffer + .iter() + .cloned() + .chain(self.unpaired_surrogate.take().iter().cloned()), + ) { + match ch { + Ok(ch) => { + char_vec.push(ch).expect("Vec was full!?"); + } + Err(e) => { + // OK, so we found half a surrogate pair and nothing to go + // with it. Was this the first codepoint in the chunk? + if is_first { + // it was - the other half is probably in the next chunk + // so save this for next time + trace!("LFN saved {:?}", e.unpaired_surrogate()); + self.unpaired_surrogate = Some(e.unpaired_surrogate()); + } else { + // it wasn't - can't deal with it these mid-sequence, so + // replace it + trace!("LFN replaced {:?}", e.unpaired_surrogate()); + char_vec.push('\u{fffd}').expect("Vec was full?!"); + } + } + } + is_first = false; + } + + for ch in char_vec.iter().rev() { + trace!("LFN push {:?}", ch); + // a buffer of length 4 is enough to encode any char + let mut encoded_ch = [0u8; 4]; + let encoded_ch = ch.encode_utf8(&mut encoded_ch); + if self.free < encoded_ch.len() { + // the LFN buffer they gave us was not long enough. Note for + // later, so we don't show them garbage. + self.overflow = true; + return; + } + // Store the encoded char in the buffer, working backwards. We + // already checked there was enough space. + for b in encoded_ch.bytes().rev() { + self.free -= 1; + self.inner[self.free] = b; + } + } + } + + /// View this LFN buffer as a string-slice + /// + /// If the buffer overflowed while parsing the LFN, or if this buffer is + /// empty, you get an empty string. + pub fn as_str(&self) -> &str { + if self.overflow { + "" + } else { + // we always only put UTF-8 encoded data in here + unsafe { core::str::from_utf8_unchecked(&self.inner[self.free..]) } + } + } +} + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn filename_no_extension() { + let sfn = ShortFileName { + contents: *b"HELLO ", + }; + assert_eq!(format!("{}", &sfn), "HELLO"); + assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap()); + assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap()); + assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap()); + assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap()); + } + + #[test] + fn filename_extension() { + let sfn = ShortFileName { + contents: *b"HELLO TXT", + }; + assert_eq!(format!("{}", &sfn), "HELLO.TXT"); + assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap()); + } + + #[test] + fn filename_get_extension() { + let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap(); + assert_eq!(sfn.extension(), "TXT".as_bytes()); + sfn = ShortFileName::create_from_str("hello").unwrap(); + assert_eq!(sfn.extension(), "".as_bytes()); + sfn = ShortFileName::create_from_str("hello.a").unwrap(); + assert_eq!(sfn.extension(), "A".as_bytes()); + } + + #[test] + fn filename_get_base_name() { + let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap(); + assert_eq!(sfn.base_name(), "HELLO".as_bytes()); + sfn = ShortFileName::create_from_str("12345678").unwrap(); + assert_eq!(sfn.base_name(), "12345678".as_bytes()); + sfn = ShortFileName::create_from_str("1").unwrap(); + assert_eq!(sfn.base_name(), "1".as_bytes()); + } + + #[test] + fn filename_fulllength() { + let sfn = ShortFileName { + contents: *b"12345678TXT", + }; + assert_eq!(format!("{}", &sfn), "12345678.TXT"); + assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap()); + } + + #[test] + fn filename_short_extension() { + let sfn = ShortFileName { + contents: *b"12345678C ", + }; + assert_eq!(format!("{}", &sfn), "12345678.C"); + assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap()); + } + + #[test] + fn filename_short() { + let sfn = ShortFileName { + contents: *b"1 C ", + }; + assert_eq!(format!("{}", &sfn), "1.C"); + assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap()); + } + + #[test] + fn filename_empty() { + assert_eq!( + ShortFileName::create_from_str("").unwrap(), + ShortFileName::this_dir() + ); + } + + #[test] + fn filename_bad() { + assert!(ShortFileName::create_from_str(" ").is_err()); + assert!(ShortFileName::create_from_str("123456789").is_err()); + assert!(ShortFileName::create_from_str("12345678.ABCD").is_err()); + } + + #[test] + fn checksum() { + assert_eq!( + 0xB3, + ShortFileName::create_from_str("UNARCH~1.DAT") + .unwrap() + .csum() + ); + } + + #[test] + fn one_piece() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + buf.push(&[ + 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, + ]); + assert_eq!(buf.as_str(), "0123∂"); + } + + #[test] + fn two_piece() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + buf.push(&[ + 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, + ]); + buf.push(&[ + 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b, + 0x004c, 0x004d, + ]); + assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂"); + } + + #[test] + fn two_piece_split_surrogate() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + + buf.push(&[ + 0xde00, 0x002e, 0x0074, 0x0078, 0x0074, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, + 0xffff, 0xffff, + ]); + buf.push(&[ + 0xd83d, 0xde00, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, + 0x0039, 0xd83d, + ]); + assert_eq!(buf.as_str(), "😀0123456789😀.txt"); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/files.rs b/examples/ios/embedded-sdmmc/src/filesystem/files.rs new file mode 100644 index 0000000..870d85d --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/files.rs @@ -0,0 +1,359 @@ +use super::TimeSource; +use crate::{ + filesystem::{ClusterId, DirEntry, Handle}, + BlockDevice, Error, RawVolume, VolumeManager, +}; +use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; + +/// A handle for an open file on disk. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and cannot update the directory entry if you drop it. +/// Additionally, the VolumeManager will think you still have the file open if +/// you just drop it, and it won't let you open the file again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_file`] to close it +/// cleanly. +/// +/// If you want your files to close themselves on drop, create your own File +/// type that wraps this one and also holds a `VolumeManager` reference. You'll +/// then also need to put your `VolumeManager` in some kind of Mutex or RefCell, +/// and deal with the fact you can't put them both in the same struct any more +/// because one refers to the other. Basically, it's complicated and there's a +/// reason we did it this way. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RawFile(pub(crate) Handle); + +impl RawFile { + /// Convert a raw file into a droppable [`File`] + pub fn to_file( + self, + volume_mgr: &VolumeManager, + ) -> File + where + D: crate::BlockDevice, + T: crate::TimeSource, + { + File::new(self, volume_mgr) + } +} + +/// A handle for an open file on disk, which closes on drop. +/// +/// In contrast to a `RawFile`, a `File` holds a mutable reference to its +/// parent `VolumeManager`, which restricts which operations you can perform. +/// +/// If you drop a value of this type, it closes the file automatically, and but +/// error that may occur will be ignored. To handle potential errors, use +/// the [`File::close`] method. +pub struct File<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + raw_file: RawFile, + volume_mgr: &'a VolumeManager, +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + /// Create a new `File` from a `RawFile` + pub fn new( + raw_file: RawFile, + volume_mgr: &'a VolumeManager, + ) -> File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { + File { + raw_file, + volume_mgr, + } + } + + /// Read from the file + /// + /// Returns how many bytes were read, or an error. + pub fn read(&self, buffer: &mut [u8]) -> Result> { + self.volume_mgr.read(self.raw_file, buffer) + } + + /// Write to the file + pub fn write(&self, buffer: &[u8]) -> Result<(), crate::Error> { + self.volume_mgr.write(self.raw_file, buffer) + } + + /// Check if a file is at End Of File. + pub fn is_eof(&self) -> bool { + self.volume_mgr + .file_eof(self.raw_file) + .expect("Corrupt file ID") + } + + /// Seek a file with an offset from the current position. + pub fn seek_from_current(&self, offset: i32) -> Result<(), crate::Error> { + self.volume_mgr + .file_seek_from_current(self.raw_file, offset) + } + + /// Seek a file with an offset from the start of the file. + pub fn seek_from_start(&self, offset: u32) -> Result<(), crate::Error> { + self.volume_mgr.file_seek_from_start(self.raw_file, offset) + } + + /// Seek a file with an offset back from the end of the file. + pub fn seek_from_end(&self, offset: u32) -> Result<(), crate::Error> { + self.volume_mgr.file_seek_from_end(self.raw_file, offset) + } + + /// Get the length of a file + pub fn length(&self) -> u32 { + self.volume_mgr + .file_length(self.raw_file) + .expect("Corrupt file ID") + } + + /// Get the current offset of a file + pub fn offset(&self) -> u32 { + self.volume_mgr + .file_offset(self.raw_file) + .expect("Corrupt file ID") + } + + /// Convert back to a raw file + pub fn to_raw_file(self) -> RawFile { + let f = self.raw_file; + core::mem::forget(self); + f + } + + /// Flush any written data by updating the directory entry. + pub fn flush(&self) -> Result<(), Error> { + self.volume_mgr.flush_file(self.raw_file) + } + + /// Consume the `File` handle and close it. The behavior of this is similar + /// to using [`core::mem::drop`] or letting the `File` go out of scope, + /// except this lets the user handle any errors that may occur in the process, + /// whereas when using drop, any errors will be discarded silently. + pub fn close(self) -> Result<(), Error> { + let result = self.volume_mgr.close_file(self.raw_file); + core::mem::forget(self); + result + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop + for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn drop(&mut self) { + _ = self.volume_mgr.close_file(self.raw_file); + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + core::fmt::Debug for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "File({})", self.raw_file.0 .0) + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > ErrorType for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + type Error = crate::Error; +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Read for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + Ok(0) + } else { + self.read(buf) + } + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Write for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn write(&mut self, buf: &[u8]) -> Result { + if buf.is_empty() { + Ok(0) + } else { + self.write(buf)?; + Ok(buf.len()) + } + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Self::flush(self) + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Seek for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + match pos { + SeekFrom::Start(offset) => { + self.seek_from_start(offset.try_into().map_err(|_| Error::InvalidOffset)?)? + } + SeekFrom::End(offset) => { + self.seek_from_end((-offset).try_into().map_err(|_| Error::InvalidOffset)?)? + } + SeekFrom::Current(offset) => { + self.seek_from_current(offset.try_into().map_err(|_| Error::InvalidOffset)?)? + } + } + Ok(self.offset().into()) + } +} + +#[cfg(feature = "defmt-log")] +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + defmt::Format for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "File({})", self.raw_file.0 .0) + } +} + +/// Errors related to file operations +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileError { + /// Tried to use an invalid offset. + InvalidOffset, +} + +/// The different ways we can open a file. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Mode { + /// Open a file for reading, if it exists. + ReadOnly, + /// Open a file for appending (writing to the end of the existing file), if it exists. + ReadWriteAppend, + /// Open a file and remove all contents, before writing to the start of the existing file, if it exists. + ReadWriteTruncate, + /// Create a new empty file. Fail if it exists. + ReadWriteCreate, + /// Create a new empty file, or truncate an existing file. + ReadWriteCreateOrTruncate, + /// Create a new empty file, or append to an existing file. + ReadWriteCreateOrAppend, +} + +/// Internal metadata about an open file +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub(crate) struct FileInfo { + /// Handle for this file + pub(crate) raw_file: RawFile, + /// The handle for the volume this directory is on + pub(crate) raw_volume: RawVolume, + /// The last cluster we accessed, and how many bytes that short-cuts us. + /// + /// This saves us walking from the very start of the FAT chain when we move + /// forward through a file. + pub(crate) current_cluster: (u32, ClusterId), + /// How far through the file we've read (in bytes). + pub(crate) current_offset: u32, + /// What mode the file was opened in + pub(crate) mode: Mode, + /// DirEntry of this file + pub(crate) entry: DirEntry, + /// Did we write to this file? + pub(crate) dirty: bool, +} + +impl FileInfo { + /// Are we at the end of the file? + pub fn eof(&self) -> bool { + self.current_offset == self.entry.size + } + + /// How long is the file? + pub fn length(&self) -> u32 { + self.entry.size + } + + /// Seek to a new position in the file, relative to the start of the file. + pub fn seek_from_start(&mut self, offset: u32) -> Result<(), FileError> { + if offset > self.entry.size { + return Err(FileError::InvalidOffset); + } + self.current_offset = offset; + Ok(()) + } + + /// Seek to a new position in the file, relative to the end of the file. + pub fn seek_from_end(&mut self, offset: u32) -> Result<(), FileError> { + if offset > self.entry.size { + return Err(FileError::InvalidOffset); + } + self.current_offset = self.entry.size - offset; + Ok(()) + } + + /// Seek to a new position in the file, relative to the current position. + pub fn seek_from_current(&mut self, offset: i32) -> Result<(), FileError> { + let new_offset = i64::from(self.current_offset) + i64::from(offset); + if new_offset < 0 || new_offset > i64::from(self.entry.size) { + return Err(FileError::InvalidOffset); + } + self.current_offset = new_offset as u32; + Ok(()) + } + + /// Amount of file left to read. + pub fn left(&self) -> u32 { + self.entry.size - self.current_offset + } + + /// Update the file's length. + pub(crate) fn update_length(&mut self, new: u32) { + self.entry.size = new; + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/handles.rs b/examples/ios/embedded-sdmmc/src/filesystem/handles.rs new file mode 100644 index 0000000..dd37903 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/handles.rs @@ -0,0 +1,48 @@ +//! Contains the Handles and the HandleGenerator. + +use core::num::Wrapping; + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +/// Unique ID used to identify things in the open Volume/File/Directory lists +pub struct Handle(pub(crate) u32); + +impl core::fmt::Debug for Handle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#08x}", self.0) + } +} + +/// A Handle Generator. +/// +/// This object will always return a different ID. +/// +/// Well, it will wrap after `2**32` IDs. But most systems won't open that many +/// files, and if they do, they are unlikely to hold one file open and then +/// open/close `2**32 - 1` others. +#[derive(Debug)] +pub struct HandleGenerator { + next_id: Wrapping, +} + +impl HandleGenerator { + /// Create a new generator of Handles. + pub const fn new(offset: u32) -> Self { + Self { + next_id: Wrapping(offset), + } + } + + /// Generate a new, unique [`Handle`]. + pub fn generate(&mut self) -> Handle { + let id = self.next_id; + self.next_id += 1; + Handle(id.0) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/mod.rs b/examples/ios/embedded-sdmmc/src/filesystem/mod.rs new file mode 100644 index 0000000..668ac86 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/mod.rs @@ -0,0 +1,32 @@ +//! Generic File System structures +//! +//! Implements generic file system components. These should be applicable to +//! most (if not all) supported filesystems. + +/// Maximum file size supported by this library +pub const MAX_FILE_SIZE: u32 = u32::MAX; + +mod attributes; +mod cluster; +mod directory; +mod filename; +mod files; +mod handles; +mod timestamp; + +pub use self::attributes::Attributes; +pub use self::cluster::ClusterId; +pub use self::directory::{DirEntry, Directory, RawDirectory}; +pub use self::filename::{FilenameError, LfnBuffer, ShortFileName, ToShortFileName}; +pub use self::files::{File, FileError, Mode, RawFile}; +pub use self::handles::{Handle, HandleGenerator}; +pub use self::timestamp::{TimeSource, Timestamp}; + +pub(crate) use self::directory::DirectoryInfo; +pub(crate) use self::files::FileInfo; + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs b/examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs new file mode 100644 index 0000000..ff2c0be --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/filesystem/timestamp.rs @@ -0,0 +1,141 @@ +/// Things that impl this can tell you the current time. +pub trait TimeSource { + /// Returns the current time + fn get_timestamp(&self) -> Timestamp; +} + +/// A Gregorian Calendar date/time, in the local time zone. +/// +/// TODO: Consider replacing this with POSIX time as a `u32`, which would save +/// two bytes at the expense of some maths. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct Timestamp { + /// Add 1970 to this file to get the calendar year + pub year_since_1970: u8, + /// Add one to this value to get the calendar month + pub zero_indexed_month: u8, + /// Add one to this value to get the calendar day + pub zero_indexed_day: u8, + /// The number of hours past midnight + pub hours: u8, + /// The number of minutes past the hour + pub minutes: u8, + /// The number of seconds past the minute + pub seconds: u8, +} + +impl Timestamp { + /// Create a `Timestamp` from the 16-bit FAT date and time fields. + pub fn from_fat(date: u16, time: u16) -> Timestamp { + let year = 1980 + (date >> 9); + let month = ((date >> 5) & 0x000F) as u8; + let day = (date & 0x001F) as u8; + let hours = ((time >> 11) & 0x001F) as u8; + let minutes = ((time >> 5) & 0x0003F) as u8; + let seconds = ((time << 1) & 0x0003F) as u8; + // Volume labels have a zero for month/day, so tolerate that... + Timestamp { + year_since_1970: (year - 1970) as u8, + zero_indexed_month: if month == 0 { 0 } else { month - 1 }, + zero_indexed_day: if day == 0 { 0 } else { day - 1 }, + hours, + minutes, + seconds, + } + } + + // TODO add tests for the method + /// Serialize a `Timestamp` to FAT format + pub fn serialize_to_fat(self) -> [u8; 4] { + let mut data = [0u8; 4]; + + let hours = (u16::from(self.hours) << 11) & 0xF800; + let minutes = (u16::from(self.minutes) << 5) & 0x07E0; + let seconds = (u16::from(self.seconds / 2)) & 0x001F; + data[..2].copy_from_slice(&(hours | minutes | seconds).to_le_bytes()[..]); + + let year = if self.year_since_1970 < 10 { + 0 + } else { + (u16::from(self.year_since_1970 - 10) << 9) & 0xFE00 + }; + let month = (u16::from(self.zero_indexed_month + 1) << 5) & 0x01E0; + let day = u16::from(self.zero_indexed_day + 1) & 0x001F; + data[2..].copy_from_slice(&(year | month | day).to_le_bytes()[..]); + data + } + + /// Create a `Timestamp` from year/month/day/hour/minute/second. + /// + /// Values should be given as you'd write then (i.e. 1980, 01, 01, 13, 30, + /// 05) is 1980-Jan-01, 1:30:05pm. + pub fn from_calendar( + year: u16, + month: u8, + day: u8, + hours: u8, + minutes: u8, + seconds: u8, + ) -> Result { + Ok(Timestamp { + year_since_1970: if (1970..=(1970 + 255)).contains(&year) { + (year - 1970) as u8 + } else { + return Err("Bad year"); + }, + zero_indexed_month: if (1..=12).contains(&month) { + month - 1 + } else { + return Err("Bad month"); + }, + zero_indexed_day: if (1..=31).contains(&day) { + day - 1 + } else { + return Err("Bad day"); + }, + hours: if hours <= 23 { + hours + } else { + return Err("Bad hours"); + }, + minutes: if minutes <= 59 { + minutes + } else { + return Err("Bad minutes"); + }, + seconds: if seconds <= 59 { + seconds + } else { + return Err("Bad seconds"); + }, + }) + } +} + +impl core::fmt::Debug for Timestamp { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "Timestamp({})", self) + } +} + +impl core::fmt::Display for Timestamp { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "{}-{:02}-{:02} {:02}:{:02}:{:02}", + u16::from(self.year_since_1970) + 1970, + self.zero_indexed_month + 1, + self.zero_indexed_day + 1, + self.hours, + self.minutes, + self.seconds + ) + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/lib.rs b/examples/ios/embedded-sdmmc/src/lib.rs new file mode 100644 index 0000000..c6af4e9 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/lib.rs @@ -0,0 +1,461 @@ +//! # embedded-sdmmc +//! +//! > An SD/MMC Library written in Embedded Rust +//! +//! This crate is intended to allow you to read/write files on a FAT formatted +//! SD card on your Rust Embedded device, as easily as using the `SdFat` Arduino +//! library. It is written in pure-Rust, is `#![no_std]` and does not use +//! `alloc` or `collections` to keep the memory footprint low. In the first +//! instance it is designed for readability and simplicity over performance. +//! +//! ## Using the crate +//! +//! You will need something that implements the `BlockDevice` trait, which can +//! read and write the 512-byte blocks (or sectors) from your card. If you were +//! to implement this over USB Mass Storage, there's no reason this crate +//! couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` +//! suitable for reading SD and SDHC cards over SPI. +//! +//! ```rust +//! use embedded_sdmmc::{Error, Mode, SdCard, SdCardError, TimeSource, VolumeIdx, VolumeManager}; +//! +//! fn example(spi: S, delay: D, ts: T) -> Result<(), Error> +//! where +//! S: embedded_hal::spi::SpiDevice, +//! D: embedded_hal::delay::DelayNs, +//! T: TimeSource, +//! { +//! let sdcard = SdCard::new(spi, delay); +//! println!("Card size is {} bytes", sdcard.num_bytes()?); +//! let volume_mgr = VolumeManager::new(sdcard, ts); +//! let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; +//! println!("Volume 0: {:?}", volume0); +//! let root_dir = volume0.open_root_dir()?; +//! let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; +//! while !my_file.is_eof() { +//! let mut buffer = [0u8; 32]; +//! let num_read = my_file.read(&mut buffer)?; +//! for b in &buffer[0..num_read] { +//! print!("{}", *b as char); +//! } +//! } +//! Ok(()) +//! } +//! ``` +//! +//! For writing files: +//! +//! ```rust +//! use embedded_sdmmc::{BlockDevice, Directory, Error, Mode, TimeSource}; +//! fn write_file( +//! root_dir: &mut Directory, +//! ) -> Result<(), Error> +//! { +//! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", Mode::ReadWriteCreateOrAppend)?; +//! my_other_file.write(b"Timestamp,Signal,Value\n")?; +//! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +//! my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +//! my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; +//! // Don't forget to flush the file so that the directory entry is updated +//! my_other_file.flush()?; +//! Ok(()) +//! } +//! ``` +//! +//! ## Features +//! +//! * `log`: Enabled by default. Generates log messages using the `log` crate. +//! * `defmt-log`: By turning off the default features and enabling the +//! `defmt-log` feature you can configure this crate to log messages over defmt +//! instead. +//! +//! You cannot enable both the `log` feature and the `defmt-log` feature. + +#![cfg_attr(not(test), no_std)] +#![deny(missing_docs)] + +// **************************************************************************** +// +// Imports +// +// **************************************************************************** + +#[cfg(test)] +#[macro_use] +extern crate hex_literal; + +#[macro_use] +mod structure; + +pub mod blockdevice; +pub mod fat; +pub mod filesystem; +pub mod sdcard; + +use core::fmt::Debug; +use embedded_io::ErrorKind; +use filesystem::Handle; + +#[doc(inline)] +pub use crate::blockdevice::{Block, BlockCache, BlockCount, BlockDevice, BlockIdx}; + +#[doc(inline)] +pub use crate::fat::{FatVolume, VolumeName}; + +#[doc(inline)] +pub use crate::filesystem::{ + Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, Mode, RawDirectory, + RawFile, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, +}; + +use filesystem::DirectoryInfo; + +#[doc(inline)] +pub use crate::sdcard::Error as SdCardError; + +#[doc(inline)] +pub use crate::sdcard::SdCard; + +mod volume_mgr; +#[doc(inline)] +pub use volume_mgr::VolumeManager; + +#[cfg(all(feature = "defmt-log", feature = "log"))] +compile_error!("Cannot enable both log and defmt-log"); + +#[cfg(feature = "log")] +use log::{debug, trace, warn}; + +#[cfg(feature = "defmt-log")] +use defmt::{debug, trace, warn}; + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::debug! but does nothing at all +macro_rules! debug { + ($($arg:tt)+) => {}; +} + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::trace! but does nothing at all +macro_rules! trace { + ($($arg:tt)+) => {}; +} + +#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))] +#[macro_export] +/// Like log::warn! but does nothing at all +macro_rules! warn { + ($($arg:tt)+) => {}; +} + +// **************************************************************************** +// +// Public Types +// +// **************************************************************************** + +/// All the ways the functions in this crate can fail. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Clone)] +pub enum Error +where + E: core::fmt::Debug, +{ + /// The underlying block device threw an error. + DeviceError(E), + /// The filesystem is badly formatted (or this code is buggy). + FormatError(&'static str), + /// The given `VolumeIdx` was bad, + NoSuchVolume, + /// The given filename was bad + FilenameError(FilenameError), + /// Out of memory opening volumes + TooManyOpenVolumes, + /// Out of memory opening directories + TooManyOpenDirs, + /// Out of memory opening files + TooManyOpenFiles, + /// Bad handle given + BadHandle, + /// That file or directory doesn't exist + NotFound, + /// You can't open a file twice or delete an open file + FileAlreadyOpen, + /// You can't open a directory twice + DirAlreadyOpen, + /// You can't open a directory as a file + OpenedDirAsFile, + /// You can't open a file as a directory + OpenedFileAsDir, + /// You can't delete a directory as a file + DeleteDirAsFile, + /// You can't close a volume with open files or directories + VolumeStillInUse, + /// You can't open a volume twice + VolumeAlreadyOpen, + /// We can't do that yet + Unsupported, + /// Tried to read beyond end of file + EndOfFile, + /// Found a bad cluster + BadCluster, + /// Error while converting types + ConversionError, + /// The device does not have enough space for the operation + NotEnoughSpace, + /// Cluster was not properly allocated by the library + AllocationError, + /// Jumped to free space during FAT traversing + UnterminatedFatChain, + /// Tried to open Read-Only file with write mode + ReadOnly, + /// Tried to create an existing file + FileAlreadyExists, + /// Bad block size - only 512 byte blocks supported + BadBlockSize(u16), + /// Bad offset given when seeking + InvalidOffset, + /// Disk is full + DiskFull, + /// A directory with that name already exists + DirAlreadyExists, + /// The filesystem tried to gain a lock whilst already locked. + /// + /// This is either a bug in the filesystem, or you tried to access the + /// filesystem API from inside a directory iterator (that isn't allowed). + LockError, +} + +impl embedded_io::Error for Error { + fn kind(&self) -> ErrorKind { + match self { + Error::DeviceError(_) + | Error::FormatError(_) + | Error::FileAlreadyOpen + | Error::DirAlreadyOpen + | Error::VolumeStillInUse + | Error::VolumeAlreadyOpen + | Error::EndOfFile + | Error::DiskFull + | Error::NotEnoughSpace + | Error::AllocationError + | Error::LockError => ErrorKind::Other, + Error::NoSuchVolume + | Error::FilenameError(_) + | Error::BadHandle + | Error::InvalidOffset => ErrorKind::InvalidInput, + Error::TooManyOpenVolumes | Error::TooManyOpenDirs | Error::TooManyOpenFiles => { + ErrorKind::OutOfMemory + } + Error::NotFound => ErrorKind::NotFound, + Error::OpenedDirAsFile + | Error::OpenedFileAsDir + | Error::DeleteDirAsFile + | Error::BadCluster + | Error::ConversionError + | Error::UnterminatedFatChain => ErrorKind::InvalidData, + Error::Unsupported | Error::BadBlockSize(_) => ErrorKind::Unsupported, + Error::ReadOnly => ErrorKind::PermissionDenied, + Error::FileAlreadyExists | Error::DirAlreadyExists => ErrorKind::AlreadyExists, + } + } +} + +impl From for Error +where + E: core::fmt::Debug, +{ + fn from(value: E) -> Error { + Error::DeviceError(value) + } +} + +/// A handle to a volume. +/// +/// A volume is a partition with a filesystem within it. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and the VolumeManager will think you still have the +/// volume open if you just drop it, and it won't let you open the file again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_volume`] to close +/// it cleanly. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RawVolume(Handle); + +impl RawVolume { + /// Convert a raw volume into a droppable [`Volume`] + pub fn to_volume< + D, + T, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + >( + self, + volume_mgr: &VolumeManager, + ) -> Volume + where + D: crate::BlockDevice, + T: crate::TimeSource, + { + Volume::new(self, volume_mgr) + } +} + +/// A handle for an open volume on disk, which closes on drop. +/// +/// In contrast to a `RawVolume`, a `Volume` holds a mutable reference to its +/// parent `VolumeManager`, which restricts which operations you can perform. +/// +/// If you drop a value of this type, it closes the volume automatically, but +/// any error that may occur will be ignored. To handle potential errors, use +/// the [`Volume::close`] method. +pub struct Volume<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + raw_volume: RawVolume, + volume_mgr: &'a VolumeManager, +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + /// Create a new `Volume` from a `RawVolume` + pub fn new( + raw_volume: RawVolume, + volume_mgr: &'a VolumeManager, + ) -> Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { + Volume { + raw_volume, + volume_mgr, + } + } + + /// Open the volume's root directory. + /// + /// You can then read the directory entries with `iterate_dir`, or you can + /// use `open_file_in_dir`. + pub fn open_root_dir( + &self, + ) -> Result, Error> { + let d = self.volume_mgr.open_root_dir(self.raw_volume)?; + Ok(d.to_directory(self.volume_mgr)) + } + + /// Convert back to a raw volume + pub fn to_raw_volume(self) -> RawVolume { + let v = self.raw_volume; + core::mem::forget(self); + v + } + + /// Consume the `Volume` handle and close it. The behavior of this is similar + /// to using [`core::mem::drop`] or letting the `Volume` go out of scope, + /// except this lets the user handle any errors that may occur in the process, + /// whereas when using drop, any errors will be discarded silently. + pub fn close(self) -> Result<(), Error> { + let result = self.volume_mgr.close_volume(self.raw_volume); + core::mem::forget(self); + result + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop + for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn drop(&mut self) { + _ = self.volume_mgr.close_volume(self.raw_volume) + } +} + +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + core::fmt::Debug for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Volume({})", self.raw_volume.0 .0) + } +} + +#[cfg(feature = "defmt-log")] +impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> + defmt::Format for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +where + D: crate::BlockDevice, + T: crate::TimeSource, +{ + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "Volume({})", self.raw_volume.0 .0) + } +} + +/// Internal information about a Volume +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct VolumeInfo { + /// Handle for this volume. + raw_volume: RawVolume, + /// Which volume (i.e. partition) we opened on the disk + idx: VolumeIdx, + /// What kind of volume this is + volume_type: VolumeType, +} + +/// This enum holds the data for the various different types of filesystems we +/// support. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq)] +pub enum VolumeType { + /// FAT16/FAT32 formatted volumes. + Fat(FatVolume), +} + +/// A number which identifies a volume (or partition) on a disk. +/// +/// `VolumeIdx(0)` is the first primary partition on an MBR partitioned disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct VolumeIdx(pub usize); + +/// Marker for a FAT32 partition. Sometimes also use for FAT16 formatted +/// partitions. +const PARTITION_ID_FAT32_LBA: u8 = 0x0C; +/// Marker for a FAT16 partition with LBA. Seen on a Raspberry Pi SD card. +const PARTITION_ID_FAT16_LBA: u8 = 0x0E; +/// Marker for a FAT16 partition. Seen on a card formatted with the official +/// SD-Card formatter. +const PARTITION_ID_FAT16: u8 = 0x06; +/// Marker for a FAT16 partition smaller than 32MB. Seen on the wowki simulated +/// microsd card +const PARTITION_ID_FAT16_SMALL: u8 = 0x04; +/// Marker for a FAT32 partition. What Macosx disk utility (and also SD-Card formatter?) +/// use. +const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B; + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +// None + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/sdcard/mod.rs b/examples/ios/embedded-sdmmc/src/sdcard/mod.rs new file mode 100644 index 0000000..553791f --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/sdcard/mod.rs @@ -0,0 +1,720 @@ +//! Implements the BlockDevice trait for an SD/MMC Protocol over SPI. +//! +//! This is currently optimised for readability and debugability, not +//! performance. + +pub mod proto; + +use crate::{trace, Block, BlockCount, BlockDevice, BlockIdx}; +use core::cell::RefCell; +use proto::*; + +// **************************************************************************** +// Imports +// **************************************************************************** + +use crate::{debug, warn}; + +// **************************************************************************** +// Types and Implementations +// **************************************************************************** + +/// Driver for an SD Card on an SPI bus. +/// +/// Built from an [`SpiDevice`] implementation and a Chip Select pin. +/// +/// Before talking to the SD Card, the caller needs to send 74 clocks cycles on +/// the SPI Clock line, at 400 kHz, with no chip-select asserted (or at least, +/// not the chip-select of the SD Card). +/// +/// This kind of breaks the embedded-hal model, so how to do this is left to +/// the caller. You could drive the SpiBus directly, or use an SpiDevice with +/// a dummy chip-select pin. Or you could try just not doing the 74 clocks and +/// see if your card works anyway - some do, some don't. +/// +/// All the APIs take `&self` - mutability is handled using an inner `RefCell`. +/// +/// [`SpiDevice`]: embedded_hal::spi::SpiDevice +pub struct SdCard +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + inner: RefCell>, +} + +impl SdCard +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + /// Create a new SD/MMC Card driver using a raw SPI interface. + /// + /// The card will not be initialised at this time. Initialisation is + /// deferred until a method is called on the object. + /// + /// Uses the default options. + pub fn new(spi: SPI, delayer: DELAYER) -> SdCard { + Self::new_with_options(spi, delayer, AcquireOpts::default()) + } + + /// Construct a new SD/MMC Card driver, using a raw SPI interface and the given options. + /// + /// See the docs of the [`SdCard`] struct for more information about + /// how to construct the needed `SPI` and `CS` types. + /// + /// The card will not be initialised at this time. Initialisation is + /// deferred until a method is called on the object. + pub fn new_with_options( + spi: SPI, + delayer: DELAYER, + options: AcquireOpts, + ) -> SdCard { + SdCard { + inner: RefCell::new(SdCardInner { + spi, + delayer, + card_type: None, + options, + }), + } + } + + /// Get a temporary borrow on the underlying SPI device. + /// + /// The given closure will be called exactly once, and will be passed a + /// mutable reference to the underlying SPI object. + /// + /// Useful if you need to re-clock the SPI, but does not perform card + /// initialisation. + pub fn spi(&self, func: F) -> T + where + F: FnOnce(&mut SPI) -> T, + { + let mut inner = self.inner.borrow_mut(); + func(&mut inner.spi) + } + + /// Return the usable size of this SD card in bytes. + /// + /// This will trigger card (re-)initialisation. + pub fn num_bytes(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.num_bytes() + } + + /// Can this card erase single blocks? + /// + /// This will trigger card (re-)initialisation. + pub fn erase_single_block_enabled(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.erase_single_block_enabled() + } + + /// Mark the card as requiring a reset. + /// + /// The next operation will assume the card has been freshly inserted. + pub fn mark_card_uninit(&self) { + let mut inner = self.inner.borrow_mut(); + inner.card_type = None; + } + + /// Get the card type. + /// + /// This will trigger card (re-)initialisation. + pub fn get_card_type(&self) -> Option { + let mut inner = self.inner.borrow_mut(); + inner.check_init().ok()?; + inner.card_type + } + + /// Tell the driver the card has been initialised. + /// + /// This is here in case you were previously using the SD Card, and then a + /// previous instance of this object got destroyed but you know for certain + /// the SD Card remained powered up and initialised, and you'd just like to + /// read/write to/from the card again without going through the + /// initialisation sequence again. + /// + /// # Safety + /// + /// Only do this if the SD Card has actually been initialised. That is, if + /// you have been through the card initialisation sequence as specified in + /// the SD Card Specification by sending each appropriate command in turn, + /// either manually or using another variable of this [`SdCard`]. The card + /// must also be of the indicated type. Failure to uphold this will cause + /// data corruption. + pub unsafe fn mark_card_as_init(&self, card_type: CardType) { + let mut inner = self.inner.borrow_mut(); + inner.card_type = Some(card_type); + } +} + +impl BlockDevice for SdCard +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + type Error = Error; + + /// Read one or more blocks, starting at the given block index. + /// + /// This will trigger card (re-)initialisation. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut inner = self.inner.borrow_mut(); + debug!("Read {} blocks @ {}", blocks.len(), start_block_idx.0,); + inner.check_init()?; + inner.read(blocks, start_block_idx) + } + + /// Write one or more blocks, starting at the given block index. + /// + /// This will trigger card (re-)initialisation. + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut inner = self.inner.borrow_mut(); + debug!("Writing {} blocks @ {}", blocks.len(), start_block_idx.0); + inner.check_init()?; + inner.write(blocks, start_block_idx) + } + + /// Determine how many blocks this device can hold. + /// + /// This will trigger card (re-)initialisation. + fn num_blocks(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + inner.check_init()?; + inner.num_blocks() + } +} + +/// Inner details for the SD Card driver. +/// +/// All the APIs required `&mut self`. +struct SdCardInner +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + spi: SPI, + delayer: DELAYER, + card_type: Option, + options: AcquireOpts, +} + +impl SdCardInner +where + SPI: embedded_hal::spi::SpiDevice, + DELAYER: embedded_hal::delay::DelayNs, +{ + /// Read one or more blocks, starting at the given block index. + fn read(&mut self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Error> { + let start_idx = match self.card_type { + Some(CardType::SD1 | CardType::SD2) => start_block_idx.0 * 512, + Some(CardType::SDHC) => start_block_idx.0, + None => return Err(Error::CardNotFound), + }; + + if blocks.len() == 1 { + // Start a single-block read + self.card_command(CMD17, start_idx)?; + self.read_data(&mut blocks[0].contents)?; + } else { + // Start a multi-block read + self.card_command(CMD18, start_idx)?; + for block in blocks.iter_mut() { + self.read_data(&mut block.contents)?; + } + // Stop the read + self.card_command(CMD12, 0)?; + } + Ok(()) + } + + /// Write one or more blocks, starting at the given block index. + fn write(&mut self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Error> { + let start_idx = match self.card_type { + Some(CardType::SD1 | CardType::SD2) => start_block_idx.0 * 512, + Some(CardType::SDHC) => start_block_idx.0, + None => return Err(Error::CardNotFound), + }; + if blocks.len() == 1 { + // Start a single-block write + self.card_command(CMD24, start_idx)?; + self.write_data(DATA_START_BLOCK, &blocks[0].contents)?; + self.wait_not_busy(Delay::new_write())?; + if self.card_command(CMD13, 0)? != 0x00 { + return Err(Error::WriteError); + } + if self.read_byte()? != 0x00 { + return Err(Error::WriteError); + } + } else { + // > It is recommended using this command preceding CMD25, some of the cards will be faster for Multiple + // > Write Blocks operation. Note that the host should send ACMD23 just before WRITE command if the host + // > wants to use the pre-erased feature + self.card_acmd(ACMD23, blocks.len() as u32)?; + // wait for card to be ready before sending the next command + self.wait_not_busy(Delay::new_write())?; + + // Start a multi-block write + self.card_command(CMD25, start_idx)?; + for block in blocks.iter() { + self.wait_not_busy(Delay::new_write())?; + self.write_data(WRITE_MULTIPLE_TOKEN, &block.contents)?; + } + // Stop the write + self.wait_not_busy(Delay::new_write())?; + self.write_byte(STOP_TRAN_TOKEN)?; + } + Ok(()) + } + + /// Determine how many blocks this device can hold. + fn num_blocks(&mut self) -> Result { + let csd = self.read_csd()?; + debug!("CSD: {:?}", csd); + let num_blocks = match csd { + Csd::V1(ref contents) => contents.card_capacity_blocks(), + Csd::V2(ref contents) => contents.card_capacity_blocks(), + }; + Ok(BlockCount(num_blocks)) + } + + /// Return the usable size of this SD card in bytes. + fn num_bytes(&mut self) -> Result { + let csd = self.read_csd()?; + debug!("CSD: {:?}", csd); + match csd { + Csd::V1(ref contents) => Ok(contents.card_capacity_bytes()), + Csd::V2(ref contents) => Ok(contents.card_capacity_bytes()), + } + } + + /// Can this card erase single blocks? + pub fn erase_single_block_enabled(&mut self) -> Result { + let csd = self.read_csd()?; + match csd { + Csd::V1(ref contents) => Ok(contents.erase_single_block_enabled()), + Csd::V2(ref contents) => Ok(contents.erase_single_block_enabled()), + } + } + + /// Read the 'card specific data' block. + fn read_csd(&mut self) -> Result { + match self.card_type { + Some(CardType::SD1) => { + let mut csd = CsdV1::new(); + if self.card_command(CMD9, 0)? != 0 { + return Err(Error::RegisterReadError); + } + self.read_data(&mut csd.data)?; + Ok(Csd::V1(csd)) + } + Some(CardType::SD2 | CardType::SDHC) => { + let mut csd = CsdV2::new(); + if self.card_command(CMD9, 0)? != 0 { + return Err(Error::RegisterReadError); + } + self.read_data(&mut csd.data)?; + Ok(Csd::V2(csd)) + } + None => Err(Error::CardNotFound), + } + } + + /// Read an arbitrary number of bytes from the card using the SD Card + /// protocol and an optional CRC. Always fills the given buffer, so make + /// sure it's the right size. + fn read_data(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + // Get first non-FF byte. + let mut delay = Delay::new_read(); + let status = loop { + let s = self.read_byte()?; + if s != 0xFF { + break s; + } + delay.delay(&mut self.delayer, Error::TimeoutReadBuffer)?; + }; + if status != DATA_START_BLOCK { + return Err(Error::ReadError); + } + + buffer.fill(0xFF); + self.transfer_bytes(buffer)?; + + // These two bytes are always sent. They are either a valid CRC, or + // junk, depending on whether CRC mode was enabled. + let mut crc_bytes = [0xFF; 2]; + self.transfer_bytes(&mut crc_bytes)?; + if self.options.use_crc { + let crc = u16::from_be_bytes(crc_bytes); + let calc_crc = crc16(buffer); + if crc != calc_crc { + return Err(Error::CrcError(crc, calc_crc)); + } + } + + Ok(()) + } + + /// Write an arbitrary number of bytes to the card using the SD protocol and + /// an optional CRC. + fn write_data(&mut self, token: u8, buffer: &[u8]) -> Result<(), Error> { + self.write_byte(token)?; + self.write_bytes(buffer)?; + let crc_bytes = if self.options.use_crc { + crc16(buffer).to_be_bytes() + } else { + [0xFF, 0xFF] + }; + // These two bytes are always sent. They are either a valid CRC, or + // junk, depending on whether CRC mode was enabled. + self.write_bytes(&crc_bytes)?; + + let status = self.read_byte()?; + if (status & DATA_RES_MASK) != DATA_RES_ACCEPTED { + Err(Error::WriteError) + } else { + Ok(()) + } + } + + /// Check the card is initialised. + fn check_init(&mut self) -> Result<(), Error> { + if self.card_type.is_none() { + // If we don't know what the card type is, try and initialise the + // card. This will tell us what type of card it is. + self.acquire() + } else { + Ok(()) + } + } + + /// Initializes the card into a known state (or at least tries to). + fn acquire(&mut self) -> Result<(), Error> { + debug!("acquiring card with opts: {:?}", self.options); + let f = |s: &mut Self| { + // Assume it hasn't worked + let mut card_type; + trace!("Reset card.."); + // Enter SPI mode. + let mut delay = Delay::new(s.options.acquire_retries); + for _attempts in 1.. { + trace!("Enter SPI mode, attempt: {}..", _attempts); + match s.card_command(CMD0, 0) { + Err(Error::TimeoutCommand(0)) => { + // Try again? + warn!("Timed out, trying again.."); + // Try flushing the card as done here: https://github.com/greiman/SdFat/blob/master/src/SdCard/SdSpiCard.cpp#L170, + // https://github.com/rust-embedded-community/embedded-sdmmc-rs/pull/65#issuecomment-1270709448 + for _ in 0..0xFF { + s.write_byte(0xFF)?; + } + } + Err(e) => { + return Err(e); + } + Ok(R1_IDLE_STATE) => { + break; + } + Ok(_r) => { + // Try again + warn!("Got response: {:x}, trying again..", _r); + } + } + + delay.delay(&mut s.delayer, Error::CardNotFound)?; + } + // Enable CRC + debug!("Enable CRC: {}", s.options.use_crc); + // "The SPI interface is initialized in the CRC OFF mode in default" + // -- SD Part 1 Physical Layer Specification v9.00, Section 7.2.2 Bus Transfer Protection + if s.options.use_crc && s.card_command(CMD59, 1)? != R1_IDLE_STATE { + return Err(Error::CantEnableCRC); + } + // Check card version + let mut delay = Delay::new_command(); + let arg = loop { + if s.card_command(CMD8, 0x1AA)? == (R1_ILLEGAL_COMMAND | R1_IDLE_STATE) { + card_type = CardType::SD1; + break 0; + } + let mut buffer = [0xFF; 4]; + s.transfer_bytes(&mut buffer)?; + let status = buffer[3]; + if status == 0xAA { + card_type = CardType::SD2; + break 0x4000_0000; + } + delay.delay(&mut s.delayer, Error::TimeoutCommand(CMD8))?; + }; + + let mut delay = Delay::new_command(); + while s.card_acmd(ACMD41, arg)? != R1_READY_STATE { + delay.delay(&mut s.delayer, Error::TimeoutACommand(ACMD41))?; + } + + if card_type == CardType::SD2 { + if s.card_command(CMD58, 0)? != 0 { + return Err(Error::Cmd58Error); + } + let mut buffer = [0xFF; 4]; + s.transfer_bytes(&mut buffer)?; + if (buffer[0] & 0xC0) == 0xC0 { + card_type = CardType::SDHC; + } + // Ignore the other three bytes + } + debug!("Card version: {:?}", card_type); + s.card_type = Some(card_type); + Ok(()) + }; + let result = f(self); + let _ = self.read_byte(); + result + } + + /// Perform an application-specific command. + fn card_acmd(&mut self, command: u8, arg: u32) -> Result { + self.card_command(CMD55, 0)?; + self.card_command(command, arg) + } + + /// Perform a command. + fn card_command(&mut self, command: u8, arg: u32) -> Result { + if command != CMD0 && command != CMD12 { + self.wait_not_busy(Delay::new_command())?; + } + + let mut buf = [ + 0x40 | command, + (arg >> 24) as u8, + (arg >> 16) as u8, + (arg >> 8) as u8, + arg as u8, + 0, + ]; + buf[5] = crc7(&buf[0..5]); + + self.write_bytes(&buf)?; + + // skip stuff byte for stop read + if command == CMD12 { + let _result = self.read_byte()?; + } + + let mut delay = Delay::new_command(); + loop { + let result = self.read_byte()?; + if (result & 0x80) == ERROR_OK { + return Ok(result); + } + delay.delay(&mut self.delayer, Error::TimeoutCommand(command))?; + } + } + + /// Receive a byte from the SPI bus by clocking out an 0xFF byte. + fn read_byte(&mut self) -> Result { + self.transfer_byte(0xFF) + } + + /// Send a byte over the SPI bus and ignore what comes back. + fn write_byte(&mut self, out: u8) -> Result<(), Error> { + let _ = self.transfer_byte(out)?; + Ok(()) + } + + /// Send one byte and receive one byte over the SPI bus. + fn transfer_byte(&mut self, out: u8) -> Result { + let mut read_buf = [0u8; 1]; + self.spi + .transfer(&mut read_buf, &[out]) + .map_err(|_| Error::Transport)?; + Ok(read_buf[0]) + } + + /// Send multiple bytes and ignore what comes back over the SPI bus. + fn write_bytes(&mut self, out: &[u8]) -> Result<(), Error> { + self.spi.write(out).map_err(|_e| Error::Transport)?; + Ok(()) + } + + /// Send multiple bytes and replace them with what comes back over the SPI bus. + fn transfer_bytes(&mut self, in_out: &mut [u8]) -> Result<(), Error> { + self.spi + .transfer_in_place(in_out) + .map_err(|_e| Error::Transport)?; + Ok(()) + } + + /// Spin until the card returns 0xFF, or we spin too many times and + /// timeout. + fn wait_not_busy(&mut self, mut delay: Delay) -> Result<(), Error> { + loop { + let s = self.read_byte()?; + if s == 0xFF { + break; + } + delay.delay(&mut self.delayer, Error::TimeoutWaitNotBusy)?; + } + Ok(()) + } +} + +/// Options for acquiring the card. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug)] +pub struct AcquireOpts { + /// Set to true to enable CRC checking on reading/writing blocks of data. + /// + /// Set to false to disable the CRC. Some cards don't support CRC correctly + /// and this option may be useful in that instance. + /// + /// On by default because without it you might get silent data corruption on + /// your card. + pub use_crc: bool, + + /// Sets the number of times we will retry to acquire the card before giving up and returning + /// `Err(Error::CardNotFound)`. By default, card acquisition will be retried 50 times. + pub acquire_retries: u32, +} + +impl Default for AcquireOpts { + fn default() -> Self { + AcquireOpts { + use_crc: true, + acquire_retries: 50, + } + } +} + +/// The possible errors this crate can generate. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone)] +pub enum Error { + /// We got an error from the SPI peripheral + Transport, + /// We failed to enable CRC checking on the SD card + CantEnableCRC, + /// We didn't get a response when reading data from the card + TimeoutReadBuffer, + /// We didn't get a response when waiting for the card to not be busy + TimeoutWaitNotBusy, + /// We didn't get a response when executing this command + TimeoutCommand(u8), + /// We didn't get a response when executing this application-specific command + TimeoutACommand(u8), + /// We got a bad response from Command 58 + Cmd58Error, + /// We failed to read the Card Specific Data register + RegisterReadError, + /// We got a CRC mismatch (card gave us, we calculated) + CrcError(u16, u16), + /// Error reading from the card + ReadError, + /// Error writing to the card + WriteError, + /// Can't perform this operation with the card in this state + BadState, + /// Couldn't find the card + CardNotFound, + /// Couldn't set a GPIO pin + GpioError, +} + +/// The different types of card we support. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CardType { + /// An standard-capacity SD Card supporting v1.x of the standard. + /// + /// Uses byte-addressing internally, so limited to 2GiB in size. + SD1, + /// An standard-capacity SD Card supporting v2.x of the standard. + /// + /// Uses byte-addressing internally, so limited to 2GiB in size. + SD2, + /// An high-capacity 'SDHC' Card. + /// + /// Uses block-addressing internally to support capacities above 2GiB. + SDHC, +} + +/// This an object you can use to busy-wait with a timeout. +/// +/// Will let you call `delay` up to `max_retries` times before `delay` returns +/// an error. +struct Delay { + retries_left: u32, +} + +impl Delay { + /// The default number of retries for a read operation. + /// + /// At ~10us each this is ~100ms. + /// + /// See `Part1_Physical_Layer_Simplified_Specification_Ver9.00-1.pdf` Section 4.6.2.1 + pub const DEFAULT_READ_RETRIES: u32 = 10_000; + + /// The default number of retries for a write operation. + /// + /// At ~10us each this is ~500ms. + /// + /// See `Part1_Physical_Layer_Simplified_Specification_Ver9.00-1.pdf` Section 4.6.2.2 + pub const DEFAULT_WRITE_RETRIES: u32 = 50_000; + + /// The default number of retries for a control command. + /// + /// At ~10us each this is ~100ms. + /// + /// No value is given in the specification, so we pick the same as the read timeout. + pub const DEFAULT_COMMAND_RETRIES: u32 = 10_000; + + /// Create a new Delay object with the given maximum number of retries. + fn new(max_retries: u32) -> Delay { + Delay { + retries_left: max_retries, + } + } + + /// Create a new Delay object with the maximum number of retries for a read operation. + fn new_read() -> Delay { + Delay::new(Self::DEFAULT_READ_RETRIES) + } + + /// Create a new Delay object with the maximum number of retries for a write operation. + fn new_write() -> Delay { + Delay::new(Self::DEFAULT_WRITE_RETRIES) + } + + /// Create a new Delay object with the maximum number of retries for a command operation. + fn new_command() -> Delay { + Delay::new(Self::DEFAULT_COMMAND_RETRIES) + } + + /// Wait for a while. + /// + /// Checks the retry counter first, and if we hit the max retry limit, the + /// value `err` is returned. Otherwise we wait for 10us and then return + /// `Ok(())`. + fn delay(&mut self, delayer: &mut T, err: Error) -> Result<(), Error> + where + T: embedded_hal::delay::DelayNs, + { + if self.retries_left == 0 { + Err(err) + } else { + delayer.delay_us(10); + self.retries_left -= 1; + Ok(()) + } + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/sdcard/proto.rs b/examples/ios/embedded-sdmmc/src/sdcard/proto.rs new file mode 100644 index 0000000..68ae248 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/sdcard/proto.rs @@ -0,0 +1,739 @@ +//! Constants from the SD Specifications +//! +//! Based on SdFat, under the following terms: +//! +//! > Copyright (c) 2011-2018 Bill Greiman +//! > This file is part of the SdFat library for SD memory cards. +//! > +//! > MIT License +//! > +//! > Permission is hereby granted, free of charge, to any person obtaining a +//! > copy of this software and associated documentation files (the "Software"), +//! > to deal in the Software without restriction, including without limitation +//! > the rights to use, copy, modify, merge, publish, distribute, sublicense, +//! > and/or sell copies of the Software, and to permit persons to whom the +//! > Software is furnished to do so, subject to the following conditions: +//! > +//! > The above copyright notice and this permission notice shall be included +//! > in all copies or substantial portions of the Software. +//! > +//! > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +//! > OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//! > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//! > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +//! > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +//! > DEALINGS IN THE SOFTWARE. + +//============================================================================== + +// Possible errors the SD card can return + +/// Card indicates last operation was a success +pub const ERROR_OK: u8 = 0x00; + +//============================================================================== + +// SD Card Commands + +/// GO_IDLE_STATE - init card in spi mode if CS low +pub const CMD0: u8 = 0x00; +/// SEND_IF_COND - verify SD Memory Card interface operating condition.*/ +pub const CMD8: u8 = 0x08; +/// SEND_CSD - read the Card Specific Data (CSD register) +pub const CMD9: u8 = 0x09; +/// STOP_TRANSMISSION - end multiple block read sequence +pub const CMD12: u8 = 0x0C; +/// SEND_STATUS - read the card status register +pub const CMD13: u8 = 0x0D; +/// READ_SINGLE_BLOCK - read a single data block from the card +pub const CMD17: u8 = 0x11; +/// READ_MULTIPLE_BLOCK - read a multiple data blocks from the card +pub const CMD18: u8 = 0x12; +/// WRITE_BLOCK - write a single data block to the card +pub const CMD24: u8 = 0x18; +/// WRITE_MULTIPLE_BLOCK - write blocks of data until a STOP_TRANSMISSION +pub const CMD25: u8 = 0x19; +/// APP_CMD - escape for application specific command +pub const CMD55: u8 = 0x37; +/// READ_OCR - read the OCR register of a card +pub const CMD58: u8 = 0x3A; +/// CRC_ON_OFF - enable or disable CRC checking +pub const CMD59: u8 = 0x3B; +/// Pre-erased before writing +/// +/// > It is recommended using this command preceding CMD25, some of the cards will be faster for Multiple +/// > Write Blocks operation. Note that the host should send ACMD23 just before WRITE command if the host +/// > wants to use the pre-erased feature +pub const ACMD23: u8 = 0x17; +/// SD_SEND_OP_COMD - Sends host capacity support information and activates +/// the card's initialization process +pub const ACMD41: u8 = 0x29; + +//============================================================================== + +/// status for card in the ready state +pub const R1_READY_STATE: u8 = 0x00; + +/// status for card in the idle state +pub const R1_IDLE_STATE: u8 = 0x01; + +/// status bit for illegal command +pub const R1_ILLEGAL_COMMAND: u8 = 0x04; + +/// start data token for read or write single block*/ +pub const DATA_START_BLOCK: u8 = 0xFE; + +/// stop token for write multiple blocks*/ +pub const STOP_TRAN_TOKEN: u8 = 0xFD; + +/// start data token for write multiple blocks*/ +pub const WRITE_MULTIPLE_TOKEN: u8 = 0xFC; + +/// mask for data response tokens after a write block operation +pub const DATA_RES_MASK: u8 = 0x1F; + +/// write data accepted token +pub const DATA_RES_ACCEPTED: u8 = 0x05; + +/// Card Specific Data, version 1 +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +pub struct CsdV1 { + /// The 16-bytes of data in this Card Specific Data block + pub data: [u8; 16], +} + +/// Card Specific Data, version 2 +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +pub struct CsdV2 { + /// The 16-bytes of data in this Card Specific Data block + pub data: [u8; 16], +} + +/// Card Specific Data +#[derive(Debug)] +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +pub enum Csd { + /// A version 1 CSD + V1(CsdV1), + /// A version 2 CSD + V2(CsdV2), +} + +impl CsdV1 { + /// Create a new, empty, CSD + pub fn new() -> CsdV1 { + CsdV1::default() + } + + define_field!(csd_ver, u8, 0, 6, 2); + define_field!(data_read_access_time1, u8, 1, 0, 8); + define_field!(data_read_access_time2, u8, 2, 0, 8); + define_field!(max_data_transfer_rate, u8, 3, 0, 8); + define_field!(card_command_classes, u16, [(4, 0, 8), (5, 4, 4)]); + define_field!(read_block_length, u8, 5, 0, 4); + define_field!(read_partial_blocks, bool, 6, 7); + define_field!(write_block_misalignment, bool, 6, 6); + define_field!(read_block_misalignment, bool, 6, 5); + define_field!(dsr_implemented, bool, 6, 4); + define_field!(device_size, u32, [(6, 0, 2), (7, 0, 8), (8, 6, 2)]); + define_field!(max_read_current_vdd_max, u8, 8, 0, 3); + define_field!(max_read_current_vdd_min, u8, 8, 3, 3); + define_field!(max_write_current_vdd_max, u8, 9, 2, 3); + define_field!(max_write_current_vdd_min, u8, 9, 5, 3); + define_field!(device_size_multiplier, u8, [(9, 0, 2), (10, 7, 1)]); + define_field!(erase_single_block_enabled, bool, 10, 6); + define_field!(erase_sector_size, u8, [(10, 0, 6), (11, 7, 1)]); + define_field!(write_protect_group_size, u8, 11, 0, 7); + define_field!(write_protect_group_enable, bool, 12, 7); + define_field!(write_speed_factor, u8, 12, 2, 3); + define_field!(max_write_data_length, u8, [(12, 0, 2), (13, 6, 2)]); + define_field!(write_partial_blocks, bool, 13, 5); + define_field!(file_format, u8, 14, 2, 2); + define_field!(temporary_write_protection, bool, 14, 4); + define_field!(permanent_write_protection, bool, 14, 5); + define_field!(copy_flag_set, bool, 14, 6); + define_field!(file_format_group_set, bool, 14, 7); + define_field!(crc, u8, 15, 0, 8); + + /// Returns the card capacity in bytes + pub fn card_capacity_bytes(&self) -> u64 { + let multiplier = self.device_size_multiplier() + self.read_block_length() + 2; + (u64::from(self.device_size()) + 1) << multiplier + } + + /// Returns the card capacity in 512-byte blocks + pub fn card_capacity_blocks(&self) -> u32 { + let multiplier = self.device_size_multiplier() + self.read_block_length() - 7; + (self.device_size() + 1) << multiplier + } +} + +impl CsdV2 { + /// Create a new, empty, CSD + pub fn new() -> CsdV2 { + CsdV2::default() + } + + define_field!(csd_ver, u8, 0, 6, 2); + define_field!(data_read_access_time1, u8, 1, 0, 8); + define_field!(data_read_access_time2, u8, 2, 0, 8); + define_field!(max_data_transfer_rate, u8, 3, 0, 8); + define_field!(card_command_classes, u16, [(4, 0, 8), (5, 4, 4)]); + define_field!(read_block_length, u8, 5, 0, 4); + define_field!(read_partial_blocks, bool, 6, 7); + define_field!(write_block_misalignment, bool, 6, 6); + define_field!(read_block_misalignment, bool, 6, 5); + define_field!(dsr_implemented, bool, 6, 4); + define_field!(device_size, u32, [(7, 0, 6), (8, 0, 8), (9, 0, 8)]); + define_field!(erase_single_block_enabled, bool, 10, 6); + define_field!(erase_sector_size, u8, [(10, 0, 6), (11, 7, 1)]); + define_field!(write_protect_group_size, u8, 11, 0, 7); + define_field!(write_protect_group_enable, bool, 12, 7); + define_field!(write_speed_factor, u8, 12, 2, 3); + define_field!(max_write_data_length, u8, [(12, 0, 2), (13, 6, 2)]); + define_field!(write_partial_blocks, bool, 13, 5); + define_field!(file_format, u8, 14, 2, 2); + define_field!(temporary_write_protection, bool, 14, 4); + define_field!(permanent_write_protection, bool, 14, 5); + define_field!(copy_flag_set, bool, 14, 6); + define_field!(file_format_group_set, bool, 14, 7); + define_field!(crc, u8, 15, 0, 8); + + /// Returns the card capacity in bytes + pub fn card_capacity_bytes(&self) -> u64 { + (u64::from(self.device_size()) + 1) * 512 * 1024 + } + + /// Returns the card capacity in 512-byte blocks + pub fn card_capacity_blocks(&self) -> u32 { + (self.device_size() + 1) * 1024 + } +} + +/// Perform the 7-bit CRC used on the SD card +pub fn crc7(data: &[u8]) -> u8 { + let mut crc = 0u8; + for mut d in data.iter().cloned() { + for _bit in 0..8 { + crc <<= 1; + if ((d & 0x80) ^ (crc & 0x80)) != 0 { + crc ^= 0x09; + } + d <<= 1; + } + } + (crc << 1) | 1 +} + +/// Perform the X25 CRC calculation, as used for data blocks. +pub fn crc16(data: &[u8]) -> u16 { + let mut crc = 0u16; + for &byte in data { + crc = ((crc >> 8) & 0xFF) | (crc << 8); + crc ^= u16::from(byte); + crc ^= (crc & 0xFF) >> 4; + crc ^= crc << 12; + crc ^= (crc & 0xFF) << 5; + } + crc +} + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_crc7() { + const DATA: [u8; 15] = hex!("00 26 00 32 5F 59 83 C8 AD DB CF FF D2 40 40"); + assert_eq!(crc7(&DATA), 0xA5); + } + + #[test] + fn test_crc16() { + // An actual CSD read from an SD card + const DATA: [u8; 16] = hex!("00 26 00 32 5F 5A 83 AE FE FB CF FF 92 80 40 DF"); + assert_eq!(crc16(&DATA), 0x9fc5); + } + + #[test] + fn test_csdv1b() { + const EXAMPLE: CsdV1 = CsdV1 { + data: hex!("00 26 00 32 5F 59 83 C8 AD DB CF FF D2 40 40 A5"), + }; + + // CSD Structure: describes version of CSD structure + // 0b00 [Interpreted: Version 1.0] + assert_eq!(EXAMPLE.csd_ver(), 0x00); + + // Data Read Access Time 1: defines Asynchronous part of the read + // access time 0x26 [Interpreted: 1.5 x 1ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x26); + + // Data Read Access Time 2: worst case clock dependent factor for data + // access time 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: 0x5f5 [Interpreted: Class 0: Yes. Class 1: + // No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: + // Yes. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class + // 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5f5); + + // Max Read Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x09); + + // Partial Blocks for Read Allowed: + // 0b1 [Interpreted: Yes] + assert!(EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on + // card 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)*device size multiplier*max read data block + // length) bytes 0xf22 [Decimal: 3874] + assert_eq!(EXAMPLE.device_size(), 3874); + + // Max Read Current @ VDD Min: + // 0x5 [Interpreted: 35mA] + assert_eq!(EXAMPLE.max_read_current_vdd_min(), 5); + + // Max Read Current @ VDD Max: + // 0x5 [Interpreted: 80mA] + assert_eq!(EXAMPLE.max_read_current_vdd_max(), 5); + + // Max Write Current @ VDD Min: + // 0x6 [Interpreted: 60mA] + assert_eq!(EXAMPLE.max_write_current_vdd_min(), 6); + + // Max Write Current @ VDD Max:: + // 0x6 [Interpreted: 200mA] + assert_eq!(EXAMPLE.max_write_current_vdd_max(), 6); + + // Device Size Multiplier: + // 0x7 [Interpreted: x512] + assert_eq!(EXAMPLE.device_size_multiplier(), 7); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x1f [Interpreted: 32 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x1F); + + // Write Protect Group Size: + // 0x7f [Interpreted: 128 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x7f); + + // Write Protect Group Enable: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x4 [Interpreted: x16] + assert_eq!(EXAMPLE.write_speed_factor(), 0x4); + + // Max Write Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0x9); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b1 [Interpreted: Non-Original] + assert!(EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0xa5 + assert_eq!(EXAMPLE.crc(), 0xa5); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 1_015_808_000); + assert_eq!(EXAMPLE.card_capacity_blocks(), 1_984_000); + } + + #[test] + fn test_csdv1() { + const EXAMPLE: CsdV1 = CsdV1 { + data: hex!("00 7F 00 32 5B 5A 83 AF 7F FF CF 80 16 80 00 6F"), + }; + // CSD Structure: describes version of CSD structure + // 0b00 [Interpreted: Version 1.0] + assert_eq!(EXAMPLE.csd_ver(), 0x00); + + // Data Read Access Time 1: defines Asynchronous part of the read access time + // 0x7f [Interpreted: 8.0 x 10ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x7F); + + // Data Read Access Time 2: worst case clock dependent factor for data access time + // 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: + // 0x5b5 [Interpreted: Class 0: Yes. Class 1: No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: No. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5b5); + + // Max Read Data Block Length: + // 0xa [Interpreted: 1024 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x0a); + + // Partial Blocks for Read Allowed: + // 0b1 [Interpreted: Yes] + assert!(EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on card + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)*device size multiplier*max read data block + // length) bytes 0xebd [Decimal: 3773] + assert_eq!(EXAMPLE.device_size(), 3773); + + // Max Read Current @ VDD Min: + // 0x7 [Interpreted: 100mA] + assert_eq!(EXAMPLE.max_read_current_vdd_min(), 7); + + // Max Read Current @ VDD Max: + // 0x7 [Interpreted: 200mA] + assert_eq!(EXAMPLE.max_read_current_vdd_max(), 7); + + // Max Write Current @ VDD Min: + // 0x7 [Interpreted: 100mA] + assert_eq!(EXAMPLE.max_write_current_vdd_min(), 7); + + // Max Write Current @ VDD Max:: + // 0x7 [Interpreted: 200mA] + assert_eq!(EXAMPLE.max_write_current_vdd_max(), 7); + + // Device Size Multiplier: + // 0x7 [Interpreted: x512] + assert_eq!(EXAMPLE.device_size_multiplier(), 7); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x1f [Interpreted: 32 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x1F); + + // Write Protect Group Size: + // 0x00 [Interpreted: 1 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x00); + + // Write Protect Group Enable: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x5 [Interpreted: x32] + assert_eq!(EXAMPLE.write_speed_factor(), 0x5); + + // Max Write Data Block Length: + // 0xa [Interpreted: 1024 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0xa); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b0 [Interpreted: Original] + assert!(!EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0x6f + assert_eq!(EXAMPLE.crc(), 0x6F); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 1_978_662_912); + assert_eq!(EXAMPLE.card_capacity_blocks(), 3_864_576); + } + + #[test] + fn test_csdv2() { + const EXAMPLE: CsdV2 = CsdV2 { + data: hex!("40 0E 00 32 5B 59 00 00 1D 69 7F 80 0A 40 00 8B"), + }; + // CSD Structure: describes version of CSD structure + // 0b01 [Interpreted: Version 2.0 SDHC] + assert_eq!(EXAMPLE.csd_ver(), 0x01); + + // Data Read Access Time 1: defines Asynchronous part of the read access time + // 0x0e [Interpreted: 1.0 x 1ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x0E); + + // Data Read Access Time 2: worst case clock dependent factor for data access time + // 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: + // 0x5b5 [Interpreted: Class 0: Yes. Class 1: No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: No. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5b5); + + // Max Read Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x09); + + // Partial Blocks for Read Allowed: + // 0b0 [Interpreted: Yes] + assert!(!EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on card + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)* 512kbytes + // 0x001d69 [Decimal: 7529] + assert_eq!(EXAMPLE.device_size(), 7529); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x7f [Interpreted: 128 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x7F); + + // Write Protect Group Size: + // 0x00 [Interpreted: 1 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x00); + + // Write Protect Group Enable: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x2 [Interpreted: x4] + assert_eq!(EXAMPLE.write_speed_factor(), 0x2); + + // Max Write Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0x9); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b0 [Interpreted: Original] + assert!(!EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0x8b + assert_eq!(EXAMPLE.crc(), 0x8b); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 3_947_888_640); + assert_eq!(EXAMPLE.card_capacity_blocks(), 7_710_720); + } + + #[test] + fn test_csdv2b() { + const EXAMPLE: CsdV2 = CsdV2 { + data: hex!("40 0E 00 32 5B 59 00 00 3A 91 7F 80 0A 40 00 05"), + }; + // CSD Structure: describes version of CSD structure + // 0b01 [Interpreted: Version 2.0 SDHC] + assert_eq!(EXAMPLE.csd_ver(), 0x01); + + // Data Read Access Time 1: defines Asynchronous part of the read access time + // 0x0e [Interpreted: 1.0 x 1ms] + assert_eq!(EXAMPLE.data_read_access_time1(), 0x0E); + + // Data Read Access Time 2: worst case clock dependent factor for data access time + // 0x00 [Decimal: 0 x 100 Clocks] + assert_eq!(EXAMPLE.data_read_access_time2(), 0x00); + + // Max Data Transfer Rate: sometimes stated as Mhz + // 0x32 [Interpreted: 2.5 x 10Mbit/s] + assert_eq!(EXAMPLE.max_data_transfer_rate(), 0x32); + + // Card Command Classes: + // 0x5b5 [Interpreted: Class 0: Yes. Class 1: No. Class 2: Yes. Class 3: No. Class 4: Yes. Class 5: Yes. Class 6: No. Class 7: Yes. Class 8: Yes. Class 9: No. Class 10: Yes. Class 11: No. ] + assert_eq!(EXAMPLE.card_command_classes(), 0x5b5); + + // Max Read Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.read_block_length(), 0x09); + + // Partial Blocks for Read Allowed: + // 0b0 [Interpreted: Yes] + assert!(!EXAMPLE.read_partial_blocks()); + + // Write Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.write_block_misalignment()); + + // Read Block Misalignment: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.read_block_misalignment()); + + // DSR Implemented: indicates configurable driver stage integrated on card + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.dsr_implemented()); + + // Device Size: to calculate the card capacity excl. security area + // ((device size + 1)* 512kbytes + // 0x003a91 [Decimal: 7529] + assert_eq!(EXAMPLE.device_size(), 14993); + + // Erase Single Block Enabled: + // 0x1 [Interpreted: Yes] + assert!(EXAMPLE.erase_single_block_enabled()); + + // Erase Sector Size: size of erasable sector in write blocks + // 0x7f [Interpreted: 128 blocks] + assert_eq!(EXAMPLE.erase_sector_size(), 0x7F); + + // Write Protect Group Size: + // 0x00 [Interpreted: 1 sectors] + assert_eq!(EXAMPLE.write_protect_group_size(), 0x00); + + // Write Protect Group Enable: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_protect_group_enable()); + + // Write Speed Factor: block program time as multiple of read access time + // 0x2 [Interpreted: x4] + assert_eq!(EXAMPLE.write_speed_factor(), 0x2); + + // Max Write Data Block Length: + // 0x9 [Interpreted: 512 Bytes] + assert_eq!(EXAMPLE.max_write_data_length(), 0x9); + + // Partial Blocks for Write Allowed: + // 0x0 [Interpreted: No] + assert!(!EXAMPLE.write_partial_blocks()); + + // File Format Group: + // 0b0 [Interpreted: is either Hard Disk with Partition Table/DOS FAT without Partition Table/Universal File Format/Other/Unknown] + assert!(!EXAMPLE.file_format_group_set()); + + // Copy Flag: + // 0b0 [Interpreted: Original] + assert!(!EXAMPLE.copy_flag_set()); + + // Permanent Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.permanent_write_protection()); + + // Temporary Write Protection: + // 0b0 [Interpreted: No] + assert!(!EXAMPLE.temporary_write_protection()); + + // File Format: + // 0x0 [Interpreted: Hard Disk with Partition Table] + assert_eq!(EXAMPLE.file_format(), 0x00); + + // CRC7 Checksum + always 1 in LSB: + // 0x05 + assert_eq!(EXAMPLE.crc(), 0x05); + + assert_eq!(EXAMPLE.card_capacity_bytes(), 7_861_174_272); + assert_eq!(EXAMPLE.card_capacity_blocks(), 15_353_856); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/structure.rs b/examples/ios/embedded-sdmmc/src/structure.rs new file mode 100644 index 0000000..fb0e553 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/structure.rs @@ -0,0 +1,64 @@ +//! Useful macros for parsing SD/MMC structures. + +macro_rules! access_field { + ($self:expr, $offset:expr, $start_bit:expr, 1) => { + ($self.data[$offset] & (1 << $start_bit)) != 0 + }; + ($self:expr, $offset:expr, $start:expr, $num_bits:expr) => { + ($self.data[$offset] >> $start) & (((1u16 << $num_bits) - 1) as u8) + }; +} + +macro_rules! define_field { + ($name:ident, bool, $offset:expr, $bit:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> bool { + access_field!(self, $offset, $bit, 1) + } + }; + ($name:ident, u8, $offset:expr, $start_bit:expr, $num_bits:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> u8 { + access_field!(self, $offset, $start_bit, $num_bits) + } + }; + ($name:ident, $type:ty, [ $( ( $offset:expr, $start_bit:expr, $num_bits:expr ) ),+ ]) => { + /// Gets the value from the $name field + pub fn $name(&self) -> $type { + let mut result = 0; + $( + result <<= $num_bits; + let part = access_field!(self, $offset, $start_bit, $num_bits) as $type; + result |= part; + )+ + result + } + }; + + ($name:ident, u8, $offset:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> u8 { + self.data[$offset] + } + }; + + ($name:ident, u16, $offset:expr) => { + /// Get the value from the $name field + pub fn $name(&self) -> u16 { + LittleEndian::read_u16(&self.data[$offset..$offset+2]) + } + }; + + ($name:ident, u32, $offset:expr) => { + /// Get the $name field + pub fn $name(&self) -> u32 { + LittleEndian::read_u32(&self.data[$offset..$offset+4]) + } + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/src/volume_mgr.rs b/examples/ios/embedded-sdmmc/src/volume_mgr.rs new file mode 100644 index 0000000..3a9ca13 --- /dev/null +++ b/examples/ios/embedded-sdmmc/src/volume_mgr.rs @@ -0,0 +1,1550 @@ +//! The Volume Manager implementation. +//! +//! The volume manager handles partitions and open files on a block device. + +use core::cell::RefCell; +use core::convert::TryFrom; +use core::ops::DerefMut; + +use byteorder::{ByteOrder, LittleEndian}; +use heapless::Vec; + +use crate::{ + debug, fat, + filesystem::{ + Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, LfnBuffer, Mode, + RawDirectory, RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, + }, + trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, + Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, + PARTITION_ID_FAT16_SMALL, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, +}; + +/// Wraps a block device and gives access to the FAT-formatted volumes within +/// it. +/// +/// Tracks which files and directories are open, to prevent you from deleting +/// a file or directory you currently have open. +#[derive(Debug)] +pub struct VolumeManager< + D, + T, + const MAX_DIRS: usize = 4, + const MAX_FILES: usize = 4, + const MAX_VOLUMES: usize = 1, +> where + D: BlockDevice, + T: TimeSource, + ::Error: core::fmt::Debug, +{ + time_source: T, + data: RefCell>, +} + +impl VolumeManager +where + D: BlockDevice, + T: TimeSource, + ::Error: core::fmt::Debug, +{ + /// Create a new Volume Manager using a generic `BlockDevice`. From this + /// object we can open volumes (partitions) and with those we can open + /// files. + /// + /// This creates a `VolumeManager` with default values + /// MAX_DIRS = 4, MAX_FILES = 4, MAX_VOLUMES = 1. Call `VolumeManager::new_with_limits(block_device, time_source)` + /// if you need different limits. + pub fn new(block_device: D, time_source: T) -> VolumeManager { + // Pick a random starting point for the IDs that's not zero, because + // zero doesn't stand out in the logs. + Self::new_with_limits(block_device, time_source, 5000) + } +} + +impl + VolumeManager +where + D: BlockDevice, + T: TimeSource, + ::Error: core::fmt::Debug, +{ + /// Create a new Volume Manager using a generic `BlockDevice`. From this + /// object we can open volumes (partitions) and with those we can open + /// files. + /// + /// You can also give an offset for all the IDs this volume manager + /// generates, which might help you find the IDs in your logs when + /// debugging. + pub fn new_with_limits( + block_device: D, + time_source: T, + id_offset: u32, + ) -> VolumeManager { + debug!("Creating new embedded-sdmmc::VolumeManager"); + VolumeManager { + time_source, + data: RefCell::new(VolumeManagerData { + block_cache: BlockCache::new(block_device), + id_generator: HandleGenerator::new(id_offset), + open_volumes: Vec::new(), + open_dirs: Vec::new(), + open_files: Vec::new(), + }), + } + } + + /// Temporarily get access to the underlying block device. + pub fn device(&self, f: F) -> T + where + F: FnOnce(&mut D) -> T, + { + let mut data = self.data.borrow_mut(); + let result = f(data.block_cache.block_device()); + result + } + + /// Get a volume (or partition) based on entries in the Master Boot Record. + /// + /// We do not support GUID Partition Table disks. Nor do we support any + /// concept of drive letters - that is for a higher layer to handle. + pub fn open_volume( + &self, + volume_idx: VolumeIdx, + ) -> Result, Error> { + let v = self.open_raw_volume(volume_idx)?; + Ok(v.to_volume(self)) + } + + /// Try to open a special volume, IE no MBR is availble + pub unsafe fn open_special( + &self, + part_type: u8, + lba_start: BlockIdx, + num_blocks: BlockCount, + ) -> Result, Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + let raw_volume = match part_type { + PARTITION_ID_FAT32_CHS_LBA + | PARTITION_ID_FAT32_LBA + | PARTITION_ID_FAT16_LBA + | PARTITION_ID_FAT16 + | PARTITION_ID_FAT16_SMALL => { + let volume = fat::parse_volume(&mut data.block_cache, lba_start, num_blocks)?; + let id = RawVolume(data.id_generator.generate()); + let info = VolumeInfo { + raw_volume: id, + idx: VolumeIdx(0), + volume_type: volume, + }; + // We already checked for space + data.open_volumes.push(info).unwrap(); + Ok(id) + } + _ => Err(Error::FormatError("Partition type not supported")), + }; + Ok(raw_volume?.to_volume(self)) + } + + /// Get a volume (or partition) based on entries in the Master Boot Record. + /// + /// We do not support GUID Partition Table disks. Nor do we support any + /// concept of drive letters - that is for a higher layer to handle. + /// + /// This function gives you a `RawVolume` and you must close the volume by + /// calling `VolumeManager::close_volume`. + pub fn open_raw_volume(&self, volume_idx: VolumeIdx) -> Result> { + const PARTITION1_START: usize = 446; + const PARTITION2_START: usize = PARTITION1_START + PARTITION_INFO_LENGTH; + const PARTITION3_START: usize = PARTITION2_START + PARTITION_INFO_LENGTH; + const PARTITION4_START: usize = PARTITION3_START + PARTITION_INFO_LENGTH; + const FOOTER_START: usize = 510; + const FOOTER_VALUE: u16 = 0xAA55; + const PARTITION_INFO_LENGTH: usize = 16; + const PARTITION_INFO_STATUS_INDEX: usize = 0; + const PARTITION_INFO_TYPE_INDEX: usize = 4; + const PARTITION_INFO_LBA_START_INDEX: usize = 8; + const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; + + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + if data.open_volumes.is_full() { + return Err(Error::TooManyOpenVolumes); + } + + for v in data.open_volumes.iter() { + if v.idx == volume_idx { + return Err(Error::VolumeAlreadyOpen); + } + } + + let (part_type, lba_start, num_blocks) = { + trace!("Reading partition table"); + let block = data + .block_cache + .read(BlockIdx(0)) + .map_err(Error::DeviceError)?; + // We only support Master Boot Record (MBR) partitioned cards, not + // GUID Partition Table (GPT) + if LittleEndian::read_u16(&block[FOOTER_START..FOOTER_START + 2]) != FOOTER_VALUE { + return Err(Error::FormatError("Invalid MBR signature")); + } + let partition = match volume_idx { + VolumeIdx(0) => { + &block[PARTITION1_START..(PARTITION1_START + PARTITION_INFO_LENGTH)] + } + VolumeIdx(1) => { + &block[PARTITION2_START..(PARTITION2_START + PARTITION_INFO_LENGTH)] + } + VolumeIdx(2) => { + &block[PARTITION3_START..(PARTITION3_START + PARTITION_INFO_LENGTH)] + } + VolumeIdx(3) => { + &block[PARTITION4_START..(PARTITION4_START + PARTITION_INFO_LENGTH)] + } + _ => { + return Err(Error::NoSuchVolume); + } + }; + // Only 0x80 and 0x00 are valid (bootable, and non-bootable) + if (partition[PARTITION_INFO_STATUS_INDEX] & 0x7F) != 0x00 { + return Err(Error::FormatError("Invalid partition status")); + } + let lba_start = LittleEndian::read_u32( + &partition[PARTITION_INFO_LBA_START_INDEX..(PARTITION_INFO_LBA_START_INDEX + 4)], + ); + let num_blocks = LittleEndian::read_u32( + &partition[PARTITION_INFO_NUM_BLOCKS_INDEX..(PARTITION_INFO_NUM_BLOCKS_INDEX + 4)], + ); + ( + partition[PARTITION_INFO_TYPE_INDEX], + BlockIdx(lba_start), + BlockCount(num_blocks), + ) + }; + match part_type { + PARTITION_ID_FAT32_CHS_LBA + | PARTITION_ID_FAT32_LBA + | PARTITION_ID_FAT16_LBA + | PARTITION_ID_FAT16 + | PARTITION_ID_FAT16_SMALL => { + let volume = fat::parse_volume(&mut data.block_cache, lba_start, num_blocks)?; + let id = RawVolume(data.id_generator.generate()); + let info = VolumeInfo { + raw_volume: id, + idx: volume_idx, + volume_type: volume, + }; + // We already checked for space + data.open_volumes.push(info).unwrap(); + Ok(id) + } + _ => Err(Error::FormatError("Partition type not supported")), + } + } + + /// Open the volume's root directory. + /// + /// You can then read the directory entries with `iterate_dir`, or you can + /// use `open_file_in_dir`. + pub fn open_root_dir(&self, volume: RawVolume) -> Result> { + debug!("Opening root on {:?}", volume); + + // Opening a root directory twice is OK + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + let directory_id = RawDirectory(data.id_generator.generate()); + let dir_info = DirectoryInfo { + raw_volume: volume, + cluster: ClusterId::ROOT_DIR, + raw_directory: directory_id, + }; + + data.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + debug!("Opened root on {:?}, got {:?}", volume, directory_id); + + Ok(directory_id) + } + + /// Open a directory. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + /// + /// Passing "." as the name results in opening the `parent_dir` a second time. + pub fn open_dir( + &self, + parent_dir: RawDirectory, + name: N, + ) -> Result> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + if data.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); + } + + // Find dir by ID + let parent_dir_idx = data.get_dir_by_id(parent_dir)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[parent_dir_idx].raw_volume)?; + let short_file_name = name.to_short_filename().map_err(Error::FilenameError)?; + + // Open the directory + + // Should we short-cut? (root dir doesn't have ".") + if short_file_name == ShortFileName::this_dir() { + let directory_id = RawDirectory(data.id_generator.generate()); + let dir_info = DirectoryInfo { + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, + cluster: data.open_dirs[parent_dir_idx].cluster, + }; + + data.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + return Ok(directory_id); + } + + // ok we'll actually look for the directory then + + let dir_entry = match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[parent_dir_idx], + &short_file_name, + )?, + }; + + debug!("Found dir entry: {:?}", dir_entry); + + if !dir_entry.attributes.is_directory() { + return Err(Error::OpenedFileAsDir); + } + + // We don't check if the directory is already open - directories hold + // no cached state and so opening a directory twice is allowable. + + // Remember this open directory. + let directory_id = RawDirectory(data.id_generator.generate()); + let dir_info = DirectoryInfo { + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, + cluster: dir_entry.cluster, + }; + + data.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + Ok(directory_id) + } + + /// Close a directory. You cannot perform operations on an open directory + /// and so must close it if you want to do something with it. + pub fn close_dir(&self, directory: RawDirectory) -> Result<(), Error> { + debug!("Closing {:?}", directory); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + + for (idx, info) in data.open_dirs.iter().enumerate() { + if directory == info.raw_directory { + data.open_dirs.swap_remove(idx); + return Ok(()); + } + } + Err(Error::BadHandle) + } + + /// Close a volume + /// + /// You can't close it if there are any files or directories open on it. + pub fn close_volume(&self, volume: RawVolume) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + for f in data.open_files.iter() { + if f.raw_volume == volume { + return Err(Error::VolumeStillInUse); + } + } + + for d in data.open_dirs.iter() { + if d.raw_volume == volume { + return Err(Error::VolumeStillInUse); + } + } + + let volume_idx = data.get_volume_by_id(volume)?; + + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.update_info_sector(&mut data.block_cache)?; + } + } + + data.open_volumes.swap_remove(volume_idx); + + Ok(()) + } + + /// Look in a directory for a named file. + pub fn find_directory_entry( + &self, + directory: RawDirectory, + name: N, + ) -> Result> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ) + } + } + } + + /// Call a callback function for each directory entry in a directory. + /// + /// Long File Names will be ignored. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir( + &self, + directory: RawDirectory, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry), + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.iterate_dir( + &mut data.block_cache, + &data.open_dirs[directory_idx], + |de| { + // Hide all the LFN directory entries + if !de.attributes.is_lfn() { + func(de); + } + }, + ) + } + } + } + + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. + /// + /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the + /// Long File Name. If you pass one that isn't large enough, any Long File + /// Names that don't fit will be ignored and presented as if they only had a + /// Short File Name. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir_lfn( + &self, + directory: RawDirectory, + lfn_buffer: &mut LfnBuffer<'_>, + func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + // This API doesn't care about the on-disk directory entry, so we discard it + fat.iterate_dir_lfn( + &mut data.block_cache, + lfn_buffer, + &data.open_dirs[directory_idx], + func, + ) + } + } + } + + /// Open a file with the given full path. A file can only be opened once. + pub fn open_file_in_dir( + &self, + directory: RawDirectory, + name: N, + mode: Mode, + ) -> Result> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + // This check is load-bearing - we do an unchecked push later. + if data.open_files.is_full() { + return Err(Error::TooManyOpenFiles); + } + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_id = data.open_dirs[directory_idx].raw_volume; + let volume_idx = data.get_volume_by_id(volume_id)?; + let volume_info = &data.open_volumes[volume_idx]; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + let dir_entry = match &volume_info.volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ), + }; + + let dir_entry = match dir_entry { + Ok(entry) => { + // we are opening an existing file + Some(entry) + } + Err(_) + if (mode == Mode::ReadWriteCreate) + | (mode == Mode::ReadWriteCreateOrTruncate) + | (mode == Mode::ReadWriteCreateOrAppend) => + { + // We are opening a non-existant file, but that's OK because they + // asked us to create it + None + } + _ => { + // We are opening a non-existant file, and that's not OK. + return Err(Error::NotFound); + } + }; + + // Check if it's open already + if let Some(dir_entry) = &dir_entry { + if data.file_is_open(volume_info.raw_volume, dir_entry) { + return Err(Error::FileAlreadyOpen); + } + } + + let mode = solve_mode_variant(mode, dir_entry.is_some()); + + match mode { + Mode::ReadWriteCreate => { + if dir_entry.is_some() { + return Err(Error::FileAlreadyExists); + } + let cluster = data.open_dirs[directory_idx].cluster; + let att = Attributes::create_from_fat(0); + let volume_idx = data.get_volume_by_id(volume_id)?; + let entry = match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.write_new_directory_entry( + &mut data.block_cache, + &self.time_source, + cluster, + sfn, + att, + )?, + }; + + let file_id = RawFile(data.id_generator.generate()); + + let file = FileInfo { + raw_file: file_id, + raw_volume: volume_id, + current_cluster: (0, entry.cluster), + current_offset: 0, + mode, + entry, + dirty: false, + }; + + // Remember this open file - can't be full as we checked already + unsafe { + data.open_files.push_unchecked(file); + } + + Ok(file_id) + } + _ => { + // Safe to unwrap, since we actually have an entry if we got here + let dir_entry = dir_entry.unwrap(); + + if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly { + return Err(Error::ReadOnly); + } + + if dir_entry.attributes.is_directory() { + return Err(Error::OpenedDirAsFile); + } + + // Check it's not already open + if data.file_is_open(volume_id, &dir_entry) { + return Err(Error::FileAlreadyOpen); + } + + let mode = solve_mode_variant(mode, true); + let raw_file = RawFile(data.id_generator.generate()); + + let file = match mode { + Mode::ReadOnly => FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }, + Mode::ReadWriteAppend => { + let mut file = FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }; + // seek_from_end with 0 can't fail + file.seek_from_end(0).ok(); + file + } + Mode::ReadWriteTruncate => { + let mut file = FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.truncate_cluster_chain( + &mut data.block_cache, + file.entry.cluster, + )?, + }; + file.update_length(0); + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + file.entry.mtime = self.time_source.get_timestamp(); + fat.write_entry_to_disk(&mut data.block_cache, &file.entry)?; + } + }; + + file + } + _ => return Err(Error::Unsupported), + }; + + // Remember this open file - can't be full as we checked already + unsafe { + data.open_files.push_unchecked(file); + } + + Ok(raw_file) + } + } + } + + /// Delete a closed file with the given filename, if it exists. + pub fn delete_file_in_dir( + &self, + directory: RawDirectory, + name: N, + ) -> Result<(), Error> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let dir_idx = data.get_dir_by_id(directory)?; + let dir_info = &data.open_dirs[dir_idx]; + let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + let dir_entry = match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry(&mut data.block_cache, dir_info, &sfn), + }?; + + if dir_entry.attributes.is_directory() { + return Err(Error::DeleteDirAsFile); + } + + if data.file_is_open(dir_info.raw_volume, &dir_entry) { + return Err(Error::FileAlreadyOpen); + } + + let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.delete_directory_entry(&mut data.block_cache, dir_info, &sfn)? + } + } + + Ok(()) + } + + /// Get the volume label + /// + /// Will look in the BPB for a volume label, and if nothing is found, will + /// search the root directory for a volume label. + pub fn get_root_volume_label( + &self, + raw_volume: RawVolume, + ) -> Result, Error> { + debug!("Reading volume label for {:?}", raw_volume); + // prefer the one in the BPB - it's easier to get + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let volume_idx = data.get_volume_by_id(raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + if !fat.name.name().is_empty() { + debug!( + "Got volume label {:?} for {:?} from BPB", + fat.name, raw_volume + ); + return Ok(Some(fat.name.clone())); + } + } + } + drop(data); + + // Nothing in the BPB, let's do it the slow way + let root_dir = self.open_root_dir(raw_volume)?.to_directory(self); + let mut maybe_volume_name = None; + root_dir.iterate_dir(|de| { + if maybe_volume_name.is_none() + && de.attributes == Attributes::create_from_fat(Attributes::VOLUME) + { + maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) + } + })?; + + debug!( + "Got volume label {:?} for {:?} from root", + maybe_volume_name, raw_volume + ); + + Ok(maybe_volume_name) + } + + /// Read from an open file. + pub fn read(&self, file: RawFile, buffer: &mut [u8]) -> Result> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let file_idx = data.get_file_by_id(file)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + + // Calculate which file block the current offset lies within + // While there is more to read, read the block and copy in to the buffer. + // If we need to find the next cluster, walk the FAT. + let mut space = buffer.len(); + let mut read = 0; + while space > 0 && !data.open_files[file_idx].eof() { + let mut current_cluster = data.open_files[file_idx].current_cluster; + let (block_idx, block_offset, block_avail) = data.find_data_on_disk( + volume_idx, + &mut current_cluster, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, + )?; + data.open_files[file_idx].current_cluster = current_cluster; + trace!("Reading file ID {:?}", file); + let block = data + .block_cache + .read(block_idx) + .map_err(Error::DeviceError)?; + let to_copy = block_avail + .min(space) + .min(data.open_files[file_idx].left() as usize); + assert!(to_copy != 0); + buffer[read..read + to_copy] + .copy_from_slice(&block[block_offset..block_offset + to_copy]); + read += to_copy; + space -= to_copy; + data.open_files[file_idx] + .seek_from_current(to_copy as i32) + .unwrap(); + } + Ok(read) + } + + /// Write to a open file. + pub fn write(&self, file: RawFile, buffer: &[u8]) -> Result<(), Error> { + #[cfg(feature = "defmt-log")] + debug!("write(file={:?}, buffer={:x}", file, buffer); + + #[cfg(feature = "log")] + debug!("write(file={:?}, buffer={:x?}", file, buffer); + + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + // Clone this so we can touch our other structures. Need to ensure we + // write it back at the end. + let file_idx = data.get_file_by_id(file)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + + if data.open_files[file_idx].mode == Mode::ReadOnly { + return Err(Error::ReadOnly); + } + + data.open_files[file_idx].dirty = true; + + if data.open_files[file_idx].entry.cluster.0 < fat::RESERVED_ENTRIES { + // file doesn't have a valid allocated cluster (possible zero-length file), allocate one + data.open_files[file_idx].entry.cluster = + match data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(ref mut fat) => { + fat.alloc_cluster(&mut data.block_cache, None, false)? + } + }; + debug!( + "Alloc first cluster {:?}", + data.open_files[file_idx].entry.cluster + ); + } + + // Clone this so we can touch our other structures. + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + + if (data.open_files[file_idx].current_cluster.1) < data.open_files[file_idx].entry.cluster { + debug!("Rewinding to start"); + data.open_files[file_idx].current_cluster = + (0, data.open_files[file_idx].entry.cluster); + } + let bytes_until_max = + usize::try_from(MAX_FILE_SIZE - data.open_files[file_idx].current_offset) + .map_err(|_| Error::ConversionError)?; + let bytes_to_write = core::cmp::min(buffer.len(), bytes_until_max); + let mut written = 0; + + while written < bytes_to_write { + let mut current_cluster = data.open_files[file_idx].current_cluster; + debug!( + "Have written bytes {}/{}, finding cluster {:?}", + written, bytes_to_write, current_cluster + ); + let current_offset = data.open_files[file_idx].current_offset; + let (block_idx, block_offset, block_avail) = match data.find_data_on_disk( + volume_idx, + &mut current_cluster, + data.open_files[file_idx].entry.cluster, + current_offset, + ) { + Ok(vars) => { + debug!( + "Found block_idx={:?}, block_offset={:?}, block_avail={}", + vars.0, vars.1, vars.2 + ); + vars + } + Err(Error::EndOfFile) => { + debug!("Extending file"); + match data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(ref mut fat) => { + if fat + .alloc_cluster( + &mut data.block_cache, + Some(current_cluster.1), + false, + ) + .is_err() + { + return Err(Error::DiskFull); + } + debug!("Allocated new FAT cluster, finding offsets..."); + let new_offset = data + .find_data_on_disk( + volume_idx, + &mut current_cluster, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, + ) + .map_err(|_| Error::AllocationError)?; + debug!("New offset {:?}", new_offset); + new_offset + } + } + } + Err(e) => return Err(e), + }; + let to_copy = core::cmp::min(block_avail, bytes_to_write - written); + let block = if (block_offset == 0) && (to_copy == block_avail) { + // we're replacing the whole Block, so the previous contents + // are irrelevant + data.block_cache.blank_mut(block_idx) + } else { + debug!("Reading for partial block write"); + data.block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)? + }; + block[block_offset..block_offset + to_copy] + .copy_from_slice(&buffer[written..written + to_copy]); + debug!("Writing block {:?}", block_idx); + data.block_cache.write_back()?; + written += to_copy; + data.open_files[file_idx].current_cluster = current_cluster; + + let to_copy = to_copy as u32; + let new_offset = data.open_files[file_idx].current_offset + to_copy; + if new_offset > data.open_files[file_idx].entry.size { + // We made it longer + data.open_files[file_idx].update_length(new_offset); + } + data.open_files[file_idx] + .seek_from_start(new_offset) + .unwrap(); + // Entry update deferred to file close, for performance. + } + data.open_files[file_idx].entry.attributes.set_archive(true); + data.open_files[file_idx].entry.mtime = self.time_source.get_timestamp(); + Ok(()) + } + + /// Close a file with the given raw file handle. + pub fn close_file(&self, file: RawFile) -> Result<(), Error> { + let flush_result = self.flush_file(file); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files.swap_remove(file_idx); + flush_result + } + + /// Flush (update the entry) for a file with the given raw file handle. + pub fn flush_file(&self, file: RawFile) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let file_id = data.get_file_by_id(file)?; + + if data.open_files[file_id].dirty { + let volume_idx = data.get_volume_by_id(data.open_files[file_id].raw_volume)?; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + debug!("Updating FAT info sector"); + fat.update_info_sector(&mut data.block_cache)?; + debug!("Updating dir entry {:?}", data.open_files[file_id].entry); + if data.open_files[file_id].entry.size != 0 { + // If you have a length, you must have a cluster + assert!(data.open_files[file_id].entry.cluster.0 != 0); + } + fat.write_entry_to_disk( + &mut data.block_cache, + &data.open_files[file_id].entry, + )?; + } + }; + } + Ok(()) + } + + /// Check if any files or folders are open. + pub fn has_open_handles(&self) -> bool { + let data = self.data.borrow(); + !(data.open_dirs.is_empty() || data.open_files.is_empty()) + } + + /// Consume self and return BlockDevice and TimeSource + pub fn free(self) -> (D, T) { + let data = self.data.into_inner(); + (data.block_cache.free(), self.time_source) + } + + /// Check if a file is at End Of File. + pub fn file_eof(&self, file: RawFile) -> Result> { + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].eof()) + } + + /// Seek a file with an offset from the start of the file. + pub fn file_seek_from_start(&self, file: RawFile, offset: u32) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] + .seek_from_start(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Seek a file with an offset from the current position. + pub fn file_seek_from_current( + &self, + file: RawFile, + offset: i32, + ) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] + .seek_from_current(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Seek a file with an offset back from the end of the file. + pub fn file_seek_from_end(&self, file: RawFile, offset: u32) -> Result<(), Error> { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] + .seek_from_end(offset) + .map_err(|_| Error::InvalidOffset)?; + Ok(()) + } + + /// Get the length of a file + pub fn file_length(&self, file: RawFile) -> Result> { + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].length()) + } + + /// Get the current offset of a file + pub fn file_offset(&self, file: RawFile) -> Result> { + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].current_offset) + } + + /// Create a directory in a given directory. + pub fn make_dir_in_dir( + &self, + directory: RawDirectory, + name: N, + ) -> Result<(), Error> + where + N: ToShortFileName, + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + // This check is load-bearing - we do an unchecked push later. + if data.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); + } + + let parent_directory_idx = data.get_dir_by_id(directory)?; + let parent_directory_info = &data.open_dirs[parent_directory_idx]; + let volume_id = data.open_dirs[parent_directory_idx].raw_volume; + let volume_idx = data.get_volume_by_id(volume_id)?; + let volume_info = &data.open_volumes[volume_idx]; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + debug!("Creating directory '{}'", sfn); + debug!( + "Parent dir is in cluster {:?}", + parent_directory_info.cluster + ); + + // Does an entry exist with this name? + let maybe_dir_entry = match &volume_info.volume_type { + VolumeType::Fat(fat) => { + fat.find_directory_entry(&mut data.block_cache, parent_directory_info, &sfn) + } + }; + + match maybe_dir_entry { + Ok(entry) if entry.attributes.is_directory() => { + return Err(Error::DirAlreadyExists); + } + Ok(_entry) => { + return Err(Error::FileAlreadyExists); + } + Err(Error::NotFound) => { + // perfect, let's make it + } + Err(e) => { + // Some other error - tell them about it + return Err(e); + } + }; + + let att = Attributes::create_from_fat(Attributes::DIRECTORY); + + // Need mutable access for this + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + debug!("Making dir entry"); + fat.make_dir( + &mut data.block_cache, + &self.time_source, + parent_directory_info.cluster, + sfn, + att, + )?; + } + }; + + Ok(()) + } +} + +/// The mutable data the VolumeManager needs to hold +/// +/// Kept separate so its easier to wrap it in a RefCell +#[derive(Debug)] + +struct VolumeManagerData< + D, + const MAX_DIRS: usize = 4, + const MAX_FILES: usize = 4, + const MAX_VOLUMES: usize = 1, +> where + D: BlockDevice, +{ + id_generator: HandleGenerator, + block_cache: BlockCache, + open_volumes: Vec, + open_dirs: Vec, + open_files: Vec, +} + +impl + VolumeManagerData +where + D: BlockDevice, +{ + /// Check if a file is open + /// + /// Returns `true` if it's open, `false`, otherwise. + fn file_is_open(&self, raw_volume: RawVolume, dir_entry: &DirEntry) -> bool { + for f in self.open_files.iter() { + if f.raw_volume == raw_volume + && f.entry.entry_block == dir_entry.entry_block + && f.entry.entry_offset == dir_entry.entry_offset + { + return true; + } + } + false + } + + fn get_volume_by_id(&self, raw_volume: RawVolume) -> Result> + where + E: core::fmt::Debug, + { + for (idx, v) in self.open_volumes.iter().enumerate() { + if v.raw_volume == raw_volume { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + fn get_dir_by_id(&self, raw_directory: RawDirectory) -> Result> + where + E: core::fmt::Debug, + { + for (idx, d) in self.open_dirs.iter().enumerate() { + if d.raw_directory == raw_directory { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + fn get_file_by_id(&self, raw_file: RawFile) -> Result> + where + E: core::fmt::Debug, + { + for (idx, f) in self.open_files.iter().enumerate() { + if f.raw_file == raw_file { + return Ok(idx); + } + } + Err(Error::BadHandle) + } + + /// This function turns `desired_offset` into an appropriate block to be + /// read. It either calculates this based on the start of the file, or + /// from the given start point - whichever is better. + /// + /// Returns: + /// + /// * the index for the block on the disk that contains the data we want, + /// * the byte offset into that block for the data we want, and + /// * how many bytes remain in that block. + fn find_data_on_disk( + &mut self, + volume_idx: usize, + start: &mut (u32, ClusterId), + file_start: ClusterId, + desired_offset: u32, + ) -> Result<(BlockIdx, usize, usize), Error> + where + D: BlockDevice, + { + let bytes_per_cluster = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.bytes_per_cluster(), + }; + // do we need to be before our start point? + if desired_offset < start.0 { + // user wants to go backwards - start from the beginning of the file + // because the FAT is only a singly-linked list. + start.0 = 0; + start.1 = file_start; + } + // How many clusters forward do we need to go? + let offset_from_cluster = desired_offset - start.0; + // walk through the FAT chain + let num_clusters = offset_from_cluster / bytes_per_cluster; + for _ in 0..num_clusters { + start.1 = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.next_cluster(&mut self.block_cache, start.1)?, + }; + start.0 += bytes_per_cluster; + } + // How many blocks in are we now? + let offset_from_cluster = desired_offset - start.0; + assert!(offset_from_cluster < bytes_per_cluster); + let num_blocks = BlockCount(offset_from_cluster / Block::LEN_U32); + let block_idx = match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.cluster_to_block(start.1), + } + num_blocks; + let block_offset = (desired_offset % Block::LEN_U32) as usize; + let available = Block::LEN - block_offset; + Ok((block_idx, block_offset, available)) + } +} + +/// Transform mode variants (ReadWriteCreate_Or_Append) to simple modes ReadWriteAppend or +/// ReadWriteCreate +fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { + let mut mode = mode; + if mode == Mode::ReadWriteCreateOrAppend { + if dir_entry_is_some { + mode = Mode::ReadWriteAppend; + } else { + mode = Mode::ReadWriteCreate; + } + } else if mode == Mode::ReadWriteCreateOrTruncate { + if dir_entry_is_some { + mode = Mode::ReadWriteTruncate; + } else { + mode = Mode::ReadWriteCreate; + } + } + mode +} + +// **************************************************************************** +// +// Unit Tests +// +// **************************************************************************** + +#[cfg(test)] +mod tests { + use super::*; + use crate::filesystem::Handle; + use crate::Timestamp; + + struct DummyBlockDevice; + + struct Clock; + + #[derive(Debug)] + enum Error { + Unknown, + } + + impl TimeSource for Clock { + fn get_timestamp(&self) -> Timestamp { + // TODO: Return actual time + Timestamp { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + } + } + } + + impl BlockDevice for DummyBlockDevice { + type Error = Error; + + /// Read one or more blocks, starting at the given block index. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + // Actual blocks taken from an SD card, except I've changed the start and length of partition 0. + static BLOCKS: [Block; 3] = [ + Block { + contents: [ + 0xfa, 0xb8, 0x00, 0x10, 0x8e, 0xd0, 0xbc, 0x00, 0xb0, 0xb8, 0x00, 0x00, + 0x8e, 0xd8, 0x8e, 0xc0, // 0x000 + 0xfb, 0xbe, 0x00, 0x7c, 0xbf, 0x00, 0x06, 0xb9, 0x00, 0x02, 0xf3, 0xa4, + 0xea, 0x21, 0x06, 0x00, // 0x010 + 0x00, 0xbe, 0xbe, 0x07, 0x38, 0x04, 0x75, 0x0b, 0x83, 0xc6, 0x10, 0x81, + 0xfe, 0xfe, 0x07, 0x75, // 0x020 + 0xf3, 0xeb, 0x16, 0xb4, 0x02, 0xb0, 0x01, 0xbb, 0x00, 0x7c, 0xb2, 0x80, + 0x8a, 0x74, 0x01, 0x8b, // 0x030 + 0x4c, 0x02, 0xcd, 0x13, 0xea, 0x00, 0x7c, 0x00, 0x00, 0xeb, 0xfe, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x040 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x050 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x060 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x080 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x090 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0B0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0F0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xca, 0xde, 0x06, + 0x00, 0x00, 0x00, 0x04, // 0x1B0 + 0x01, 0x04, 0x0c, 0xfe, 0xc2, 0xff, 0x01, 0x00, 0x00, 0x00, 0x33, 0x22, + 0x11, 0x00, 0x00, 0x00, // 0x1C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x55, 0xaa, // 0x1F0 + ], + }, + Block { + contents: [ + 0xeb, 0x58, 0x90, 0x6d, 0x6b, 0x66, 0x73, 0x2e, 0x66, 0x61, 0x74, 0x00, + 0x02, 0x08, 0x20, 0x00, // 0x000 + 0x02, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x10, 0x00, 0x04, 0x00, + 0x00, 0x08, 0x00, 0x00, // 0x010 + 0x00, 0x20, 0x76, 0x00, 0x80, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, // 0x020 + 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x030 + 0x80, 0x01, 0x29, 0x0b, 0xa8, 0x89, 0x27, 0x50, 0x69, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x20, // 0x040 + 0x20, 0x20, 0x46, 0x41, 0x54, 0x33, 0x32, 0x20, 0x20, 0x20, 0x0e, 0x1f, + 0xbe, 0x77, 0x7c, 0xac, // 0x050 + 0x22, 0xc0, 0x74, 0x0b, 0x56, 0xb4, 0x0e, 0xbb, 0x07, 0x00, 0xcd, 0x10, + 0x5e, 0xeb, 0xf0, 0x32, // 0x060 + 0xe4, 0xcd, 0x16, 0xcd, 0x19, 0xeb, 0xfe, 0x54, 0x68, 0x69, 0x73, 0x20, + 0x69, 0x73, 0x20, 0x6e, // 0x070 + 0x6f, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x20, 0x64, 0x69, // 0x080 + 0x73, 0x6b, 0x2e, 0x20, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x20, + 0x69, 0x6e, 0x73, 0x65, // 0x090 + 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x20, 0x66, 0x6c, // 0x0A0 + 0x6f, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6e, 0x64, 0x0d, 0x0a, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x20, // 0x0B0 + 0x61, 0x6e, 0x79, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, + 0x72, 0x79, 0x20, 0x61, // 0x0C0 + 0x67, 0x61, 0x69, 0x6e, 0x20, 0x2e, 0x2e, 0x2e, 0x20, 0x0d, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x0F0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1A0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1B0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1C0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1D0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // 0x1E0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x55, 0xaa, // 0x1F0 + ], + }, + Block { + contents: hex!( + "52 52 61 41 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 72 72 41 61 FF FF FF FF FF FF FF FF + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA" + ), + }, + ]; + println!( + "Reading block {} to {}", + start_block_idx.0, + start_block_idx.0 as usize + blocks.len() + ); + for (idx, block) in blocks.iter_mut().enumerate() { + let block_idx = start_block_idx.0 as usize + idx; + if block_idx < BLOCKS.len() { + *block = BLOCKS[block_idx].clone(); + } else { + return Err(Error::Unknown); + } + } + Ok(()) + } + + /// Write one or more blocks, starting at the given block index. + fn write(&self, _blocks: &[Block], _start_block_idx: BlockIdx) -> Result<(), Self::Error> { + unimplemented!(); + } + + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result { + Ok(BlockCount(2)) + } + } + + #[test] + fn partition0() { + let c: VolumeManager = + VolumeManager::new_with_limits(DummyBlockDevice, Clock, 0xAA00_0000); + + let v = c.open_raw_volume(VolumeIdx(0)).unwrap(); + let expected_id = RawVolume(Handle(0xAA00_0000)); + assert_eq!(v, expected_id); + assert_eq!( + &c.data.borrow().open_volumes[0], + &VolumeInfo { + raw_volume: expected_id, + idx: VolumeIdx(0), + volume_type: VolumeType::Fat(crate::FatVolume { + lba_start: BlockIdx(1), + num_blocks: BlockCount(0x0011_2233), + blocks_per_cluster: 8, + first_data_block: BlockCount(15136), + fat_start: BlockCount(32), + second_fat_start: Some(BlockCount(32 + 0x0000_1D80)), + name: fat::VolumeName::create_from_str("Pictures").unwrap(), + free_clusters_count: None, + next_free_cluster: None, + cluster_count: 965_788, + fat_specific_info: fat::FatSpecificInfo::Fat32(fat::Fat32Info { + first_root_dir_cluster: ClusterId(2), + info_location: BlockIdx(1) + BlockCount(1), + }) + }) + } + ); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/directories.rs b/examples/ios/embedded-sdmmc/tests/directories.rs new file mode 100644 index 0000000..e10a901 --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/directories.rs @@ -0,0 +1,599 @@ +//! Directory related tests + +use embedded_sdmmc::{LfnBuffer, Mode, ShortFileName}; + +mod utils; + +#[derive(Debug, Clone)] +struct ExpectedDirEntry { + name: String, + mtime: String, + ctime: String, + size: u32, + is_dir: bool, +} + +impl PartialEq for ExpectedDirEntry { + fn eq(&self, other: &embedded_sdmmc::DirEntry) -> bool { + if other.name.to_string() != self.name { + return false; + } + if format!("{}", other.mtime) != self.mtime { + return false; + } + if format!("{}", other.ctime) != self.ctime { + return false; + } + if other.size != self.size { + return false; + } + if other.attributes.is_directory() != self.is_dir { + return false; + } + true + } +} + +#[test] +fn fat16_root_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + + let expected = [ + ( + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2018-12-09 19:22:34"), + ctime: String::from("2018-12-09 19:22:34"), + size: 258, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("EMPTY.DAT"), + mtime: String::from("2018-12-09 19:21:16"), + ctime: String::from("2018-12-09 19:21:16"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("TEST"), + mtime: String::from("2018-12-09 19:23:16"), + ctime: String::from("2018-12-09 19:23:16"), + size: 0, + is_dir: true, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:21:38"), + ctime: String::from("2018-12-09 19:21:38"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("FSEVEN~4"), + mtime: String::from("2024-10-25 16:30:42"), + ctime: String::from("2024-10-25 16:30:42"), + size: 0, + is_dir: true, + }, + Some(String::from(".fseventsd")), + ), + ( + ExpectedDirEntry { + name: String::from("P-FAT16"), + mtime: String::from("2024-10-30 18:43:12"), + ctime: String::from("2024-10-30 18:43:12"), + size: 0, + is_dir: false, + }, + None, + ), + ]; + + let mut listing = Vec::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); + + volume_mgr + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); + }) + .expect("iterate directory"); + + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry.0, given_entry.0, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + assert_eq!( + expected_entry.1, given_entry.1, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); +} + +#[test] +fn fat16_sub_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("open test dir"); + + let expected = [ + ExpectedDirEntry { + name: String::from("."), + mtime: String::from("2018-12-09 19:21:02"), + ctime: String::from("2018-12-09 19:21:02"), + size: 0, + is_dir: true, + }, + ExpectedDirEntry { + name: String::from(".."), + mtime: String::from("2018-12-09 19:21:02"), + ctime: String::from("2018-12-09 19:21:02"), + size: 0, + is_dir: true, + }, + ExpectedDirEntry { + name: String::from("TEST.DAT"), + mtime: String::from("2018-12-09 19:22:12"), + ctime: String::from("2018-12-09 19:22:12"), + size: 3500, + is_dir: false, + }, + ]; + + let mut listing = Vec::new(); + let mut count = 0; + + volume_mgr + .iterate_dir(test_dir, |d| { + if count == 0 { + assert!(d.name == ShortFileName::this_dir()); + } else if count == 1 { + assert!(d.name == ShortFileName::parent_dir()); + } + count += 1; + listing.push(d.clone()); + }) + .expect("iterate directory"); + + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry, given_entry, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); +} + +#[test] +fn fat32_root_directory_listing() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let expected = [ + ( + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("EMPTY.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2023-09-21 09:48:06"), + ctime: String::from("2018-12-09 19:22:56"), + size: 258, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("TEST"), + mtime: String::from("2018-12-09 19:23:20"), + ctime: String::from("2018-12-09 19:23:20"), + size: 0, + is_dir: true, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("FSEVEN~4"), + mtime: String::from("2024-10-25 16:30:42"), + ctime: String::from("2024-10-25 16:30:42"), + size: 0, + is_dir: true, + }, + Some(String::from(".fseventsd")), + ), + ( + ExpectedDirEntry { + name: String::from("P-FAT32"), + mtime: String::from("2024-10-30 18:43:16"), + ctime: String::from("2024-10-30 18:43:16"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("THISIS~9"), + mtime: String::from("2024-10-25 16:30:54"), + ctime: String::from("2024-10-25 16:30:50"), + size: 0, + is_dir: true, + }, + Some(String::from("This is a long file name £99")), + ), + ( + ExpectedDirEntry { + name: String::from("COPYO~13.TXT"), + mtime: String::from("2024-10-25 16:31:14"), + ctime: String::from("2018-12-09 19:22:56"), + size: 258, + is_dir: false, + }, + Some(String::from("Copy of Readme.txt")), + ), + ]; + + let mut listing = Vec::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); + + volume_mgr + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); + }) + .expect("iterate directory"); + + for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { + assert_eq!( + expected_entry.0, given_entry.0, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + assert_eq!( + expected_entry.1, given_entry.1, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); +} + +#[test] +fn open_dir_twice() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let root_dir2 = volume_mgr + .open_root_dir(fat32_volume) + .expect("open it again"); + + assert!(matches!( + volume_mgr.open_dir(root_dir, "README.TXT"), + Err(embedded_sdmmc::Error::OpenedFileAsDir) + )); + + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("open test dir"); + + let test_dir2 = volume_mgr.open_dir(root_dir, "TEST").unwrap(); + + volume_mgr.close_dir(root_dir).expect("close root dir"); + volume_mgr.close_dir(test_dir).expect("close test dir"); + volume_mgr.close_dir(test_dir2).expect("close test dir"); + volume_mgr.close_dir(root_dir2).expect("close test dir"); + + assert!(matches!( + volume_mgr.close_dir(test_dir), + Err(embedded_sdmmc::Error::BadHandle) + )); +} + +#[test] +fn open_too_many_dirs() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: embedded_sdmmc::VolumeManager< + utils::RamDisk>, + utils::TestTimeSource, + 1, + 4, + 2, + > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + assert!(matches!( + volume_mgr.open_dir(root_dir, "TEST"), + Err(embedded_sdmmc::Error::TooManyOpenDirs) + )); +} + +#[test] +fn find_dir_entry() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let dir_entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("Find directory entry"); + assert!(dir_entry.attributes.is_archive()); + assert!(!dir_entry.attributes.is_directory()); + assert!(!dir_entry.attributes.is_hidden()); + assert!(!dir_entry.attributes.is_lfn()); + assert!(!dir_entry.attributes.is_system()); + assert!(!dir_entry.attributes.is_volume()); + + assert!(matches!( + volume_mgr.find_directory_entry(root_dir, "README.TXS"), + Err(embedded_sdmmc::Error::NotFound) + )); +} + +#[test] +fn delete_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let file = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly) + .unwrap(); + + assert!(matches!( + volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), + Err(embedded_sdmmc::Error::FileAlreadyOpen) + )); + + assert!(matches!( + volume_mgr.delete_file_in_dir(root_dir, "README2.TXT"), + Err(embedded_sdmmc::Error::NotFound) + )); + + volume_mgr.close_file(file).unwrap(); + + volume_mgr + .delete_file_in_dir(root_dir, "README.TXT") + .unwrap(); + + assert!(matches!( + volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), + Err(embedded_sdmmc::Error::NotFound) + )); + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), + Err(embedded_sdmmc::Error::NotFound) + )); +} + +#[test] +fn make_directory() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let test_dir_name = ShortFileName::create_from_str("12345678.ABC").unwrap(); + let test_file_name = ShortFileName::create_from_str("ABC.TXT").unwrap(); + + volume_mgr + .make_dir_in_dir(root_dir, &test_dir_name) + .unwrap(); + + let new_dir = volume_mgr.open_dir(root_dir, &test_dir_name).unwrap(); + + let mut has_this = false; + let mut has_parent = false; + volume_mgr + .iterate_dir(new_dir, |item| { + if item.name == ShortFileName::parent_dir() { + has_parent = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == ShortFileName::this_dir() { + has_this = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else { + panic!("Unexpected item in new dir"); + } + }) + .unwrap(); + assert!(has_this); + assert!(has_parent); + + let new_file = volume_mgr + .open_file_in_dir( + new_dir, + &test_file_name, + embedded_sdmmc::Mode::ReadWriteCreate, + ) + .expect("open new file"); + volume_mgr + .write(new_file, b"Hello") + .expect("write to new file"); + volume_mgr.close_file(new_file).expect("close new file"); + + let mut has_this = false; + let mut has_parent = false; + let mut has_new_file = false; + volume_mgr + .iterate_dir(new_dir, |item| { + if item.name == ShortFileName::parent_dir() { + has_parent = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == ShortFileName::this_dir() { + has_this = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == test_file_name { + has_new_file = true; + // We wrote "Hello" to it + assert_eq!(item.size, 5); + assert!(!item.attributes.is_directory()); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else { + panic!("Unexpected item in new dir"); + } + }) + .unwrap(); + assert!(has_this); + assert!(has_parent); + assert!(has_new_file); + + // Close the root dir and look again + volume_mgr.close_dir(root_dir).expect("close root"); + volume_mgr.close_dir(new_dir).expect("close new_dir"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + // Check we can't make it again now it exists + assert!(volume_mgr + .make_dir_in_dir(root_dir, &test_dir_name) + .is_err()); + let new_dir = volume_mgr + .open_dir(root_dir, &test_dir_name) + .expect("find new dir"); + let new_file = volume_mgr + .open_file_in_dir(new_dir, &test_file_name, embedded_sdmmc::Mode::ReadOnly) + .expect("re-open new file"); + volume_mgr.close_dir(root_dir).expect("close root"); + volume_mgr.close_dir(new_dir).expect("close new dir"); + volume_mgr.close_file(new_file).expect("close file"); +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/disk.img.gz b/examples/ios/embedded-sdmmc/tests/disk.img.gz new file mode 100644 index 0000000000000000000000000000000000000000..507ec9487ddfcebab03baec2c837d3ebd3cbe629 GIT binary patch literal 707976 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

5Nfer%f9^mEhHrtwXao6t?u;16&D{0UBh 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 &+?~N#Vuby)^ zmc4iQU~x&c{`B4W?UKWLf?VF?<7Q(Hl@A}tb*a|vpB<2`pr3f}CAYrMx9;+>Q=gxx z>@?i#zvQdT@ojI=GWXGATtANa%47FRALprGeleVM6qPQo%+?z6biw%OiO4$a!TgZG z$>F1zk~-xhb0IxKTZ(SlsPAGL94s+rlSXXB6o@1f6K3%kbc_Q{Z_E2eS;Pk>vOvQI)uGQ8c+RmFtA09^jd@LC7HQ*+9 z;^C&lHG)rPu{V#0zCiY;2nLQLH_-(znm9%UgI4%%9`n~i_CC)K7|FVc%G7FN>&$;j zvAlUA`U`TfC_iv&@+PM2OVg3X{2(gF;8CA*$eyRR0Yfo^+^Of9SetF1&g%^xk9dO| z$gvG1_YR_q-!ySf+Xk&k4jv0~M)t+c@r}~$9BP?w%=wYZzw*y4&)lcc_uPMS`MBWq zCyK%e`yRgd@Xf<;pU;~QC#0F=>$bbgAKx4LC`evWP#S%nM_%C+F7$C>(g`WnI(f8U zsCSm}35oM{3a4#DeaeSV$ZS0+e{2W-QNsSjbEuOFJc{_o`C*BY2h!wGr|{kxYKh|U zKX~G2&x_!w_joyu<8 z5dYrcV*)F6s=x2RJ$b+X)ya=1RhSfUeyw4z_(o2uZaszbZBTo4sw1r#BJqu(-DR_bpnh-pZJXeecRO#LtUlcQ$5lbU z&jGjih7PxEK@0lUV{e^md(phpRM5W*xpjK}Ma!mx`Aa?n$lWSDqH<=6{L|*>d`cO?!MV8z=l3e)}?J+gaa_ zYah_N@A}HQ?|t@+W%=1))x0HL@f!u%UnUuRq$Cb))>vKU4PZl_F1Uj zFq*}sUe0sJwP%<1r~Gk+=jvU>YazI>l$|C#axSN?1uLqh>`+g?>LC3UFFj;u@nfq2 zE;M&J%4Yfb%yJypag4(<^u|I8i^sT}*S0gyF77n_LzcDX&eby^*8@y< z8nMv*`82v-;e9c{zJ0bX((Vb@JYh6y?30Be@-Jw}h z<6wO!RJV6@w^K>2$B`e;mULXiTwY1MHS~Uf@o^#TIz`d^ z(1$U@$HiRgl%(hI_a5v|ICJHs;#oiZgOIQUVUv?e=Ns_%{nQc!)zcJ3GUl1$=UC(M z4+WM(qTD}{+^dV-YnRsF2DK-MI;1H{O|;#6ESzvwW>!e~wxW>dtMgTJ?EK5IGPf0Y zb>=Z%vtm}a8)ep&B)Z$~KebN~(*Ld~R@(M3ZXrR;o@FRP&}tO# zEVv(NX(*)or9rf);9=~fp_tp3M(IVndk;Cjosm1&aQ3O)gV307!e-|h&o|rM_t*O- zsQIQrB**SybniD&r#FpK)ARRyB)^?ib#4%fn|}~d{!PTjxlw{9Q9cBb@$(~NFb-B3 zKmWtFY#B{%%kjOiYW|O2H3y8952A9No7ns3p3-+uu0b&W{9w+ShhYBs!Thoxf_Vdi zxs(pU{4-8x{Oe%;4lCxLzhW+nffe%xe8@J074rsMo>D4-74uIy&-|}~`SdWXm^UDp z17O9x0l|D6R?Hg_%x_@D{L_PZ3D+$Hr!&pC8Hv5XwJ4l=&c(e|{+ELnv=RD4Rkk|NKxcgHZnYq09lH{PRQE7D9OgLK)5} z|MW|J&H}{{%0EApB_Wi5ekjjDC~rV0!#U**2<0jW<)0tQn;?{bekkvNQ2zO$%nRp~ ze|{+EzJ^f#`JsFqLiy*1vLA%<281%4Q{I43egmQW^Fw(rg!0c1Wkp6PJ1Og#Vx{SfJD6s@x8Zane14h8>{e=qGj@xzXpZ zz<%< z8jG73tCjliqMl`coIJiO!QRQq>BQbmt;hQMjt1UW@H;M_e$()i$*Y%1t=UIgZ|zMe zk7$|aGAkb{pCQeE4Y*6Mc~j$8GS*9IB2Ejq<%(eH<^Mb;RhV_=&Sl?bl(YV$nHAah?P*xGw0{ylC|^YZ~eKB zGAbi04S5-rk&J3aM$z6s+8G(`k^gf+BiD*AqodKRd5-yC*EHs?*UYTHV-!jL-PgF@ z#L4LTot_)uTAew@Xvak=dgb;`V|ZAe1^iP%?skP1SxM~wUfre2E{lI^ww(JT&^fH; zuVzcxs~%lOaVMiBoYD0A-wJgHdcShf zMo!bIp)*JJG8#pD8$GzcdWOvX21EU`hFWGBhWa0Uu0MCEJ>HDLQ2(QF`{yS0_O)q9 z>VGLI{>ej~wgf}HK|>A8kN@7h{ga0}5|aAA-x}fPCiS5eNa_t*e)vLCZ`4r3ntFqh znzMWemLD6l{J8H1Nxd;i%~?o>q28FG{vAFjZd6id&BIV{%uwHVg;RPPl+;OUFw`3} z)W5^oj}1!d*cez-Z_o!tUl&+YZ%k6hhQXS8gNB+3PU&q_QU}1AdV_}A*BRE-8g%t$vt zN^j6e$G}K$&`3i{Z_r3TfRWyqkv;_}y+I=#0wcXaBMm9NK_l%4BfT*rodGGmK_eXn zBfUW*4Jo}rBmI~$(oQq|$`pEyDr%WtN%fkcPgS1uOMqV>00O_3z;`;GS2N{qHb(}& zz~t!`rl&3O{m!mK^a||4z_$+$>|%tjMY_WZJ!hcYx=B2cJX-fTKX62sURGsE-omt3 zgr`T`A@I=>xBRDkYyaoxX~peKd-c|3$aJU3H#J^l65sQeW)E&LxpE(lJgWYnL2B<# z?3v!CX?np_Sq3S=I1K6|;;t70WVqz*LF zJv@oL=(J^e7j2F{wzzLGzdnv>mHWHRTBF;Wvu7?wY}qXNYDtEfxHnqmM`E zZpqA4ey(9NUD4ZL5^ni39iThWIaXv^W?nRM)92SMC_m2lU=Ay1Y~iIZInf5Z=u$*_ zOWTUg8t-ot^Fe)&dYKlhL8}@u(c>}q30sRF(`=_n=<;10+d?l3;-}k)k*M+1?wYyh zIkD7uvARbg{1ykA!jF>9&OD{}b1_xOgmlV3TcwZL%=}uD2hSD+{@)AG1QrJ`{Px$Q zj!xX8rVbAf009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2Lz zUtQqK!_B|{wHM$hopkluuYNG_{6PQ&KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l z00ck)1V8`;KmY_l00h);vFzUR*Is~o>jvx>!2<+900ck)1V8`;KmY_l00ck)1V8`; zKmY_l00ck)1V8`;KmY_l00ck)1VG@I7x0!k_vc@G0e+kFvfA>?9}OG;2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9z=6fV3xDqgsG}42sDTFv zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@AXf01yBH5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI z5C8!X0D)U9ySMzc7vSEy0sBSZ009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI z5C8!X009sH0T2KI5cuTW=_qt9%^2TooYvgx`D@xl*5GP z-%SI5?J$4`2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x{Ff70 zueyG&)=PfhpLYzu_~dTcqUR=3oB6}7;zvCB_utPO1j@v%C060SXSOvCB=2yZn^UE| z;Y`)P5OlBT_X-2f+zI{7u9c-bcb(k-iL9=hd++r<3obP)HRANkZ(aj~@k^>&$Cd`> z{kIkFxJ_LYoR8oubImh%zIX?AL=_D^@fq z`U?FSecfO*VkLK_{#5x=)hdc{@pjN)t&Q$Aaqr%ITTMGFek|y`r}qHao32g`qCLyt zdbD1lm24>g)GG&*_L}b_z0d8h3%>O9TOYGlq8Fd6*{#n+Nltp*HW*mg>S@u<^D%AL zOofAo!o%uFN2@Lk;@#FJU7e1nO=?#CDHl5opX!}2EVmCTzkg_+FTmW()-GWtmG6)E zg^PJ_nk*_#?s0mT@<`uqK;Ct@EOuncDziK7+pu{VsiKRo@AFK{>5`JM!J^k@yFyR* z$9dcs6F0hgMgRQy3EJ#1Pu$X~abt$;(C8zgOmFH!ceQzhM}F$C-xb!~qS9i5A3yl* zDG;?{st88i?Y|Y6V9V#lo6;7%uUMB$S#zPHlv?$qf~{ozlW(TMp+BYow4;8I^z2Teb>oX-$QP$RMv{m88E$^!q%5pO@8*HwKRr**`N>Hu2M5zh4={nQ)%>a!m5lb7IOri2}#^+&l z$s8C_>n}nH-CsHFFj6_^liDe+k&gPgpVAvhR+ro&r28zD*P5FaYxODzrd-G+T_Ngk zW-Tw5Zjw-Xyb%3)q$WtjiKcb)=Cv`BAGKTj=HlZ{htMRCA79(-+Cp(`;g1wTZ%Qd-b)%Bn9giUhL2obliP%WvF4mqBfvO zyZA13wS<{ZVZS5h;?SHdA>PI#eSrG)K|43`icA6Jp-@Qf{GLlUV>Ry_Q zz5dVn%}>e|C%HsCH}^S)0++O?8Tx%<-A_hnZ*_>LTO`#L_lIrX4q zxxv@=xlwI_pq1Nl#o;mLXi;t$P3AS*Kdi(LfvY1DZHNODeP&+vUeTVP6;O3Y^yl%{6}+k zyY|2iCccidhO0SSrFO(|ilX{-uRI-Nms;vvJy(!0KtAXAKJImyer?jqVqb^cUQhbu zkh1xSZI_?sh%7Ir3z8n<2AHS!ILs9Yd$&Hdh!Rg}?L=4{Ee)h8D;?LFm{SbiIQO@_ z62WcJSaI)tcd$!Zvdvj5B9(o&r{2+vK?nHRQ%~93o7}8he9TWcpiNT>p4?|Qz*On9 zmU_SAcy3ndSr@$pG4@8p>#kU#7QMDljH=__JZG^su$D7HR58<3U!QWd|M51Y@2RIu z`nQK``!csbjSk_VEK4+vWgUL)_8}$>#X7(#fpJ|(p6clj8aRD%N++mQq`X^zZ@wPa z*Ss!nsGa{V{ZNI6$3jOBaW32DI^P|AN?%@Z{FVb5cRk*}eKvu26j;7xJ#%q$Twwj{ zv0)Ok<|w<9IU(dzwTwX4+~_@NDK-N)+bP2?_bP9*Cj(QqWk1rxCp@IAd&jy=!&oBe z`B%v3;+i6_AzX&jrumu@nYp*b!THVkSuLwKNqGy(j+8$A16sE1L8KT5Yn?+=29c z0+zR*^5q^1Q0_wApl&1C9Lb(rX#c>~TG-P`8{<3RqglVtxr|m$+C5g*dXyf(#9FYk z$NlBial@dh=>>)RYi1J%PU@u0b?JRBaJ953rKP5G{a@|e`CpCs8&CI#DSpjWtBHC`uiY(yo0!*N6U! z@A=*y=ZAjkcAe{SKj)Wo-JZ8s>6+AWuWBzRWLm9pO0(!F>K{Af*EGMOx3n&q>|Z`| z@Psk*8u#7^j?=75X}cYA)%|AZ2)%dB4)&8%#WS6AUUxg0m3;T(jfsWcXT&$=Nmdk` zUX?k)*R3w0%=hl`VUs#GHCZoD47=9V+WzBvtFD;psz06@%^DHzyrfmr^4XJzB|q0( zeBN^0-s)R}a9z>4tOUtQ>*bz3ZHbv{gHl$zzbS}sO;-*Zv^s2})!~gEgHLV~J8le^ zZYFAQe}1m9{qpIgussPLM<07VdThEbFC#lLc>ka@{e+iCeh5%gO*xwW>i*Yeg;T!m z8hgGg=i!O*HJX0P^Tu7<(bSyxZqbVoJyuz=yA~?qS5=0F(&?*2b=6~LHSP&nvt3h? zo?D$8_xP1au$a_&->ZLWVr}vjr%+?1%VEW<=X5IEzrM{(n*PT%uQ9=@F(XBtotkEc zC5u%&c9^D)HFx_uY~%`Ovu!Q;#do92Pm5bU{a1E6)D;)ryn8*uV(IS7_pJvFDILAb zxgm36?)j<{f0<@23W#%k8ebcFya_vkEh_8tt}z*_#v9i?zSGFyL@%zJpNSv{38!BeAlM zyY*+Tz5hK}SKQ#xz5i?1(eY(xQkD!=>D=+8dZqNi$d{Aa@2N>__2+5Wy&mvBTeHeC zJ@4x9jD5C41}sq>?PAbklGh~Goi^c@oC;g(pzz+R?7brG;Fw`^9sWD(`;$j_`1W9t z=gl7~iIf#k85UhJOn?)VK+*M+)kAA%poj7*kz78q1)mR&cvw=Ox= z@Rixnws6nhW#K_X3L3Pn{;I31Eo{D0^Zeg;5|7e#H=B~GU$m8Kb(OU3dVipPa<{3F z`7UVZ%aAv3{S-=sqMa_)JAYb~o22AEXGT*&wo;aHW`W1`?vfNGyQIe_c6YpreJ9fQ zcm7#?FaPAiE{{OZj<7K{?`jW+>1K4+nd@F!Ri`tj_im`QitoY28S`hkS6r_-6Fcc< zgN^iMZ$iR-VO-92PxHM2+n*F%w+M(-%dX8x&?wH1-_ofUF?U$T$KJd9ElTuMUE3Ep zhRpk_D@|(O@}$Z}@}t%%=jMdx=VodLZHt&aZOtX+Ib&0$TjI~Py?Xkr=z3}LmOYKH zX9s8KS60SMU8O(ywdc!&?;?_|^qY#B%eDyDJsjWuGfVsKOc%B3y2X!m7Pt9NQRyr^ znl;ql&ST#D5|5L%Cq^FC?(MZ0q8(rsthuT4zmbFLl^%LFH&0Z~TKObRBdxCP$g>M8 ztSq}8-BU<5`!(a#FG-J1JXc=eR-szgU4QGWG&uIV9gaN@TxQQRZ77m>M1N)Loi#Yj zDrRx;tEb&k?br1#XWvv0C<}h_%4KKBj@X)V?c}#U!84OB#LOyB(|H>I`hnP2Q~C)dqyquQ_6n||MHS=G|2R=Bh&enrWtLH_e1 zbaO8_o$)%nE~ij&l_bgbW@$>D2BQ)@`!X&CWTQQ=MDVt&&_d!SMV9D zm1Z?xd^B#ufO4Ww%WI;lhxjP>wde?{Hk=itn$wSXOntoN{fO_S)>mt4EF9hFd?) z-st_q+9S526-T-{Lto!;ZfI{mCDIi3G=xbTtVCtQrQ+E29Rmin>r@Q!u<31Dqm-aF zx$Di_Jn6R0yDCqAxn|2VQ+@IM*o-l2mwzm|IHYuKPW|=wS;-!0?sb=K9oCdiwR;u5 z$~;w?FZ*|Xg5C)?Pxt%nahA^mWgp+TOMaQ&(dH2qEZAn2Cg?ec9fXr=#`CXue9Z0^ zD(gmG45+K^ZT{); zajB&VMy>0b>x>It>)G~P7-EpJzU-y3!|T>ehox~P&mEfI3p<`Xs;R82PrFny)Ie6e zR6{(fcYSfNI3oSc*$`ioO)A~T6MTB|GUwD~{QGW*{bbd&Jxv$H?mJx97Ceh99)6?p zs&+-V5FIwMa=xr#YRm4ohLT>9L#j=Eb(Zo@aZi)J$H4a3hpThzL&CEZ?9JTWt2Ydi z&C;xLGbnPp-MKyZN>PE(CYB|aw{%(Ry5BUtxcNy{NA02D?dh>K1-D%@225QdifYQ3 zVAbVVeeH6wX2z_Gb>hkhXE$?qC;M?uFT(1MD2*??)zdaJBzS61$G<1LrLp!S#Lp^* zFZ@QBUiF|yUt|^G+;k))D{k%15*uZu%XVG&OKwQRSGoyq753K7FCMr1Jo}KaxpHyC ztTeaM)Rw;*U(b|%EGx*{^t*IVTaD`&&3!kO#Et%)z7-jwvKvF)OhxC*PQ`9;`(=hK zNTEF~$K9eWV2O|Z$vNvA$`5A>^|#i!mHd$#d$nQvoc7Jd^Lw(&O`mNVQdP7|>R?iR zvo$nn%I4KMx+AW;d^oh<<3O0j8H4g`@9Mi_nkB)mo=ycBrj~9w_f$oiv7@Jn8WP8x zd-h1hrM|Av@?*B;y504m@nvVLh9s`bs7$Z>D$g{;slmL}ELobL6!R|Edh)HQ@4fX_ zE#2nkvEjkNp61;7sUMGA_f?ZgZFQ!+N~x)`_H?P{2H zzi$zpw=S_9Bn~X8KjzSCu;Fg#_H@(Vs^ia!pB0N<_!oW5KRsKoG$i-EUsIIsv#=e) zskGUW!RtD1DjU1bt&EB_)>~Cxqp3YP)ci!aWngH~(eU8Vkl8ad-G8lr`2C~(Zyhbx z+pjnM!+t?&j?8b*4wH3NE4FrzPWMO@#(eq3KU0_0GzyYLKkEQVapjy$p{zq#HGiZq zi zQ~oON(3rDk+43227cXuR|7*0Y{k@9fr>jU^7EfN1P&z(OYtwC1`71_F3^*`hRglF}hw!}K%aYbc@wM*!NH=ry=2r)MPaZft z&}2sL!kH%LhBwYU`opNgM<=d6PK5_o}E1rF; z^dx(d>e1z^mpNIOI6v~clAr&)yleA$*SDuv`@C4?^VsBE)FGz`mocY*=uRDLbGBpH zzNbfb-SkOZ_Q&wUroOX$gtX-;yMBKZ)mYkFKJwFZ8@q)vad}UM*+QRt1&vjf()0^0 zm6h2gK`*APNZ$FXymIgPK{rF&SCk%a_p8xMOKJG;O5&Hg$`VWW2Jc*(#rtiO)Lw0B z-oCVCZ()(a^o@7Dl=gNdU-Wn<+COM{;iT0i-wm+dlbgG4isjH@Q|eE&d-k3Q)3Sdi ziZt&n(^UQ7;_*zJHYvUS=~89W^lRI8de%RFlGH0M5T0Cdl$O7>9lSbYb6(?RwXNoR z)_x~$vpN&m6q{4***!i&`_o6(F>l^Dp`odMc6arG($?2X&6UdEdQNle)m>w^s^UPz zliu#ow6_611(Qz+d#Z!hDo?-qbe%8e=l#?yM13>5 z*e^SET+K1{gOZ!&b21GkKTmf}b9Ub2HtW2I7Z4DD<`+AQ2_ z{rsTpjrJQsUl45PjEoS(AI4?$d5=AX)g$r)$+b>Jb}<3k|-{gyUsHzg3Za`=8F_ z?wkE@FOH}X#ve`HoU+x#b)LqQ@CH)?O?UDw>7}1VZP_vGiolYcQkHy>)l%a&(kFV+h=;3b@g`p zYTK7yZ~QOxe*Zyd9)F{>r#9MldzEM86}y~|esN0<)Q=FCuFI3EcJ~GwrOgyeUtT>M zrEOn(HRWxWv|)&wxa&ql=<7~Nx6voRjwR(nxKP0SI#P{2 zoqL`)3bAE}y4GCWqVO&=>4;k4pBd`&4XiB!N1g9q;(c?dbKCfFrDl&!!U}WmRNel# zutv?nbN;7Gb%d(u3TEXEpFgZZ1*0Q_luztic=p(U@n<`BO4WDRL^?d5l2I&8EEhEP z?pQE4NC=psX!5ti^1mfR|F$vtJL`PKHx*I$t0PqXM!w9I88$b6`ZE4kStU^wBEcq*QGa)Na1&b05G(P<^ zF9Z-k009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_Kd|K9}u6Rs*K^!r?8D3ka{ zUgCuS0tg_000IagfB*srAbkweJ{>FTUu^ z(D9b{815ed1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**E)5hps`s6C zP%acH;(-7H2q1s}0tg_000IagfB*srAb+pBL2 z5GdGb#K?UP0SF*~00IagfB*srAbM$p;XnWZ1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY-` z7tpG`?bWvh2o&rzV&p!D00a;~009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q3W|N|uOR>d1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**28tZj`?dh(LXjdI2q1s}0tg_000IagfB*srAbxz#UZ;#lC|9Ix}><<-Udh1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5Ev+ORPWmYlnX_Qa3FvH0tg_000IagfB*srAb;?n00IagfB*srAbJa?sEu0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~fp`UlexJtxhBAqNBpe7JfB*srAb>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), + Err(Error::FileAlreadyOpen) + )); + + volume_mgr.close_file(f).expect("close file"); + + // Open with SFN + + let dir_entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find file"); + + let f = volume_mgr + .open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadWriteCreateOrAppend) + .expect("open file with dir entry"); + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, &dir_entry.name, Mode::ReadOnly), + Err(Error::FileAlreadyOpen) + )); + + // Can still spot duplicates even if name given the other way around + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), + Err(Error::FileAlreadyOpen) + )); + + let f2 = volume_mgr + .open_file_in_dir(root_dir, "64MB.DAT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Hit file limit + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "EMPTY.DAT", Mode::ReadOnly), + Err(Error::TooManyOpenFiles) + )); + + volume_mgr.close_file(f).expect("close file"); + volume_mgr.close_file(f2).expect("close file"); + + // File not found + + assert!(matches!( + volume_mgr.open_file_in_dir(root_dir, "README.TXS", Mode::ReadOnly), + Err(Error::NotFound) + )); + + // Create a new file + let f3 = volume_mgr + .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadWriteCreate) + .expect("open file"); + + volume_mgr.write(f3, b"12345").expect("write to file"); + volume_mgr.write(f3, b"67890").expect("write to file"); + volume_mgr.close_file(f3).expect("close file"); + + // Open our new file + let f3 = volume_mgr + .open_file_in_dir(root_dir, "NEWFILE.DAT", Mode::ReadOnly) + .expect("open file"); + // Should have 10 bytes in it + assert_eq!(volume_mgr.file_length(f3).expect("file length"), 10); + volume_mgr.close_file(f3).expect("close file"); + + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} + +#[test] +fn open_non_raw() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); + let root_dir = volume.open_root_dir().expect("open root dir"); + let f = root_dir + .open_file_in_dir("README.TXT", Mode::ReadOnly) + .expect("open file"); + + let mut buffer = [0u8; 512]; + let len = f.read(&mut buffer).expect("read from file"); + // See directory listing in utils.rs, to see that README.TXT is 258 bytes long + assert_eq!(len, 258); + assert_eq!(f.length(), 258); + f.seek_from_current(0).unwrap(); + assert!(f.is_eof()); + assert_eq!(f.offset(), 258); + assert!(matches!(f.seek_from_current(1), Err(Error::InvalidOffset))); + f.seek_from_current(-258).unwrap(); + assert!(!f.is_eof()); + assert_eq!(f.offset(), 0); + f.seek_from_current(10).unwrap(); + assert!(!f.is_eof()); + assert_eq!(f.offset(), 10); + f.seek_from_end(0).unwrap(); + assert!(f.is_eof()); + assert_eq!(f.offset(), 258); + assert!(matches!( + f.seek_from_current(-259), + Err(Error::InvalidOffset) + )); + f.seek_from_start(25).unwrap(); + assert!(!f.is_eof()); + assert_eq!(f.offset(), 25); + + drop(f); + + let Err(Error::FileAlreadyExists) = + root_dir.open_file_in_dir("README.TXT", Mode::ReadWriteCreate) + else { + panic!("Expected to file to exist"); + }; +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/read_file.rs b/examples/ios/embedded-sdmmc/tests/read_file.rs new file mode 100644 index 0000000..904964f --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/read_file.rs @@ -0,0 +1,212 @@ +//! Reading related tests + +use sha2::Digest; + +mod utils; + +static TEST_DAT_SHA256_SUM: &[u8] = + b"\x59\xe3\x46\x8e\x3b\xef\x8b\xfe\x37\xe6\x0a\x82\x21\xa1\x89\x6e\x10\x5b\x80\xa6\x1a\x23\x63\x76\x12\xac\x8c\xd2\x4c\xa0\x4a\x75"; + +#[test] +fn read_file_512_blocks() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = Vec::new(); + + let mut partial = false; + while !volume_mgr.file_eof(test_file).expect("check eof") { + let mut buffer = [0u8; 512]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); + if len != buffer.len() { + if partial { + panic!("Two partial reads!"); + } else { + partial = true; + } + } + contents.extend(&buffer[0..len]); + } + + let mut hasher = sha2::Sha256::new(); + hasher.update(contents); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_all() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = vec![0u8; 4096]; + let len = volume_mgr + .read(test_file, &mut contents) + .expect("read data"); + if len != 3500 { + panic!("Failed to read all of TEST.DAT"); + } + + let mut hasher = sha2::Sha256::new(); + hasher.update(&contents[0..3500]); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_prime_blocks() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = Vec::new(); + + let mut partial = false; + while !volume_mgr.file_eof(test_file).expect("check eof") { + // Exercise the alignment code by reading in chunks of 53 bytes + let mut buffer = [0u8; 53]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read data"); + if len != buffer.len() { + if partial { + panic!("Two partial reads!"); + } else { + partial = true; + } + } + contents.extend(&buffer[0..len]); + } + + let mut hasher = sha2::Sha256::new(); + hasher.update(&contents[0..3500]); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_backwards() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr + .open_root_dir(fat16_volume) + .expect("open root dir"); + let test_dir = volume_mgr + .open_dir(root_dir, "TEST") + .expect("Open test dir"); + + let test_file = volume_mgr + .open_file_in_dir(test_dir, "TEST.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + let mut contents = std::collections::VecDeque::new(); + + const CHUNK_SIZE: u32 = 100; + let length = volume_mgr.file_length(test_file).expect("file length"); + let mut read = 0; + + // go to end + volume_mgr.file_seek_from_end(test_file, 0).expect("seek"); + + // We're going to read the file backwards in chunks of 100 bytes. This + // checks we didn't make any assumptions about only going forwards. + while read < length { + // go to start of next chunk + volume_mgr + .file_seek_from_current(test_file, -(CHUNK_SIZE as i32)) + .expect("seek"); + // read chunk + let mut buffer = [0u8; CHUNK_SIZE as usize]; + let len = volume_mgr.read(test_file, &mut buffer).expect("read"); + assert_eq!(len, CHUNK_SIZE as usize); + contents.push_front(buffer.to_vec()); + read += CHUNK_SIZE; + // go to start of chunk we just read + volume_mgr + .file_seek_from_current(test_file, -(CHUNK_SIZE as i32)) + .expect("seek"); + } + + assert_eq!(read, length); + + let flat: Vec = contents.iter().flatten().copied().collect(); + + let mut hasher = sha2::Sha256::new(); + hasher.update(flat); + let hash = hasher.finalize(); + assert_eq!(&hash[..], TEST_DAT_SHA256_SUM); +} + +#[test] +fn read_file_with_odd_seek() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let volume = volume_mgr + .open_volume(embedded_sdmmc::VolumeIdx(0)) + .unwrap(); + let root_dir = volume.open_root_dir().unwrap(); + let f = root_dir + .open_file_in_dir("64MB.DAT", embedded_sdmmc::Mode::ReadOnly) + .unwrap(); + f.seek_from_start(0x2c).unwrap(); + while f.offset() < 1000000 { + let mut buffer = [0u8; 2048]; + f.read(&mut buffer).unwrap(); + f.seek_from_current(-1024).unwrap(); + } +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/utils/mod.rs b/examples/ios/embedded-sdmmc/tests/utils/mod.rs new file mode 100644 index 0000000..3f89a0e --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/utils/mod.rs @@ -0,0 +1,190 @@ +//! Useful library code for tests + +use std::io::prelude::*; + +use embedded_sdmmc::{Block, BlockCount, BlockDevice, BlockIdx}; + +/// This file contains: +/// +/// ```console +/// $ fdisk ./disk.img +/// Disk: ./disk.img geometry: 520/32/63 [1048576 sectors] +/// Signature: 0xAA55 +/// Starting Ending +/// #: id cyl hd sec - cyl hd sec [ start - size] +/// ------------------------------------------------------------------------ +/// 1: 0E 0 32 33 - 16 113 33 [ 2048 - 262144] DOS FAT-16 +/// 2: 0C 16 113 34 - 65 69 4 [ 264192 - 784384] Win95 FAT32L +/// 3: 00 0 0 0 - 0 0 0 [ 0 - 0] unused +/// 4: 00 0 0 0 - 0 0 0 [ 0 - 0] unused +/// $ ls -l /Volumes/P-FAT16 +/// total 131080 +/// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT +/// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT +/// -rwxrwxrwx@ 1 jonathan staff 258 9 Dec 2018 README.TXT +/// drwxrwxrwx 1 jonathan staff 2048 9 Dec 2018 TEST +/// $ ls -l /Volumes/P-FAT16/TEST +/// total 8 +/// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT +/// $ ls -l /Volumes/P-FAT32 +/// total 131088 +/// -rwxrwxrwx 1 jonathan staff 67108864 9 Dec 2018 64MB.DAT +/// -rwxrwxrwx 1 jonathan staff 0 9 Dec 2018 EMPTY.DAT +/// -rwxrwxrwx@ 1 jonathan staff 258 21 Sep 09:48 README.TXT +/// drwxrwxrwx 1 jonathan staff 4096 9 Dec 2018 TEST +/// $ ls -l /Volumes/P-FAT32/TEST +/// total 8 +/// -rwxrwxrwx 1 jonathan staff 3500 9 Dec 2018 TEST.DAT +/// ``` +/// +/// It will unpack to a Vec that is 1048576 * 512 = 512 MiB in size. +pub static DISK_SOURCE: &[u8] = include_bytes!("../disk.img.gz"); + +#[derive(Debug)] +#[allow(dead_code)] +pub enum Error { + /// Failed to read the source image + Io(std::io::Error), + /// Failed to unzip the source image + Decode(flate2::DecompressError), + /// Asked for a block we don't have + OutOfBounds(BlockIdx), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: flate2::DecompressError) -> Self { + Self::Decode(value) + } +} + +/// Implements the block device traits for a chunk of bytes in RAM. +/// +/// The slice should be a multiple of `embedded_sdmmc::Block::LEN` bytes in +/// length. If it isn't the trailing data is discarded. +pub struct RamDisk { + contents: std::cell::RefCell, +} + +impl RamDisk { + fn new(contents: T) -> RamDisk { + RamDisk { + contents: std::cell::RefCell::new(contents), + } + } +} + +impl BlockDevice for RamDisk +where + T: AsMut<[u8]> + AsRef<[u8]>, +{ + type Error = Error; + + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let borrow = self.contents.borrow(); + let contents: &[u8] = borrow.as_ref(); + let mut block_idx = start_block_idx; + for block in blocks.iter_mut() { + let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; + let end_offset = start_offset + embedded_sdmmc::Block::LEN; + if end_offset > contents.len() { + return Err(Error::OutOfBounds(block_idx)); + } + block + .as_mut_slice() + .copy_from_slice(&contents[start_offset..end_offset]); + block_idx.0 += 1; + } + Ok(()) + } + + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + let mut borrow = self.contents.borrow_mut(); + let contents: &mut [u8] = borrow.as_mut(); + let mut block_idx = start_block_idx; + for block in blocks.iter() { + let start_offset = block_idx.0 as usize * embedded_sdmmc::Block::LEN; + let end_offset = start_offset + embedded_sdmmc::Block::LEN; + if end_offset > contents.len() { + return Err(Error::OutOfBounds(block_idx)); + } + contents[start_offset..end_offset].copy_from_slice(block.as_slice()); + block_idx.0 += 1; + } + Ok(()) + } + + fn num_blocks(&self) -> Result { + let borrow = self.contents.borrow(); + let contents: &[u8] = borrow.as_ref(); + let len_blocks = contents.len() / embedded_sdmmc::Block::LEN; + if len_blocks > u32::MAX as usize { + panic!("Test disk too large! Only 2**32 blocks allowed"); + } + Ok(BlockCount(len_blocks as u32)) + } +} + +/// Unpack the fixed, static, disk image. +fn unpack_disk(gzip_bytes: &[u8]) -> Result, Error> { + let disk_cursor = std::io::Cursor::new(gzip_bytes); + let mut gz_decoder = flate2::read::GzDecoder::new(disk_cursor); + let mut output = Vec::with_capacity(512 * 1024 * 1024); + gz_decoder.read_to_end(&mut output)?; + Ok(output) +} + +/// Turn some gzipped bytes into a block device, +pub fn make_block_device(gzip_bytes: &[u8]) -> Result>, Error> { + let data = unpack_disk(gzip_bytes)?; + Ok(RamDisk::new(data)) +} + +pub struct TestTimeSource { + fixed: embedded_sdmmc::Timestamp, +} + +impl embedded_sdmmc::TimeSource for TestTimeSource { + fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { + self.fixed + } +} + +/// Make a time source that gives a fixed time. +/// +/// It always claims to be 4 April 2003, at 13:30:05. +/// +/// This is an interesting time, because FAT will round it down to 13:30:04 due +/// to only have two-second resolution. Hey, Real Time Clocks were optional back +/// in 1981. +pub fn make_time_source() -> TestTimeSource { + TestTimeSource { + fixed: embedded_sdmmc::Timestamp { + year_since_1970: 33, + zero_indexed_month: 3, + zero_indexed_day: 3, + hours: 13, + minutes: 30, + seconds: 5, + }, + } +} + +/// Get the test time source time, as a string. +/// +/// We apply the FAT 2-second rounding here. +#[allow(unused)] +pub fn get_time_source_string() -> &'static str { + "2003-04-04 13:30:04" +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/volume.rs b/examples/ios/embedded-sdmmc/tests/volume.rs new file mode 100644 index 0000000..c2ea49b --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/volume.rs @@ -0,0 +1,115 @@ +//! Volume related tests + +mod utils; + +#[test] +fn open_all_volumes() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: embedded_sdmmc::VolumeManager< + utils::RamDisk>, + utils::TestTimeSource, + 4, + 4, + 2, + > = embedded_sdmmc::VolumeManager::new_with_limits(disk, time_source, 0x1000_0000); + + // Open Volume 0 + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + + // Fail to Open Volume 0 again + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(0)), + Err(embedded_sdmmc::Error::VolumeAlreadyOpen) + )); + + volume_mgr.close_volume(fat16_volume).expect("close fat16"); + + // Open Volume 1 + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + // Fail to Volume 1 again + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(1)), + Err(embedded_sdmmc::Error::VolumeAlreadyOpen) + )); + + // Open Volume 0 again + let fat16_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + + // Open any volume - too many volumes (0 and 1 are open) + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(0)), + Err(embedded_sdmmc::Error::TooManyOpenVolumes) + )); + + volume_mgr.close_volume(fat16_volume).expect("close fat16"); + volume_mgr.close_volume(fat32_volume).expect("close fat32"); + + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(2)), + Err(embedded_sdmmc::Error::FormatError(_e)) + )); + + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(3)), + Err(embedded_sdmmc::Error::FormatError(_e)) + )); + + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(9)), + Err(embedded_sdmmc::Error::NoSuchVolume) + )); + + let _root_dir = volume_mgr.open_root_dir(fat32_volume).expect("Open dir"); + + assert!(matches!( + volume_mgr.close_volume(fat32_volume), + Err(embedded_sdmmc::Error::VolumeStillInUse) + )); +} + +#[test] +fn close_volume_too_early() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) + .expect("open volume 0"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Dir open + assert!(matches!( + volume_mgr.close_volume(volume), + Err(embedded_sdmmc::Error::VolumeStillInUse) + )); + + let _test_file = volume_mgr + .open_file_in_dir(root_dir, "64MB.DAT", embedded_sdmmc::Mode::ReadOnly) + .expect("open test file"); + + volume_mgr.close_dir(root_dir).unwrap(); + + // File open, not dir open + assert!(matches!( + volume_mgr.close_volume(volume), + Err(embedded_sdmmc::Error::VolumeStillInUse) + )); +} + +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/embedded-sdmmc/tests/write_file.rs b/examples/ios/embedded-sdmmc/tests/write_file.rs new file mode 100644 index 0000000..9a5e4ab --- /dev/null +++ b/examples/ios/embedded-sdmmc/tests/write_file.rs @@ -0,0 +1,168 @@ +//! File opening related tests + +use embedded_sdmmc::{Mode, VolumeIdx, VolumeManager}; + +mod utils; + +#[test] +fn append_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Should be enough to cause a few more clusters to be allocated + let test_data = vec![0xCC; 1024 * 1024]; + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, 1024 * 1024); + + let offset = volume_mgr.file_offset(f).expect("offset"); + assert_eq!(offset, 1024 * 1024); + + // Now wind it back 1 byte; + volume_mgr.file_seek_from_current(f, -1).expect("Seeking"); + + let offset = volume_mgr.file_offset(f).expect("offset"); + assert_eq!(offset, (1024 * 1024) - 1); + + // Write another megabyte, making `2 MiB - 1` + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, (1024 * 1024 * 2) - 1); + + volume_mgr.close_file(f).expect("close dir"); + + // Now check the file length again + + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("Find entry"); + assert_eq!(entry.size, (1024 * 1024 * 2) - 1); + + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} + +#[test] +fn flush_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + // Write some data to the file + let test_data = vec![0xCC; 64]; + volume_mgr.write(f, &test_data).expect("file write"); + + // Check that the file length is zero in the directory entry, as we haven't + // flushed yet + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find entry"); + assert_eq!(entry.size, 0); + + volume_mgr.flush_file(f).expect("flush"); + + // Now check the file length again after flushing + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find entry"); + assert_eq!(entry.size, 64); + + // Flush more writes + volume_mgr.write(f, &test_data).expect("file write"); + volume_mgr.write(f, &test_data).expect("file write"); + volume_mgr.flush_file(f).expect("flush"); + + // Now check the file length again, again + let entry = volume_mgr + .find_directory_entry(root_dir, "README.TXT") + .expect("find entry"); + assert_eq!(entry.size, 64 * 3); +} + +#[test] +fn random_access_write_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + let test_data = vec![0xCC; 1024]; + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, 1024); + + for seek_offset in [100, 0] { + let mut expected_buffer = [0u8; 4]; + + // fetch some data at offset seek_offset + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("Seeking"); + volume_mgr.read(f, &mut expected_buffer).expect("read file"); + + // modify first byte + expected_buffer[0] ^= 0xff; + + // write only first byte, expecting the rest to not change + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("Seeking"); + volume_mgr + .write(f, &expected_buffer[0..1]) + .expect("file write"); + volume_mgr.flush_file(f).expect("file flush"); + + // read and verify + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("file seek"); + let mut read_buffer = [0xffu8, 0xff, 0xff, 0xff]; + volume_mgr.read(f, &mut read_buffer).expect("file read"); + assert_eq!( + read_buffer, expected_buffer, + "mismatch seek+write at offset {seek_offset} from start" + ); + } + + volume_mgr.close_file(f).expect("close file"); + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} +// **************************************************************************** +// +// End Of File +// +// **************************************************************************** diff --git a/examples/ios/src/main.rs b/examples/ios/src/main.rs index 377d13e..55f4ba0 100644 --- a/examples/ios/src/main.rs +++ b/examples/ios/src/main.rs @@ -11,6 +11,53 @@ use ogc_rs::{ print, println, }; extern crate alloc; +use embedded_sdmmc::{ + blockdevice::{Block, BlockCount, BlockIdx}, + BlockDevice, TimeSource, Timestamp, VolumeIdx, VolumeManager, +}; + +pub struct SdCardDevice(Device); +pub struct DummyTimesource; +impl BlockDevice for SdCardDevice { + type Error = (); + + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + if blocks.len() == 1 { + let mut block = [0u8; 512]; + let res = self + .0 + .read_sectors(core::slice::from_mut(&mut block), start_block_idx.0 as _) + .map_err(|_| ()); + + blocks[0].contents = block; + res + } else { + todo!("read is not currently implemented") + } + } + + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { + if blocks.len() == 1 { + let block = blocks[0].contents; + let res = self + .0 + .write_sectors(core::slice::from_ref(&block), start_block_idx.0 as _) + .map_err(|_| ()); + res + } else { + todo!("write is not currently implemented") + } + } + + fn num_blocks(&self) -> Result { + todo!("num_blocks is not currently implemented") + } +} +impl TimeSource for DummyTimesource { + fn get_timestamp(&self) -> Timestamp { + todo!() + } +} #[no_mangle] extern "C" fn main() { @@ -33,27 +80,41 @@ extern "C" fn main() { let (is_sdhc, mut device) = try_init_sd(); - let mut block = [0u8; 512]; - - //read_sectors(blocks: &mut [[0u8; 512]], offset: usize]) -> Result<(), ios::Error>; - device - .read_sectors(core::slice::from_mut(&mut block), 0) - .unwrap(); - - let bpb = BPB::from_bytes(block[0x00B..].try_into().unwrap()); - - let mut info_block = [0u8; 512]; - device - .read_sectors( - core::slice::from_mut(&mut info_block), - bpb.fs_info_sector as usize, - ) - .unwrap(); + // let mut block = [0u8; 512]; + // + // //read_sectors(blocks: &mut [[0u8; 512]], offset: usize]) -> Result<(), ios::Error>; + // device + // .read_sectors(core::slice::from_mut(&mut block), 0) + // .unwrap(); + // + // let bpb = BPB::from_bytes(block[0x00B..].try_into().unwrap()); + // + // let mut info_block = [0u8; 512]; + // device + // .read_sectors( + // core::slice::from_mut(&mut info_block), + // bpb.fs_info_sector as usize, + // ) + // .unwrap(); + // + // let info = FSInfo::from_bytes(&info_block); + // + // println!("{:?}", bpb); + // println!("{:?}", info); + // + let volmgr = VolumeManager::new(SdCardDevice(device), DummyTimesource); + let volume = unsafe { + volmgr + .open_special(0x0C, BlockIdx(0), BlockCount(67108864)) + .unwrap() + }; + let root_dir = volume.open_root_dir().unwrap(); - let info = FSInfo::from_bytes(&info_block); + let func = |entry: &embedded_sdmmc::DirEntry| { + println!("{:?}", entry.name); + }; - println!("{:?}", bpb); - println!("{:?}", info); + root_dir.iterate_dir(func).unwrap(); loop { core::hint::spin_loop(); @@ -83,7 +144,7 @@ impl SDCard { impl DeviceExt for Device { fn read_sectors( - &mut self, + &self, sectors: &mut [[u8; 512]], offset: usize, ) -> Result<(), ogc_rs::ios::Error> { @@ -120,14 +181,54 @@ impl DeviceExt for Device { Ok(()) } + + fn write_sectors( + &self, + sectors: &[[u8; 512]], + offset: usize, + ) -> Result<(), ogc_rs::ios::Error> { + let resp_rca = self.send_command(&Request::SEND_RCA)?; + let rca = resp_rca.rsp_field0; + + self.send_command(&Request::select(rca))?; + + const SDIO_CMD_WRITEMULTIBLOCK: u32 = 0x19; + const SDIO_CMD_TYPE_AC: u32 = 3; + const SDIO_RESPONSE_TYPE_R1: u32 = 1; + + // SDIO requires 32 byte alignment + // On hardware this probably needs to be in the IPC memory space :shrug: + let mut aligned_buffer = ogc_rs::utils::alloc_aligned_buffer(sectors.as_flattened()); + + self.send_command(&Request::new( + SDIO_CMD_WRITEMULTIBLOCK, + SDIO_CMD_TYPE_AC, + SDIO_RESPONSE_TYPE_R1, + offset as u32, + sectors.len() as u32, + 512, + aligned_buffer.as_mut_ptr(), + ))?; + + self.send_command(&Request::DE_SELECT)?; + + // sectors + // .as_flattened_mut() + // .copy_from_slice(&mut aligned_buffer); + // + Ok(()) + } } trait DeviceExt { fn read_sectors( - &mut self, + &self, sectors: &mut [[u8; 512]], offset: usize, ) -> Result<(), ogc_rs::ios::Error>; + + fn write_sectors(&self, sectors: &[[u8; 512]], offset: usize) + -> Result<(), ogc_rs::ios::Error>; } pub fn try_init_sd() -> (bool, Device) { @@ -161,6 +262,15 @@ pub fn try_init_sd() -> (bool, Device) { // let mut device = Device::open().unwrap(); + const SOFTWARE_RESET_REGISTER: u8 = 0x2F; + bitflags::bitflags! { + pub struct SoftwareResetRegister: u8 { + const RESET_ALL = 0b1; + const RESET_CMD = 0b10; + const RESET_DATA = 0b100; + } + } + // Reset let mut rca = device.reset().unwrap(); let status = device.get_status().unwrap(); @@ -177,57 +287,188 @@ pub fn try_init_sd() -> (bool, Device) { let mut device = Device::open().unwrap(); // Reset host controller using device + let reset = SoftwareResetRegister::RESET_ALL + | SoftwareResetRegister::RESET_CMD + | SoftwareResetRegister::RESET_DATA; + device - .write_to_host_controller_register(HOST_CONTROLLER_REG_SOFT_RESET, 1, 7) + .write_to_host_controller_register( + SOFTWARE_RESET_REGISTER, + core::mem::size_of::().try_into().unwrap(), + reset.bits().into(), + ) .unwrap(); - let software_reset = 7; - // Wait until properly reset - while software_reset - == device - .read_from_host_controller_register(HOST_CONTROLLER_REG_SOFT_RESET, 1) - .unwrap() + // Wait until all bits are 0 + while device + .read_from_host_controller_register(SOFTWARE_RESET_REGISTER, 1) + .unwrap() + != 0 { core::hint::spin_loop(); } - let _ = device.write_to_host_controller_register(0x34, 4, 0x13f00c3); - let _ = device.write_to_host_controller_register(0x38, 4, 0x13f00c3); + bitflags::bitflags! { + pub struct NormalInterruptStatusEnableRegister: u16 { + const COMMAND_COMPLETE_STATUS_ENABLE = 0b1; + const TRANSFER_COMPLETE_STATUS_ENABLE = 0b10; + const BLOCK_GAP_EVENT_STATUS_ENABLE = 0b100; + const DMA_INTERRUPT_STATUS_ENABLE = 0b1000; + const BUFFER_WRITE_READY_STATUS_ENABLE = 0b10000; + const BUFFER_READ_READY_STATUS_ENABLE = 0b100000; + const CARD_INSERTION_STATUS_ENABLE = 0b1000000; + const CARD_REMOVAL_STATUS_ENABLE = 0b10000000; + const CARD_INTERRUPT_STATUS_ENABLE = 0b100000000; + const INT_A_STATUS_ENABLE = 0b1000000000; + const INT_B_STATUS_ENABLE = 0b10000000000; + const INT_C_STATUS_ENABLE = 0b100000000000; + const RETUNING_EVENT_STATUS_ENABLE = 0b1000000000000; + const FX_EVENT_STATUS_ENABLE = 0b10000000000000; + } + } + + bitflags::bitflags! { + pub struct ErrorInterruptStatusEnableRegister: u16 { + const COMMAND_TIMEOUT_ERROR_STATUS_ENABLE = 0b1; + const COMMAND_CRC_ERROR_STATUS_ENABLE = 0b10; + const COMMAND_END_BIT_ERROR_STATUS_ENABLE = 0b100; + const COMMAND_INDEX_ERROR_STATUS_ENABLE = 0b1000; + const DATA_TIMEOUT_ERROR_STATUS_ENABLE = 0b10000; + const DATA_CRC_ERROR_STATUS_ENABLE = 0b100000; + const DATA_END_BIT_ERROR_STATUS_ENABLE = 0b1000000; + const CURRENT_LIMIT_ERROR_STATUS_ENABLE = 0b10000000; + const AUTO_CMD_ERROR_STATUS_ENABLE = 0b100000000; + const ADMA_ERROR_STATUS_ENABLE = 0b1000000000; + const TUNING_STATUS_ERROR_STATUS_ENABLE = 0b10000000000; + const REPSONSE_ERROR_STATUS_ENABLE = 0b100000000000; + } + } + + let normal_interrupt_status = + NormalInterruptStatusEnableRegister::COMMAND_COMPLETE_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::TRANSFER_COMPLETE_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::BLOCK_GAP_EVENT_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::DMA_INTERRUPT_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::BUFFER_WRITE_READY_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::BUFFER_READ_READY_STATUS_ENABLE + | NormalInterruptStatusEnableRegister::CARD_INTERRUPT_STATUS_ENABLE; + + let error_interrupt_status = + ErrorInterruptStatusEnableRegister::COMMAND_TIMEOUT_ERROR_STATUS_ENABLE + | ErrorInterruptStatusEnableRegister::COMMAND_CRC_ERROR_STATUS_ENABLE + | ErrorInterruptStatusEnableRegister::DATA_END_BIT_ERROR_STATUS_ENABLE + | ErrorInterruptStatusEnableRegister::CURRENT_LIMIT_ERROR_STATUS_ENABLE; + + let status: u32 = u32::from(normal_interrupt_status.bits()) << 16 + | u32::from(error_interrupt_status.bits()); + + const NORMAL_INTERRUPT_STATUS_REGISTER: u8 = 0x34; + //const ERROR_INTERRUPT_STATUS_REGISTER: u8 = 0x36; + + const NORMAL_INTERRUPT_SIGNAL_STATUS_REGISTER: u8 = 0x38; + //const ERROR_INTERRUPT_SIGNAL_STATUS_REGISTER: u8 = 0x3A; + + // This writes to `NORMAL_INTERRUPT_STATUS_REGISTER` and `ERROR_INTERRUPT_STATUS_REGISTER` as + // one call + let _ = + device.write_to_host_controller_register(NORMAL_INTERRUPT_STATUS_REGISTER, 4, status); + + // This writes to both `NORMAL_INTERRUPT_SIGNAL_STATUS_REGISTER` and + // `ERROR_INTERRUPT_STATUS_REGISTER` as one call + let _ = device.write_to_host_controller_register( + NORMAL_INTERRUPT_SIGNAL_STATUS_REGISTER, + 4, + status, + ); + + bitflags::bitflags! { + pub struct PowerControlRegister: u8 { + const SD_BUS_POWER_VDD1 = 0b1; + const SD_BUS_VOLTAGE_SELECT_18V = 0b1010; + const SD_BUS_VOLTAGE_SELECT_3V = 0b1100; + const SD_BUS_VOLTAGE_SELECT_33V = 0b1110; + } + } + + let select = PowerControlRegister::SD_BUS_VOLTAGE_SELECT_33V; // Set power device - .write_to_host_controller_register(HOST_CONTROLLER_REG_PWR_CTRL, 1, 14) + .write_to_host_controller_register( + HOST_CONTROLLER_REG_PWR_CTRL, + 1, + select.bits().into(), + ) .unwrap(); device - .write_to_host_controller_register(HOST_CONTROLLER_REG_PWR_CTRL, 1, 15) + .write_to_host_controller_register( + HOST_CONTROLLER_REG_PWR_CTRL, + 1, + (select | PowerControlRegister::SD_BUS_POWER_VDD1) + .bits() + .into(), + ) .unwrap(); + bitflags::bitflags! { + pub struct ClockControlRegister: u16 { + const INTERNAL_CLOCK_ENABLE = 0b1; + const INTERNAL_CLOCK_STABLE = 0b10; + const SD_CLOCK_ENABLE = 0b100; + const PLL_ENABLE = 0b1000; + const CLK_DIV_BY_2 = 0b100000000; + const CLK_DIV_BY_4 = 0b1000000000; + const CLK_DIV_BY_8 = 0b10000000000; + const CLK_DIV_BY_16 = 0b100000000000; + const CLK_DIV_BY_32 = 0b1000000000000; + const CLK_DIV_BY_64 = 0b10000000000000; + const CLK_DIV_BY_128 = 0b100000000000000; + const CLK_DIV_BY_256 = 0b1000000000000000; + } + } + // Clock device .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0) .unwrap(); + + let clock = + ClockControlRegister::INTERNAL_CLOCK_ENABLE | ClockControlRegister::CLK_DIV_BY_2; + device - .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0x101) + .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, clock.bits().into()) .unwrap(); - let clk_ctrl = 0x101; - - while clk_ctrl - == device - .read_from_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2) - .unwrap() + while device + .read_from_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2) + .unwrap() + & u32::from(ClockControlRegister::INTERNAL_CLOCK_STABLE.bits()) + != u32::from(ClockControlRegister::INTERNAL_CLOCK_STABLE.bits()) { core::hint::spin_loop(); } device - .write_to_host_controller_register(HOST_CONTROLLER_REG_CLK_CTRL, 2, 0x107) + .write_to_host_controller_register( + HOST_CONTROLLER_REG_CLK_CTRL, + 2, + (clock + | ClockControlRegister::INTERNAL_CLOCK_STABLE + | ClockControlRegister::SD_CLOCK_ENABLE) + .bits() + .into(), + ) .unwrap(); + //max timeout for stand sd cards not sdxc + //CLK x 2^27 + const TIMEOUT_CLOCK: u32 = 0b1110; + // Timeout device - .write_to_host_controller_register(HOST_CONTROLLER_REG_TIMEOUT_CTRL, 1, 14) + .write_to_host_controller_register(HOST_CONTROLLER_REG_TIMEOUT_CTRL, 1, TIMEOUT_CLOCK) .unwrap(); + let _ = device.send_command(&Request::GO_IDLE).unwrap(); let resp = device.send_command(&Request::SEND_IF_COND).unwrap(); diff --git a/src/ios/sdio.rs b/src/ios/sdio.rs index 07f1bdc..149b8d8 100644 --- a/src/ios/sdio.rs +++ b/src/ios/sdio.rs @@ -378,7 +378,7 @@ mod dev { } /// Send SDIO command - pub fn send_command(&mut self, request: &Request) -> Result { + pub fn send_command(&self, request: &Request) -> Result { let mut in_buf: [u8; _] = [0u8; core::mem::size_of::()]; in_buf[0..4].copy_from_slice(&request.command.to_be_bytes()); in_buf[4..8].copy_from_slice(&request.command_type.to_be_bytes()); @@ -415,7 +415,6 @@ mod dev { &mut out_buf, )?; } - let resp = Response { rsp_field0: u32::from_be_bytes( out_buf[0..4].try_into().map_err(|_| ios::Error::Invalid)?, From 76d25789ccfa400b3b2b0b534e0fb694e3ab8033 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Tue, 7 Oct 2025 21:36:27 -0500 Subject: [PATCH 7/9] :bug: fix(CI): Re add proper github CI --- .github/workflows/workflow.yml | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/workflow.yml diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..6e0b647 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,58 @@ +# Rust CI + +on: [push, pull_request] + +name: Rust CI + +jobs: + check: + name: Check + runs-on: ubuntu-latest + container: + image: "devkitpro/devkitppc" + steps: + - name: Install required packages + run: | + sudo apt-get update + sudo apt-get install -y gcc libc6-dev nodejs clang + + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: rust-src + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + lints: + name: Lints + runs-on: ubuntu-latest + container: + image: "devkitpro/devkitppc" + steps: + - name: Install required packages + run: | + sudo apt-get update + sudo apt-get install -y gcc libc6-dev nodejs clang + + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: clippy, rust-src + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + From e8d25fe22ffe1ae95326d502427bcb794cdbbc49 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Tue, 7 Oct 2025 22:22:07 -0500 Subject: [PATCH 8/9] :memo: docs(ios/sdio): Add `# Errors` docs. Make response an opaque buffer. --- src/ios/sdio.rs | 88 +++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/src/ios/sdio.rs b/src/ios/sdio.rs index 149b8d8..a410e2c 100644 --- a/src/ios/sdio.rs +++ b/src/ios/sdio.rs @@ -1,5 +1,3 @@ -use crate::ios; - /// SDIO supported Ioctls pub enum Ioctl { /// Write to a SD host controller register @@ -84,11 +82,11 @@ impl Request { const SDIO_RESPONSE_TYPE_R1B: u32 = 2; const SDIO_RESPONSE_TYPE_R3: u32 = 4; - /// SDIO_CMD_GO_IDLE + /// `SDIO_CMD_GO_IDLE` pub const GO_IDLE: Request = Request::new(Self::SDIO_CMD_GO_IDLE, 0, 0, 0, 0, 0, core::ptr::null_mut()); - /// SDIO_CMD_SEND_IF_COND + /// `SDIO_CMD_SEND_IF_COND` pub const SEND_IF_COND: Request = Request::new( Self::SDIO_CMD_SEND_IF_COND, 0, @@ -99,7 +97,7 @@ impl Request { core::ptr::null_mut(), ); - /// SDIO_CMD_APPCMD + /// `SDIO_CMD_APPCMD` pub const APP_CMD: Request = Request::new( Self::SDIO_CMD_APPCMD, Self::SDIO_CMD_TYPE_AC, @@ -110,18 +108,18 @@ impl Request { core::ptr::null_mut(), ); - /// SDIO_CMD_APPCMD_SEND_OP_COND + /// `SDIO_CMD_APPCMD_SEND_OP_COND` pub const SEND_OP_COND: Request = Request::new( Self::SDIO_APPCMD_SENDOPCOND, 0, Self::SDIO_RESPONSE_TYPE_R3, - 0x40300000, + 0x4030_0000, 0, 0, core::ptr::null_mut(), ); - /// SDIO_CMD_DESELECT + /// `SDIO_CMD_DESELECT` pub const DE_SELECT: Request = Request::new( Self::SDIO_CMD_SELECT, Self::SDIO_CMD_TYPE_AC, @@ -132,7 +130,7 @@ impl Request { core::ptr::null_mut(), ); - /// SDIO_CMD_SENDRCA + /// `SDIO_CMD_SENDRCA` pub const SEND_RCA: Request = Request::new( Self::SDIO_CMD_SEND_RCA, 0, @@ -143,7 +141,8 @@ impl Request { core::ptr::null_mut(), ); - /// SDIO_CMD_APPCMD_SET_BUS_WIDTH + /// `SDIO_CMD_APPCMD_SET_BUS_WIDTH` + #[must_use] pub const fn set_bus_width(width: u32) -> Request { Request::new( Self::SDIO_APPCMD_SET_BUS_WIDTH, @@ -156,7 +155,8 @@ impl Request { ) } - /// SDIO_CMD_SEND_CID + /// `SDIO_CMD_SEND_CID` + #[must_use] pub const fn send_cid(rca: u32) -> Request { Request::new( Self::SDIO_CMD_SEND_CID, @@ -169,7 +169,8 @@ impl Request { ) } - /// SDIO_CMD_SET_BLOCK_LENGTH + /// `SDIO_CMD_SET_BLOCK_LENGTH` + #[must_use] pub const fn set_block_length(length: u32) -> Request { Request::new( Self::SDIO_CMD_SET_BLOCK_LENGTH, @@ -181,6 +182,9 @@ impl Request { core::ptr::null_mut(), ) } + + /// Select the SD Card with `rca` + #[must_use] pub const fn select(rca: u32) -> Request { Request::new( Self::SDIO_CMD_SELECT, @@ -193,6 +197,8 @@ impl Request { ) } + /// Build a APPCMD with RCA as the arg + #[must_use] pub const fn appcmd_with_rca(rca: u32) -> Request { Request::new( Self::SDIO_CMD_APPCMD, @@ -206,6 +212,7 @@ impl Request { } /// Create a new `Request` for the SDIO device + #[must_use] pub const fn new( command: u32, command_type: u32, @@ -230,14 +237,23 @@ impl Request { } } -//#[repr(C, align(32))] /// SDIO response -#[derive(Debug)] pub struct Response { - pub rsp_field0: u32, - pub rsp_field1: u32, - pub rsp_field2: u32, - pub acmd12_response: u32, + buf: [u8; 16], +} + +impl Response { + /// Create a response from a byte buffer + #[must_use] + pub fn from_bytes(bytes: &[u8; 16]) -> Self { + Self { buf: *bytes } + } + + /// Get bytes of response + #[must_use] + pub fn bytes(&self) -> &[u8; 16] { + &self.buf + } } pub use dev::Device; @@ -304,6 +320,8 @@ mod dev { impl Device { /// Try to open `/dev/sdio/slot0` + /// # Errors + /// See [`ios::Error`] pub fn open() -> Result { let sdio = ios::open(c"/dev/sdio/slot0", ios::Mode::Read)?; let fd = unsafe { OwnedFd::from_raw_fd(sdio.0) }.ok_or(ios::Error::Invalid)?; @@ -311,6 +329,8 @@ mod dev { } /// Write to a SD host controller register + /// # Errors + /// See [`ios::Error`] pub fn write_to_host_controller_register( &mut self, register: u8, @@ -333,6 +353,8 @@ mod dev { } /// Read from a SD host controller register + /// # Errors + /// See [`ios::Error`] pub fn read_from_host_controller_register( &mut self, register: u8, @@ -353,6 +375,8 @@ mod dev { Ok(u32::from_be_bytes(value)) } /// Reset SD card + /// # Errors + /// See [`ios::Error`] pub fn reset(&mut self) -> Result { let mut buffer = [0u8; 4]; ios::ioctl( @@ -366,6 +390,8 @@ mod dev { } /// Enable SD card clock + /// # Errors + /// See [`ios::Error`] pub fn enable_clock(&mut self, enable: bool) -> Result<(), ios::Error> { ios::ioctl( self.fd.as_file_descriptor(), @@ -378,6 +404,8 @@ mod dev { } /// Send SDIO command + /// # Errors + /// See [`ios::Error`] pub fn send_command(&self, request: &Request) -> Result { let mut in_buf: [u8; _] = [0u8; core::mem::size_of::()]; in_buf[0..4].copy_from_slice(&request.command.to_be_bytes()); @@ -390,7 +418,7 @@ mod dev { in_buf[28..32].copy_from_slice(&request.is_dma.to_be_bytes()); //in_buf[32..36].copy_from_slice(&request.pad0.to_be_bytes()); - let mut out_buf: [u8; _] = [0u8; core::mem::size_of::()]; + let mut out_buf: [u8; 16] = [0u8; 16]; if !request.dma_addr.is_null() && request.is_dma != 0 { let dma_bytes = unsafe { @@ -415,27 +443,13 @@ mod dev { &mut out_buf, )?; } - let resp = Response { - rsp_field0: u32::from_be_bytes( - out_buf[0..4].try_into().map_err(|_| ios::Error::Invalid)?, - ), - rsp_field1: u32::from_be_bytes( - out_buf[4..8].try_into().map_err(|_| ios::Error::Invalid)?, - ), - rsp_field2: u32::from_be_bytes( - out_buf[8..12].try_into().map_err(|_| ios::Error::Invalid)?, - ), - acmd12_response: u32::from_be_bytes( - out_buf[12..16] - .try_into() - .map_err(|_| ios::Error::Invalid)?, - ), - }; - + let resp = Response::from_bytes(&out_buf); Ok(resp) } /// Read SD card status returning the relative card address + /// # Errors + /// See [`ios::Error`] pub fn get_status(&mut self) -> Result { let mut buffer = [0u8; 4]; ios::ioctl( @@ -451,6 +465,8 @@ mod dev { } /// Get operation conditions register + /// # Errors + /// See [`ios::Error`] pub fn get_operating_conditions_register(&mut self) -> Result { let mut buffer = [0u8; 4]; ios::ioctl( From fc9b129b308b4b2fe6ed946352d163f045a76400 Mon Sep 17 00:00:00 2001 From: ProfElements Date: Tue, 7 Oct 2025 22:22:35 -0500 Subject: [PATCH 9/9] :bug: fix(examples/ios): Fix example to build with `Response` opaque buffer --- examples/ios/src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/ios/src/main.rs b/examples/ios/src/main.rs index 55f4ba0..06dc6d0 100644 --- a/examples/ios/src/main.rs +++ b/examples/ios/src/main.rs @@ -132,7 +132,7 @@ impl SDCard { let (is_sdhc, mut device) = try_init_sd(); let resp_rca = device.send_command(&Request::SEND_RCA).ok()?; - let rca = resp_rca.rsp_field0; + let rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); Some(Self { rca, @@ -149,8 +149,7 @@ impl DeviceExt for Device { offset: usize, ) -> Result<(), ogc_rs::ios::Error> { let resp_rca = self.send_command(&Request::SEND_RCA)?; - let rca = resp_rca.rsp_field0; - + let rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); self.send_command(&Request::select(rca))?; const SDIO_CMD_READMULTIBLOCK: u32 = 0x12; @@ -188,8 +187,7 @@ impl DeviceExt for Device { offset: usize, ) -> Result<(), ogc_rs::ios::Error> { let resp_rca = self.send_command(&Request::SEND_RCA)?; - let rca = resp_rca.rsp_field0; - + let rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); self.send_command(&Request::select(rca))?; const SDIO_CMD_WRITEMULTIBLOCK: u32 = 0x19; @@ -472,15 +470,17 @@ pub fn try_init_sd() -> (bool, Device) { let _ = device.send_command(&Request::GO_IDLE).unwrap(); let resp = device.send_command(&Request::SEND_IF_COND).unwrap(); - if resp.rsp_field0 & 0xFF != 0xAA { - println!("Response from IF_COND: {}", resp.rsp_field0); + let resp_bytes = u32::from_be_bytes(resp.bytes()[0..4].try_into().unwrap()); + if resp_bytes & 0xFF != 0xAA { + println!("Response from IF_COND: {}", resp_bytes); } let is_sdhc = loop { let _ = device.send_command(&Request::APP_CMD).unwrap(); let resp = device.send_command(&Request::SEND_OP_COND).unwrap(); - if resp.rsp_field0 & 1 << 31 == 1 << 31 { - break resp.rsp_field0 & 1 << 30 == 1 << 30; + let resp_bytes = u32::from_be_bytes(resp.bytes()[0..4].try_into().unwrap()); + if resp_bytes & 1 << 31 == 1 << 31 { + break resp_bytes & 1 << 30 == 1 << 30; } }; @@ -489,7 +489,7 @@ pub fn try_init_sd() -> (bool, Device) { let resp_rca = device.send_command(&Request::SEND_RCA).unwrap(); println!("{:?}", resp_rca); - rca = resp_rca.rsp_field0; + rca = u32::from_be_bytes(resp_rca.bytes()[0..4].try_into().unwrap()); let mut host_ctrl = device .read_from_host_controller_register(HOST_CONTROLLER_REG_HOST_CTRL, 1)