diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8cf9f04..c9a57a0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -56,6 +56,51 @@ jobs: run: | cargo check + sanitize-c: + name: sanitize Rust wrappers around C code + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + + - name: Run ASan + working-directory: ./common/littlefs + env: + RUSTFLAGS: -Z sanitizer=address + run: | + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-close + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget-2 + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-close + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-forget + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-close --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget-2 --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-close --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-forget --release + + - name: Run MSan + working-directory: ./common/littlefs + env: + RUSTFLAGS: -Z sanitizer=memory + run: | + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-close + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget-2 + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-close + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-forget + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-close --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example dir-forget-2 --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-close --release + cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-forget --release + # NOTE the `common` directory is currently empty so this is a no-op # common-test: # name: Run tests on the host diff --git a/common/Cargo.lock b/common/Cargo.lock index b8764fd..53abfda 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -1,18 +1,497 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +dependencies = [ + "memchr 2.3.3", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "as-slice" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37dfb65bc03b2bc85ee827004f14a6817e04160e3b1a28931986a666a9290e70" +dependencies = [ + "generic-array 0.12.3", + "generic-array 0.13.2", + "stable_deref_trait", +] + +[[package]] +name = "ascii" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bindgen" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb26d6a69a335b8cb0e7c7e9775cd5666611dc50a37177c3f2cedcfc040e8c8" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "c-stubs" version = "0.0.0" dependencies = [ - "cty", + "cty 0.2.1", +] + +[[package]] +name = "cc" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "clang-sys" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] name = "consts" version = "0.0.0" +[[package]] +name = "cortex-a" +version = "0.0.0" + +[[package]] +name = "cstr_core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7829882406e7b36cff95319f674b72fc51dd3b0e6968f33db8f6a26903c1e128" +dependencies = [ + "cty 0.1.5", + "memchr 1.0.2", +] + +[[package]] +name = "cty" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e1d41c471573612df00397113557693b5bf5909666a8acb253930612b93312" + [[package]] name = "cty" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7313c0d620d0cb4dbd9d019e461a4beb501071ff46ec0ab933efb4daa76d73e3" + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" +dependencies = [ + "typenum", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffa511365b12346c5fbe759d82f80d3aa70d9f1ba01955594f84a1a6bbab985" +dependencies = [ + "as-slice", + "generic-array 0.13.2", + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi", +] + +[[package]] +name = "littlefs" +version = "0.0.0" +dependencies = [ + "ascii", + "bitflags", + "c-stubs", + "cortex-a", + "cstr_core", + "cty 0.2.1", + "generic-array 0.13.2", + "heapless", + "littlefs2-sys", +] + +[[package]] +name = "littlefs2-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c47073d0d700b19b987e44159383a44c6b99665153c368af0e64e0a66d1954" +dependencies = [ + "bindgen", + "cc", + "cty 0.2.1", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "nom" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +dependencies = [ + "memchr 2.3.3", + "version_check", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "proc-macro2" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" +dependencies = [ + "aho-corasick", + "memchr 2.3.3", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "stable_deref_trait" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/common/Cargo.toml b/common/Cargo.toml index 19e0b22..f9efe92 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,2 +1,21 @@ [workspace] -members = ["consts", "c-stubs"] \ No newline at end of file +members = ["consts", "c-stubs", "littlefs"] + +# `bindgen` and other build dependencies take very long to build when optimized +# this disables optimizations for them significantly reducing the time it takes +# to build the whole dependency graph from scratch +[profile.dev.build-override] +codegen-units = 16 +debug = false +debug-assertions = false +incremental = true +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 16 +debug = false +debug-assertions = false +incremental = true +opt-level = 0 +overflow-checks = false diff --git a/common/c-stubs/src/lib.rs b/common/c-stubs/src/lib.rs index d68c1b5..560f142 100644 --- a/common/c-stubs/src/lib.rs +++ b/common/c-stubs/src/lib.rs @@ -15,11 +15,13 @@ extern "C" { /// - `src` must be a valid C string (null terminated) /// - `dst` must be large enough to hold `src` #[no_mangle] -pub unsafe fn strcpy(dst: *mut c_char, src: *const c_char) -> *mut c_char { +unsafe fn strcpy(dst: *mut c_char, src: *const c_char) -> *mut c_char { memcpy(dst as *mut c_void, src as *const c_void, strlen(src)) as *mut c_char } -unsafe fn strlen(mut s: *const c_char) -> size_t { +/// # Safety +/// `s` must point to valid memory; `s` will be treated as a null terminated string +pub unsafe fn strlen(mut s: *const c_char) -> size_t { let mut n = 0; while *s != 0 { s = s.add(1); diff --git a/common/littlefs/Cargo.toml b/common/littlefs/Cargo.toml new file mode 100644 index 0000000..2ce2c6f --- /dev/null +++ b/common/littlefs/Cargo.toml @@ -0,0 +1,43 @@ +[package] +authors = ["iqlusion"] +edition = "2018" +license = "Apache-2.0 OR MIT" +name = "littlefs" +version = "0.0.0" + +[dependencies] +bitflags = "1.2.1" +c-stubs = { path = "../c-stubs" } +cortex-a = { path = "../../firmware/cortex-a", optional = true } +cty = "0.2.1" +generic-array = "0.13.2" +heapless = "0.5.4" +ll = { version = "0.1.4", package = "littlefs2-sys" } + +[dependencies.ascii] +default-features = false +version = "1.0.0" + +[dependencies.cstr_core] +default-features = false +# HACK TL;DR :sadface: we are using an older version here to avoid +# rust-lang/cargo#4361 which has been fixed in nightly but lives behind a +# unstable flag as of Rust 1.42.0 +# +# longer explanation: this crate depends on bindgen (build dependency) and +# bindgen depends on `memchr` "2" with default features enabled, which include a +# "std" feature that makes the crate depend on `std`. `cstr_core` ">0.1.1" and +# "0.2" also depend on "memchr" but with default features disabled. As this +# crate depends on both it ends up enabling the "std" of the `memchr` crate +# (that's the bug because that shouldn't happen). +# +# To avoid `bindgen` enabling the "std" dependency of `memchr` "2" we use an +# older version of `cstr_core` that depends on version of "1" of `memchr`. This +# way the bug won't enable the "std" of `memchr` "1" (because `memchr` "1" and +# `memchr` "2" are considered different crates) +version = "=0.1.0" + +[features] +# makes `impl Filesystem` interrupt safe on ARM Cortex-A +sync-cortex-a = ["cortex-a"] +unsafe-x86 = [] \ No newline at end of file diff --git a/common/littlefs/examples/dir-close.rs b/common/littlefs/examples/dir-close.rs new file mode 100644 index 0000000..c8183f9 --- /dev/null +++ b/common/littlefs/examples/dir-close.rs @@ -0,0 +1,41 @@ +//! [Sanitizer test] `lfs_dir_t` is properly closed on `ReadDir::drop` + +// Based on https://github.com/nickray/littlefs2/issues/3 (STR1) + +use core::convert::TryInto; + +use littlefs::{ + filesystem, + fs::{self, File}, + storage, +}; + +// RAM `Storage` +storage!(S, block_count = 16); + +// Filesystem on top of storage `S` +filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); + +fn main() { + let s = S::claim().unwrap(); + let f = F::mount(s, true).unwrap(); + + foo(f); + bar(f); + + println!("OK"); +} + +#[inline(never)] +fn foo(f: F) { + // `ReaDir` will close `lfs_dir_t` on drop + drop(fs::read_dir(f, b".\0".try_into().unwrap()).unwrap()); +} + +// linked list operations performed by the `File` API will not corrupt memory +#[inline(never)] +fn bar(f: F) { + let mut file = File::create(f, b"a.txt\0".try_into().unwrap()).unwrap(); + file.write(b"Hello!").unwrap(); + file.close().unwrap(); +} diff --git a/common/littlefs/examples/dir-forget-2.rs b/common/littlefs/examples/dir-forget-2.rs new file mode 100644 index 0000000..dfdb33c --- /dev/null +++ b/common/littlefs/examples/dir-forget-2.rs @@ -0,0 +1,34 @@ +//! [Sanitizer test] Not dropping `ReadDir` should not corrupt memory + +// Based on https://github.com/nickray/littlefs2/issues/3 (STR2) + +use core::{convert::TryInto, mem}; + +use littlefs::{ + filesystem, + fs::{self, File}, + storage, +}; + +// RAM `Storage` +storage!(S, block_count = 16); + +// Filesystem on top of storage `S` +filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); + +fn main() { + let s = S::claim().unwrap(); + let f = F::mount(s, true).unwrap(); + + let filename = b"a.txt\0".try_into().unwrap(); + + let mut file = File::create(f, filename).unwrap(); + file.write(b"Hello!").unwrap(); + file.close().unwrap(); + + mem::forget(fs::read_dir(f, b".\0".try_into().unwrap()).unwrap()); + + fs::remove(f, filename).unwrap(); + + println!("OK"); +} diff --git a/common/littlefs/examples/dir-forget.rs b/common/littlefs/examples/dir-forget.rs new file mode 100644 index 0000000..30b71a7 --- /dev/null +++ b/common/littlefs/examples/dir-forget.rs @@ -0,0 +1,42 @@ +//! [Sanitizer test] Not dropping `ReadDir` should not corrupt memory + +// Based on https://github.com/nickray/littlefs2/issues/3 (STR1) + +use core::{convert::TryInto, mem}; + +use littlefs::{ + filesystem, + fs::{self, File}, + storage, +}; + +// RAM `Storage` +storage!(S, block_count = 16); + +// Filesystem on top of storage `S` +filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); + +fn main() { + let s = S::claim().unwrap(); + let f = F::mount(s, true).unwrap(); + + foo(f); + bar(f); + + println!("OK"); +} + +#[inline(never)] +fn foo(f: F) { + // `lfs_dir_t` will not be closed but its allocation will be leaked (never deallocated) + // this `lfs_dir_t` will remain in the linked list forever + mem::forget(fs::read_dir(f, b".\0".try_into().unwrap()).unwrap()); +} + +// linked list operations performed by the `File` API will not corrupt memory +#[inline(never)] +fn bar(f: F) { + let mut file = File::create(f, b"a.txt\0".try_into().unwrap()).unwrap(); + file.write(b"Hello!").unwrap(); + file.close().unwrap(); +} diff --git a/common/littlefs/examples/file-close.rs b/common/littlefs/examples/file-close.rs new file mode 100644 index 0000000..30c1e0f --- /dev/null +++ b/common/littlefs/examples/file-close.rs @@ -0,0 +1,37 @@ +//! [Sanitizer test] `lfs_file_t` is properly closed on `File::drop` + +// Based on https://github.com/nickray/littlefs2/issues/5 + +use core::convert::TryInto; + +use littlefs::{filesystem, fs::File, storage}; + +// RAM `Storage` +storage!(S, block_count = 16); + +// Filesystem on top of storage `S` +filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); + +fn main() { + let s = S::claim().unwrap(); + let f = F::mount(s, true).unwrap(); + + foo(f); + bar(f); + + println!("OK"); +} + +#[inline(never)] +fn foo(f: F) { + // `File::drop` will close `lfs_file_t` + drop(File::create(f, b"a.txt\0".try_into().unwrap()).unwrap()); +} + +// linked list operations performed by the `File` API will not corrupt memory +#[inline(never)] +fn bar(f: F) { + let mut f = File::create(f, b"b.txt\0".try_into().unwrap()).unwrap(); + f.write(b"Hello!").unwrap(); + f.close().unwrap(); +} diff --git a/common/littlefs/examples/file-forget.rs b/common/littlefs/examples/file-forget.rs new file mode 100644 index 0000000..a848105 --- /dev/null +++ b/common/littlefs/examples/file-forget.rs @@ -0,0 +1,38 @@ +//! [Sanitizer test] `lfs_file_t` is properly closed on `File::drop` + +// Based on https://github.com/nickray/littlefs2/issues/5 + +use core::{convert::TryInto, mem}; + +use littlefs::{filesystem, fs::File, storage}; + +// RAM `Storage` +storage!(S, block_count = 16); + +// Filesystem on top of storage `S` +filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); + +fn main() { + let s = S::claim().unwrap(); + let f = F::mount(s, true).unwrap(); + + foo(f); + bar(f); + + println!("OK"); +} + +#[inline(never)] +fn foo(f: F) { + // `lfs_file_t` will not be closed but its allocation will be leaked (never deallocated) + // this `lfs_file_t` will remain in the linked list forever + mem::forget(File::create(f, b"a.txt\0".try_into().unwrap()).unwrap()); +} + +// linked list operations performed by the `File` API will not corrupt memory +#[inline(never)] +fn bar(f: F) { + let mut f = File::create(f, b"b.txt\0".try_into().unwrap()).unwrap(); + f.write(b"Hello!").unwrap(); + f.close().unwrap(); +} diff --git a/common/littlefs/src/consts.rs b/common/littlefs/src/consts.rs new file mode 100644 index 0000000..21ac020 --- /dev/null +++ b/common/littlefs/src/consts.rs @@ -0,0 +1,18 @@ +// **WARNING** changing these values can cause undefined behavior + +// block must be multiple of cache +// cache must be multiple of read +// cache must be multiple of write +pub const BLOCK_SIZE: u32 = CACHE_SIZE; +pub const CACHE_SIZE: u32 = READ_SIZE; +pub const READ_SIZE: u32 = WRITE_SIZE; +pub const WRITE_SIZE: u32 = 512; + +pub const ATTRBYTES_MAX: u32 = 1022; +pub const FILEBYTES_MAX: u32 = 2_147_483_647; +pub const LOOKAHEADWORDS_SIZE: u32 = 16; +pub const BLOCK_CYCLES: i32 = -1; + +// NOTE it seems these HAVE to be 256 because of that's the size of the `lfs_info.name` +pub const FILENAME_MAX_PLUS_ONE: u32 = 255 + 1; +pub const PATH_MAX_PLUS_ONE: usize = 255 + 1; diff --git a/common/littlefs/src/fs.rs b/common/littlefs/src/fs.rs new file mode 100644 index 0000000..d5a7594 --- /dev/null +++ b/common/littlefs/src/fs.rs @@ -0,0 +1,1000 @@ +//! Filesystem operations + +use core::{ + cell::{Cell, RefCell}, + convert::TryInto, + fmt, + marker::PhantomData, + mem::{self, ManuallyDrop, MaybeUninit}, + slice, +}; + +use bitflags::bitflags; +pub use heapless::consts; +use heapless::{ + pool::singleton::{Box, Pool}, + ArrayLength, +}; + +use crate::{ + io, + mem::{Arena, D, F}, + path::{Path, PathBuf}, + storage::Storage, +}; + +/// A filesystem +/// +/// *NOTE* do not implement this trait yourself; use the `filesystem!` macro +/// +/// # Safety +/// - Implementer must be a singleton +pub unsafe trait Filesystem: Copy { + /// Storage device this filesystem commits changes to + type Storage: Storage + 'static; + + #[doc(hidden)] + fn lock(self, f: impl FnOnce(&Inner) -> T) -> T; + + /// Mounts the filesystem + /// + /// This consume the `Storage` device (singleton) and thus can only be called at most once + /// + /// The `format` flag indicates whether to format the filesystem before mounting it + fn mount(storage: Self::Storage, format: bool) -> io::Result; +} + +/// Declares a filesystem named `$fs` that uses `$Storage` as the storage device +/// +/// `$Storage` must implement the `Storage` trait +/// +/// This macro will (safely) implement the unsafe `Filesystem` trait +/// +/// `$fs` will become a share-able handle to a `Filesystem` singleton. Think of `$fs` as a +/// `&'static _` reference. +#[macro_export] +macro_rules! filesystem { + ( + $(#[$attr:meta])* + $fs:ident, + Storage=$storage:ty, + max_open_files=$max_open_files:expr, + read_dir_depth=$read_dir_depth:expr + ) => { + $(#[$attr])* + #[derive(Clone, Copy)] + pub struct $fs { + _inner: $crate::Private, + } + + impl $fs { + fn ptr() -> *mut $crate::fs::Inner<$storage> { + use core::{cell::RefCell, mem::MaybeUninit}; + + use $crate::fs::Inner; + + static mut INNER: MaybeUninit> = MaybeUninit::uninit(); + + unsafe { INNER.as_mut_ptr() } + } + + /// See `Filesystem.mount` + pub fn mount(storage: $storage, format: bool) -> $crate::io::Result { + use core::{ + cell::RefCell, + mem::MaybeUninit, + sync::atomic::{AtomicBool, Ordering}, + }; + + use $crate::{ + fs::{Buffers, Config, Inner, State}, + Private, + }; + + // NOTE(unsafe) this section is executed at most once because `storage` is an owned + // singleton + static mut BUFFERS: Buffers = Buffers::uninit(); + // NOTE cannot (partially) construct `Config` in `const` context + static mut CONFIG: MaybeUninit> = MaybeUninit::uninit(); + static mut STATE: State = State::uninit(); + static mut STORAGE: MaybeUninit<$storage> = MaybeUninit::uninit(); + + unsafe { + STORAGE.as_mut_ptr().write(storage); + CONFIG + .as_mut_ptr() + .write(Config::new(&mut BUFFERS, &*STORAGE.as_ptr())); + let mut inner = Inner::new(&mut *CONFIG.as_mut_ptr(), &mut STATE); + if format { + inner.format()?; + } + inner.mount()?; + Self::ptr().write(inner); + + // add memory to the pools before the filesystem is used + use $crate::mem::Pool as _; // grow_exact method + + static mut MD: MaybeUninit<[$crate::mem::DNode; $read_dir_depth]> = + MaybeUninit::uninit(); + $crate::mem::D::grow_exact(&mut MD); + + static mut MF: MaybeUninit<[$crate::mem::FNode; $max_open_files]> = + MaybeUninit::uninit(); + $crate::mem::F::grow_exact(&mut MF); + + Ok($fs { + _inner: Private::new(), + }) + } + } + } + + unsafe impl $crate::fs::Filesystem for $fs { + type Storage = $storage; + + fn lock(self, f: impl FnOnce(&$crate::fs::Inner<$storage>) -> T) -> T { + $crate::lock(|| { + f(unsafe { &*Self::ptr() }) + }) + } + + fn mount(storage: $storage, format: bool) -> $crate::io::Result { + Self::mount(storage, format) + } + } + }; +} + +/// Returns the number of available blocks +/// +/// *NOTE* this is an approximation of free space on the storage device +pub fn available_blocks(fs: FS) -> io::Result +where + FS: Filesystem, +{ + Ok(FS::Storage::BLOCK_COUNT - used_blocks(fs)?) +} + +fn used_blocks(fs: impl Filesystem) -> io::Result { + let ret = fs.lock(|inner| { + let mut state = inner.state.borrow_mut(); + // XXX does this (FFI call) really need a `*mut` pointer? + unsafe { ll::lfs_fs_size(state.as_mut_ptr()) } + }); + io::check_ret(ret) +} + +/// Creates a new, empty directory at the provided path +pub fn create_dir(fs: impl Filesystem, path: &Path) -> io::Result<()> { + fs.lock(|inner| { + if inner.transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } + + let mut state = inner.state.borrow_mut(); + Ok(unsafe { ll::lfs_mkdir(state.as_mut_ptr(), path.as_ptr()) }) + }) + .and_then(|ret| io::check_ret(ret).map(drop)) +} + +/// Given a path, query the file system to get information about a file, directory, etc. +pub fn metadata(fs: impl Filesystem, path: &Path) -> io::Result { + let mut info = MaybeUninit::uninit(); + let ret = fs.lock(|inner| { + let mut state = inner.state.borrow_mut(); + unsafe { ll::lfs_stat(state.as_mut_ptr(), path.as_ptr(), info.as_mut_ptr()) } + }); + io::check_ret(ret)?; + Ok(Metadata::from_info(unsafe { info.assume_init() })) +} + +/// Returns an iterator over the entries within a directory. +pub fn read_dir(fs: FS, path: &Path) -> io::Result> +where + FS: Filesystem, +{ + let mut dir = ManuallyDrop::new( + D::alloc() + .ok_or(io::Error::NoMemory)? + // FIXME(upstream) it should not be necessary to zero the allocation + .init(unsafe { mem::zeroed() }), + ); + let ret = fs.lock(|inner| unsafe { + let mut state = inner.state.borrow_mut(); + ll::lfs_dir_open(state.as_mut_ptr(), &mut **dir, path.as_ptr()) + }); + io::check_ret(ret)?; + Ok(ReadDir { dir, fs }) +} + +/// Removes a file or directory from the filesystem. +pub fn remove(fs: impl Filesystem, path: &Path) -> io::Result<()> { + fs.lock(|inner| { + if inner.transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } + + let mut state = inner.state.borrow_mut(); + Ok(unsafe { ll::lfs_remove(state.as_mut_ptr(), path.as_ptr()) }) + }) + .and_then(|ret| io::check_ret(ret).map(drop)) +} + +/// Rename a file or directory to a new name, replacing the original file if `to` already exists. +pub fn rename(fs: impl Filesystem, from: &Path, to: &Path) -> io::Result<()> { + fs.lock(|inner| { + if inner.transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } + + let mut state = inner.state.borrow_mut(); + Ok(unsafe { ll::lfs_rename(state.as_mut_ptr(), from.as_ptr(), to.as_ptr()) }) + }) + .and_then(|ret| io::check_ret(ret).map(drop)) +} + +/// Starts a filesystem transaction +/// +/// Up to `N` (type level integer) files can be modified during this transaction +/// +/// In this mode all writes to disk will be deferred to the `Transaction.commit` operation +/// +/// # Errors +/// +/// This call will error if there's at least one file currently open +/// +/// While in this mode the following APIs will error: +/// +/// - `fs::create_dir` +/// - `fs::remove` +/// - `fs::rename` +/// - `File::create` +/// - `File::open` -- use `Transaction::open` +/// - `File::write`, if you attempt to write more data that what can be held in the file's write +/// cache +pub fn transaction(fs: F) -> io::Result> +where + F: Filesystem, + N: ArrayLength>, +{ + fs.lock(|inner| { + if inner.transaction_mode.get() { + Err(io::Error::TransactionInProgress) + } else if inner.open_files.get() != 0 { + Err(io::Error::OpenFilesExist) + } else { + inner.storage().lock(); + inner.transaction_mode.set(true); + Ok(Transaction { + arena: Arena::new(), + fs, + }) + } + }) +} + +/// A filesystem transaction +pub struct Transaction +where + F: Filesystem, + N: ArrayLength>, +{ + arena: Arena, N>, + fs: F, +} + +impl Transaction +where + F: Filesystem, + N: ArrayLength>, +{ + /// Opens an existing file in read/write mode + pub fn open(&self, path: &Path) -> io::Result<&mut File> { + if self.arena.has_space() { + let f = File::checked_open(self.fs, path, false)?; + self.arena.alloc(f) + } else { + // fast path: do not try to open the file if the arena is already full + Err(io::Error::NoMemory) + } + } + + /// Commits all cached writes to disk + pub fn commit(self) -> io::Result<()> { + self.fs.lock(|inner| { + inner.storage().unlock(); + for f in self.arena.into_iter() { + // FIXME don't lock again + f.close()?; + } + inner.transaction_mode.set(false); + Ok(()) + }) + } +} + +/// Iterator over the entries in a directory. +/// +/// *NOTE* this value is effectively an *open* directory that must eventually be closed. Its +/// destructor will close the directory and panic if any I/O error occurred during the close +/// operation. To handle potential I/O errors call `close` on this value. +pub struct ReadDir +where + FS: Filesystem, +{ + // NOTE this must be freed only if `lfs_dir_close` was called successfully + dir: ManuallyDrop>, + fs: FS, +} + +impl Iterator for ReadDir +where + FS: Filesystem, +{ + type Item = io::Result; + + fn next(&mut self) -> Option { + let mut info = MaybeUninit::::uninit(); + + let ret = self.fs.lock(|inner| { + let mut state = inner.state.borrow_mut(); + unsafe { ll::lfs_dir_read(state.as_mut_ptr(), &mut **self.dir, info.as_mut_ptr()) } + }); + + if ret == 0 { + None + } else if let Err(e) = io::check_ret(ret) { + Some(Err(e)) + } else { + let info = unsafe { info.assume_init() }; + let entry = DirEntry { + metadata: Metadata::from_info(info), + }; + + Some(Ok(entry)) + } + } +} + +impl ReadDir +where + FS: Filesystem, +{ + /// Closes this directory, releasing resources (e.g. memory) associated to it + pub fn close(mut self) -> io::Result<()> { + self.close_in_place()?; + // no need to run the destructor because we already closed the directory + mem::forget(self); + Ok(()) + } + + fn close_in_place(&mut self) -> io::Result<()> { + let ret = self.fs.lock(|inner| unsafe { + let mut state = inner.state.borrow_mut(); + ll::lfs_dir_close(state.as_mut_ptr(), &mut **self.dir) + }); + io::check_ret(ret)?; + // now that we have unliked (self.)`dir` from (self.)`fs` we can release `dir`'s memory + unsafe { ManuallyDrop::drop(&mut self.dir) } + Ok(()) + } +} + +impl Drop for ReadDir +where + FS: Filesystem, +{ + fn drop(&mut self) { + self.close_in_place() + .expect("error while closing directory") + } +} + +/// Entry returned by the `ReadDir` iterator +pub struct DirEntry { + metadata: Metadata, +} + +impl fmt::Debug for DirEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DirEntry") + .field("metadata", self.metadata()) + .finish() + } +} + +impl DirEntry { + /// Returns the bare file name of this directory entry without any other leading path component. + pub fn file_name(&self) -> &Path { + self.metadata.file_name() + } + + /// Returns the file type for the file that this entry points at. + pub fn file_type(&self) -> FileType { + self.metadata.file_type() + } + + /// Returns the metadata for the file or directory that this entry points at. + pub fn metadata(&self) -> &Metadata { + &self.metadata + } +} + +/// Metadata information about a file or directory +#[derive(Clone, Debug)] +pub struct Metadata { + file_name: PathBuf, + file_type: FileType, + size: usize, +} + +// NOTE(allow) `std::fs` version does not have an `is_empty` method +#[allow(clippy::len_without_is_empty)] +impl Metadata { + fn from_info(info: ll::lfs_info) -> Self { + Self { + file_name: unsafe { PathBuf::from_buffer(info.name) }, + file_type: match info.type_ as ll::lfs_type { + ll::lfs_type_LFS_TYPE_DIR => FileType::Dir, + ll::lfs_type_LFS_TYPE_REG => FileType::File, + _ => unreachable!(), + }, + size: info.size as usize, + } + } + + /// Returns `true` if this metadata is for a directory. + pub fn is_dir(&self) -> bool { + self.file_type.is_dir() + } + + /// Returns `true` if this metadata is for a regular file + pub fn is_file(&self) -> bool { + self.file_type.is_file() + } + + /// Returns the bare file name of this directory entry without any other leading path component. + pub fn file_name(&self) -> &Path { + &self.file_name + } + + /// Returns the file type for this metadata. + pub fn file_type(&self) -> FileType { + self.file_type + } + + /// Returns the size of the file, in bytes, this metadata is for. + pub fn len(&self) -> usize { + self.size + } +} + +/// A structure representing a type of file with accessors for each file type +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FileType { + /// File + File, + /// Directory + Dir, +} + +impl FileType { + /// Tests whether this file type represents a directory. + pub fn is_dir(self) -> bool { + self == FileType::Dir + } + + /// Tests whether this file type represents a regular file + pub fn is_file(self) -> bool { + self == FileType::File + } +} + +bitflags! { + struct FileOpenFlags: u32 { + const READ = 0x1; + const WRITE = 0x2; + const READWRITE = Self::READ.bits | Self::WRITE.bits; + const CREATE = 0x0100; + const EXCL = 0x0200; + const TRUNCATE = 0x0400; + const APPEND = 0x0800; + } +} + +struct OpenOptions(FileOpenFlags); + +impl Default for OpenOptions { + fn default() -> Self { + Self::new() + } +} + +impl OpenOptions { + fn new() -> Self { + OpenOptions(FileOpenFlags::empty()) + } + + fn create(&mut self, create: bool) -> &mut Self { + if create { + self.0.insert(FileOpenFlags::CREATE) + } else { + self.0.remove(FileOpenFlags::CREATE) + } + self + } + + fn read(&mut self, read: bool) -> &mut Self { + if read { + self.0.insert(FileOpenFlags::READ) + } else { + self.0.remove(FileOpenFlags::READ) + } + self + } + + fn truncate(&mut self, truncate: bool) -> &mut Self { + if truncate { + self.0.insert(FileOpenFlags::TRUNCATE) + } else { + self.0.remove(FileOpenFlags::TRUNCATE) + } + self + } + + fn write(&mut self, write: bool) -> &mut Self { + if write { + self.0.insert(FileOpenFlags::WRITE) + } else { + self.0.remove(FileOpenFlags::WRITE) + } + self + } + + fn open(&self, fs: FS, path: &Path) -> io::Result> + where + FS: Filesystem, + { + let mut state = ManuallyDrop::new( + F::alloc() + .ok_or(io::Error::NoMemory)? + // FIXME(upstream) it should not be necessary to zero the memory block + .init(unsafe { mem::zeroed() }), + ); + // NOTE this makes `state` into a self-referential struct but that's fine because it's pinned + // in a box + state.config.buffer = state.cache.as_mut_ptr().cast(); + + fs.lock(|inner| { + let mut fsstate = inner.state.borrow_mut(); + let ret = unsafe { + ll::lfs_file_opencfg( + fsstate.as_mut_ptr(), + &mut state.file, + path.as_ptr(), + self.0.bits() as i32, + &state.config, + ) + }; + drop(fsstate); + + io::check_ret(ret)?; + inner.incr(); + Ok(()) + })?; + + Ok(File { fs, state }) + } +} + +/// Enumeration of possible methods to seek within an I/O object. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SeekFrom { + Current(i32), + End(i32), + Start(u32), +} + +impl SeekFrom { + fn off(self) -> i32 { + match self { + SeekFrom::Current(i) => i, + SeekFrom::End(i) => i, + // XXX handle wrap around? + SeekFrom::Start(u) => u as i32, + } + } + + fn whence(self) -> u32 { + match self { + SeekFrom::Current(_) => ll::lfs_whence_flags_LFS_SEEK_CUR, + SeekFrom::End(_) => ll::lfs_whence_flags_LFS_SEEK_END, + SeekFrom::Start(_) => ll::lfs_whence_flags_LFS_SEEK_SET, + } + } +} + +/// An open file +/// +/// *NOTE* files will be automatically closed when `drop`-ped. If an I/O error occurs while closing +/// the file then the destructor will panic. To handle I/O errors that may occur when closing a file +/// use the `close` method. +pub struct File +where + FS: Filesystem, +{ + fs: FS, + // NOTE this must be freed only if `lfs_dir_close` was called successfully + state: ManuallyDrop>, +} + +// NOTE(unsafe) this is safe because `Box` ("boxed FileState") owns its contents and is pinned, +// plus `FS` (handle to the filesystem) is marked as interrupt-safe (only true when "sync-cortex-a" +// is enabled) +unsafe impl Send for File where FS: Filesystem + Send {} + +// NOTE(allow) `std::fs` version does not have an `is_empty` method +#[allow(clippy::len_without_is_empty)] +impl File +where + FS: Filesystem, +{ + /// Opens a file in write-only mode. + /// + /// This function will create a file if it does not exist, and will truncate it if it does. + pub fn create(fs: FS, path: &Path) -> io::Result { + fs.lock(|inner| { + if inner.transaction_mode.get() { + Err(io::Error::TransactionInProgress) + } else { + OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(fs, path) + } + }) + } + + /// Attempts to open a file in read-only mode. + pub fn open(fs: FS, path: &Path) -> io::Result { + Self::checked_open(fs, path, true) + } + + fn checked_open(fs: FS, path: &Path, check: bool) -> io::Result { + fs.lock(|inner| { + if check && inner.transaction_mode.get() { + Err(io::Error::TransactionInProgress) + } else { + // XXX it seems that the C code lets you write to files opened in read-only mode? + OpenOptions::default().read(true).open(fs, path) + } + }) + } + + /// Synchronizes the file to disk and consumes this file handle, releasing resources (e.g. + /// memory) associated to it + pub fn close(mut self) -> io::Result<()> { + self.close_in_place()?; + // no need to run the destructor because we already closed the file + mem::forget(self); + Ok(()) + } + + /// Returns a handle to the filesystem this file lives in + pub fn fs(&self) -> FS { + self.fs + } + + /// Returns the size of the file, in bytes, this metadata is for. + pub fn len(&mut self) -> io::Result { + let ret = self.fs.lock(|inner| { + let mut state = inner.state.borrow_mut(); + unsafe { ll::lfs_file_size(state.as_mut_ptr(), &mut self.state.file) } + }); + io::check_ret(ret).map(|sz| sz as usize) + } + + /// Reads data from the file + pub fn read(&mut self, buf: &mut [u8]) -> io::Result { + let ret = self.fs.lock(|inner| { + let mut state = inner.state.borrow_mut(); + unsafe { + ll::lfs_file_read( + state.as_mut_ptr(), + &mut self.state.file, + buf.as_mut_ptr().cast(), + buf.len().try_into().unwrap_or(u32::max_value()), + ) + } + }); + io::check_ret(ret).map(|sz| sz as usize) + } + + /// Changes the position of the file + pub fn seek(&mut self, pos: SeekFrom) -> io::Result { + let ret = self.fs.lock(|inner| unsafe { + let mut state = inner.state.borrow_mut(); + ll::lfs_file_seek( + state.as_mut_ptr(), + &mut self.state.file, + pos.off(), + pos.whence() as i32, + ) + }); + io::check_ret(ret).map(|off| off as usize) + } + + /// Synchronizes the file to disk + pub fn sync(&mut self) -> io::Result<()> { + let ret = self.fs.lock(|inner| unsafe { + ll::lfs_file_sync(inner.state.borrow_mut().as_mut_ptr(), &mut self.state.file) + }); + io::check_ret(ret).map(drop) + } + + /// Writes data into the file's cache + /// + /// To synchronize the file to disk call the `sync` method + pub fn write(&mut self, data: &[u8]) -> io::Result { + let ret = self.fs.lock(|inner| unsafe { + let mut state = inner.state.borrow_mut(); + ll::lfs_file_write( + state.as_mut_ptr(), + &mut self.state.file, + data.as_ptr().cast(), + data.len().try_into().unwrap_or(u32::max_value()), + ) + }); + io::check_ret(ret).map(|sz| sz as usize) + } + + fn close_in_place(&mut self) -> io::Result<()> { + self.fs.lock(|inner| unsafe { + let mut state = inner.state.borrow_mut(); + let ret = ll::lfs_file_close(state.as_mut_ptr(), &mut self.state.file); + io::check_ret(ret)?; + inner.decr(); + Ok(()) + })?; + // now that we have unliked (self.)`dir` from (self.)`fs` we can release `dir`'s memory + unsafe { ManuallyDrop::drop(&mut self.state) } + Ok(()) + } +} + +impl Drop for File +where + FS: Filesystem, +{ + fn drop(&mut self) { + self.close_in_place().expect("error while closing file") + } +} + +#[doc(hidden)] +pub struct FileState { + cache: [u8; crate::consts::CACHE_SIZE as usize], + config: ll::lfs_file_config, + file: ll::lfs_file_t, +} + +#[doc(hidden)] +pub struct Inner +where + S: 'static + Storage, +{ + config: &'static Config, + state: RefCell<&'static mut State>, + /// Number of files currently open + open_files: Cell, + /// Whether a `Transaction` is active + transaction_mode: Cell, +} + +#[doc(hidden)] +impl Inner +where + S: Storage, +{ + pub fn new(config: &'static mut Config, state: &'static mut State) -> Self { + Self { + state: RefCell::new(state), + config, + open_files: Cell::new(0), + transaction_mode: Cell::new(false), + } + } + + fn decr(&self) { + // NOTE impossible to underflow this value in safe code -- possible (and `unsafe`) if you + // transmute a File out of thin air + self.open_files.set(self.open_files.get() - 1) + } + + fn incr(&self) { + self.open_files.set( + self.open_files + .get() + .checked_add(1) + // NOTE possible in theory (`loop { forget(File::open(..)) }`) but unlikely to occur + // in practice (due to limited amount of memory) + .expect("file counter overflowed"), + ) + } + + fn storage(&self) -> &S { + unsafe { &*(self.config.inner.context as *const S) } + } + + pub fn format(&mut self) -> io::Result<()> { + let ret = + unsafe { ll::lfs_format(self.state.get_mut().as_mut_ptr(), self.config.as_ptr()) }; + io::check_ret(ret).map(drop) + } + + pub fn mount(&mut self) -> io::Result<()> { + let ret = unsafe { ll::lfs_mount(self.state.get_mut().as_mut_ptr(), self.config.as_ptr()) }; + io::check_ret(ret).map(drop) + } +} + +#[doc(hidden)] +pub struct Buffers { + lookahead: MaybeUninit<[u32; crate::consts::LOOKAHEADWORDS_SIZE as usize]>, + read: MaybeUninit<[u8; crate::consts::READ_SIZE as usize]>, + write: MaybeUninit<[u8; crate::consts::WRITE_SIZE as usize]>, +} + +#[doc(hidden)] +impl Buffers { + pub const fn uninit() -> Self { + Self { + lookahead: MaybeUninit::uninit(), + read: MaybeUninit::uninit(), + write: MaybeUninit::uninit(), + } + } +} + +#[doc(hidden)] +pub struct State { + inner: MaybeUninit, +} + +#[doc(hidden)] +impl State { + pub const fn uninit() -> Self { + Self { + inner: MaybeUninit::uninit(), + } + } + + fn as_mut_ptr(&mut self) -> *mut ll::lfs_t { + self.inner.as_mut_ptr() + } +} + +#[doc(hidden)] +pub struct Config +where + S: Storage, +{ + _storage: PhantomData, + inner: ll::lfs_config, +} + +#[doc(hidden)] +impl Config +where + S: Storage, +{ + pub fn new(buffers: &'static mut Buffers, storage: &'static S) -> Self { + Self { + _storage: PhantomData, + inner: ll::lfs_config { + read: Some(Self::lfs_config_read), + prog: Some(Self::lfs_config_prog), + erase: Some(Self::lfs_config_erase), + sync: Some(Self::lfs_config_sync), + + attr_max: crate::consts::ATTRBYTES_MAX, + block_count: S::BLOCK_COUNT, + block_cycles: crate::consts::BLOCK_CYCLES, + block_size: crate::consts::BLOCK_SIZE, + cache_size: crate::consts::CACHE_SIZE, + file_max: crate::consts::FILEBYTES_MAX, + lookahead_size: 32 * crate::consts::LOOKAHEADWORDS_SIZE, + name_max: crate::consts::FILENAME_MAX_PLUS_ONE - 1, + prog_size: crate::consts::WRITE_SIZE, + read_size: crate::consts::READ_SIZE, + + context: storage as *const S as *mut _, + lookahead_buffer: buffers.lookahead.as_mut_ptr().cast(), + read_buffer: buffers.read.as_mut_ptr().cast(), + prog_buffer: buffers.write.as_mut_ptr().cast(), + }, + } + } + + fn as_ptr(&self) -> *const ll::lfs_config { + &self.inner + } + + // NOTE these (C) free functions deserve some comments. + // + // These are basically C ABI versions of `Storage`'s methods. Because C aliasing information + // cannot be trusted we stick to shared references (`&self`) in `Storage` methods to be on the + // safe side. + // + // A more troubling issue is that we do not require `Storage` to be `Sync`. This a bit of a + // gamble because the C library could be spawning threads and calling these `lfs_config_*` + // functions concurrently. Ensuring soundness on this front pretty much requires reading the C + // source code. As far as we could tell these functions are only called by the main `lfs_*` API + // (e.g. `lfs_file_open`). This Rust wrapper (`impl Filesystem`) will only call those functions + // after `lock`-ing the filesystem so `lfs_config_*`, themselves, do not need to be thread / + // interrupt safe (as they'll always be called from a critical section -- on single core at + // least) + extern "C" fn lfs_config_read( + config: *const ll::lfs_config, + block: ll::lfs_block_t, + off: ll::lfs_off_t, + buffer: *mut cty::c_void, + size: ll::lfs_size_t, + ) -> cty::c_int { + let storage = unsafe { &*((*config).context as *const S) }; + + let block_size = crate::consts::BLOCK_SIZE as u32; + let off = (block * block_size + off) as usize; + let buf: &mut [u8] = unsafe { slice::from_raw_parts_mut(buffer as *mut u8, size as usize) }; + + if let Err(e) = storage.read(off, buf) { + e.into_i32() + } else { + 0 + } + } + + extern "C" fn lfs_config_prog( + config: *const ll::lfs_config, + block: ll::lfs_block_t, + off: ll::lfs_off_t, + buffer: *const cty::c_void, + size: ll::lfs_size_t, + ) -> cty::c_int { + let storage = unsafe { &*((*config).context as *const S) }; + + let block_size = crate::consts::BLOCK_SIZE as u32; + let off = (block * block_size + off) as usize; + let buf: &[u8] = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) }; + + if let Err(e) = storage.write(off, buf) { + e.into_i32() + } else { + 0 + } + } + + /// C callback interface used by LittleFS to erase data with the lower level system below the + /// filesystem. + extern "C" fn lfs_config_erase( + config: *const ll::lfs_config, + block: ll::lfs_block_t, + ) -> cty::c_int { + let storage = unsafe { &mut *((*config).context as *mut S) }; + let off = block as usize * crate::consts::BLOCK_SIZE as usize; + + if let Err(e) = storage.erase(off, crate::consts::BLOCK_SIZE as usize) { + e.into_i32() + } else { + 0 + } + } + + /// C callback interface used by LittleFS to sync data with the lower level interface below the + /// filesystem. Note that this function currently does nothing. + extern "C" fn lfs_config_sync(_config: *const ll::lfs_config) -> i32 { + // Do nothing; we presume that data is synchronized. + 0 + } +} diff --git a/common/littlefs/src/io.rs b/common/littlefs/src/io.rs new file mode 100644 index 0000000..e509372 --- /dev/null +++ b/common/littlefs/src/io.rs @@ -0,0 +1,112 @@ +//! Input / Output + +/// Result with Error variant set to I/O error +pub type Result = core::result::Result; + +/// Definition of errors that might be returned by filesystem functionality. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Error { + // C API errors + /// Input / output error occurred. + Io, + /// File or filesystem was corrupt. + Corruption, + /// No entry found with that name. + NoSuchEntry, + /// File or directory already exists. + EntryAlreadyExisted, + /// Path name is not a directory. + PathNotDir, + /// Path specification is to a directory. + PathIsDir, + /// Directory was not empty. + DirNotEmpty, + /// Bad file descriptor. + BadFileDescriptor, + /// File is too big. + FileTooBig, + /// Incorrect value specified to function. + Invalid, + /// No space left available for operation. + NoSpace, + /// No memory available for completing request. + NoMemory, + /// No attribute or data available + NoAttribute, + /// Filename too long + FilenameTooLong, + + // Rust API errors + /// This API cannot be called while in `Transaction` mode + TransactionInProgress, + + /// Couldn't switch to `Transaction` mode because there are open files + OpenFilesExist, + + /// Attempted to write to disk while the Storage device is locked + WriteWhileLocked, + + /// Unknown error occurred, integer code specified. + Unknown(i32), +} + +// Rust error codes +const TRANSACTION_IN_PROGRESS: i32 = -100; +const OPEN_FILES_EXIST: i32 = -101; +const WRITE_WHILE_LOCKED: i32 = -102; + +impl Error { + pub(crate) fn into_i32(self) -> i32 { + match self { + // C errors + Error::Io => ll::lfs_error_LFS_ERR_IO, + Error::Corruption => ll::lfs_error_LFS_ERR_CORRUPT, + Error::NoSuchEntry => ll::lfs_error_LFS_ERR_NOENT, + Error::EntryAlreadyExisted => ll::lfs_error_LFS_ERR_EXIST, + Error::PathNotDir => ll::lfs_error_LFS_ERR_NOTDIR, + Error::PathIsDir => ll::lfs_error_LFS_ERR_ISDIR, + Error::DirNotEmpty => ll::lfs_error_LFS_ERR_NOTEMPTY, + Error::BadFileDescriptor => ll::lfs_error_LFS_ERR_BADF, + Error::FileTooBig => ll::lfs_error_LFS_ERR_FBIG, + Error::Invalid => ll::lfs_error_LFS_ERR_INVAL, + Error::NoSpace => ll::lfs_error_LFS_ERR_NOSPC, + Error::NoMemory => ll::lfs_error_LFS_ERR_NOMEM, + Error::NoAttribute => ll::lfs_error_LFS_ERR_NOATTR, + Error::FilenameTooLong => ll::lfs_error_LFS_ERR_NAMETOOLONG, + + // Rust errors + Error::TransactionInProgress => TRANSACTION_IN_PROGRESS, + Error::OpenFilesExist => OPEN_FILES_EXIST, + Error::WriteWhileLocked => WRITE_WHILE_LOCKED, + + Error::Unknown(code) => code, + } + } +} + +pub(crate) fn check_ret(ret: ll::lfs_error) -> Result { + match ret { + n if n >= 0 => Ok(n as u32), + // C error codes + ll::lfs_error_LFS_ERR_IO => Err(Error::Io), + ll::lfs_error_LFS_ERR_CORRUPT => Err(Error::Corruption), + ll::lfs_error_LFS_ERR_NOENT => Err(Error::NoSuchEntry), + ll::lfs_error_LFS_ERR_EXIST => Err(Error::EntryAlreadyExisted), + ll::lfs_error_LFS_ERR_NOTDIR => Err(Error::PathNotDir), + ll::lfs_error_LFS_ERR_ISDIR => Err(Error::PathIsDir), + ll::lfs_error_LFS_ERR_NOTEMPTY => Err(Error::DirNotEmpty), + ll::lfs_error_LFS_ERR_BADF => Err(Error::BadFileDescriptor), + ll::lfs_error_LFS_ERR_FBIG => Err(Error::FileTooBig), + ll::lfs_error_LFS_ERR_INVAL => Err(Error::Invalid), + ll::lfs_error_LFS_ERR_NOSPC => Err(Error::NoSpace), + ll::lfs_error_LFS_ERR_NOMEM => Err(Error::NoMemory), + ll::lfs_error_LFS_ERR_NOATTR => Err(Error::NoAttribute), + ll::lfs_error_LFS_ERR_NAMETOOLONG => Err(Error::FilenameTooLong), + + // Rust error codes + TRANSACTION_IN_PROGRESS => Err(Error::TransactionInProgress), + OPEN_FILES_EXIST => Err(Error::OpenFilesExist), + WRITE_WHILE_LOCKED => Err(Error::WriteWhileLocked), + _ => Err(Error::Unknown(ret)), + } +} diff --git a/common/littlefs/src/lib.rs b/common/littlefs/src/lib.rs new file mode 100644 index 0000000..d86864d --- /dev/null +++ b/common/littlefs/src/lib.rs @@ -0,0 +1,154 @@ +//! A very opinionated littlefs (v2.1.4) wrapper +//! +//! **NOTE** This contains bits and pieces of the `littlefs2` crate +//! +//! # Limitations +//! +//! - All the filesystem settings are hard-coded in this crate (see `src/consts.rs`) +//! +//! - Buffers (used by the `littlefs` C library) are managed in memory pools; they cannot be +//! allocated on the stack (atm). Furthermore, these managed buffers will be shared by *all* mounted +//! filesystems so it's easy to run out of memory if you mount many filesystems. +//! +//! TL;DR this library is meant to be used in programs that will only use a *single* filesystem +//! +//! # Example usage +//! +//! ``` ignore +//! use littlefs::{ +//! filesystem, +//! fs::{self, File}, +//! storage, +//! }; +//! +//! // RAM `Storage` +//! storage!(S, block_count = 16); +//! +//! // Filesystem on top of storage `S` +//! filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); +//! +//! // claim ownership over the ram storage +//! let storage = S::claim().expect("Storage already claimed"); +//! +//! // mount the filesystem but format a the storage device first +//! let format = true; +//! let f = F::mount(storage, format).unwrap(); +//! +//! // create a directory +//! fs::create_dir(f, b"/foo\0".try_into().unwrap()).unwrap(); +//! +//! // create a file +//! let mut f1 = File::create(f, b"/foo/bar.txt\0".try_into().unwrap()).unwrap(); +//! +//! // write data to the file cache +//! f1.write(b"Hello, world!").unwrap(); +//! +//! // commit data to the storage device and discard the file handle +//! f1.close().unwrap(); +//! +//! // iterate over the contents of the root directory +//! for entry in fs::read_dir(f, b"/\0".try_into().unwrap()).unwrap() { +//! println!("{:?}", entry.unwrap()); +//! } +//! ``` +//! +//! # Cargo features +//! +//! The `unsafe-x86` feature is required to use this crate on the x86_64 architecture. It is +//! *unsafe* to enable the Cargo feature because `heapless::Pool` is not (yet) thread-safe on +//! x86_64. If you enable that feature note that manually calling `D::alloc` or `FS::alloc` can +//! result in memory corruption. +//! +//! The `fs` and `File` API are sound to use on x86_64 provided that all filesystems are used from +//! the same thread -- the `Filesystem` trait will prevent you (at compile time) from using *one* +//! filesystem from different threads but won't prevent you from mounting different filesystems from +//! different threads. All these issues can be avoided by using a *single* filesystem in the +//! application, which is the intended use case. + +#![no_std] +#![warn(rust_2018_idioms, unused_qualifications)] + +use core::marker::PhantomData; + +#[cfg(any(not(target_arch = "x86_64"), feature = "unsafe-x86"))] +#[doc(hidden)] +pub mod consts; +#[cfg(any(not(target_arch = "x86_64"), feature = "unsafe-x86"))] +pub mod fs; +#[cfg(any(not(target_arch = "x86_64"), feature = "unsafe-x86"))] +pub mod io; +#[cfg(any(not(target_arch = "x86_64"), feature = "unsafe-x86"))] +#[doc(hidden)] +pub mod mem; +#[cfg(any(not(target_arch = "x86_64"), feature = "unsafe-x86"))] +pub mod path; +#[cfg(any(not(target_arch = "x86_64"), feature = "unsafe-x86"))] +pub mod storage; + +#[cfg(all(target_arch = "x86_64", not(feature = "unsafe-x86")))] +compile_error!( + "the `unsafe-x86` Cargo feature must be enabled -- READ THE DOCS FIRST -- to use this crate on x86_64" +); + +/// Implementation detail +/// +/// We use this type to *prevent* the creation of singletons in safe code -- in particular we do +/// *not* want the `Filesystem` singleton (handle) to be created before the filesystem has been +/// mounted +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct Private { + _inner: PhantomData<*mut ()>, +} + +#[doc(hidden)] +impl Private { + /// Macro implementation detail + /// + /// # Safety + /// `unsafe` to prevent construction of singletons in safe code + pub unsafe fn new() -> Self { + Self { + _inner: PhantomData, + } + } +} + +#[cfg(feature = "sync-cortex-a")] +unsafe impl Send for Private {} + +#[cfg(feature = "sync-cortex-a")] +unsafe impl Sync for Private {} + +// not interrupt/thread safe +#[cfg(not(feature = "sync-cortex-a"))] +pub fn lock(f: impl FnOnce() -> T) -> T { + f() +} + +// interrupt safe +#[cfg(feature = "sync-cortex-a")] +pub fn lock(f: impl FnOnce() -> T) -> T { + cortex_a::no_interrupts(f) +} + +/// Implementation detail +/// Variation of `Private` that's always `!Send` and `!Sync` +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct NotSendOrSync { + _inner: PhantomData<*mut ()>, +} + +#[doc(hidden)] +impl NotSendOrSync { + /// Macro implementation detail + /// + /// # Safety + /// `unsafe` to prevent construction of singletons in safe code + pub unsafe fn new() -> Self { + Self { + _inner: PhantomData, + } + } +} diff --git a/common/littlefs/src/mem.rs b/common/littlefs/src/mem.rs new file mode 100644 index 0000000..e8390d7 --- /dev/null +++ b/common/littlefs/src/mem.rs @@ -0,0 +1,112 @@ +use core::{ + cell::{Cell, UnsafeCell}, + mem::MaybeUninit, +}; + +use generic_array::{ArrayLength, GenericArray}; +#[cfg(not(target_arch = "x86_64"))] +use heapless::pool; +pub use heapless::pool::singleton::Pool; +use heapless::pool::Node; + +use crate::{fs, io}; + +#[cfg(all(target_arch = "x86_64", feature = "unsafe-x86"))] +macro_rules! pool { + ($(#[$($attr:tt)*])* $ident:ident: $ty:ty) => { + /// A global handle to the memory pool + pub struct $ident; + + impl Pool for $ident { + type Data = $ty; + + fn ptr() -> &'static heapless::pool::Pool<$ty> { + $(#[$($attr)*])* + static mut $ident: heapless::pool::Pool<$ty> = heapless::pool::Pool::new(); + + unsafe { &$ident } + } + } + }; +} + +pool!(D: ll::lfs_dir); + +pub type DNode = Node; + +pool!(F: fs::FileState); + +pub type FNode = Node; + +// a heapless Arena +// NOTE this is missing a `Drop` implementation but it doesn't matter because we'll `close` all +// files by hand (rather than relying on destructors running) in `Fs.sync` +pub(crate) struct Arena +where + N: ArrayLength, +{ + buffer: UnsafeCell>>, + pos: Cell, +} + +impl Arena +where + N: ArrayLength, +{ + pub(crate) fn new() -> Self { + Self { + buffer: UnsafeCell::new(MaybeUninit::uninit()), + pos: Cell::new(0), + } + } + + pub(crate) fn alloc(&self, value: T) -> io::Result<&mut T> { + let i = self.pos.get(); + if i < N::USIZE { + let bufferp = self.buffer.get() as *mut T; + unsafe { bufferp.add(i).write(value) } + self.pos.set(i + 1); + Ok(unsafe { &mut *bufferp.add(i) }) + } else { + Err(io::Error::NoMemory) + } + } + + pub(crate) fn has_space(&self) -> bool { + self.pos.get() < N::USIZE + } + + pub(crate) fn into_iter(self) -> IntoIter { + IntoIter { + buffer: self.buffer, + len: self.pos.get(), + pos: 0, + } + } +} + +pub(crate) struct IntoIter +where + N: ArrayLength, +{ + buffer: UnsafeCell>>, + len: usize, + pos: usize, +} + +impl Iterator for IntoIter +where + N: ArrayLength, +{ + type Item = T; + + fn next(&mut self) -> Option { + if self.pos < self.len { + let item = unsafe { self.buffer.get().cast::().add(self.pos).read() }; + self.pos += 1; + Some(item) + } else { + None + } + } +} diff --git a/common/littlefs/src/path.rs b/common/littlefs/src/path.rs new file mode 100644 index 0000000..00a2cab --- /dev/null +++ b/common/littlefs/src/path.rs @@ -0,0 +1,275 @@ +//! Paths + +use core::{convert::TryFrom, fmt, mem::MaybeUninit, ops, ptr, slice, str}; + +use cstr_core::CStr; +use cty::c_char; + +use crate::consts; + +/// A path +/// +/// Paths must be null terminated ASCII strings +pub struct Path { + inner: CStr, +} + +impl Path { + /// Creates a path from a byte buffer + /// + /// The buffer will be first interpreted as a `CStr` and then checked to be comprised only of + /// ASCII characters. + pub fn from_bytes_with_nul<'b>(bytes: &'b [u8]) -> Result<&'b Self> { + let cstr = CStr::from_bytes_with_nul(bytes).map_err(|_| Error::NotCStr)?; + Self::from_cstr(cstr) + } + + /// Unchecked version of `from_bytes_with_nul` + /// + /// # Safety + /// `bytes` must be null terminated string comprised of only ASCII characters + pub unsafe fn from_bytes_with_nul_unchecked(bytes: &[u8]) -> &Self { + &*(bytes as *const [u8] as *const Path) + } + + /// Creates a path from a C string + /// + /// The string will be checked to be comprised only of ASCII characters + // XXX should we reject empty paths (`""`) here? + pub fn from_cstr<'s>(cstr: &'s CStr) -> Result<&'s Self> { + let bytes = cstr.to_bytes(); + let n = cstr.to_bytes().len(); + if n + 1 > consts::PATH_MAX_PLUS_ONE { + Err(Error::TooLarge) + } else if bytes.is_ascii() { + Ok(unsafe { Self::from_cstr_unchecked(cstr) }) + } else { + Err(Error::NotAscii) + } + } + + /// Unchecked version of `from_cstr` + /// + /// # Safety + /// `cstr` must be comprised only of ASCII characters + pub unsafe fn from_cstr_unchecked(cstr: &CStr) -> &Self { + &*(cstr as *const CStr as *const Path) + } + + /// Returns the inner pointer to this C string. + pub(crate) fn as_ptr(&self) -> *const c_char { + self.inner.as_ptr() + } + + /// Creates an owned `PathBuf` with `path` adjoined to `self`. + pub fn join(&self, path: &Path) -> PathBuf { + let mut p = PathBuf::from(self); + p.push(path); + p + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &str { + // NOTE(unsafe) ASCII is valid UTF-8 + unsafe { str::from_utf8_unchecked(self.inner.to_bytes()) } + } +} + +impl fmt::Debug for Path { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.as_ref()) + } +} + +impl fmt::Display for Path { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_ref()) + } +} + +impl<'b> TryFrom<&'b [u8]> for &'b Path { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result<&Path> { + Path::from_bytes_with_nul(bytes) + } +} + +impl PartialEq for Path { + fn eq(&self, rhs: &str) -> bool { + self.as_ref() == rhs + } +} + +// without this you need to slice byte string literals (`b"foo\0"[..].try_into()`) +macro_rules! array_impls { + ($($N:expr),+) => { + $( + impl<'b> TryFrom<&'b [u8; $N]> for &'b Path { + type Error = Error; + + fn try_from(bytes: &[u8; $N]) -> Result<&Path> { + Path::from_bytes_with_nul(&bytes[..]) + } + } + )+ + } +} + +array_impls!( + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32 +); + +/// An owned, mutable path +#[derive(Clone)] +pub struct PathBuf { + buf: [c_char; consts::PATH_MAX_PLUS_ONE], + // NOTE `len` DOES include the final null byte + len: usize, +} + +impl PathBuf { + pub(crate) unsafe fn from_buffer(buf: [c_char; consts::PATH_MAX_PLUS_ONE]) -> Self { + let len = c_stubs::strlen(buf.as_ptr()) + 1 /* null byte */; + PathBuf { buf, len } + } + + /// Extends `self` with `path` + pub fn push(&mut self, path: &Path) { + match path.as_ref() { + // no-operation + "" => return, + + // `self` becomes `/` (root), to match `std::Path` implementation + // NOTE(allow) cast is necessary on some architectures (e.g. x86) + #[allow(clippy::unnecessary_cast)] + "/" => { + self.buf[0] = b'/' as c_char; + self.buf[1] = 0; + self.len = 2; + return; + } + _ => {} + } + + let src = path.as_ref().as_bytes(); + let needs_separator = self + .as_ref() + .as_bytes() + .last() + .map(|byte| *byte != b'/') + .unwrap_or(false); + let slen = src.len(); + assert!( + self.len + + slen + + if needs_separator { + // b'/' + 1 + } else { + 0 + } + <= consts::PATH_MAX_PLUS_ONE + ); + + let len = self.len; + unsafe { + let mut p = self.buf.as_mut_ptr().cast::().add(len - 1); + if needs_separator { + p.write(b'/'); + p = p.add(1); + self.len += 1; + } + ptr::copy_nonoverlapping(src.as_ptr(), p, slen); + p.add(slen).write(0); // null byte + self.len += slen; + } + } +} + +impl From<&Path> for PathBuf { + fn from(path: &Path) -> Self { + let mut buf = MaybeUninit::<[c_char; consts::PATH_MAX_PLUS_ONE]>::uninit(); + let bytes = path.as_ref().as_bytes(); + let len = bytes.len(); + unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), buf.as_mut_ptr().cast(), len) } + Self { + buf: unsafe { buf.assume_init() }, + len: len + 1, + } + } +} + +impl ops::Deref for PathBuf { + type Target = Path; + + fn deref(&self) -> &Path { + unsafe { + Path::from_bytes_with_nul_unchecked(slice::from_raw_parts( + self.buf.as_ptr().cast(), + self.len, + )) + } + } +} + +impl fmt::Debug for PathBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + +impl fmt::Display for PathBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + +/// Errors that arise from converting byte buffers into paths +#[derive(Clone, Copy, Debug)] +pub enum Error { + /// Byte buffer contains non-ASCII characters + NotAscii, + /// Byte buffer is not a C string + NotCStr, + /// Byte buffer is too long (longer than `consts::PATH_MAX_PLUS_ONE`) + TooLarge, +} + +/// Result type that has its Error variant set to `path::Error` +pub type Result = core::result::Result; + +#[cfg(tests)] +mod tests { + use super::Path; + + #[test] + fn join() { + let empty = Path::from_bytes_with_nul(b"\0").unwrap(); + let slash = Path::from_bytes_with_nul(b"/\0").unwrap(); + let a = Path::from_bytes_with_nul(b"a\0").unwrap(); + let b = Path::from_bytes_with_nul(b"b\0").unwrap(); + + assert_eq!(empty.join(empty).as_ref(), ""); + assert_eq!(empty.join(slash).as_ref(), "/"); + assert_eq!(empty.join(a).as_ref(), "a"); + assert_eq!(empty.join(b).as_ref(), "b"); + + assert_eq!(slash.join(empty).as_ref(), "/"); + assert_eq!(slash.join(slash).as_ref(), "/"); + assert_eq!(slash.join(a).as_ref(), "/a"); + assert_eq!(slash.join(b).as_ref(), "/b"); + + assert_eq!(a.join(empty).as_ref(), "a"); + assert_eq!(a.join(slash).as_ref(), "/"); + assert_eq!(a.join(a).as_ref(), "a/a"); + assert_eq!(a.join(b).as_ref(), "a/b"); + + assert_eq!(b.join(empty).as_ref(), "b"); + assert_eq!(b.join(slash).as_ref(), "/"); + assert_eq!(b.join(a).as_ref(), "b/a"); + assert_eq!(b.join(b).as_ref(), "b/b"); + } +} diff --git a/common/littlefs/src/storage.rs b/common/littlefs/src/storage.rs new file mode 100644 index 0000000..3385694 --- /dev/null +++ b/common/littlefs/src/storage.rs @@ -0,0 +1,159 @@ +//! Storage interface + +use crate::{consts, io}; + +/// Interface to an storage device +/// +/// # Safety +/// +/// Implementer (`Self`) must be an owned singleton handle: i.e. only a single instance of `Self` +/// can ever exist +pub unsafe trait Storage { + /// Number of blocks associated to this storage device + const BLOCK_COUNT: u32; + + /// Reads data from the storage device + // NOTE(`Result<()`) C API expects the return value to be `<= 0` + fn read(&self, off: usize, buf: &mut [u8]) -> io::Result<()>; + + /// Write data to the storage device + // NOTE(`Result<()`) C API expects the return value to be `<= 0` + fn write(&self, off: usize, data: &[u8]) -> io::Result<()>; + + /// Erases data from the storage device + // NOTE(`Result<()`) C API expects the return value to be `<= 0` + fn erase(&self, off: usize, len: usize) -> io::Result<()>; + + /// Locks the storage device; any attempt to write to it will result in a panic + fn lock(&self); + + /// Unlocks the storage device, re-allowing writes to it + fn unlock(&self); +} + +/// Declares a storage device backed by a statically block of RAM +#[macro_export] +macro_rules! storage { + ($storage:ident, block_count=$n:expr) => { + pub struct $storage { + _inner: $crate::NotSendOrSync, + } + + impl $storage { + pub fn claim() -> Option<$storage> { + use core::sync::atomic::{AtomicBool, Ordering}; + + use $crate::NotSendOrSync; + + static ONCE: AtomicBool = AtomicBool::new(false); + + if ONCE + .compare_exchange_weak(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + Some(Self { + _inner: unsafe { NotSendOrSync::new() }, + }) + } else { + None + } + } + + /// # Safety + /// Aliases memory; only one instance of `Inner` must be alive at any point in time + unsafe fn inner() -> $crate::storage::Inner { + use $crate::storage::Inner; + + static mut MEMORY: [u8; $n * $crate::consts::BLOCK_SIZE as usize] = + [0; $n * $crate::consts::BLOCK_SIZE as usize]; + static mut LOCKED: bool = false; + + Inner::new(&mut MEMORY, &mut LOCKED) + } + } + + unsafe impl $crate::storage::Storage for $storage { + const BLOCK_COUNT: u32 = $n; + + fn read(&self, offset: usize, buf: &mut [u8]) -> $crate::io::Result<()> { + unsafe { Self::inner().read(offset, buf) } + } + + fn write(&self, offset: usize, data: &[u8]) -> $crate::io::Result<()> { + unsafe { Self::inner().write(offset, data) } + } + + fn erase(&self, offset: usize, len: usize) -> $crate::io::Result<()> { + unsafe { Self::inner().erase(offset, len) } + } + + fn lock(&self) { + unsafe { Self::inner().lock() } + } + + fn unlock(&self) { + unsafe { Self::inner().unlock() } + } + } + }; +} + +#[doc(hidden)] +pub struct Inner { + data: &'static mut [u8], + locked: &'static mut bool, +} + +#[doc(hidden)] +impl Inner { + pub fn new(data: &'static mut [u8], locked: &'static mut bool) -> Self { + Self { data, locked } + } + + pub fn read(&self, offset: usize, buf: &mut [u8]) -> io::Result<()> { + let read_size = consts::READ_SIZE as usize; + + debug_assert!(offset % read_size == 0); + debug_assert!(buf.len() % read_size == 0); + + let n = buf.len(); + buf.copy_from_slice(&self.data[offset..offset + n]); + Ok(()) + } + + pub fn write(&mut self, offset: usize, data: &[u8]) -> io::Result<()> { + if *self.locked { + return Err(io::Error::WriteWhileLocked); + } + + let write_size = consts::WRITE_SIZE as usize; + + debug_assert!(offset % write_size == 0); + debug_assert!(data.len() % write_size == 0); + + let n = data.len(); + self.data[offset..offset + n].copy_from_slice(data); + Ok(()) + } + + pub fn erase(&mut self, offset: usize, len: usize) -> io::Result<()> { + const ERASE_VALUE: u8 = 0xFF; + + let block_size = consts::BLOCK_SIZE as usize; + + debug_assert!(offset % block_size == 0); + debug_assert!(len % block_size == 0); + for byte in self.data[offset..offset + len].iter_mut() { + *byte = ERASE_VALUE; + } + Ok(()) + } + + pub fn lock(&mut self) { + *self.locked = true; + } + + pub fn unlock(&mut self) { + *self.locked = false; + } +} diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index f9aa9db..1e8362a 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -6,7 +6,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" dependencies = [ - "memchr", + "memchr 2.3.3", ] [[package]] @@ -35,6 +35,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "ascii" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" + [[package]] name = "atty" version = "0.2.14" @@ -101,7 +107,7 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" name = "c-stubs" version = "0.0.0" dependencies = [ - "cty", + "cty 0.2.1", ] [[package]] @@ -180,6 +186,22 @@ dependencies = [ "syn", ] +[[package]] +name = "cstr_core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7829882406e7b36cff95319f674b72fc51dd3b0e6968f33db8f6a26903c1e128" +dependencies = [ + "cty 0.1.5", + "memchr 1.0.2", +] + +[[package]] +name = "cty" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e1d41c471573612df00397113557693b5bf5909666a8acb253930612b93312" + [[package]] name = "cty" version = "0.2.1" @@ -269,13 +291,14 @@ dependencies = [ [[package]] name = "heapless" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b591a0032f114b7a77d4fbfab452660c553055515b7d7ece355db080d19087" +checksum = "8ffa511365b12346c5fbe759d82f80d3aa70d9f1ba01955594f84a1a6bbab985" dependencies = [ "as-slice", "generic-array 0.13.2", "hash32", + "stable_deref_trait", ] [[package]] @@ -337,6 +360,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "littlefs" +version = "0.0.0" +dependencies = [ + "ascii", + "bitflags", + "c-stubs", + "cortex-a", + "cstr_core", + "cty 0.2.1", + "generic-array 0.13.2", + "heapless", + "littlefs2-sys", +] + [[package]] name = "littlefs2" version = "0.1.0-alpha.0" @@ -344,20 +382,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abdd4ddd5caedd104a0627e4abbe1c001a088fc3ce996a210ce5547193f542a3" dependencies = [ "bitflags", - "cty", + "cty 0.2.1", "generic-array 0.13.2", "littlefs2-sys", ] [[package]] name = "littlefs2-sys" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd7c91f37cf16e17a81aecd2ad7d4ef2085dbfbcd42b29fc9d89a0be43543f3" +checksum = "d3c47073d0d700b19b987e44159383a44c6b99665153c368af0e64e0a66d1954" dependencies = [ "bindgen", "cc", - "cty", + "cty 0.2.1", ] [[package]] @@ -369,6 +407,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" + [[package]] name = "memchr" version = "2.3.3" @@ -388,7 +432,7 @@ version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" dependencies = [ - "memchr", + "memchr 2.3.3", "version_check", ] @@ -449,7 +493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" dependencies = [ "aho-corasick", - "memchr", + "memchr 2.3.3", "regex-syntax", "thread_local", ] @@ -587,7 +631,7 @@ dependencies = [ "digest", "heapless", "imx6ul-pac", - "littlefs2", + "littlefs", "memlog", "rand_core", "typenum", diff --git a/firmware/cortex-a-rtfm/macros/src/codegen/util.rs b/firmware/cortex-a-rtfm/macros/src/codegen/util.rs index fc39565..8e355bc 100644 --- a/firmware/cortex-a-rtfm/macros/src/codegen/util.rs +++ b/firmware/cortex-a-rtfm/macros/src/codegen/util.rs @@ -138,7 +138,7 @@ pub fn impl_mutex( type T = #ty; #[inline(always)] - fn lock(&mut self, f: impl FnOnce(&mut #ty) -> R) -> R { + fn lock(&mut self, __f: impl FnOnce(&mut #ty) -> R) -> R { /// Priority ceiling const CEILING: u8 = #ceiling; @@ -147,7 +147,7 @@ pub fn impl_mutex( #ptr, #priority, CEILING, - f, + __f, ) } } diff --git a/firmware/examples/Cargo.toml b/firmware/examples/Cargo.toml index db89cca..e448c8a 100644 --- a/firmware/examples/Cargo.toml +++ b/firmware/examples/Cargo.toml @@ -21,10 +21,30 @@ required-features = ["fs"] name = "emmc-fs4" required-features = ["fs"] +[[example]] +name = "emmc-fs5" +required-features = ["fs"] + [[example]] name = "emmc-fs-format" required-features = ["fs"] +[[example]] +name = "emmc-fs-transaction" +required-features = ["fs"] + +[[example]] +name = "emmc-fs-transaction-err" +required-features = ["fs"] + +[[example]] +name = "emmc-fs-read-dir" +required-features = ["fs"] + +[[example]] +name = "rtfm-10-fs" +required-features = ["fs"] + [dependencies] block-cipher-trait = "0.6.2" consts = { path = "../../common/consts" } diff --git a/firmware/examples/examples/emmc-fs-format.rs b/firmware/examples/examples/emmc-fs-format.rs index 2839e48..6cc9d57 100644 --- a/firmware/examples/examples/emmc-fs-format.rs +++ b/firmware/examples/examples/emmc-fs-format.rs @@ -5,7 +5,7 @@ use exception_reset as _; // default exception handler use panic_serial as _; // panic handler -use usbarmory::{emmc::eMMC, fs::LittleFs, memlog, memlog_flush_and_reset, storage::MbrDevice}; +use usbarmory::{emmc::eMMC, fs::Fs, memlog, memlog_flush_and_reset, storage::MbrDevice}; // NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe // as no type checking is performed by the compiler; stick to safe interfaces @@ -14,10 +14,11 @@ use usbarmory::{emmc::eMMC, fs::LittleFs, memlog, memlog_flush_and_reset, storag fn main() -> ! { let emmc = eMMC::take().expect("eMMC").unwrap(); - let mut mbr = MbrDevice::open(emmc).unwrap(); - let mut main_part = mbr.partition(0).unwrap(); + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); - LittleFs::format(&mut main_part).unwrap(); + let format = true; + Fs::mount(part, format).unwrap(); memlog!("formatting DONE"); diff --git a/firmware/examples/examples/emmc-fs-read-dir.rs b/firmware/examples/examples/emmc-fs-read-dir.rs new file mode 100644 index 0000000..1a0a655 --- /dev/null +++ b/firmware/examples/examples/emmc-fs-read-dir.rs @@ -0,0 +1,64 @@ +//! Check that littlefs2 behaves sanely in an edge case +//! +//! NOTE if you haven't already create an MBR partition (`emmc-new-mbr` example) +//! +//! HEADS UP this example will format the `littlefs` on the first MBR partition + +#![no_main] +#![no_std] + +use core::convert::TryInto; + +use exception_reset as _; // default exception handler +use panic_serial as _; // panic handler +use usbarmory::{ + emmc::eMMC, + fs::{self, File, Fs, Path}, + memlog, memlog_flush_and_reset, + storage::MbrDevice, +}; + +// NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe +// as no type checking is performed by the compiler; stick to safe interfaces +// like `#[rtfm::app]` +#[no_mangle] +fn main() -> ! { + let emmc = eMMC::take().expect("eMMC").unwrap(); + + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); + + let format = true; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); + + fs::create_dir(f, b"foo\0".try_into().unwrap()).unwrap(); + fs::create_dir(f, b"foo/bar\0".try_into().unwrap()).unwrap(); + fs::create_dir(f, b"baz\0".try_into().unwrap()).unwrap(); + + File::create(f, b"/foo/bar/quux.txt\0".try_into().unwrap()) + .unwrap() + .write(b"Hello") + .unwrap(); + + recurse(f, b"/\0".try_into().unwrap()); + + // then reset the board + memlog_flush_and_reset!() +} + +fn recurse(f: Fs, path: &Path) { + for entry in fs::read_dir(f, path).unwrap() { + let entry = entry.unwrap(); + let filename = entry.file_name(); + + if filename != "." && filename != ".." { + memlog!("{:?} @ {}", entry, path); + + if entry.file_type().is_dir() { + let path = path.join(filename); + recurse(f, &path); + } + } + } +} diff --git a/firmware/examples/examples/emmc-fs-transaction-err.rs b/firmware/examples/examples/emmc-fs-transaction-err.rs new file mode 100644 index 0000000..fc6782a --- /dev/null +++ b/firmware/examples/examples/emmc-fs-transaction-err.rs @@ -0,0 +1,62 @@ +//! Check that overwriting to a file, in transaction mode, triggers an error +//! +//! NOTE if you haven't already create an MBR partition (`emmc-new-mbr` example) +//! +//! HEADS UP! This example will format the `littlefs` on the first MBR partition + +#![no_main] +#![no_std] + +use core::convert::TryInto; + +use exception_reset as _; // default exception handler +use panic_serial as _; // panic handler +use usbarmory::{ + emmc::eMMC, + fs::{self, consts, File, Fs}, + memlog, memlog_flush_and_reset, + storage::MbrDevice, +}; + +static FN1: &[u8] = b"a.txt\0"; +static TEXT: &[u8] = b"some great text"; + +// NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe +// as no type checking is performed by the compiler; stick to safe interfaces +// like `#[rtfm::app]` +#[no_mangle] +fn main() -> ! { + let emmc = eMMC::take().expect("eMMC").unwrap(); + + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); + + let format = true; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); + + let fn1 = FN1.try_into().unwrap(); + + for filename in [fn1].iter() { + let f = File::create(f, *filename).unwrap(); + memlog!("created {}", filename); + f.close().unwrap(); + memlog!("file closed"); + } + + let transaction = fs::transaction::(f).unwrap(); + + let f1 = transaction.open(fn1).unwrap(); + for _ in 0..100 { + if let Err(e) = f1.write(TEXT) { + if e == fs::Error::WriteWhileLocked { + memlog!("error: attempted to write to disk in transaction mode (as expected)"); + memlog_flush_and_reset!(); + } else { + panic!("{:?}", e); + } + } + } + + panic!("didn't trigger a write to disk?"); +} diff --git a/firmware/examples/examples/emmc-fs-transaction.rs b/firmware/examples/examples/emmc-fs-transaction.rs new file mode 100644 index 0000000..641ae40 --- /dev/null +++ b/firmware/examples/examples/emmc-fs-transaction.rs @@ -0,0 +1,93 @@ +//! Test the `fs::transaction` API +//! +//! NOTE if you haven't already create an MBR partition (`emmc-new-mbr` example) +//! +//! HEADS UP! This example will format the `littlefs` on the first MBR partition + +#![no_main] +#![no_std] + +use core::convert::TryInto; + +use exception_reset as _; // default exception handler +use panic_serial as _; // panic handler +use usbarmory::{ + emmc::eMMC, + fs::{self, consts, File, Fs}, + memlog, memlog_flush_and_reset, + storage::MbrDevice, +}; + +static FN1: &[u8] = b"a.txt\0"; +static FN2: &[u8] = b"b.txt\0"; +static TEXT: &[u8] = b"some great text"; + +// NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe +// as no type checking is performed by the compiler; stick to safe interfaces +// like `#[rtfm::app]` +#[no_mangle] +fn main() -> ! { + let emmc = eMMC::take().expect("eMMC").unwrap(); + + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); + + let format = true; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); + + let fn1 = FN1.try_into().unwrap(); + let fn2 = FN2.try_into().unwrap(); + + for filename in [fn1, fn2].iter() { + let f = File::create(f, *filename).unwrap(); + memlog!("created {}", filename); + f.close().unwrap(); + memlog!("file closed"); + } + + let transaction = fs::transaction::(f).unwrap(); + let f1 = transaction.open(fn1).unwrap(); + f1.write(TEXT).unwrap(); + memlog!("wrote to file 1's cache"); + let f2 = transaction.open(fn2).unwrap(); + f2.write(TEXT).unwrap(); + memlog!("wrote to file 2's cache"); + + // sanity checks: these operations are not allowed + assert_eq!( + fs::create_dir(f, b"foo\0".try_into().unwrap()), + Err(fs::Error::TransactionInProgress) + ); + assert_eq!(fs::remove(f, fn1), Err(fs::Error::TransactionInProgress)); + assert_eq!( + fs::rename(f, fn1, fn2), + Err(fs::Error::TransactionInProgress) + ); + assert_eq!( + File::create(f, fn1).err(), + Some(fs::Error::TransactionInProgress) + ); + assert_eq!( + File::open(f, fn1).err(), + Some(fs::Error::TransactionInProgress) + ); + + transaction.commit().unwrap(); + memlog!("committed files to disk"); + + // check the files' contents + let mut buf = [0; 32]; + for filename in [fn1, fn2].iter() { + let mut f = File::open(f, *filename).unwrap(); + assert_eq!(f.len().unwrap(), TEXT.len()); + let n = f.read(&mut buf).unwrap(); + assert_eq!(&buf[..n], TEXT); + memlog!("contents of {} look OK", filename); + } + + memlog!("DONE"); + + // then reset the board + memlog_flush_and_reset!(); +} diff --git a/firmware/examples/examples/emmc-fs.rs b/firmware/examples/examples/emmc-fs.rs index 23b7d8d..931139c 100644 --- a/firmware/examples/examples/emmc-fs.rs +++ b/firmware/examples/examples/emmc-fs.rs @@ -7,17 +7,18 @@ #![no_main] #![no_std] +use core::convert::TryInto; + use exception_reset as _; // default exception handler -use littlefs2::io; use panic_serial as _; // panic handler use usbarmory::{ emmc::eMMC, - fs::{File, LittleFs}, + fs::{File, Fs}, memlog, memlog_flush_and_reset, storage::MbrDevice, }; -static FILENAME: &str = "hello.txt"; +static FILENAME: &[u8] = b"hello.txt\0"; static TESTSTR: &[u8] = b"Hello File!"; // NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe @@ -27,56 +28,40 @@ static TESTSTR: &[u8] = b"Hello File!"; fn main() -> ! { let emmc = eMMC::take().expect("eMMC").unwrap(); - let mut mbr = MbrDevice::open(emmc).unwrap(); - let mut main_part = mbr.partition(0).unwrap(); - - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); - - let mut success = false; - File::create_and_then(fs, FILENAME, |file| -> io::Result<()> { - success = true; - memlog!("file created"); - file.write(TESTSTR)?; - memlog!("wrote data to file (but have not yet committed it to disk)"); - Ok(()) - })?; + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); - if success { - memlog!("file committed to disk"); - } + let format = false; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); - Ok(()) - }) - .unwrap(); + let filename = FILENAME.try_into().unwrap(); + let mut file = File::create(f, filename).unwrap(); + memlog!("file created"); - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); + file.write(TESTSTR).unwrap(); + memlog!("wrote data to file (but have not yet committed it to disk)"); - File::open_and_then(fs, FILENAME, |file| -> io::Result<()> { - memlog!("file opened"); + file.close().unwrap(); + memlog!("committed data to disk"); - assert_eq!( - file.len()?, - TESTSTR.len(), - "file length doesn't match our expectations" - ); + let mut file = File::open(f, filename).unwrap(); - let mut buf = [0; 32]; - let n = file.read(&mut buf)?; - assert_eq!( - &buf[..n], - TESTSTR, - "file contents don't match our expectations" - ); - Ok(()) - })?; + assert_eq!( + file.len().unwrap(), + TESTSTR.len(), + "file length doesn't match our expectations" + ); - memlog!("all OK"); + let mut buf = [0; 32]; + let n = file.read(&mut buf).unwrap(); + assert_eq!( + &buf[..n], + TESTSTR, + "file contents don't match our expectations" + ); - Ok(()) - }) - .unwrap(); + memlog!("all OK"); // then reset the board memlog_flush_and_reset!(); diff --git a/firmware/examples/examples/emmc-fs2.rs b/firmware/examples/examples/emmc-fs2.rs index ab5da49..bc68066 100644 --- a/firmware/examples/examples/emmc-fs2.rs +++ b/firmware/examples/examples/emmc-fs2.rs @@ -7,11 +7,16 @@ #![no_main] #![no_std] +use core::convert::TryInto; + use exception_reset as _; // default exception handler -use littlefs2::io; -use littlefs2::io::Error; use panic_serial as _; // panic handler -use usbarmory::{emmc::eMMC, fs::LittleFs, memlog, memlog_flush_and_reset, storage::MbrDevice}; +use usbarmory::{ + emmc::eMMC, + fs::{self, Fs}, + memlog, memlog_flush_and_reset, + storage::MbrDevice, +}; // NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe // as no type checking is performed by the compiler; stick to safe interfaces @@ -20,45 +25,37 @@ use usbarmory::{emmc::eMMC, fs::LittleFs, memlog, memlog_flush_and_reset, storag fn main() -> ! { let emmc = eMMC::take().expect("eMMC").unwrap(); - let mut mbr = MbrDevice::open(emmc).unwrap(); - let mut main_part = mbr.partition(0).unwrap(); - - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); - - let res = fs.create_dir("foo"); - if res != Err(Error::EntryAlreadyExisted) { - memlog!("created directory `foo`"); - res?; - } else { - memlog!("directory `foo` already exists"); - } - - let res = fs.create_dir("foo/bar"); - if res != Err(Error::EntryAlreadyExisted) { - memlog!("created directory `foo/bar`"); - res?; - } else { - memlog!("directory `foo/bar` already exists"); - } - - Ok(()) - }) - .unwrap(); - - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); - - memlog!("iterating over the contents of directory `foo`"); - for (i, entry) in fs.read_dir("foo")?.enumerate() { - let entry = entry?; - // NOTE omitted the name because it gets `Debug` printed as an array - memlog!("{}: {:?}", i, entry.metadata()); - } - - Ok(()) - }) - .unwrap(); + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); + let format = false; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); + + let foo = b"foo\0".try_into().unwrap(); + let res = fs::create_dir(f, foo); + + if res != Err(fs::Error::EntryAlreadyExisted) { + res.unwrap(); + memlog!("created directory `foo`"); + } else { + memlog!("directory `foo` already exists"); + } + + let res = fs::create_dir(f, b"foo/bar\0".try_into().unwrap()); + if res != Err(fs::Error::EntryAlreadyExisted) { + res.unwrap(); + memlog!("created directory `foo/bar`"); + } else { + memlog!("directory `foo/bar` already exists"); + } + + memlog!("iterating over the contents of directory `foo`"); + + for (i, entry) in fs::read_dir(f, foo).unwrap().enumerate() { + let entry = entry.unwrap(); + // NOTE omitted the name because it gets `Debug` printed as an array + memlog!("{}: {:?}", i, entry.metadata()); + } // then reset the board memlog_flush_and_reset!(); diff --git a/firmware/examples/examples/emmc-fs3.rs b/firmware/examples/examples/emmc-fs3.rs index 429fcb2..528a340 100644 --- a/firmware/examples/examples/emmc-fs3.rs +++ b/firmware/examples/examples/emmc-fs3.rs @@ -7,18 +7,19 @@ #![no_main] #![no_std] +use core::convert::TryInto; + use exception_reset as _; // default exception handler -use littlefs2::io; use panic_serial as _; // panic handler use usbarmory::{ emmc::eMMC, - fs::{File, FileAlloc, LittleFs}, + fs::{File, Fs}, memlog, memlog_flush_and_reset, storage::MbrDevice, }; -static FILE1: &str = "foo.txt"; -static FILE2: &str = "bar.txt"; +static FILE1: &[u8] = b"foo.txt\0"; +static FILE2: &[u8] = b"bar.txt\0"; static TESTSTR: &[u8] = b"Hello File!"; // NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe @@ -28,62 +29,55 @@ static TESTSTR: &[u8] = b"Hello File!"; fn main() -> ! { let emmc = eMMC::take().expect("eMMC").unwrap(); - let mut mbr = MbrDevice::open(emmc).unwrap(); - let mut main_part = mbr.partition(0).unwrap(); - - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); - - let mut f1 = FileAlloc::new(); - let f1 = File::create(fs, &mut f1, FILE1)?; - memlog!("file1 created"); - f1.write(TESTSTR)?; - memlog!("wrote data to file1"); + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); - let mut f2 = FileAlloc::new(); - let f2 = File::create(fs, &mut f2, FILE2)?; - memlog!("file2 created"); + let format = false; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); - f2.write(TESTSTR)?; - memlog!("wrote data to file2"); + let file1 = FILE1.try_into().unwrap(); + let file2 = FILE2.try_into().unwrap(); - Ok(()) - }) - .unwrap(); + let mut f1 = File::create(f, file1).unwrap(); + memlog!("file1 created"); + f1.write(TESTSTR).unwrap(); + memlog!("wrote data to file1"); - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); + let mut f2 = File::create(f, file2).unwrap(); + memlog!("file2 created"); - let mut f1 = FileAlloc::new(); - let mut f2 = FileAlloc::new(); + f2.write(TESTSTR).unwrap(); + memlog!("wrote data to file2"); - let f1 = File::open(fs, &mut f1, FILE1)?; - memlog!("file1 opened"); + // close files + drop(f1); + drop(f2); + memlog!("closed files"); - let f2 = File::open(fs, &mut f2, FILE2)?; - memlog!("file2 opened"); + let f1 = File::open(f, file1).unwrap(); + memlog!("file1 opened"); - for f in [f1, f2].iter() { - assert_eq!( - f.len()?, - TESTSTR.len(), - "file length doesn't match our expectations" - ); + let f2 = File::open(f, file2).unwrap(); + memlog!("file2 opened"); - let mut buf = [0; 32]; - let n = f.read(&mut buf)?; - assert_eq!( - &buf[..n], - TESTSTR, - "file contents don't match our expectations" - ); - } + let mut buf = [0; 32]; + for f in [f1, f2].iter_mut() { + assert_eq!( + f.len().unwrap(), + TESTSTR.len(), + "file length doesn't match our expectations" + ); - memlog!("all OK"); + let n = f.read(&mut buf).unwrap(); + assert_eq!( + &buf[..n], + TESTSTR, + "file contents don't match our expectations" + ); + } - Ok(()) - }) - .unwrap(); + memlog!("all OK"); // then reset the board memlog_flush_and_reset!(); diff --git a/firmware/examples/examples/emmc-fs4.rs b/firmware/examples/examples/emmc-fs4.rs index 9fd7ced..61f3647 100644 --- a/firmware/examples/examples/emmc-fs4.rs +++ b/firmware/examples/examples/emmc-fs4.rs @@ -7,19 +7,18 @@ #![no_main] #![no_std] -use core::str; +use core::convert::TryInto; use exception_reset as _; // default exception handler -use littlefs2::io::{self, Error}; use panic_serial as _; // panic handler use usbarmory::{ emmc::eMMC, - fs::{File, FileAlloc, LittleFs}, + fs::{self, File, Fs}, memlog, memlog_flush_and_reset, storage::MbrDevice, }; -static FILENAME: &str = "baz.txt"; +static FILENAME: &[u8] = b"baz.txt\0"; static TESTSTR: &[u8] = b"Hello File!"; // NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe @@ -29,38 +28,31 @@ static TESTSTR: &[u8] = b"Hello File!"; fn main() -> ! { let emmc = eMMC::take().expect("eMMC").unwrap(); - let mut mbr = MbrDevice::open(emmc).unwrap(); - let mut main_part = mbr.partition(0).unwrap(); + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); - LittleFs::mount_and_then(&mut main_part, |fs| -> io::Result<()> { - memlog!("fs mounted"); + let format = false; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); - let mut f = FileAlloc::new(); - let f = File::create(fs, &mut f, FILENAME)?; - memlog!("file created"); - f.write(TESTSTR)?; - memlog!("wrote data to file (but not yet committed it to disk)"); + let filename = FILENAME.try_into().unwrap(); + let mut f1 = File::create(f, filename).unwrap(); + memlog!("file created"); + f1.write(TESTSTR).unwrap(); + memlog!("wrote data to file (but not yet committed it to disk)"); - fs.remove(FILENAME)?; - memlog!("removed file from disk"); + fs::remove(f, filename).unwrap(); + memlog!("removed file from disk"); - f.close()?; - memlog!("closed file handle"); - - let mut f = FileAlloc::new(); - if let Err(e) = File::open(fs, &mut f, FILENAME) { - if e == Error::NoSuchEntry { - memlog!("file doesn't exist (as expected)"); - } else { - return Err(e); - } + if let Err(e) = File::open(f, filename) { + if e == fs::Error::NoSuchEntry { + memlog!("file doesn't exist (as expected)"); } else { - panic!("file exists on disk"); + panic!("{:?}", e); } - - Ok(()) - }) - .unwrap(); + } else { + panic!("file exists on disk"); + } memlog!("DONE"); diff --git a/firmware/examples/examples/emmc-fs5.rs b/firmware/examples/examples/emmc-fs5.rs new file mode 100644 index 0000000..ded5815 --- /dev/null +++ b/firmware/examples/examples/emmc-fs5.rs @@ -0,0 +1,62 @@ +//! Check that littlefs2 behaves sanely in an edge case +//! +//! NOTE if you haven't already create an MBR partition (`emmc-new-mbr` example) and format the +//! partition (`emmc-fs-format` example) before running this; otherwise you'll run into a "corrupted +//! filesystem" error + +#![no_main] +#![no_std] + +use core::convert::TryInto; + +use exception_reset as _; // default exception handler +use panic_serial as _; // panic handler +use usbarmory::{ + emmc::eMMC, + fs::{File, Fs, SeekFrom}, + memlog, memlog_flush_and_reset, + storage::MbrDevice, +}; + +static FILENAME: &[u8] = b"hello.txt\0"; +static TEXT: &[u8] = b"Hello File!"; + +// NOTE binary interfaces, using `no_mangle` and `extern`, are extremely unsafe +// as no type checking is performed by the compiler; stick to safe interfaces +// like `#[rtfm::app]` +#[no_mangle] +fn main() -> ! { + let emmc = eMMC::take().expect("eMMC").unwrap(); + + let mbr = MbrDevice::open(emmc).unwrap(); + let part = mbr.into_partition(0).unwrap(); + + let format = false; + let f = Fs::mount(part, format).unwrap(); + memlog!("fs mounted"); + + let filename = FILENAME.try_into().unwrap(); + let mut f1 = File::create(f, filename).unwrap(); + memlog!("created {}", filename); + let n = f1.write(TEXT).unwrap(); + f1.close().unwrap(); + memlog!("wrote {}B to file", n); + + let mut f1 = File::open(f, filename).unwrap(); + memlog!("opened {}", filename); + + let off = f1.seek(SeekFrom::Start(4)).unwrap(); + memlog!("moved cursor to byte {}", off); + + let mut buf = [0; 32]; + let n = f1.read(&mut buf).unwrap(); + + let off = off as usize; + assert_eq!(&buf[..n], &TEXT[off..]); + + memlog!("file contents look OK"); + memlog!("DONE"); + + // then reset the board + memlog_flush_and_reset!() +} diff --git a/firmware/examples/examples/emmc-mbr.rs b/firmware/examples/examples/emmc-mbr.rs index 19347c5..bf93270 100644 --- a/firmware/examples/examples/emmc-mbr.rs +++ b/firmware/examples/examples/emmc-mbr.rs @@ -17,15 +17,15 @@ use usbarmory::{ #[no_mangle] fn main() -> ! { let emmc = eMMC::take().expect("eMMC").unwrap(); - let mut mbr = MbrDevice::open(emmc).unwrap(); + let mbr = MbrDevice::open(emmc).unwrap(); memlog!("{:#?}", mbr.debug()); - for part_idx in 0..4 { - if let Ok(part) = mbr.partition(part_idx) { - let bytes = part.total_blocks() * u64::from(BLOCK_SIZE); - memlog!("Partition {} is {} MiB", part_idx, bytes / 1024 / 1024); - } + if let Ok(part) = mbr.into_partition(0) { + memlog!( + "first partition is {} MiB", + (part.total_blocks() * u64::from(BLOCK_SIZE)) / 1024 / 1024 + ); } // then reset the board diff --git a/firmware/examples/examples/rtfm-10-fs.rs b/firmware/examples/examples/rtfm-10-fs.rs new file mode 100644 index 0000000..210928d --- /dev/null +++ b/firmware/examples/examples/rtfm-10-fs.rs @@ -0,0 +1,127 @@ +//! Using the FS in a RTFM application +//! +//! Highlights: +//! +//! - The `fs` and `File` API can be used from tasks running at different priorities, without using +//! RTFM's `lock` API -- the `fs` and `File` APIs already use locks internally. +//! +//! - Access control: only tasks that list `Fs` in `resources` can perform FS operations. +//! +//! Expected output: +//! +//! ``` +//! (..) +//! [idle] DirEntry { metadata: Metadata { file_name: ".", file_type: Dir, size: 0 } } +//! [foo] created file foo.txt +//! [bar] no FS access +//! [idle] DirEntry { metadata: Metadata { file_name: "..", file_type: Dir, size: 0 } } +//! [idle] DirEntry { metadata: Metadata { file_name: "foo.txt", file_type: File, size: 6 } } +//! ``` + +#![deny(unsafe_code)] +#![deny(warnings)] +#![no_main] +#![no_std] + +use core::{convert::TryInto, str}; + +use exception_reset as _; // default exception handler +use panic_serial as _; // panic handler +use usbarmory::{ + emmc::eMMC, + fs::{self, File, Fs, Path}, + println, + storage::MbrDevice, +}; + +fn filename() -> &'static Path { + b"foo.txt\0".try_into().unwrap() +} + +#[rtfm::app] +const APP: () = { + struct Resources { + f: Fs, + } + + #[init] + fn init(_cx: init::Context) -> init::LateResources { + let emmc = eMMC::take().expect("eMMC").expect("eMMC already taken"); + + let mbr = MbrDevice::open(emmc).expect("eMMC not formatted"); + let part = mbr.into_partition(0).expect("eMMC has 0 MBR partitions"); + + let format = true; + let f = Fs::mount(part, format).expect("failed to mount filesystem"); + + init::LateResources { f } + } + + // NOTE `&f` denotes a "share-only" resource; this resource will always appear as a shared + // reference (`&-`) in tasks. One does not need to call `lock` on these resources to use them. + #[idle(resources = [&f], spawn = [foo, bar, baz])] + fn idle(cx: idle::Context) -> ! { + // resource appears as a shared reference to the resource data: `&Fs` + let f: &Fs = cx.resources.f; + + for (i, ent) in fs::read_dir(*f, b"/\0".try_into().unwrap()) + .unwrap() + .into_iter() + .enumerate() + { + let ent = ent.unwrap(); + + if i == 1 { + // these tasks will preempt `idle` + cx.spawn.foo().unwrap(); + cx.spawn.bar().unwrap(); + } + + println!("[idle] {:?}", ent); + } + + let filename = filename(); + let file = File::open(*f, filename).unwrap(); + println!("[idle] opened {}", filename); + + // files can be send between tasks + cx.spawn.baz(file).ok().unwrap(); + + usbarmory::reset() + } + + // this task interrupts `idle`, who's walking over the contents of the `/` directory + #[task(resources = [&f])] + fn foo(cx: foo::Context) { + // makes a copy of the `Fs` handle + let f: Fs = *cx.resources.f; + + let filename = filename(); + let mut file = File::create(f, filename).unwrap(); + file.write(b"Hello!").unwrap(); + file.close().unwrap(); + + println!("[foo] created file {}", filename); + } + + // this task cannot perform FS operations because it doesn't have access to the `Fs` handle + // (resource `f`) + #[task] + fn bar(_cx: bar::Context) { + println!("[bar] no FS access"); + } + + #[task] + fn baz(_cx: baz::Context, mut f: File) { + let filename = filename(); + let mut buf = [0; 32]; + let n = f.read(&mut buf).unwrap(); + println!( + "[baz] read({}) -> {:?}", + filename, + str::from_utf8(&buf[..n]) + ); + f.close().unwrap(); + println!("[baz] closed {}", filename); + } +}; diff --git a/firmware/usbarmory/Cargo.toml b/firmware/usbarmory/Cargo.toml index fde6248..7385a48 100644 --- a/firmware/usbarmory/Cargo.toml +++ b/firmware/usbarmory/Cargo.toml @@ -25,9 +25,9 @@ typenum = "1.11.2" usb-device = "0.2.5" zerocopy = "0.3.0" -[dependencies.littlefs2] +[dependencies.littlefs] optional = true -version = "0.1.0-alpha.0" +path = "../../common/littlefs" [dependencies.pac] features = ["ccm_analog", "hw_dcp", "rng", "src", "uart", "usb_analog", "usb_uog", "usbphy", "usdhc", "wdog"] @@ -38,7 +38,7 @@ path = "../imx6ul-pac" path = "../usbarmory-rt" [features] -fs = ["littlefs2"] +fs = ["littlefs/sync-cortex-a"] # choose the location of the .text and .rodata sections -- pick only one feature dram = ["usbarmory-rt/dram"] -ocram = ["usbarmory-rt/ocram"] \ No newline at end of file +ocram = ["usbarmory-rt/ocram"] diff --git a/firmware/usbarmory/src/emmc.rs b/firmware/usbarmory/src/emmc.rs index c802eb5..83c5cc8 100644 --- a/firmware/usbarmory/src/emmc.rs +++ b/firmware/usbarmory/src/emmc.rs @@ -828,7 +828,8 @@ impl fmt::Display for Error { } } -impl ManagedBlockDevice for eMMC { +// NOTE(unsafe) eMMC is a singleton +unsafe impl ManagedBlockDevice for eMMC { type Error = Error; fn total_blocks(&self) -> u64 { @@ -844,7 +845,7 @@ impl ManagedBlockDevice for eMMC { Ok(()) } - fn write(&mut self, block: &Block, lba: u64) -> Result<(), Self::Error> { + fn write(&self, block: &Block, lba: u64) -> Result<(), Self::Error> { if lba > self.total_blocks() { return Err(Error::Other); } @@ -852,9 +853,4 @@ impl ManagedBlockDevice for eMMC { Self::write(self, lba as u32, block)?; Ok(()) } - - fn flush(&mut self) -> Result<(), Self::Error> { - // no-operation - Ok(()) - } } diff --git a/firmware/usbarmory/src/fs.rs b/firmware/usbarmory/src/fs.rs index 3ef87f8..afd2844 100644 --- a/firmware/usbarmory/src/fs.rs +++ b/firmware/usbarmory/src/fs.rs @@ -1,16 +1,21 @@ //! File system access. - -use core::cell::RefCell; - -use crate::storage::{Block, ManagedBlockDevice, BLOCK_SIZE}; -use littlefs2::{ - consts, - driver::Storage, - fs::{self, FileAllocation, FileType, Filesystem, FilesystemAllocation, Metadata, SeekFrom}, - io::{self, Read, Seek, Write}, - path::Filename, +//! +//! The filesystem can be configured, via `const`s, in the following files: +//! +//! - `firmware/usbarmory/src/fs.rs` +//! - `common/littlefs/src/consts.rs` + +use littlefs::{filesystem, io, storage::Storage}; +pub use littlefs::{fs::*, io::Error, path::Path}; + +use crate::{ + emmc::eMMC, + storage::{Block, ManagedBlockDevice, MbrPartition, BLOCK_SIZE}, }; -use memlog::memlog; + +// NOTE end-users should only modify these constants +const READ_DIR_DEPTH: usize = 4; +const MAX_OPEN_FILES: usize = 4; /// Hardcoded filesystem block count. /// @@ -19,397 +24,63 @@ use memlog::memlog; /// littlefs2 has a hard 2^32 Byte limit. // NOTE if you modify this you may need to modify the size of the MBR partition; the MBR partition // must be bigger than this number -const BLOCK_COUNT: usize = 131_072; // 64 MiB / 512 (=block_size) - -/// Backing storage used by littlefs. -pub struct LittleFsAlloc { - inner: FilesystemAllocation>, -} - -impl LittleFsAlloc { - /// Creates a new filesystem allocation. - pub fn new() -> Self { - Self { - inner: Filesystem::allocate(), - } - } -} - -impl Default for LittleFsAlloc { - fn default() -> Self { - Self::new() - } -} - -/// A littlefs2 file system. -pub struct LittleFs<'a, D: ManagedBlockDevice> { - storage: RefCell>, - fs: RefCell>>, -} - -impl<'a, D: ManagedBlockDevice> LittleFs<'a, D> { - /// Mounts a littlefs2 file system. - pub fn mount(alloc: &'a mut LittleFsAlloc, blockdev: D) -> io::Result { - if blockdev.total_blocks() < BLOCK_COUNT as u64 { - memlog!( - "expected at least {} blocks, got {}", - BLOCK_COUNT, - blockdev.total_blocks() - ); - - return Err(littlefs2::io::Error::NoSpace); // close enough? - } - - let mut storage = RefCell::new(LfsStorage { inner: blockdev }); - let fs = RefCell::new(Filesystem::mount(&mut alloc.inner, storage.get_mut())?); - - Ok(Self { storage, fs }) - } - - /// Mounts a littlefs2 file system for the duration of a closure `f`. - /// - /// This API avoids the need for using `LittleFsAlloc`. - pub fn mount_and_then( - blockdev: D, - f: impl FnOnce(&LittleFs<'_, D>) -> io::Result, - ) -> io::Result { - let mut alloc = LittleFsAlloc::new(); - let fs = LittleFs::mount(&mut alloc, blockdev)?; - - f(&fs) - } - - /// Formats `blockdev`, creating a fresh littlefs file system (this erases all data!). - pub fn format(blockdev: D) -> io::Result<()> { - Filesystem::format(&mut LfsStorage { inner: blockdev }) - } - - /// Returns the available space in Bytes (approximated). - pub fn available_space(&self) -> io::Result { - self.fs - .borrow_mut() - .available_space(&mut self.storage.borrow_mut()) - .map(|space| space as u64) - } - - /// Creates a new directory at `path`. - pub fn create_dir(&self, path: impl AsRef<[u8]>) -> io::Result<()> { - self.fs - .borrow_mut() - .create_dir(path.as_ref(), &mut self.storage.borrow_mut()) - } - - /// Removes the file or directory at `path`. - pub fn remove(&self, path: impl AsRef<[u8]>) -> io::Result<()> { - self.fs - .borrow_mut() - .remove(path.as_ref(), &mut self.storage.borrow_mut()) - } - - /// Returns an iterator over the contents of the directory at `path`. - pub fn read_dir<'r>(&'r self, path: impl AsRef<[u8]>) -> io::Result> { - self.fs - .borrow_mut() - .read_dir(path.as_ref(), &mut self.storage.borrow_mut()) - .map(move |inner| ReadDir { fs: self, inner }) - } -} - -/// Allocation backing a `File` instance. -pub struct FileAlloc { - inner: FileAllocation>, -} - -impl FileAlloc { - /// Creates a new file allocation. - pub fn new() -> Self { - Self { - inner: fs::File::allocate(), - } - } -} - -impl Default for FileAlloc { - fn default() -> Self { - Self::new() - } -} - -/// An open file. -/// -/// NOTE unlike `littlefs2::File`, this newtype has close on drop semantics. Any error that arises -/// while closing the file will result in a panic. Use the `close` method to handle IO errors -/// instead of potentially panicking. -pub struct File<'a, 'fs, D: ManagedBlockDevice> { - inner: Option>>>, - fs: &'a LittleFs<'fs, D>, -} - -#[allow(clippy::len_without_is_empty)] -impl<'a, 'fs, D: ManagedBlockDevice> File<'a, 'fs, D> { - /// Opens the file at `path`. - pub fn open( - fs: &'a LittleFs<'fs, D>, - alloc: &'a mut FileAlloc, - path: impl AsRef<[u8]>, - ) -> io::Result { - let mut inner = fs::File::open( - path.as_ref(), - &mut alloc.inner, - &mut fs.fs.borrow_mut(), - &mut fs.storage.borrow_mut(), - )?; - inner.seek( - &mut fs.fs.borrow_mut(), - &mut fs.storage.borrow_mut(), - SeekFrom::Start(0), - )?; - Ok(Self { - inner: Some(RefCell::new(inner)), - fs, - }) - } - - /// Creates or overwrites a file at `path`. - pub fn create( - fs: &'a LittleFs<'fs, D>, - alloc: &'a mut FileAlloc, - path: impl AsRef<[u8]>, - ) -> io::Result { - Ok(Self { - inner: Some(RefCell::new(fs::File::create( - path.as_ref(), - &mut alloc.inner, - &mut fs.fs.borrow_mut(), - &mut fs.storage.borrow_mut(), - )?)), - fs, - }) - } - - /// Calls a closure with the file at `path`. - /// - /// This avoids having to use `FileAlloc`. - /// - /// NOTE the file will be `sync`-ed and `close`-d after `f` is executed - pub fn open_and_then( - fs: &LittleFs<'a, D>, - path: impl AsRef<[u8]>, - f: impl FnOnce(&File<'_, '_, D>) -> io::Result, - ) -> io::Result { - let mut alloc = FileAlloc::new(); - let file = File::open(fs, &mut alloc, path)?; - - let res = f(&file); - file.close()?; - res - } +const BLOCK_COUNT: u32 = 131_072; // 64 MiB / 512 (=block_size) - /// Calls a closure with a file created at `path`. - /// - /// This avoids having to use `FileAlloc`. - /// - /// NOTE the file will be `sync`-ed and `close`-d after `f` is executed - pub fn create_and_then( - fs: &LittleFs<'a, D>, - path: impl AsRef<[u8]>, - f: impl FnOnce(&File<'_, '_, D>) -> io::Result, - ) -> io::Result { - let mut alloc = FileAlloc::new(); - let file = File::create(fs, &mut alloc, path)?; +filesystem!( + /// Filesystem backed by an eMMC + Fs, + Storage = MbrPartition, + max_open_files = MAX_OPEN_FILES, + read_dir_depth = READ_DIR_DEPTH +); - let r = f(&file)?; - file.close()?; - Ok(r) - } - - /// Consumes and closes the file. - /// - /// This will also synchronize the contents of the file to disk (i.e. flush the file write - /// cache) - /// - /// NOTE the file will also be closed when dropped; but you can use this method to handle IO - /// errors that may occur while closing the file - pub fn close(mut self) -> io::Result<()> { - self.inner - .take() - .unwrap_or_else(|| unsafe { assume_unreachable!() }) - .into_inner() - .close( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - ) - } - - /// Returns the length of this file in Bytes. - pub fn len(&self) -> io::Result { - self.inner - .as_ref() - .unwrap_or_else(|| unsafe { assume_unreachable!() }) - .borrow_mut() - .len( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - ) - } - - /// Reads bytes from this file into `buf`. - pub fn read(&self, buf: &mut [u8]) -> io::Result { - self.inner - .as_ref() - .unwrap_or_else(|| unsafe { assume_unreachable!() }) - .borrow_mut() - .read( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - buf, - ) - } - - /// Writes byte from `buf` into this file. - /// - /// NOTE writes are cached in memory; use `sync` to flush the cache to disk - pub fn write(&self, buf: &[u8]) -> io::Result { - self.inner - .as_ref() - .unwrap_or_else(|| unsafe { assume_unreachable!() }) - .borrow_mut() - .write( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - buf, - ) - } - - /// Synchronize file contents to storage - pub fn sync(&self) -> io::Result<()> { - self.inner - .as_ref() - .unwrap_or_else(|| unsafe { assume_unreachable!() }) - .borrow_mut() - .sync( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - ) - } -} - -impl Drop for File<'_, '_, D> +unsafe impl Storage for MbrPartition where D: ManagedBlockDevice, { - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { - inner - .into_inner() - .close( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - ) - .unwrap() - } - } -} - -/// An iterator over entries in a directory. -pub struct ReadDir<'a, 'fs, D: ManagedBlockDevice> { - inner: fs::ReadDir>, - fs: &'a LittleFs<'fs, D>, -} - -impl<'a, 'fs, D: ManagedBlockDevice> Iterator for ReadDir<'a, 'fs, D> { - type Item = littlefs2::io::Result>; - - fn next(&mut self) -> Option { - match self.inner.next( - &mut self.fs.fs.borrow_mut(), - &mut self.fs.storage.borrow_mut(), - ) { - Some(res) => Some(res.map(|inner| DirEntry { inner })), - None => None, - } - } -} - -/// A directory entry returned by `ReadDir`. -pub struct DirEntry { - inner: fs::DirEntry>, -} - -impl DirEntry { - /// Returns the type of this entry. - pub fn file_type(&self) -> FileType { - self.inner.file_type() - } - - /// Returns the name of this entry - pub fn file_name(&self) -> Filename> { - self.inner.file_name() - } - - /// Returns the metadata of this entry - pub fn metadata(&self) -> Metadata { - self.inner.metadata() - } -} - -#[doc(hidden)] -pub struct LfsStorage { - inner: D, -} - -impl Storage for LfsStorage { - type CACHE_SIZE = consts::U512; - type LOOKAHEADWORDS_SIZE = consts::U16; - type FILENAME_MAX_PLUS_ONE = consts::U256; - type PATH_MAX_PLUS_ONE = consts::U256; - type ATTRBYTES_MAX = consts::U1022; - - const READ_SIZE: usize = BLOCK_SIZE as usize; - const WRITE_SIZE: usize = BLOCK_SIZE as usize; - const BLOCK_SIZE: usize = BLOCK_SIZE as usize; - // FIXME: This really shouldn't be a constant. - const BLOCK_COUNT: usize = BLOCK_COUNT; - - // Disable wear leveling since the `ManagedBlockDevice` is assumed to already implement that. - const BLOCK_CYCLES: isize = -1; - const FILEBYTES_MAX: usize = 2_147_483_647; + const BLOCK_COUNT: u32 = BLOCK_COUNT; - fn read(&self, off: usize, buf: &mut [u8]) -> littlefs2::io::Result { - let mut lba = off / Self::BLOCK_SIZE; + fn read(&self, off: usize, buf: &mut [u8]) -> io::Result<()> { + let mut lba = off / usize::from(BLOCK_SIZE); let mut block = Block::zeroed(); - for buf_block in buf.chunks_mut(Self::BLOCK_SIZE) { - self.inner - .read(&mut block, lba as u64) - .map_err(|_| littlefs2::io::Error::Io)?; + for buf_block in buf.chunks_mut(BLOCK_SIZE.into()) { + ManagedBlockDevice::read(self, &mut block, lba as u64).map_err(|_| io::Error::Io)?; buf_block.copy_from_slice(&block.bytes); lba += 1; } - Ok(buf.len()) + Ok(()) } - fn write(&mut self, off: usize, data: &[u8]) -> littlefs2::io::Result { - let mut lba = off / Self::BLOCK_SIZE; + fn write(&self, off: usize, data: &[u8]) -> io::Result<()> { + if self.lock.get() { + return Err(io::Error::WriteWhileLocked); + } + + let mut lba = off / usize::from(BLOCK_SIZE); let mut block = Block::zeroed(); - for buf_block in data.chunks(Self::BLOCK_SIZE) { + for buf_block in data.chunks(BLOCK_SIZE.into()) { block.bytes.copy_from_slice(buf_block); - self.inner - .write(&block, lba as u64) - .map_err(|_| littlefs2::io::Error::Io)?; + ManagedBlockDevice::write(self, &block, lba as u64).map_err(|_| io::Error::Io)?; lba += 1; } - self.inner.flush().map_err(|_| littlefs2::io::Error::Io)?; - - Ok(data.len()) + Ok(()) } - fn erase(&mut self, _off: usize, len: usize) -> littlefs2::io::Result { + fn erase(&self, _off: usize, _len: usize) -> io::Result<()> { // A `ManagedBlockDevice` can just overwrite individual blocks, no need to erase any. - Ok(len) + Ok(()) + } + + fn lock(&self) { + self.lock.set(true) + } + + fn unlock(&self) { + self.lock.set(false) } } diff --git a/firmware/usbarmory/src/storage.rs b/firmware/usbarmory/src/storage.rs index 2ffcd08..85ad85f 100644 --- a/firmware/usbarmory/src/storage.rs +++ b/firmware/usbarmory/src/storage.rs @@ -1,6 +1,6 @@ //! Partition table and block device access. -use core::{fmt, mem::size_of}; +use core::{cell::Cell, fmt, mem::size_of}; use memlog::memlog; use zerocopy::{AsBytes, FromBytes, LayoutVerified}; @@ -9,7 +9,11 @@ use zerocopy::{AsBytes, FromBytes, LayoutVerified}; /// /// This is meant to be implemented for "managed" devices that have their own controller for /// scheduling page erases and doing wear leveling, such as SD and MMC cards used by the Armory. -pub trait ManagedBlockDevice { +/// +/// # Safety +/// +/// - Implementers of this trait must be *owned* singletons +pub unsafe trait ManagedBlockDevice { /// The error type used by the block device implementation. type Error: fmt::Debug + fmt::Display; @@ -27,32 +31,8 @@ pub trait ManagedBlockDevice { /// The `lba` parameter indicates the linera block address to write to. If it is outside of the /// valid range, an error must be returned. /// - /// This may write to a buffer and not to persistent storage. `flush` may be used to write all - /// buffered data to persistent storage. - fn write(&mut self, block: &Block, lba: u64) -> Result<(), Self::Error>; - - /// Flushes all buffered writes to persistent storage. - fn flush(&mut self) -> Result<(), Self::Error>; -} - -impl<'a, D: ManagedBlockDevice> ManagedBlockDevice for &'a mut D { - type Error = D::Error; - - fn total_blocks(&self) -> u64 { - (**self).total_blocks() - } - - fn read(&self, block: &mut Block, lba: u64) -> Result<(), Self::Error> { - (**self).read(block, lba) - } - - fn write(&mut self, block: &Block, lba: u64) -> Result<(), Self::Error> { - (**self).write(block, lba) - } - - fn flush(&mut self) -> Result<(), Self::Error> { - (**self).flush() - } + /// This may write to a buffer and not to persistent storage. + fn write(&self, block: &Block, lba: u64) -> Result<(), Self::Error>; } /// Block size used by the storage subsystem. @@ -91,7 +71,7 @@ pub struct MbrDevice { impl MbrDevice { /// Creates a new MBR-partitioned block device by writing the given partition `table` into it - pub fn create(mut raw: D, part_table: &PartitionTable) -> Result> { + pub fn create(raw: D, part_table: &PartitionTable) -> Result> { let total_blocks = raw.total_blocks(); let end = part_table .as_slice() @@ -164,12 +144,13 @@ impl MbrDevice { /// Obtains access to the partition at index `part` (0 ..= 3). /// /// Returns a `NoPartition` error if `part` does not refer to an allocated partition. - pub fn partition(&mut self, part: u8) -> Result, MbrError> { + pub fn into_partition(self, part: u8) -> Result, MbrError> { let extent = self.part_extent(part)?; - Ok(MbrPartitionRef { - raw: &mut self.raw, + Ok(MbrPartition { extent, + lock: Cell::new(false), + raw: self.raw, }) } @@ -349,15 +330,16 @@ impl fmt::Display for MbrError { } } -/// Provides borrowed access to an MBR partition. -/// -/// This implements `ManagedBlockDevice` and maps any access to the partition. -pub struct MbrPartitionRef<'a, D: ManagedBlockDevice> { - raw: &'a mut D, +/// Provides exclusive access to an MBR partition. +pub struct MbrPartition { + raw: D, extent: PartExtent, + // only used when the `fs` feature is enabled + #[allow(dead_code)] + pub(crate) lock: Cell, } -impl<'a, D: ManagedBlockDevice> ManagedBlockDevice for MbrPartitionRef<'a, D> { +unsafe impl ManagedBlockDevice for MbrPartition { type Error = MbrError; fn total_blocks(&self) -> u64 { @@ -374,7 +356,7 @@ impl<'a, D: ManagedBlockDevice> ManagedBlockDevice for MbrPartitionRef<'a, D> { .map_err(MbrError::Device) } - fn write(&mut self, block: &Block, lba: u64) -> Result<(), Self::Error> { + fn write(&self, block: &Block, lba: u64) -> Result<(), Self::Error> { if lba >= u64::from(self.extent.sectors) { return Err(MbrError::OutOfRangeAccess); } @@ -383,8 +365,4 @@ impl<'a, D: ManagedBlockDevice> ManagedBlockDevice for MbrPartitionRef<'a, D> { .write(block, lba + u64::from(self.extent.start)) .map_err(MbrError::Device) } - - fn flush(&mut self) -> Result<(), Self::Error> { - self.raw.flush().map_err(MbrError::Device) - } }