From f4aa90c63ae783fc903fed145ef19254988e15f1 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 16 Apr 2020 19:00:42 +0200 Subject: [PATCH 01/19] memory pool backed FS API --- common/Cargo.lock | 475 +++++++++++- common/Cargo.toml | 2 +- common/c-stubs/src/lib.rs | 2 +- common/littlefs/Cargo.toml | 39 + common/littlefs/examples/dir-close.rs | 39 + common/littlefs/examples/dir-forget-2.rs | 32 + common/littlefs/examples/dir-forget.rs | 40 + common/littlefs/examples/file-close.rs | 35 + common/littlefs/examples/file-forget.rs | 36 + common/littlefs/src/consts.rs | 18 + common/littlefs/src/fs.rs | 761 +++++++++++++++++++ common/littlefs/src/io.rs | 61 ++ common/littlefs/src/lib.rs | 108 +++ common/littlefs/src/mem.rs | 33 + common/littlefs/src/path.rs | 261 +++++++ common/littlefs/src/storage.rs | 128 ++++ firmware/Cargo.lock | 46 +- firmware/examples/examples/emmc-fs-format.rs | 9 +- firmware/examples/examples/emmc-fs.rs | 75 +- firmware/examples/examples/emmc-fs2.rs | 81 +- firmware/examples/examples/emmc-fs3.rs | 92 ++- firmware/examples/examples/emmc-fs4.rs | 52 +- firmware/examples/examples/emmc-mbr.rs | 12 +- firmware/usbarmory/Cargo.toml | 8 +- firmware/usbarmory/src/emmc.rs | 10 +- firmware/usbarmory/src/fs.rs | 411 +--------- firmware/usbarmory/src/storage.rs | 58 +- 27 files changed, 2300 insertions(+), 624 deletions(-) create mode 100644 common/littlefs/Cargo.toml create mode 100644 common/littlefs/examples/dir-close.rs create mode 100644 common/littlefs/examples/dir-forget-2.rs create mode 100644 common/littlefs/examples/dir-forget.rs create mode 100644 common/littlefs/examples/file-close.rs create mode 100644 common/littlefs/examples/file-forget.rs create mode 100644 common/littlefs/src/consts.rs create mode 100644 common/littlefs/src/fs.rs create mode 100644 common/littlefs/src/io.rs create mode 100644 common/littlefs/src/lib.rs create mode 100644 common/littlefs/src/mem.rs create mode 100644 common/littlefs/src/path.rs create mode 100644 common/littlefs/src/storage.rs diff --git a/common/Cargo.lock b/common/Cargo.lock index b8764fd..013eec5 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -1,18 +1,491 @@ # 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 = "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", + "cstr_core", + "cty 0.2.1", + "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..4485fa2 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["consts", "c-stubs"] \ No newline at end of file +members = ["consts", "c-stubs", "littlefs"] \ No newline at end of file diff --git a/common/c-stubs/src/lib.rs b/common/c-stubs/src/lib.rs index d68c1b5..82c1640 100644 --- a/common/c-stubs/src/lib.rs +++ b/common/c-stubs/src/lib.rs @@ -19,7 +19,7 @@ pub 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 { +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..d2a4bdb --- /dev/null +++ b/common/littlefs/Cargo.toml @@ -0,0 +1,39 @@ +[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" } +cty = "0.2.1" +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] +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..cf05d21 --- /dev/null +++ b/common/littlefs/examples/dir-close.rs @@ -0,0 +1,39 @@ +//! [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); +} + +#[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..5c8694c --- /dev/null +++ b/common/littlefs/examples/dir-forget-2.rs @@ -0,0 +1,32 @@ +//! [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(); +} diff --git a/common/littlefs/examples/dir-forget.rs b/common/littlefs/examples/dir-forget.rs new file mode 100644 index 0000000..7514c9c --- /dev/null +++ b/common/littlefs/examples/dir-forget.rs @@ -0,0 +1,40 @@ +//! [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); +} + +#[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..83864dd --- /dev/null +++ b/common/littlefs/examples/file-close.rs @@ -0,0 +1,35 @@ +//! [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); +} + +#[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..977c560 --- /dev/null +++ b/common/littlefs/examples/file-forget.rs @@ -0,0 +1,36 @@ +//! [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); +} + +#[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..338c25b --- /dev/null +++ b/common/littlefs/src/fs.rs @@ -0,0 +1,761 @@ +//! Filesystem operations + +use core::{ + cell::RefCell, + convert::TryInto, + fmt, + marker::PhantomData, + mem::{self, ManuallyDrop, MaybeUninit}, + slice, +}; + +use bitflags::bitflags; +use heapless::pool::singleton::{Box, Pool}; + +use crate::{ + consts, io, + mem::{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 handle(self) -> &'static RefCell>; + + /// 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::NotSendOrSync, + } + + impl $fs { + fn ptr() -> *mut core::cell::RefCell<$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}, + NotSendOrSync, + }; + + // 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(RefCell::new(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: NotSendOrSync::new(), + }) + } + } + } + + unsafe impl $crate::fs::Filesystem for $fs { + type Storage = $storage; + + fn handle(self) -> &'static core::cell::RefCell<$crate::fs::Inner<$storage>> { + 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 mut fs = fs.handle().borrow_mut(); + // XXX does this really need a `*mut` pointer? + let ret = unsafe { ll::lfs_fs_size(fs.state.as_mut_ptr()) }; + drop(fs); + io::check_ret(ret) +} + +/// Creates a new, empty directory at the provided path +pub fn create_dir(fs: impl Filesystem, path: &Path) -> io::Result<()> { + let mut fs = fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_mkdir(fs.state.as_mut_ptr(), path.as_ptr()) }; + drop(fs); + 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 f = fs.handle().borrow_mut(); + let mut info = MaybeUninit::uninit(); + let ret = unsafe { ll::lfs_stat(f.state.as_mut_ptr(), path.as_ptr(), info.as_mut_ptr()) }; + drop(f); + 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 mut f = fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_dir_open(f.state.as_mut_ptr(), &mut **dir, path.as_ptr()) }; + drop(f); + 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<()> { + let mut fs = fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_remove(fs.state.as_mut_ptr(), path.as_ptr()) }; + drop(fs); + 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<()> { + let mut fs = fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_rename(fs.state.as_mut_ptr(), from.as_ptr(), to.as_ptr()) }; + drop(fs); + io::check_ret(ret).map(drop) +} + +/// 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 mut fs = self.fs.handle().borrow_mut(); + let ret = + unsafe { ll::lfs_dir_read(fs.state.as_mut_ptr(), &mut **self.dir, info.as_mut_ptr()) }; + drop(fs); + + 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 mut fs = self.fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_dir_close(fs.state.as_mut_ptr(), &mut **self.dir) }; + drop(fs); + 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, +} + +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 it's fined because it's pinned + // in a box + state.config.buffer = state.cache.as_mut_ptr().cast(); + + let mut f = fs.handle().borrow_mut(); + let ret = unsafe { + ll::lfs_file_opencfg( + f.state.as_mut_ptr(), + &mut state.file, + path.as_ptr(), + self.0.bits() as i32, + &state.config, + ) + }; + drop(f); + + io::check_ret(ret)?; + Ok(File { fs, state }) + } +} + +/// An open file +pub struct File +where + FS: Filesystem, +{ + fs: FS, + // NOTE this must be freed only if `lfs_dir_close` was called successfully + state: ManuallyDrop>, +} + +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 { + 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 { + 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 the size of the file, in bytes, this metadata is for. + pub fn len(&mut self) -> io::Result { + let mut fs = self.fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_file_size(fs.state.as_mut_ptr(), &mut self.state.file) }; + drop(fs); + 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 mut fs = self.fs.handle().borrow_mut(); + let ret = unsafe { + ll::lfs_file_read( + fs.state.as_mut_ptr(), + &mut self.state.file, + buf.as_mut_ptr().cast(), + buf.len().try_into().unwrap_or(u32::max_value()), + ) + }; + drop(fs); + io::check_ret(ret).map(|sz| sz as usize) + } + + /// Synchronizes the file to disk + pub fn sync(&mut self) -> io::Result<()> { + let mut fs = self.fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_file_sync(fs.state.as_mut_ptr(), &mut self.state.file) }; + drop(fs); + 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 mut fs = self.fs.handle().borrow_mut(); + let ret = unsafe { + ll::lfs_file_write( + fs.state.as_mut_ptr(), + &mut self.state.file, + data.as_ptr().cast(), + data.len().try_into().unwrap_or(u32::max_value()), + ) + }; + drop(fs); + io::check_ret(ret).map(|sz| sz as usize) + } + + fn close_in_place(&mut self) -> io::Result<()> { + let mut fs = self.fs.handle().borrow_mut(); + let ret = unsafe { ll::lfs_file_close(fs.state.as_mut_ptr(), &mut self.state.file) }; + drop(fs); + 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.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; 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: &'static mut State, +} + +#[doc(hidden)] +impl Inner +where + S: Storage, +{ + pub fn new(config: &'static mut Config, state: &'static mut State) -> Self { + Self { state, config } + } + + pub fn format(&mut self) -> io::Result<()> { + let ret = unsafe { ll::lfs_format(self.state.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.as_mut_ptr(), self.config.as_ptr()) }; + io::check_ret(ret).map(drop) + } +} + +#[doc(hidden)] +pub struct Buffers { + lookahead: MaybeUninit<[u32; consts::LOOKAHEADWORDS_SIZE as usize]>, + read: MaybeUninit<[u8; consts::READ_SIZE as usize]>, + write: MaybeUninit<[u8; 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: consts::ATTRBYTES_MAX, + block_count: S::BLOCK_COUNT, + block_cycles: consts::BLOCK_CYCLES, + block_size: consts::BLOCK_SIZE, + cache_size: consts::CACHE_SIZE, + file_max: consts::FILEBYTES_MAX, + lookahead_size: 32 * consts::LOOKAHEADWORDS_SIZE, + name_max: consts::FILENAME_MAX_PLUS_ONE - 1, + prog_size: consts::WRITE_SIZE, + read_size: 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 + } + + 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 = 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) }; + + // TODO error handling? + storage.read(off, buf).unwrap(); + 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 = 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) }; + + // TODO error handling? + storage.write(off, buf).unwrap(); + 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 * consts::BLOCK_SIZE as usize; + + // TODO error handling? + storage.erase(off, consts::BLOCK_SIZE as usize).unwrap(); + 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..5c1fae7 --- /dev/null +++ b/common/littlefs/src/io.rs @@ -0,0 +1,61 @@ +//! 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 { + /// 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, + /// Unknown error occurred, integer code specified. + Unknown(i32), +} + +pub(crate) fn check_ret(ret: ll::lfs_error) -> Result { + match ret { + n if n >= 0 => Ok(n as u32), + // negative 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), + _ => Err(Error::Unknown(ret)), + } +} diff --git a/common/littlefs/src/lib.rs b/common/littlefs/src/lib.rs new file mode 100644 index 0000000..00db3f2 --- /dev/null +++ b/common/littlefs/src/lib.rs @@ -0,0 +1,108 @@ +//! 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); +//! +//! fn main() { +//! // 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" +); + +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct NotSendOrSync { + _inner: PhantomData<*mut ()>, +} + +#[doc(hidden)] +impl NotSendOrSync { + 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..c5f2b6c --- /dev/null +++ b/common/littlefs/src/mem.rs @@ -0,0 +1,33 @@ +use crate::fs; + +#[cfg(not(target_arch = "x86_64"))] +use heapless::pool; +pub use heapless::pool::singleton::Pool; +use heapless::pool::Node; + +#[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; diff --git a/common/littlefs/src/path.rs b/common/littlefs/src/path.rs new file mode 100644 index 0000000..cd6a1e2 --- /dev/null +++ b/common/littlefs/src/path.rs @@ -0,0 +1,261 @@ +//! 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` + pub unsafe fn from_bytes_with_nul_unchecked<'b>(bytes: &'b [u8]) -> &'b 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` + pub unsafe fn from_cstr_unchecked<'s>(cstr: &'s CStr) -> &'s 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) + } +} + +// 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 + "/" => { + 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..f1028ae --- /dev/null +++ b/common/littlefs/src/storage.rs @@ -0,0 +1,128 @@ +//! 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 + fn read(&self, off: usize, buf: &mut [u8]) -> io::Result; + + /// Write data to the storage device + fn write(&self, off: usize, data: &[u8]) -> io::Result; + + /// Erases data from the storage device + fn erase(&self, off: usize, len: usize) -> io::Result; +} + +/// 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]; + + Inner::new(&mut MEMORY) + } + } + + 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) } + } + } + }; +} + +#[doc(hidden)] +pub struct Inner { + data: &'static mut [u8], +} + +#[doc(hidden)] +impl Inner { + pub fn new(data: &'static mut [u8]) -> Self { + Self { data } + } + + 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(n) + } + + pub fn write(&mut self, offset: usize, data: &[u8]) -> io::Result { + 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(n) + } + + 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(len) + } +} diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index f9aa9db..61c1480 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]] @@ -269,13 +275,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]] @@ -338,26 +345,27 @@ dependencies = [ ] [[package]] -name = "littlefs2" -version = "0.1.0-alpha.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abdd4ddd5caedd104a0627e4abbe1c001a088fc3ce996a210ce5547193f542a3" +name = "littlefs" +version = "0.0.0" dependencies = [ + "ascii", "bitflags", - "cty", - "generic-array 0.13.2", + "c-stubs", + "cstr_core", + "cty 0.2.1", + "heapless", "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 +377,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 +402,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 +463,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 +601,7 @@ dependencies = [ "digest", "heapless", "imx6ul-pac", - "littlefs2", + "littlefs", "memlog", "rand_core", "typenum", 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.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-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/usbarmory/Cargo.toml b/firmware/usbarmory/Cargo.toml index fde6248..3c0509f 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"] # 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..f0afff8 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}; + +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 = 2; +const MAX_OPEN_FILES: usize = 4; /// Hardcoded filesystem block count. /// @@ -19,371 +24,29 @@ 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>, -} +const BLOCK_COUNT: u32 = 131_072; // 64 MiB / 512 (=block_size) -#[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 - } +filesystem!( + /// Filesystem backed by an eMMC + Fs, + Storage = MbrPartition, + max_open_files = MAX_OPEN_FILES, + read_dir_depth = READ_DIR_DEPTH +); - /// 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)?; - - 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; + const BLOCK_COUNT: u32 = 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; - - 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; } @@ -391,24 +54,20 @@ impl Storage for LfsStorage { Ok(buf.len()) } - 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 { + 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()) } - 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) } diff --git a/firmware/usbarmory/src/storage.rs b/firmware/usbarmory/src/storage.rs index 2ffcd08..0733809 100644 --- a/firmware/usbarmory/src/storage.rs +++ b/firmware/usbarmory/src/storage.rs @@ -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,11 +144,11 @@ 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 { + raw: self.raw, extent, }) } @@ -349,15 +329,13 @@ 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, } -impl<'a, D: ManagedBlockDevice> ManagedBlockDevice for MbrPartitionRef<'a, D> { +unsafe impl ManagedBlockDevice for MbrPartition { type Error = MbrError; fn total_blocks(&self) -> u64 { @@ -374,7 +352,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 +361,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) - } } From 939feda2cdf25c7f07855dc4269cefc8985274b1 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 16 Apr 2020 19:43:46 +0200 Subject: [PATCH 02/19] make clippy happy --- common/c-stubs/src/lib.rs | 4 +++- common/littlefs/src/fs.rs | 28 ++++++++++++++------------- common/littlefs/src/lib.rs | 38 +++++++++++++++++++------------------ common/littlefs/src/path.rs | 12 ++++++++++-- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/common/c-stubs/src/lib.rs b/common/c-stubs/src/lib.rs index 82c1640..560f142 100644 --- a/common/c-stubs/src/lib.rs +++ b/common/c-stubs/src/lib.rs @@ -15,10 +15,12 @@ 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 } +/// # 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 { diff --git a/common/littlefs/src/fs.rs b/common/littlefs/src/fs.rs index 338c25b..ca1188c 100644 --- a/common/littlefs/src/fs.rs +++ b/common/littlefs/src/fs.rs @@ -239,17 +239,15 @@ where if ret == 0 { None + } else if let Err(e) = io::check_ret(ret) { + Some(Err(e)) } 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), - }; + let info = unsafe { info.assume_init() }; + let entry = DirEntry { + metadata: Metadata::from_info(info), + }; - Some(Ok(entry)) - } + Some(Ok(entry)) } } } @@ -325,6 +323,8 @@ pub struct Metadata { 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 { @@ -375,13 +375,13 @@ pub enum FileType { impl FileType { /// Tests whether this file type represents a directory. - pub fn is_dir(&self) -> bool { - *self == FileType::Dir + 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 + pub fn is_file(self) -> bool { + self == FileType::File } } @@ -487,6 +487,8 @@ where state: ManuallyDrop>, } +// NOTE(allow) `std::fs` version does not have an `is_empty` method +#[allow(clippy::len_without_is_empty)] impl File where FS: Filesystem, diff --git a/common/littlefs/src/lib.rs b/common/littlefs/src/lib.rs index 00db3f2..5002374 100644 --- a/common/littlefs/src/lib.rs +++ b/common/littlefs/src/lib.rs @@ -27,30 +27,28 @@ //! // Filesystem on top of storage `S` //! filesystem!(F, Storage = S, max_open_files = 4, read_dir_depth = 2); //! -//! fn main() { -//! // claim ownership over the ram storage -//! let storage = S::claim().expect("Storage already claimed"); +//! // 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(); +//! // 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 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(); +//! // 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(); +//! // 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(); +//! // 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()); -//! } +//! // iterate over the contents of the root directory +//! for entry in fs::read_dir(f, b"/\0".try_into().unwrap()).unwrap() { +//! println!("{:?}", entry.unwrap()); //! } //! ``` //! @@ -100,6 +98,10 @@ pub struct NotSendOrSync { #[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/path.rs b/common/littlefs/src/path.rs index cd6a1e2..b84f2cc 100644 --- a/common/littlefs/src/path.rs +++ b/common/littlefs/src/path.rs @@ -25,7 +25,10 @@ impl Path { } /// Unchecked version of `from_bytes_with_nul` - pub unsafe fn from_bytes_with_nul_unchecked<'b>(bytes: &'b [u8]) -> &'b Self { + /// + /// # 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) } @@ -46,7 +49,10 @@ impl Path { } /// Unchecked version of `from_cstr` - pub unsafe fn from_cstr_unchecked<'s>(cstr: &'s CStr) -> &'s Self { + /// + /// # 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) } @@ -131,6 +137,8 @@ impl PathBuf { "" => 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; From 4717a177b7f0be690bd0d5da7e627d69bd23dfba Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 16 Apr 2020 19:49:49 +0200 Subject: [PATCH 03/19] GHA: run sanitizer tests --- .github/workflows/rust.yml | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8cf9f04..1922d10 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 + 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 From c46dc00a749967efd0825838c66f49c50ed05784 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 16 Apr 2020 19:53:58 +0200 Subject: [PATCH 04/19] print OK from sanitizer tests --- common/littlefs/examples/dir-close.rs | 2 ++ common/littlefs/examples/dir-forget-2.rs | 2 ++ common/littlefs/examples/dir-forget.rs | 2 ++ common/littlefs/examples/file-close.rs | 2 ++ common/littlefs/examples/file-forget.rs | 2 ++ 5 files changed, 10 insertions(+) diff --git a/common/littlefs/examples/dir-close.rs b/common/littlefs/examples/dir-close.rs index cf05d21..c8183f9 100644 --- a/common/littlefs/examples/dir-close.rs +++ b/common/littlefs/examples/dir-close.rs @@ -22,6 +22,8 @@ fn main() { foo(f); bar(f); + + println!("OK"); } #[inline(never)] diff --git a/common/littlefs/examples/dir-forget-2.rs b/common/littlefs/examples/dir-forget-2.rs index 5c8694c..dfdb33c 100644 --- a/common/littlefs/examples/dir-forget-2.rs +++ b/common/littlefs/examples/dir-forget-2.rs @@ -29,4 +29,6 @@ fn main() { 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 index 7514c9c..30b71a7 100644 --- a/common/littlefs/examples/dir-forget.rs +++ b/common/littlefs/examples/dir-forget.rs @@ -22,6 +22,8 @@ fn main() { foo(f); bar(f); + + println!("OK"); } #[inline(never)] diff --git a/common/littlefs/examples/file-close.rs b/common/littlefs/examples/file-close.rs index 83864dd..30c1e0f 100644 --- a/common/littlefs/examples/file-close.rs +++ b/common/littlefs/examples/file-close.rs @@ -18,6 +18,8 @@ fn main() { foo(f); bar(f); + + println!("OK"); } #[inline(never)] diff --git a/common/littlefs/examples/file-forget.rs b/common/littlefs/examples/file-forget.rs index 977c560..a848105 100644 --- a/common/littlefs/examples/file-forget.rs +++ b/common/littlefs/examples/file-forget.rs @@ -18,6 +18,8 @@ fn main() { foo(f); bar(f); + + println!("OK"); } #[inline(never)] From 81f713101ac89f6dd3b28b2662c2372187c0cc78 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 16 Apr 2020 19:55:10 +0200 Subject: [PATCH 05/19] GHA/san: do not optimize build dependencies --- common/Cargo.toml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/common/Cargo.toml b/common/Cargo.toml index 4485fa2..f9efe92 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,2 +1,21 @@ [workspace] -members = ["consts", "c-stubs", "littlefs"] \ 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 From 5a249c650a8043eb581f7724d0bc66975778f9b2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 16 Apr 2020 19:55:34 +0200 Subject: [PATCH 06/19] fix the working directory of msan tests --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1922d10..c9a57a0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -86,7 +86,7 @@ jobs: cargo r --features unsafe-x86 --target x86_64-unknown-linux-gnu --example file-forget --release - name: Run MSan - working-directory: ./common + working-directory: ./common/littlefs env: RUSTFLAGS: -Z sanitizer=memory run: | From 94ec8cec64b33980c84b0108bc61c141abe95439 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 17 Apr 2020 18:33:40 +0200 Subject: [PATCH 07/19] add fs::transaction --- common/Cargo.lock | 1 + common/littlefs/Cargo.toml | 1 + common/littlefs/src/fs.rs | 320 +++++++++++++----- common/littlefs/src/io.rs | 53 ++- common/littlefs/src/mem.rs | 81 ++++- common/littlefs/src/storage.rs | 15 +- firmware/Cargo.lock | 1 + .../examples/emmc-fs-transaction-err.rs | 62 ++++ .../examples/examples/emmc-fs-transaction.rs | 93 +++++ firmware/usbarmory/src/fs.rs | 26 +- firmware/usbarmory/src/storage.rs | 8 +- 11 files changed, 560 insertions(+), 101 deletions(-) create mode 100644 firmware/examples/examples/emmc-fs-transaction-err.rs create mode 100644 firmware/examples/examples/emmc-fs-transaction.rs diff --git a/common/Cargo.lock b/common/Cargo.lock index 013eec5..38a4a64 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -275,6 +275,7 @@ dependencies = [ "c-stubs", "cstr_core", "cty 0.2.1", + "generic-array 0.13.2", "heapless", "littlefs2-sys", ] diff --git a/common/littlefs/Cargo.toml b/common/littlefs/Cargo.toml index d2a4bdb..45237f6 100644 --- a/common/littlefs/Cargo.toml +++ b/common/littlefs/Cargo.toml @@ -9,6 +9,7 @@ version = "0.0.0" bitflags = "1.2.1" c-stubs = { path = "../c-stubs" } cty = "0.2.1" +generic-array = "0.13.2" heapless = "0.5.4" ll = { version = "0.1.4", package = "littlefs2-sys" } diff --git a/common/littlefs/src/fs.rs b/common/littlefs/src/fs.rs index ca1188c..78f8526 100644 --- a/common/littlefs/src/fs.rs +++ b/common/littlefs/src/fs.rs @@ -1,7 +1,7 @@ //! Filesystem operations use core::{ - cell::RefCell, + cell::{Cell, RefCell}, convert::TryInto, fmt, marker::PhantomData, @@ -10,11 +10,15 @@ use core::{ }; use bitflags::bitflags; -use heapless::pool::singleton::{Box, Pool}; +pub use heapless::consts; +use heapless::{ + pool::singleton::{Box, Pool}, + ArrayLength, +}; use crate::{ - consts, io, - mem::{D, F}, + io, + mem::{Arena, D, F}, path::{Path, PathBuf}, storage::Storage, }; @@ -30,7 +34,7 @@ pub unsafe trait Filesystem: Copy { type Storage: Storage + 'static; #[doc(hidden)] - fn handle(self) -> &'static RefCell>; + fn handle(self) -> &'static Inner; /// Mounts the filesystem /// @@ -64,12 +68,12 @@ macro_rules! filesystem { } impl $fs { - fn ptr() -> *mut core::cell::RefCell<$crate::fs::Inner<$storage>> { + fn ptr() -> *mut $crate::fs::Inner<$storage> { use core::{cell::RefCell, mem::MaybeUninit}; use $crate::fs::Inner; - static mut INNER: MaybeUninit>> = MaybeUninit::uninit(); + static mut INNER: MaybeUninit> = MaybeUninit::uninit(); unsafe { INNER.as_mut_ptr() } } @@ -105,7 +109,7 @@ macro_rules! filesystem { inner.format()?; } inner.mount()?; - Self::ptr().write(RefCell::new(inner)); + Self::ptr().write(inner); // add memory to the pools before the filesystem is used use $crate::mem::Pool as _; // grow exact method @@ -128,7 +132,7 @@ macro_rules! filesystem { unsafe impl $crate::fs::Filesystem for $fs { type Storage = $storage; - fn handle(self) -> &'static core::cell::RefCell<$crate::fs::Inner<$storage>> { + fn handle(self) -> &'static $crate::fs::Inner<$storage> { unsafe { &*Self::ptr() } } @@ -150,27 +154,31 @@ where } fn used_blocks(fs: impl Filesystem) -> io::Result { - let mut fs = fs.handle().borrow_mut(); + let mut state = fs.handle().state.borrow_mut(); // XXX does this really need a `*mut` pointer? - let ret = unsafe { ll::lfs_fs_size(fs.state.as_mut_ptr()) }; - drop(fs); + let ret = unsafe { ll::lfs_fs_size(state.as_mut_ptr()) }; + drop(state); io::check_ret(ret) } /// Creates a new, empty directory at the provided path pub fn create_dir(fs: impl Filesystem, path: &Path) -> io::Result<()> { - let mut fs = fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_mkdir(fs.state.as_mut_ptr(), path.as_ptr()) }; - drop(fs); + if fs.handle().transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } + + let mut state = fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_mkdir(state.as_mut_ptr(), path.as_ptr()) }; + drop(state); 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 f = fs.handle().borrow_mut(); + let mut state = fs.handle().state.borrow_mut(); let mut info = MaybeUninit::uninit(); - let ret = unsafe { ll::lfs_stat(f.state.as_mut_ptr(), path.as_ptr(), info.as_mut_ptr()) }; - drop(f); + let ret = unsafe { ll::lfs_stat(state.as_mut_ptr(), path.as_ptr(), info.as_mut_ptr()) }; + drop(state); io::check_ret(ret)?; Ok(Metadata::from_info(unsafe { info.assume_init() })) } @@ -186,29 +194,111 @@ where // FIXME(upstream) it should not be necessary to zero the allocation .init(unsafe { mem::zeroed() }), ); - let mut f = fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_dir_open(f.state.as_mut_ptr(), &mut **dir, path.as_ptr()) }; - drop(f); + let mut state = fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_dir_open(state.as_mut_ptr(), &mut **dir, path.as_ptr()) }; + drop(state); 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<()> { - let mut fs = fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_remove(fs.state.as_mut_ptr(), path.as_ptr()) }; - drop(fs); + if fs.handle().transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } + + let mut state = fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_remove(state.as_mut_ptr(), path.as_ptr()) }; + drop(state); 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<()> { - let mut fs = fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_rename(fs.state.as_mut_ptr(), from.as_ptr(), to.as_ptr()) }; - drop(fs); + if fs.handle().transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } + + let mut state = fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_rename(state.as_mut_ptr(), from.as_ptr(), to.as_ptr()) }; + drop(state); io::check_ret(ret).map(drop) } +/// Starts a filesystem 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>, +{ + let handle = fs.handle(); + if handle.transaction_mode.get() { + Err(io::Error::TransactionInProgress) + } else if handle.open_files.get() != 0 { + Err(io::Error::OpenFilesExist) + } else { + handle.storage().lock(); + handle.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.handle().storage().unlock(); + for f in self.arena.into_iter() { + f.close()?; + } + self.fs.handle().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 @@ -232,10 +322,10 @@ where fn next(&mut self) -> Option { let mut info = MaybeUninit::::uninit(); - let mut fs = self.fs.handle().borrow_mut(); + let mut state = self.fs.handle().state.borrow_mut(); let ret = - unsafe { ll::lfs_dir_read(fs.state.as_mut_ptr(), &mut **self.dir, info.as_mut_ptr()) }; - drop(fs); + unsafe { ll::lfs_dir_read(state.as_mut_ptr(), &mut **self.dir, info.as_mut_ptr()) }; + drop(state); if ret == 0 { None @@ -265,9 +355,9 @@ where } fn close_in_place(&mut self) -> io::Result<()> { - let mut fs = self.fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_dir_close(fs.state.as_mut_ptr(), &mut **self.dir) }; - drop(fs); + let mut state = self.fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_dir_close(state.as_mut_ptr(), &mut **self.dir) }; + drop(state); 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) } @@ -456,28 +546,33 @@ impl OpenOptions { // 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 it's fined because it's pinned + // 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(); - let mut f = fs.handle().borrow_mut(); + let mut fsstate = fs.handle().state.borrow_mut(); let ret = unsafe { ll::lfs_file_opencfg( - f.state.as_mut_ptr(), + fsstate.as_mut_ptr(), &mut state.file, path.as_ptr(), self.0.bits() as i32, &state.config, ) }; - drop(f); + drop(fsstate); io::check_ret(ret)?; + fs.handle().incr(); Ok(File { fs, state }) } } /// 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, @@ -497,16 +592,29 @@ where /// /// 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 { - OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(fs, path) + if fs.handle().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 { - OpenOptions::default().read(true).open(fs, path) + Self::checked_open(fs, path, true) + } + + fn checked_open(fs: FS, path: &Path, check: bool) -> io::Result { + if check && fs.handle().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. @@ -520,32 +628,32 @@ where /// Returns the size of the file, in bytes, this metadata is for. pub fn len(&mut self) -> io::Result { - let mut fs = self.fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_file_size(fs.state.as_mut_ptr(), &mut self.state.file) }; - drop(fs); + let mut state = self.fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_file_size(state.as_mut_ptr(), &mut self.state.file) }; + drop(state); 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 mut fs = self.fs.handle().borrow_mut(); + let mut state = self.fs.handle().state.borrow_mut(); let ret = unsafe { ll::lfs_file_read( - fs.state.as_mut_ptr(), + state.as_mut_ptr(), &mut self.state.file, buf.as_mut_ptr().cast(), buf.len().try_into().unwrap_or(u32::max_value()), ) }; - drop(fs); + drop(state); io::check_ret(ret).map(|sz| sz as usize) } /// Synchronizes the file to disk pub fn sync(&mut self) -> io::Result<()> { - let mut fs = self.fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_file_sync(fs.state.as_mut_ptr(), &mut self.state.file) }; - drop(fs); + let mut state = self.fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_file_sync(state.as_mut_ptr(), &mut self.state.file) }; + drop(state); io::check_ret(ret).map(drop) } @@ -553,26 +661,27 @@ where /// /// To synchronize the file to disk call the `sync` method pub fn write(&mut self, data: &[u8]) -> io::Result { - let mut fs = self.fs.handle().borrow_mut(); + let mut state = self.fs.handle().state.borrow_mut(); let ret = unsafe { ll::lfs_file_write( - fs.state.as_mut_ptr(), + state.as_mut_ptr(), &mut self.state.file, data.as_ptr().cast(), data.len().try_into().unwrap_or(u32::max_value()), ) }; - drop(fs); + drop(state); io::check_ret(ret).map(|sz| sz as usize) } fn close_in_place(&mut self) -> io::Result<()> { - let mut fs = self.fs.handle().borrow_mut(); - let ret = unsafe { ll::lfs_file_close(fs.state.as_mut_ptr(), &mut self.state.file) }; - drop(fs); + let mut state = self.fs.handle().state.borrow_mut(); + let ret = unsafe { ll::lfs_file_close(state.as_mut_ptr(), &mut self.state.file) }; + drop(state); 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.state) } + self.fs.handle().decr(); Ok(()) } } @@ -588,7 +697,7 @@ where #[doc(hidden)] pub struct FileState { - cache: [u8; consts::CACHE_SIZE as usize], + cache: [u8; crate::consts::CACHE_SIZE as usize], config: ll::lfs_file_config, file: ll::lfs_file_t, } @@ -599,7 +708,11 @@ where S: 'static + Storage, { config: &'static Config, - state: &'static mut State, + state: RefCell<&'static mut State>, + /// Number of files currently open + open_files: Cell, + /// Whether a `Transaction` is active + transaction_mode: Cell, } #[doc(hidden)] @@ -608,25 +721,52 @@ where S: Storage, { pub fn new(config: &'static mut Config, state: &'static mut State) -> Self { - Self { state, config } + 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.as_mut_ptr(), self.config.as_ptr()) }; + 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.as_mut_ptr(), self.config.as_ptr()) }; + 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; consts::LOOKAHEADWORDS_SIZE as usize]>, - read: MaybeUninit<[u8; consts::READ_SIZE as usize]>, - write: MaybeUninit<[u8; consts::WRITE_SIZE as usize]>, + 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)] @@ -681,16 +821,16 @@ where erase: Some(Self::lfs_config_erase), sync: Some(Self::lfs_config_sync), - attr_max: consts::ATTRBYTES_MAX, + attr_max: crate::consts::ATTRBYTES_MAX, block_count: S::BLOCK_COUNT, - block_cycles: consts::BLOCK_CYCLES, - block_size: consts::BLOCK_SIZE, - cache_size: consts::CACHE_SIZE, - file_max: consts::FILEBYTES_MAX, - lookahead_size: 32 * consts::LOOKAHEADWORDS_SIZE, - name_max: consts::FILENAME_MAX_PLUS_ONE - 1, - prog_size: consts::WRITE_SIZE, - read_size: consts::READ_SIZE, + 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(), @@ -713,13 +853,15 @@ where ) -> cty::c_int { let storage = unsafe { &*((*config).context as *const S) }; - let block_size = consts::BLOCK_SIZE as u32; + 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) }; - // TODO error handling? - storage.read(off, buf).unwrap(); - 0 + if let Err(e) = storage.read(off, buf) { + e.into_i32() + } else { + 0 + } } extern "C" fn lfs_config_prog( @@ -731,13 +873,15 @@ where ) -> cty::c_int { let storage = unsafe { &*((*config).context as *const S) }; - let block_size = consts::BLOCK_SIZE as u32; + 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) }; - // TODO error handling? - storage.write(off, buf).unwrap(); - 0 + 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 @@ -747,11 +891,13 @@ where block: ll::lfs_block_t, ) -> cty::c_int { let storage = unsafe { &mut *((*config).context as *mut S) }; - let off = block as usize * consts::BLOCK_SIZE as usize; + let off = block as usize * crate::consts::BLOCK_SIZE as usize; - // TODO error handling? - storage.erase(off, consts::BLOCK_SIZE as usize).unwrap(); - 0 + 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 diff --git a/common/littlefs/src/io.rs b/common/littlefs/src/io.rs index 5c1fae7..e509372 100644 --- a/common/littlefs/src/io.rs +++ b/common/littlefs/src/io.rs @@ -6,6 +6,7 @@ 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. @@ -34,14 +35,59 @@ pub enum Error { 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), - // negative codes + // 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), @@ -56,6 +102,11 @@ pub(crate) fn check_ret(ret: ll::lfs_error) -> Result { 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/mem.rs b/common/littlefs/src/mem.rs index c5f2b6c..e8390d7 100644 --- a/common/littlefs/src/mem.rs +++ b/common/littlefs/src/mem.rs @@ -1,10 +1,16 @@ -use crate::fs; +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) => { @@ -31,3 +37,76 @@ 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/storage.rs b/common/littlefs/src/storage.rs index f1028ae..9ce14d0 100644 --- a/common/littlefs/src/storage.rs +++ b/common/littlefs/src/storage.rs @@ -13,13 +13,22 @@ pub unsafe trait Storage { const BLOCK_COUNT: u32; /// Reads data from the storage device - fn read(&self, off: usize, buf: &mut [u8]) -> io::Result; + // 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 - fn write(&self, off: usize, data: &[u8]) -> io::Result; + // 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 - fn erase(&self, off: usize, len: usize) -> io::Result; + // 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 diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 61c1480..13bceab 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "c-stubs", "cstr_core", "cty 0.2.1", + "generic-array 0.13.2", "heapless", "littlefs2-sys", ] 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/usbarmory/src/fs.rs b/firmware/usbarmory/src/fs.rs index f0afff8..1f5cc3e 100644 --- a/firmware/usbarmory/src/fs.rs +++ b/firmware/usbarmory/src/fs.rs @@ -5,8 +5,8 @@ //! - `firmware/usbarmory/src/fs.rs` //! - `common/littlefs/src/consts.rs` -use littlefs::{filesystem, io, storage::Storage}; pub use littlefs::{fs::*, io::Error}; +use littlefs::{filesystem, io, storage::Storage}; use crate::{ emmc::eMMC, @@ -41,7 +41,7 @@ where // FIXME: This really shouldn't be a constant. const BLOCK_COUNT: u32 = BLOCK_COUNT; - fn read(&self, off: usize, buf: &mut [u8]) -> io::Result { + fn read(&self, off: usize, buf: &mut [u8]) -> io::Result<()> { let mut lba = off / usize::from(BLOCK_SIZE); let mut block = Block::zeroed(); @@ -51,10 +51,14 @@ where lba += 1; } - Ok(buf.len()) + Ok(()) } - fn write(&self, off: usize, data: &[u8]) -> io::Result { + 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(); @@ -64,11 +68,19 @@ where lba += 1; } - Ok(data.len()) + Ok(()) } - fn erase(&self, _off: usize, len: usize) -> 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 0733809..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}; @@ -148,8 +148,9 @@ impl MbrDevice { let extent = self.part_extent(part)?; Ok(MbrPartition { - raw: self.raw, extent, + lock: Cell::new(false), + raw: self.raw, }) } @@ -333,6 +334,9 @@ impl fmt::Display for MbrError { pub struct MbrPartition { raw: D, extent: PartExtent, + // only used when the `fs` feature is enabled + #[allow(dead_code)] + pub(crate) lock: Cell, } unsafe impl ManagedBlockDevice for MbrPartition { From 590476bc55339b6faa08efd056521c082c15c92d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 17 Apr 2020 18:35:57 +0200 Subject: [PATCH 08/19] cargo fmt --- firmware/usbarmory/src/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/usbarmory/src/fs.rs b/firmware/usbarmory/src/fs.rs index 1f5cc3e..9900852 100644 --- a/firmware/usbarmory/src/fs.rs +++ b/firmware/usbarmory/src/fs.rs @@ -5,8 +5,8 @@ //! - `firmware/usbarmory/src/fs.rs` //! - `common/littlefs/src/consts.rs` -pub use littlefs::{fs::*, io::Error}; use littlefs::{filesystem, io, storage::Storage}; +pub use littlefs::{fs::*, io::Error}; use crate::{ emmc::eMMC, From 04120381d49b492900c4380c9ec9d2264638d149 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 17 Apr 2020 18:58:27 +0200 Subject: [PATCH 09/19] add File::seek API --- common/littlefs/src/fs.rs | 42 +++++++++++++++++ firmware/examples/Cargo.toml | 12 +++++ firmware/examples/examples/emmc-fs5.rs | 62 ++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 firmware/examples/examples/emmc-fs5.rs diff --git a/common/littlefs/src/fs.rs b/common/littlefs/src/fs.rs index 78f8526..46ba377 100644 --- a/common/littlefs/src/fs.rs +++ b/common/littlefs/src/fs.rs @@ -568,6 +568,33 @@ impl OpenOptions { } } +/// 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 @@ -649,6 +676,21 @@ where 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 mut state = self.fs.handle().state.borrow_mut(); + let ret = unsafe { + ll::lfs_file_seek( + state.as_mut_ptr(), + &mut self.state.file, + pos.off(), + pos.whence() as i32, + ) + }; + drop(state); + io::check_ret(ret).map(|off| off as usize) + } + /// Synchronizes the file to disk pub fn sync(&mut self) -> io::Result<()> { let mut state = self.fs.handle().state.borrow_mut(); diff --git a/firmware/examples/Cargo.toml b/firmware/examples/Cargo.toml index db89cca..4aec6d8 100644 --- a/firmware/examples/Cargo.toml +++ b/firmware/examples/Cargo.toml @@ -21,10 +21,22 @@ 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"] + [dependencies] block-cipher-trait = "0.6.2" consts = { path = "../../common/consts" } diff --git a/firmware/examples/examples/emmc-fs5.rs b/firmware/examples/examples/emmc-fs5.rs new file mode 100644 index 0000000..4ef828c --- /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::{self, 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!() +} From 51b36bcbaffb34b4bcac035ae9fdf3d933a770a6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 17 Apr 2020 19:15:10 +0200 Subject: [PATCH 10/19] impl PartialEq for Path --- common/littlefs/src/path.rs | 6 ++ firmware/examples/Cargo.toml | 4 ++ .../examples/examples/emmc-fs-read-dir.rs | 64 +++++++++++++++++++ firmware/usbarmory/src/fs.rs | 4 +- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 firmware/examples/examples/emmc-fs-read-dir.rs diff --git a/common/littlefs/src/path.rs b/common/littlefs/src/path.rs index b84f2cc..00a2cab 100644 --- a/common/littlefs/src/path.rs +++ b/common/littlefs/src/path.rs @@ -96,6 +96,12 @@ impl<'b> TryFrom<&'b [u8]> for &'b Path { } } +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),+) => { diff --git a/firmware/examples/Cargo.toml b/firmware/examples/Cargo.toml index 4aec6d8..48a45f1 100644 --- a/firmware/examples/Cargo.toml +++ b/firmware/examples/Cargo.toml @@ -37,6 +37,10 @@ required-features = ["fs"] name = "emmc-fs-transaction-err" required-features = ["fs"] +[[example]] +name = "emmc-fs-read-dir" +required-features = ["fs"] + [dependencies] block-cipher-trait = "0.6.2" consts = { path = "../../common/consts" } 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/usbarmory/src/fs.rs b/firmware/usbarmory/src/fs.rs index 9900852..afd2844 100644 --- a/firmware/usbarmory/src/fs.rs +++ b/firmware/usbarmory/src/fs.rs @@ -6,7 +6,7 @@ //! - `common/littlefs/src/consts.rs` use littlefs::{filesystem, io, storage::Storage}; -pub use littlefs::{fs::*, io::Error}; +pub use littlefs::{fs::*, io::Error, path::Path}; use crate::{ emmc::eMMC, @@ -14,7 +14,7 @@ use crate::{ }; // NOTE end-users should only modify these constants -const READ_DIR_DEPTH: usize = 2; +const READ_DIR_DEPTH: usize = 4; const MAX_OPEN_FILES: usize = 4; /// Hardcoded filesystem block count. From 43e4ba9e8ab84aeb63db546165fad9b8256d5f16 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 17 Apr 2020 19:18:02 +0200 Subject: [PATCH 11/19] add File::fs API --- common/littlefs/src/fs.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/littlefs/src/fs.rs b/common/littlefs/src/fs.rs index 46ba377..6f969be 100644 --- a/common/littlefs/src/fs.rs +++ b/common/littlefs/src/fs.rs @@ -227,6 +227,8 @@ pub fn rename(fs: impl Filesystem, from: &Path, to: &Path) -> io::Result<()> { /// 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 @@ -653,6 +655,11 @@ where 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 mut state = self.fs.handle().state.borrow_mut(); From e5b20412fa6b01c78e5ce68eb1f3d57f0a03dd4d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 17 Apr 2020 19:26:30 +0200 Subject: [PATCH 12/19] fix storage! macro --- common/littlefs/src/storage.rs | 46 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/common/littlefs/src/storage.rs b/common/littlefs/src/storage.rs index 9ce14d0..3385694 100644 --- a/common/littlefs/src/storage.rs +++ b/common/littlefs/src/storage.rs @@ -66,25 +66,34 @@ macro_rules! storage { 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) + 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 { + 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 { + 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 { + 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() } + } } }; } @@ -92,15 +101,16 @@ macro_rules! storage { #[doc(hidden)] pub struct Inner { data: &'static mut [u8], + locked: &'static mut bool, } #[doc(hidden)] impl Inner { - pub fn new(data: &'static mut [u8]) -> Self { - Self { data } + 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 { + 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); @@ -108,10 +118,14 @@ impl Inner { let n = buf.len(); buf.copy_from_slice(&self.data[offset..offset + n]); - Ok(n) + Ok(()) } - pub fn write(&mut self, offset: usize, data: &[u8]) -> io::Result { + 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); @@ -119,10 +133,10 @@ impl Inner { let n = data.len(); self.data[offset..offset + n].copy_from_slice(data); - Ok(n) + Ok(()) } - pub fn erase(&mut self, offset: usize, len: usize) -> io::Result { + pub fn erase(&mut self, offset: usize, len: usize) -> io::Result<()> { const ERASE_VALUE: u8 = 0xFF; let block_size = consts::BLOCK_SIZE as usize; @@ -132,6 +146,14 @@ impl Inner { for byte in self.data[offset..offset + len].iter_mut() { *byte = ERASE_VALUE; } - Ok(len) + Ok(()) + } + + pub fn lock(&mut self) { + *self.locked = true; + } + + pub fn unlock(&mut self) { + *self.locked = false; } } From 9678528d8a4ef6e251893ee59b257b8c65587b5a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 20 Apr 2020 19:32:04 +0200 Subject: [PATCH 13/19] make fs API interrupt safe --- common/Cargo.lock | 5 + common/littlefs/Cargo.toml | 3 + common/littlefs/src/fs.rs | 279 +++++++++++++---------- common/littlefs/src/lib.rs | 44 ++++ firmware/Cargo.lock | 1 + firmware/examples/Cargo.toml | 4 + firmware/examples/examples/rtfm-10-fs.rs | 84 +++++++ firmware/usbarmory/Cargo.toml | 2 +- 8 files changed, 300 insertions(+), 122 deletions(-) create mode 100644 firmware/examples/examples/rtfm-10-fs.rs diff --git a/common/Cargo.lock b/common/Cargo.lock index 38a4a64..53abfda 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -140,6 +140,10 @@ dependencies = [ name = "consts" version = "0.0.0" +[[package]] +name = "cortex-a" +version = "0.0.0" + [[package]] name = "cstr_core" version = "0.1.0" @@ -273,6 +277,7 @@ dependencies = [ "ascii", "bitflags", "c-stubs", + "cortex-a", "cstr_core", "cty 0.2.1", "generic-array 0.13.2", diff --git a/common/littlefs/Cargo.toml b/common/littlefs/Cargo.toml index 45237f6..2ce2c6f 100644 --- a/common/littlefs/Cargo.toml +++ b/common/littlefs/Cargo.toml @@ -8,6 +8,7 @@ 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" @@ -37,4 +38,6 @@ default-features = false 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/src/fs.rs b/common/littlefs/src/fs.rs index 6f969be..50dc38a 100644 --- a/common/littlefs/src/fs.rs +++ b/common/littlefs/src/fs.rs @@ -34,7 +34,7 @@ pub unsafe trait Filesystem: Copy { type Storage: Storage + 'static; #[doc(hidden)] - fn handle(self) -> &'static Inner; + fn lock(self, f: impl FnOnce(&Inner) -> T) -> T; /// Mounts the filesystem /// @@ -64,7 +64,7 @@ macro_rules! filesystem { $(#[$attr])* #[derive(Clone, Copy)] pub struct $fs { - _inner: $crate::NotSendOrSync, + _inner: $crate::Private, } impl $fs { @@ -88,7 +88,7 @@ macro_rules! filesystem { use $crate::{ fs::{Buffers, Config, Inner, State}, - NotSendOrSync, + Private, }; // NOTE(unsafe) this section is executed at most once because `storage` is an owned @@ -112,7 +112,7 @@ macro_rules! filesystem { Self::ptr().write(inner); // add memory to the pools before the filesystem is used - use $crate::mem::Pool as _; // grow exact method + use $crate::mem::Pool as _; // grow_exact method static mut MD: MaybeUninit<[$crate::mem::DNode; $read_dir_depth]> = MaybeUninit::uninit(); @@ -123,7 +123,7 @@ macro_rules! filesystem { $crate::mem::F::grow_exact(&mut MF); Ok($fs { - _inner: NotSendOrSync::new(), + _inner: Private::new(), }) } } @@ -132,8 +132,10 @@ macro_rules! filesystem { unsafe impl $crate::fs::Filesystem for $fs { type Storage = $storage; - fn handle(self) -> &'static $crate::fs::Inner<$storage> { - unsafe { &*Self::ptr() } + 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 { @@ -154,31 +156,34 @@ where } fn used_blocks(fs: impl Filesystem) -> io::Result { - let mut state = fs.handle().state.borrow_mut(); - // XXX does this really need a `*mut` pointer? - let ret = unsafe { ll::lfs_fs_size(state.as_mut_ptr()) }; - drop(state); + 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<()> { - if fs.handle().transaction_mode.get() { - return Err(io::Error::TransactionInProgress); - } + fs.lock(|inner| { + if inner.transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } - let mut state = fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_mkdir(state.as_mut_ptr(), path.as_ptr()) }; - drop(state); - io::check_ret(ret).map(drop) + 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 state = fs.handle().state.borrow_mut(); let mut info = MaybeUninit::uninit(); - let ret = unsafe { ll::lfs_stat(state.as_mut_ptr(), path.as_ptr(), info.as_mut_ptr()) }; - drop(state); + 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() })) } @@ -194,35 +199,38 @@ where // FIXME(upstream) it should not be necessary to zero the allocation .init(unsafe { mem::zeroed() }), ); - let mut state = fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_dir_open(state.as_mut_ptr(), &mut **dir, path.as_ptr()) }; - drop(state); + 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<()> { - if fs.handle().transaction_mode.get() { - return Err(io::Error::TransactionInProgress); - } + fs.lock(|inner| { + if inner.transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } - let mut state = fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_remove(state.as_mut_ptr(), path.as_ptr()) }; - drop(state); - io::check_ret(ret).map(drop) + 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<()> { - if fs.handle().transaction_mode.get() { - return Err(io::Error::TransactionInProgress); - } + fs.lock(|inner| { + if inner.transaction_mode.get() { + return Err(io::Error::TransactionInProgress); + } - let mut state = fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_rename(state.as_mut_ptr(), from.as_ptr(), to.as_ptr()) }; - drop(state); - io::check_ret(ret).map(drop) + 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 @@ -249,19 +257,20 @@ where F: Filesystem, N: ArrayLength>, { - let handle = fs.handle(); - if handle.transaction_mode.get() { - Err(io::Error::TransactionInProgress) - } else if handle.open_files.get() != 0 { - Err(io::Error::OpenFilesExist) - } else { - handle.storage().lock(); - handle.transaction_mode.set(true); - Ok(Transaction { - arena: Arena::new(), - fs, - }) - } + 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 @@ -292,12 +301,15 @@ where /// Commits all cached writes to disk pub fn commit(self) -> io::Result<()> { - self.fs.handle().storage().unlock(); - for f in self.arena.into_iter() { - f.close()?; - } - self.fs.handle().transaction_mode.set(false); - Ok(()) + 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(()) + }) } } @@ -324,10 +336,10 @@ where fn next(&mut self) -> Option { let mut info = MaybeUninit::::uninit(); - let mut state = self.fs.handle().state.borrow_mut(); - let ret = - unsafe { ll::lfs_dir_read(state.as_mut_ptr(), &mut **self.dir, info.as_mut_ptr()) }; - drop(state); + 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 @@ -357,9 +369,10 @@ where } fn close_in_place(&mut self) -> io::Result<()> { - let mut state = self.fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_dir_close(state.as_mut_ptr(), &mut **self.dir) }; - drop(state); + 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) } @@ -552,20 +565,24 @@ impl OpenOptions { // in a box state.config.buffer = state.cache.as_mut_ptr().cast(); - let mut fsstate = fs.handle().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); + 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(()) + })?; - io::check_ret(ret)?; - fs.handle().incr(); Ok(File { fs, state }) } } @@ -621,15 +638,17 @@ where /// /// 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 { - if fs.handle().transaction_mode.get() { - Err(io::Error::TransactionInProgress) - } else { - OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(fs, path) - } + 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. @@ -638,12 +657,14 @@ where } fn checked_open(fs: FS, path: &Path, check: bool) -> io::Result { - if check && fs.handle().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) - } + 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. @@ -662,47 +683,48 @@ where /// Returns the size of the file, in bytes, this metadata is for. pub fn len(&mut self) -> io::Result { - let mut state = self.fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_file_size(state.as_mut_ptr(), &mut self.state.file) }; - drop(state); + 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 mut state = self.fs.handle().state.borrow_mut(); - let ret = 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()), - ) - }; - drop(state); + 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 mut state = self.fs.handle().state.borrow_mut(); - let ret = unsafe { + 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, ) - }; - drop(state); + }); io::check_ret(ret).map(|off| off as usize) } /// Synchronizes the file to disk pub fn sync(&mut self) -> io::Result<()> { - let mut state = self.fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_file_sync(state.as_mut_ptr(), &mut self.state.file) }; - drop(state); + 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) } @@ -710,27 +732,28 @@ where /// /// To synchronize the file to disk call the `sync` method pub fn write(&mut self, data: &[u8]) -> io::Result { - let mut state = self.fs.handle().state.borrow_mut(); - let ret = unsafe { + 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()), ) - }; - drop(state); + }); io::check_ret(ret).map(|sz| sz as usize) } fn close_in_place(&mut self) -> io::Result<()> { - let mut state = self.fs.handle().state.borrow_mut(); - let ret = unsafe { ll::lfs_file_close(state.as_mut_ptr(), &mut self.state.file) }; - drop(state); - io::check_ret(ret)?; + 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) } - self.fs.handle().decr(); Ok(()) } } @@ -893,6 +916,20 @@ where &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, diff --git a/common/littlefs/src/lib.rs b/common/littlefs/src/lib.rs index 5002374..d86864d 100644 --- a/common/littlefs/src/lib.rs +++ b/common/littlefs/src/lib.rs @@ -90,6 +90,50 @@ 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 { diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 13bceab..89afa0d 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -351,6 +351,7 @@ dependencies = [ "ascii", "bitflags", "c-stubs", + "cortex-a", "cstr_core", "cty 0.2.1", "generic-array 0.13.2", diff --git a/firmware/examples/Cargo.toml b/firmware/examples/Cargo.toml index 48a45f1..e448c8a 100644 --- a/firmware/examples/Cargo.toml +++ b/firmware/examples/Cargo.toml @@ -41,6 +41,10 @@ required-features = ["fs"] 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/rtfm-10-fs.rs b/firmware/examples/examples/rtfm-10-fs.rs new file mode 100644 index 0000000..95a681a --- /dev/null +++ b/firmware/examples/examples/rtfm-10-fs.rs @@ -0,0 +1,84 @@ +//! 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. + +#![deny(unsafe_code)] +#![deny(warnings)] +#![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}, + println, + storage::MbrDevice, +}; + +#[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 } + } + + #[idle(resources = [&f], spawn = [foo, bar])] + fn idle(cx: idle::Context) -> ! { + let f = *cx.resources.f; + + let mut first = true; + for ent in fs::read_dir(f, b"/\0".try_into().unwrap()).unwrap() { + let ent = ent.unwrap(); + + if first { + first = false; + // these tasks will preempt `idle` + cx.spawn.foo().unwrap(); + cx.spawn.bar().unwrap(); + } + + println!("[idle] {:?}", ent); + } + + usbarmory::reset() + } + + // interrupts `idle`, who's walking over the contents of the `/` directory + #[task(resources = [&f])] + fn foo(cx: foo::Context) { + let f = *cx.resources.f; + + let mut file = File::create(f, b"foo.txt\0".try_into().unwrap()).unwrap(); + file.write(b"Hello!").unwrap(); + file.close().unwrap(); + + println!("[foo] created file foo.txt"); + } + + // 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]"); + } +}; diff --git a/firmware/usbarmory/Cargo.toml b/firmware/usbarmory/Cargo.toml index 3c0509f..7385a48 100644 --- a/firmware/usbarmory/Cargo.toml +++ b/firmware/usbarmory/Cargo.toml @@ -38,7 +38,7 @@ path = "../imx6ul-pac" path = "../usbarmory-rt" [features] -fs = ["littlefs"] +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"] From ebb9732ccd49adde4a67a9d8a59ce8cff19e5c13 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 20 Apr 2020 19:32:18 +0200 Subject: [PATCH 14/19] rtfm/codegen: fix name collision when a resource is named `f` --- firmware/cortex-a-rtfm/macros/src/codegen/util.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, ) } } From 5e701c613222477ac4df096d6d44c6a2e9fbb21d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 20 Apr 2020 19:33:21 +0200 Subject: [PATCH 15/19] fix warning --- firmware/examples/examples/emmc-fs5.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/examples/examples/emmc-fs5.rs b/firmware/examples/examples/emmc-fs5.rs index 4ef828c..ded5815 100644 --- a/firmware/examples/examples/emmc-fs5.rs +++ b/firmware/examples/examples/emmc-fs5.rs @@ -13,7 +13,7 @@ use exception_reset as _; // default exception handler use panic_serial as _; // panic handler use usbarmory::{ emmc::eMMC, - fs::{self, File, Fs, SeekFrom}, + fs::{File, Fs, SeekFrom}, memlog, memlog_flush_and_reset, storage::MbrDevice, }; From 77ded47abbd8ecc5e461cc8916dd01389e675ca2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 20 Apr 2020 19:46:23 +0200 Subject: [PATCH 16/19] tweak example --- firmware/examples/examples/rtfm-10-fs.rs | 35 ++++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/firmware/examples/examples/rtfm-10-fs.rs b/firmware/examples/examples/rtfm-10-fs.rs index 95a681a..e7028bf 100644 --- a/firmware/examples/examples/rtfm-10-fs.rs +++ b/firmware/examples/examples/rtfm-10-fs.rs @@ -6,6 +6,17 @@ //! 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)] @@ -42,16 +53,21 @@ const APP: () = { 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])] fn idle(cx: idle::Context) -> ! { - let f = *cx.resources.f; - - let mut first = true; - for ent in fs::read_dir(f, b"/\0".try_into().unwrap()).unwrap() { + // 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 first { - first = false; + if i == 1 { // these tasks will preempt `idle` cx.spawn.foo().unwrap(); cx.spawn.bar().unwrap(); @@ -63,10 +79,11 @@ const APP: () = { usbarmory::reset() } - // interrupts `idle`, who's walking over the contents of the `/` directory + // this task interrupts `idle`, who's walking over the contents of the `/` directory #[task(resources = [&f])] fn foo(cx: foo::Context) { - let f = *cx.resources.f; + // makes a copy of the `Fs` handle + let f: Fs = *cx.resources.f; let mut file = File::create(f, b"foo.txt\0".try_into().unwrap()).unwrap(); file.write(b"Hello!").unwrap(); @@ -79,6 +96,6 @@ const APP: () = { // (resource `f`) #[task] fn bar(_cx: bar::Context) { - println!("[bar]"); + println!("[bar] no FS access"); } }; From bdabaf5c63f14d27725d53e9b64d4f5e40ecd308 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 20 Apr 2020 20:07:02 +0200 Subject: [PATCH 17/19] Files can be send between tasks --- common/littlefs/src/fs.rs | 5 ++++ firmware/examples/examples/rtfm-10-fs.rs | 32 ++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/common/littlefs/src/fs.rs b/common/littlefs/src/fs.rs index 50dc38a..d5a7594 100644 --- a/common/littlefs/src/fs.rs +++ b/common/littlefs/src/fs.rs @@ -628,6 +628,11 @@ where 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 diff --git a/firmware/examples/examples/rtfm-10-fs.rs b/firmware/examples/examples/rtfm-10-fs.rs index e7028bf..d865b4e 100644 --- a/firmware/examples/examples/rtfm-10-fs.rs +++ b/firmware/examples/examples/rtfm-10-fs.rs @@ -23,17 +23,21 @@ #![no_main] #![no_std] -use core::convert::TryInto; +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}, + 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 { @@ -55,7 +59,7 @@ const APP: () = { // 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])] + #[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; @@ -76,6 +80,13 @@ const APP: () = { 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() } @@ -85,11 +96,12 @@ const APP: () = { // makes a copy of the `Fs` handle let f: Fs = *cx.resources.f; - let mut file = File::create(f, b"foo.txt\0".try_into().unwrap()).unwrap(); + let filename = filename(); + let mut file = File::create(f, filename).unwrap(); file.write(b"Hello!").unwrap(); file.close().unwrap(); - println!("[foo] created file foo.txt"); + println!("[foo] created file {}", filename); } // this task cannot perform FS operations because it doesn't have access to the `Fs` handle @@ -98,4 +110,14 @@ const APP: () = { 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); + } }; From 2ef4c038265d7705abcbed411b1fed2ba598f6be Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 20 Apr 2020 20:08:48 +0200 Subject: [PATCH 18/19] cargo fmt --- firmware/examples/examples/rtfm-10-fs.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/firmware/examples/examples/rtfm-10-fs.rs b/firmware/examples/examples/rtfm-10-fs.rs index d865b4e..210928d 100644 --- a/firmware/examples/examples/rtfm-10-fs.rs +++ b/firmware/examples/examples/rtfm-10-fs.rs @@ -116,7 +116,11 @@ const APP: () = { let filename = filename(); let mut buf = [0; 32]; let n = f.read(&mut buf).unwrap(); - println!("[baz] read({}) -> {:?}", filename, str::from_utf8(&buf[..n])); + println!( + "[baz] read({}) -> {:?}", + filename, + str::from_utf8(&buf[..n]) + ); f.close().unwrap(); println!("[baz] closed {}", filename); } From d521e2c342ec1fc3504aba0053f3166ab9c67fc3 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 23 Apr 2020 21:00:31 +0200 Subject: [PATCH 19/19] update Cargo.lock --- firmware/Cargo.lock | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 89afa0d..1e8362a 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -186,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" @@ -359,6 +375,18 @@ dependencies = [ "littlefs2-sys", ] +[[package]] +name = "littlefs2" +version = "0.1.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abdd4ddd5caedd104a0627e4abbe1c001a088fc3ce996a210ce5547193f542a3" +dependencies = [ + "bitflags", + "cty 0.2.1", + "generic-array 0.13.2", + "littlefs2-sys", +] + [[package]] name = "littlefs2-sys" version = "0.1.5"