From ab50acfcc7dffaeae8ed391d427e3fe3fc34b13f Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sat, 27 Jun 2026 09:21:43 +0300 Subject: [PATCH] Red Bear OS redoxfs baseline from 0.1.0 pre-patched archive --- .gitignore | 4 + .gitlab-ci.yml | 30 + .travis.yml | 21 + Cargo.lock | 1017 ++++++++++++ Cargo.toml | 96 ++ LICENSE | 21 + Makefile | 37 + README.md | 53 + fuzz/.gitignore | 4 + fuzz/Cargo.lock | 858 +++++++++++ fuzz/Cargo.toml | 30 + fuzz/fuzz_targets/fuse_fuzz_target.rs | 338 ++++ src/allocator.rs | 394 +++++ src/archive.rs | 146 ++ src/bin/ar.rs | 108 ++ src/bin/clone.rs | 153 ++ src/bin/mkfs.rs | 121 ++ src/bin/mount.rs | 409 +++++ src/bin/resize.rs | 206 +++ src/block.rs | 393 +++++ src/clone.rs | 90 ++ src/dir.rs | 300 ++++ src/disk/cache.rs | 110 ++ src/disk/file.rs | 84 + src/disk/io.rs | 38 + src/disk/memory.rs | 42 + src/disk/mod.rs | 41 + src/disk/sparse.rs | 53 + src/filesystem.rs | 331 ++++ src/header.rs | 242 +++ src/htree.rs | 691 +++++++++ src/key.rs | 104 ++ src/lib.rs | 70 + src/mount/fuse.rs | 580 +++++++ src/mount/mod.rs | 23 + src/mount/redox/mod.rs | 89 ++ src/mount/redox/resource.rs | 794 ++++++++++ src/mount/redox/scheme.rs | 1253 +++++++++++++++ src/mount/stub.rs | 19 + src/node.rs | 584 +++++++ src/record.rs | 42 + src/tests.rs | 1104 +++++++++++++ src/transaction.rs | 2053 +++++++++++++++++++++++++ src/tree.rs | 280 ++++ src/unmount.rs | 51 + test.sh | 67 + tests/tests.rs | 639 ++++++++ 47 files changed, 14213 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .travis.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/fuse_fuzz_target.rs create mode 100644 src/allocator.rs create mode 100644 src/archive.rs create mode 100644 src/bin/ar.rs create mode 100644 src/bin/clone.rs create mode 100644 src/bin/mkfs.rs create mode 100644 src/bin/mount.rs create mode 100644 src/bin/resize.rs create mode 100644 src/block.rs create mode 100644 src/clone.rs create mode 100644 src/dir.rs create mode 100644 src/disk/cache.rs create mode 100644 src/disk/file.rs create mode 100644 src/disk/io.rs create mode 100644 src/disk/memory.rs create mode 100644 src/disk/mod.rs create mode 100644 src/disk/sparse.rs create mode 100644 src/filesystem.rs create mode 100644 src/header.rs create mode 100644 src/htree.rs create mode 100644 src/key.rs create mode 100644 src/lib.rs create mode 100644 src/mount/fuse.rs create mode 100644 src/mount/mod.rs create mode 100644 src/mount/redox/mod.rs create mode 100644 src/mount/redox/resource.rs create mode 100644 src/mount/redox/scheme.rs create mode 100644 src/mount/stub.rs create mode 100644 src/node.rs create mode 100644 src/record.rs create mode 100644 src/tests.rs create mode 100644 src/transaction.rs create mode 100644 src/tree.rs create mode 100644 src/unmount.rs create mode 100755 test.sh create mode 100644 tests/tests.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..8322b69057 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target +image.bin +image +image* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..b5a0896e65 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,30 @@ +image: "redoxos/redoxer" + +stages: + - build + - test + +cache: + paths: + - target/ + +build:linux: + stage: build + script: cargo +nightly build --verbose + +build:redox: + stage: build + script: redoxer build --verbose + +test:linux: + stage: test + dependencies: + - build:linux + script: cargo +nightly test --verbose + +test:redox: + stage: test + dependencies: + - build:redox + # only run integration test as without KVM unit tests is super slow + script: redoxer test --verbose -- --test '*' -- --nocapture diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..f559abf27c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +sudo: required +language: rust +rust: + - nightly +os: + - linux + - osx +dist: trusty +before_install: + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then + sudo apt-get install -qq pkg-config fuse libfuse-dev; + sudo modprobe fuse; + sudo chmod 666 /dev/fuse; + sudo chown root:$USER /etc/fuse.conf; + fi + - if [ "$TRAVIS_OS_NAME" = "osx" ]; then + brew update; + brew install Caskroom/cask/osxfuse; + fi +notifications: + email: false diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..9cb6d72fd5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1017 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", +] + +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "endian-num" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f59926911ef34d1efb9ea1ee8ca78385df62ce700ccf2bcb149011bd226888" + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fuser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb29a3ae32279fe3e79a958fe01899f5fb23eadccee919cf88e145b54ed9367" +dependencies = [ + "libc", + "log", + "memchr", + "nix", + "page_size", + "smallvec", + "zerocopy", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062b52cd41eb8d929e81b592a47df833c33c15684933a9329440137a6d9f134c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "range-tree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384c2842d4e069d5ccacf5fe1dca4ef8d07a5444329715f0fc3c61813502d4d1" + +[[package]] +name = "redox-path" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717" + +[[package]] +name = "redox-scheme" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58cb7dee1058575f320f9bfa32c7c0eda857e285c2a163a0eb595d37add09cd" +dependencies = [ + "libredox", + "redox_syscall", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redoxfs" +version = "0.9.0" +dependencies = [ + "aes", + "argon2", + "assert_cmd", + "base64ct", + "bitflags", + "endian-num", + "env_logger", + "fuser", + "getrandom 0.2.17", + "humansize", + "libc", + "libredox", + "log", + "lz4_flex", + "parse-size", + "range-tree", + "redox-path", + "redox-scheme", + "redox_syscall", + "seahash", + "termion", + "uuid", + "xts-mode", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termion" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" +dependencies = [ + "libc", + "numtoa", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xts-mode" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cbddb7545ca0b9ffa7bdc653e8743303e1712687a6918ced25f2cdbed42520" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..c03bb63d61 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,96 @@ +[package] +name = "redoxfs" +description = "The Redox Filesystem" +repository = "https://gitlab.redox-os.org/redox-os/redoxfs" +version = "0.9.0" +license-file = "LICENSE" +readme = "README.md" +authors = ["Jeremy Soller "] +edition = "2021" + +[lib] +name = "redoxfs" +path = "src/lib.rs" + +[[bin]] +name = "redoxfs" +path = "src/bin/mount.rs" +doc = false +required-features = ["std"] + +[[bin]] +name = "redoxfs-ar" +path = "src/bin/ar.rs" +doc = false +required-features = ["std"] + +[[bin]] +name = "redoxfs-clone" +path = "src/bin/clone.rs" +doc = false +required-features = ["std"] + +[[bin]] +name = "redoxfs-mkfs" +path = "src/bin/mkfs.rs" +doc = false +required-features = ["std"] + +[[bin]] +name = "redoxfs-resize" +path = "src/bin/resize.rs" +doc = false +required-features = ["std"] + +[dependencies] +aes = { version = "0.8", default-features = false } +argon2 = { version = "0.4", default-features = false, features = ["alloc"] } +base64ct = { version = "1", default-features = false } +bitflags = "2" +endian-num = "0.1" +env_logger = { version = "0.11", optional = true } +getrandom = { version = "0.2.5", optional = true } +humansize = { version = "2", optional = true } +libc = "0.2" +log = { version = "0.4.14", default-features = false, optional = true } +lz4_flex = { version = "0.11", default-features = false, features = ["checked-decode"] } +parse-size = { version = "1", optional = true } +range-tree = { version = "0.1", optional = true } +redox_syscall = "0.7.3" +seahash = { version = "4.1.0", default-features = false } +termion = { version = "4", optional = true } +uuid = { version = "1.4", default-features = false } +xts-mode = { version = "0.5", default-features = false } + +[features] +default = ["std", "log", "fuse"] +fuse = [ + "fuser", + "std", +] +std = [ + "env_logger", + "getrandom", + "humansize", + "libredox", + "parse-size", + "range-tree", + "termion", + "uuid/v4", + "redox_syscall/std", + "redox-scheme", +] + +[target.'cfg(not(target_os = "redox"))'.dependencies] +fuser = { version = "0.16", optional = true } + +[target.'cfg(target_os = "redox")'.dependencies] +libredox = { version = "0.1.13", optional = true } +redox-path = "0.3.0" +redox-scheme = { version = "0.11.0", optional = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } + +[dev-dependencies] +assert_cmd = "2.0.17" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..7a0e9c2714 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Jeremy Soller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..443d50d19a --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +UNAME := $(shell uname) + +ifeq ($(UNAME),Darwin) + FUMOUNT=umount +else ifeq ($(UNAME),FreeBSD) + FUMOUNT=sudo umount +else + # Detect which version of the fusermount binary is available. + ifneq (, $(shell which fusermount3)) + FUMOUNT=fusermount3 -u + else + FUMOUNT=fusermount -u + endif +endif + +image.bin: + cargo build --release --bin redoxfs-mkfs + dd if=/dev/zero of=image.bin bs=1048576 count=1024 + target/release/redoxfs-mkfs image.bin + +mount: image.bin FORCE + mkdir -p image + cargo build --release --bin redoxfs + target/release/redoxfs image.bin image + +unmount: FORCE + sync + -${FUMOUNT} image + rm -rf image + +clean: FORCE + sync + -${FUMOUNT} image + rm -rf image image.bin + cargo clean + +FORCE: diff --git a/README.md b/README.md new file mode 100644 index 0000000000..5b8a69d388 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# RedoxFS + +This is the default filesystem of Redox OS inspired by [ZFS](https://docs.freebsd.org/en/books/handbook/zfs/) and adapted to a microkernel architecture. + +(It's a replacement for [TFS](https://gitlab.redox-os.org/redox-os/tfs)) + +Current features: + +- Compatible with Redox and Linux (FUSE) +- Copy-on-write +- Data/metadata checksums +- Transparent encryption +- Standard Unix file attributes +- File/directory size limit up to 193TiB (212TB) +- File/directory quantity limit up to 4 billion per 193TiB (2^32 - 1 = 4294967295) +- MIT licensed +- Disk encryption fully supported by the Redox bootloader, letting it load the kernel off an encrypted partition. + +Being MIT licensed, RedoxFS can be bundled on GPL-licensed operating systems (Linux, for example). + +### Install RedoxFS + +```sh +cargo install redoxfs +``` + +You can also build RedoxFS from this repository. + +### Configure your storage device to allow rootless usage + +If you are on Linux you need root permission to acess block devices (storage), but it's recommended to run RedoxFS as rootless. + +To do that you need to configure your storage device permission to your user with the following command: + +```sh +sudo setfacl -m u:your-username:rw /path/to/disk +``` + +### Create, mount and customize your RedoxFS partition + +See [the instructions in the book](https://doc.redox-os.org/book/redoxfs.html) for RedoxFS tooling usage. + +Currently RedoxFS tooling are: + +- `redoxfs` mount a RedoxFS disk +- `redoxfs-ar` write files to a RedoxFS disk +- `redoxfs-clone` clone a RedoxFS disk +- `redoxfs-mkfs` create an empty RedoxFS disk +- `redoxfs-resize` resize a RedoxFS disk + +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![crates.io](http://meritbadge.herokuapp.com/redoxfs)](https://crates.io/crates/redoxfs) +[![docs.rs](https://docs.rs/redoxfs/badge.svg)](https://docs.rs/redoxfs) diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000000..1a45eee776 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000000..3a1d92c8f4 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,858 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "endian-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad847bb2094f110bbdd6fa564894ca4556fd978958e93985420d680d3cb6d14" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fuser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb29a3ae32279fe3e79a958fe01899f5fb23eadccee919cf88e145b54ed9367" +dependencies = [ + "libc", + "log", + "memchr", + "nix", + "page_size", + "smallvec", + "zerocopy", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "jiff" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "range-tree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384c2842d4e069d5ccacf5fe1dca4ef8d07a5444329715f0fc3c61813502d4d1" + +[[package]] +name = "redox-path" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717" + +[[package]] +name = "redox-scheme" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cea2668a5932f878a4298a1d7f8950249bbbb77120fb263da252c589152f5ea" +dependencies = [ + "libredox", + "redox_syscall 0.6.0", +] + +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + +[[package]] +name = "redoxfs" +version = "0.8.4" +dependencies = [ + "aes", + "argon2", + "base64ct", + "bitflags", + "endian-num", + "env_logger", + "fuser", + "getrandom", + "humansize", + "libc", + "libredox", + "log", + "lz4_flex", + "parse-size", + "range-tree", + "redox-path", + "redox-scheme", + "redox_syscall 0.6.0", + "seahash", + "termion", + "uuid", + "xts-mode", +] + +[[package]] +name = "redoxfs-fuzz" +version = "0.0.0" +dependencies = [ + "anyhow", + "arbitrary", + "fuser", + "libfuzzer-sys", + "nix", + "redoxfs", + "tempfile", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termion" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb" +dependencies = [ + "libc", + "libredox", + "numtoa", + "redox_termios", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xts-mode" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cbddb7545ca0b9ffa7bdc653e8743303e1712687a6918ced25f2cdbed42520" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000000..b93a05b8f6 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "redoxfs-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[features] +default = [] +log = [] + +[package.metadata] +cargo-fuzz = true + +[dependencies] +anyhow = "1.0.86" +arbitrary = { version = "1.3.2", features = ["derive"] } +fuser = { version = "0.16" } +libfuzzer-sys = "0.4" +nix = { version = "0.29.0", features = ["fs"] } +tempfile = "3.10.1" + +[dependencies.redoxfs] +path = ".." + +[[bin]] +name = "fuse_fuzz_target" +path = "fuzz_targets/fuse_fuzz_target.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fuse_fuzz_target.rs b/fuzz/fuzz_targets/fuse_fuzz_target.rs new file mode 100644 index 0000000000..b46cccbfe8 --- /dev/null +++ b/fuzz/fuzz_targets/fuse_fuzz_target.rs @@ -0,0 +1,338 @@ +//! Fuzzer that exercises random file system operations against a FUSE-mounted redoxfs. + +#![no_main] + +use anyhow::{ensure, Result}; +use fuser; +use libfuzzer_sys::{arbitrary::Arbitrary, fuzz_target, Corpus}; +use nix::sys::statvfs::statvfs; +use std::{ + fs::{self, File, FileTimes, OpenOptions}, + io::{Read, Seek, SeekFrom, Write}, + os::unix::fs::{self as unix_fs, PermissionsExt}, + path::{Path, PathBuf}, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use tempfile; + +use redoxfs::{mount::fuse::Fuse, DiskSparse, FileSystem}; + +/// Maximum size for files and buffers. Chosen arbitrarily with fuzzing performance in mind. +const MAX_SIZE: u64 = 10_000_000; +/// Limit on the number of remounts in a single test case. Chosen arbitrarily with fuzzing +/// performance in mind: remounts are costly. +const MAX_MOUNT_SEQUENCES: usize = 3; + +/// An operation to be performed by the fuzzer. +#[derive(Arbitrary, Clone, Debug)] +enum Operation { + Chown { + path: PathBuf, + uid: Option, + gid: Option, + }, + CreateDir { + path: PathBuf, + }, + HardLink { + original: PathBuf, + link: PathBuf, + }, + Metadata { + path: PathBuf, + }, + Read { + path: PathBuf, + }, + ReadDir { + path: PathBuf, + }, + ReadLink { + path: PathBuf, + }, + RemoveDir { + path: PathBuf, + }, + RemoveFile { + path: PathBuf, + }, + Rename { + from: PathBuf, + to: PathBuf, + }, + SeekRead { + path: PathBuf, + seek_pos: u64, + buf_size: usize, + }, + SeekWrite { + path: PathBuf, + seek_pos: u64, + buf_size: usize, + }, + SetLen { + path: PathBuf, + size: u64, + }, + SetPermissions { + path: PathBuf, + readonly: Option, + mode: Option, + }, + SetTimes { + path: PathBuf, + accessed_since_epoch: Option, + modified_since_epoch: Option, + }, + Statvfs {}, + SymLink { + original: PathBuf, + link: PathBuf, + }, + Write { + path: PathBuf, + buf_size: usize, + }, +} + +/// Parameters for mounting the file system and operations to be performed afterwards. +#[derive(Arbitrary, Clone, Debug)] +struct MountSequence { + cleanup: bool, + operations: Vec, +} + +/// The whole input to a single fuzzer invocation. +#[derive(Arbitrary, Clone, Debug)] +struct TestCase { + disk_size: u64, + reserved_size: u64, + mount_sequences: Vec, +} + +/// Creates the disk for backing the Redoxfs. +fn create_disk(temp_path: &Path, disk_size: u64) -> DiskSparse { + let disk_path = temp_path.join("disk.img"); + DiskSparse::create(disk_path, disk_size).unwrap() +} + +/// Creates an empty Redoxfs. +fn create_redoxfs(disk: DiskSparse, reserved_size: u64) -> bool { + let password = None; + let reserved = vec![0; reserved_size as usize]; + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + FileSystem::create_reserved( + disk, + password, + &reserved, + ctime.as_secs(), + ctime.subsec_nanos(), + ) + .is_ok() +} + +/// Mounts an existing Redoxfs, runs the callback and performs the unmount. +fn with_redoxfs_mount(temp_path: &Path, disk: DiskSparse, cleanup: bool, callback: F) +where + F: FnOnce(&Path) + Send + 'static, +{ + let password = None; + let block = None; + let mut fs = FileSystem::open(disk, password, block, cleanup).unwrap(); + + let mount_path = temp_path.join("mount"); + fs::create_dir_all(&mount_path).unwrap(); + let mut session = fuser::Session::new(Fuse { fs: &mut fs }, &mount_path, &[]).unwrap(); + let mut unmounter = session.unmount_callable(); + + let join_handle = thread::spawn(move || { + callback(&mount_path); + unmounter.unmount().unwrap(); + }); + + session.run().unwrap(); + join_handle.join().unwrap(); +} + +fn get_path_within_fs(fs_path: &Path, path_to_add: &Path) -> Result { + ensure!(path_to_add.is_relative()); + ensure!(path_to_add + .components() + .all(|c| c != std::path::Component::ParentDir)); + Ok(fs_path.join(path_to_add)) +} + +fn do_operation(fs_path: &Path, op: &Operation) -> Result<()> { + match op { + Operation::Chown { path, uid, gid } => { + let path = get_path_within_fs(fs_path, path)?; + unix_fs::chown(path, *uid, *gid)?; + } + Operation::CreateDir { path } => { + let path = get_path_within_fs(fs_path, path)?; + fs::create_dir(path)?; + } + Operation::HardLink { original, link } => { + let original = get_path_within_fs(fs_path, original)?; + let link = get_path_within_fs(fs_path, link)?; + fs::hard_link(original, link)?; + } + Operation::Metadata { path } => { + let path = get_path_within_fs(fs_path, path)?; + fs::metadata(path)?; + } + Operation::Read { path } => { + let path = get_path_within_fs(fs_path, path)?; + fs::read(path)?; + } + Operation::ReadDir { path } => { + let path = get_path_within_fs(fs_path, path)?; + let _ = fs::read_dir(path)?.count(); + } + Operation::ReadLink { path } => { + let path = get_path_within_fs(fs_path, path)?; + fs::read_link(path)?; + } + Operation::RemoveDir { path } => { + let path = get_path_within_fs(fs_path, path)?; + fs::remove_dir(path)?; + } + Operation::RemoveFile { path } => { + let path = get_path_within_fs(fs_path, path)?; + fs::remove_file(path)?; + } + Operation::Rename { from, to } => { + let from = get_path_within_fs(fs_path, from)?; + let to = get_path_within_fs(fs_path, to)?; + fs::rename(from, to)?; + } + Operation::SeekRead { + path, + seek_pos, + buf_size, + } => { + ensure!(*buf_size as u64 <= MAX_SIZE); + let path = get_path_within_fs(fs_path, path)?; + let mut file = File::open(path)?; + file.seek(SeekFrom::Start(*seek_pos))?; + let mut buf = vec![0; *buf_size]; + file.read(&mut buf)?; + } + Operation::SeekWrite { + path, + seek_pos, + buf_size, + } => { + ensure!(*seek_pos <= MAX_SIZE); + ensure!(*buf_size as u64 <= MAX_SIZE); + let path = get_path_within_fs(fs_path, path)?; + let mut file = OpenOptions::new().write(true).open(path)?; + file.seek(SeekFrom::Start(*seek_pos))?; + let buf = vec![0; *buf_size]; + file.write(&buf)?; + } + Operation::SetLen { path, size } => { + let path = get_path_within_fs(fs_path, path)?; + let file = OpenOptions::new().write(true).open(path)?; + file.set_len(*size)?; + } + Operation::SetPermissions { + path, + readonly, + mode, + } => { + let path = get_path_within_fs(fs_path, path)?; + let metadata = fs::metadata(&path)?; + let mut perms = metadata.permissions(); + if let Some(readonly) = readonly { + perms.set_readonly(*readonly); + } + if let Some(mode) = mode { + perms.set_mode(*mode); + } + fs::set_permissions(path, perms)?; + } + Operation::SetTimes { + path, + accessed_since_epoch, + modified_since_epoch, + } => { + let path = get_path_within_fs(fs_path, path)?; + let file = File::options().write(true).open(path)?; + let mut times = FileTimes::new(); + if let Some(accessed_since_epoch) = accessed_since_epoch { + if let Some(accessed) = UNIX_EPOCH.checked_add(*accessed_since_epoch) { + times = times.set_accessed(accessed); + } + } + if let Some(modified_since_epoch) = modified_since_epoch { + if let Some(modified) = UNIX_EPOCH.checked_add(*modified_since_epoch) { + times = times.set_modified(modified); + } + } + file.set_times(times)?; + } + Operation::Statvfs {} => { + statvfs(fs_path)?; + } + Operation::SymLink { original, link } => { + let original = get_path_within_fs(fs_path, original)?; + let link = get_path_within_fs(fs_path, link)?; + unix_fs::symlink(original, link)?; + } + Operation::Write { path, buf_size } => { + ensure!(*buf_size as u64 <= MAX_SIZE); + let path = get_path_within_fs(fs_path, path)?; + let buf = vec![0; *buf_size]; + fs::write(path, &buf)?; + } + } + Ok(()) +} + +fuzz_target!(|test_case: TestCase| -> Corpus { + if test_case.disk_size > MAX_SIZE + || test_case.reserved_size > MAX_SIZE + || test_case.mount_sequences.len() > MAX_MOUNT_SEQUENCES + { + return Corpus::Reject; + } + + let temp_dir = tempfile::Builder::new() + .prefix("fuse_fuzz_target") + .tempdir() + .unwrap(); + + #[cfg(feature = "log")] + eprintln!("create fs"); + let disk = create_disk(temp_dir.path(), test_case.disk_size); + if !create_redoxfs(disk, test_case.reserved_size) { + // File system creation failed (e.g., due to insufficient space) so we bail out, still + // exercising this code path is useful. + return Corpus::Keep; + } + + for mount_seq in test_case.mount_sequences.iter() { + #[cfg(feature = "log")] + eprintln!("mount fs: path {:?}, size{}", temp_dir.path(), test_case.disk_size); + + let disk = create_disk(temp_dir.path(), test_case.disk_size); + let operations = mount_seq.operations.clone(); + with_redoxfs_mount(temp_dir.path(), disk, mount_seq.cleanup, move |fs_path| { + for operation in operations.iter() { + #[cfg(feature = "log")] + eprintln!("do operation {operation:?}"); + + let _result = do_operation(fs_path, operation); + + #[cfg(feature = "log")] + eprintln!("operation result {:?}", _result.err()); + } + }); + + #[cfg(feature = "log")] + eprintln!("unmounted fs"); + } + Corpus::Keep +}); diff --git a/src/allocator.rs b/src/allocator.rs new file mode 100644 index 0000000000..66c622029e --- /dev/null +++ b/src/allocator.rs @@ -0,0 +1,394 @@ +use alloc::{collections::BTreeSet, vec::Vec}; +use core::{fmt, mem, ops, slice}; +use endian_num::Le; + +use crate::{BlockAddr, BlockLevel, BlockMeta, BlockPtr, BlockTrait, Node, TreePtr, BLOCK_SIZE}; + +pub const ALLOC_LIST_ENTRIES: usize = + (BLOCK_SIZE as usize - mem::size_of::>()) / mem::size_of::(); +pub const RELEASE_LIST_ENTRIES: usize = (BLOCK_SIZE as usize + - mem::size_of::>()) + / mem::size_of::>(); + +/// The RedoxFS block allocator. This struct manages all "data" blocks in RedoxFS +/// (i.e, all blocks that aren't reserved or part of the header chain). +/// +/// [`Allocator`] can allocate blocks of many "levels"---that is, it can +/// allocate multiple consecutive [`BLOCK_SIZE`] blocks in one operation. +/// +/// This reduces the amount of memory that the [`Allocator`] uses: +/// Instead of storing the index of each free [`BLOCK_SIZE`] block, +/// the `levels` array can keep track of higher-level blocks, splitting +/// them when a smaller block is requested. +/// +/// Higher-level blocks also allow us to more efficiently allocate memory +/// for large files. +#[derive(Clone, Default)] +pub struct Allocator { + /// This array keeps track of all free blocks of each level, + /// and is initialized using the AllocList chain when we open the filesystem. + /// + /// Every element of the outer array represents a block level: + /// - item 0: free level 0 blocks (with size [`BLOCK_SIZE`]) + /// - item 1: free level 1 blocks (with size 2*[`BLOCK_SIZE`]) + /// - item 2: free level 2 blocks (with size 4*[`BLOCK_SIZE`]) + /// ...and so on. + /// + /// Each inner array contains a list of free block indices, + levels: Vec>, +} + +impl Allocator { + pub fn levels(&self) -> &Vec> { + &self.levels + } + + /// Count the number of free [`BLOCK_SIZE`] available to this [`Allocator`]. + pub fn free(&self) -> u64 { + let mut free = 0; + for level in 0..self.levels.len() { + let level_size = 1 << level; + free += self.levels[level].len() as u64 * level_size; + } + free + } + + /// Find a free block of the given level, mark it as "used", and return its address. + /// Returns [`None`] if there are no free blocks with this level. + pub fn allocate(&mut self, meta: BlockMeta) -> Option { + // First, find the lowest level with a free block + let mut free_opt = None; + { + let mut level = meta.level.0; + // Start searching at the level we want. Smaller levels are too small! + while level < self.levels.len() { + if let Some(&index) = self.levels[level].first() { + // Find the index closest to the start of the filesystem + free_opt = match free_opt { + Some((free_level, free_index)) if free_index <= index => { + Some((free_level, free_index)) + } + _ => Some((level, index)), + }; + } + level += 1; + } + } + + // If a free block was found, split it until we find a usable block of the right level. + // The left side of the split block is kept free, and the right side is allocated. + let (mut level, index) = free_opt?; + self.levels[level].remove(&index); + while level > meta.level.0 { + level -= 1; + let level_size = 1 << level; + self.levels[level].insert(index + level_size); + } + + Some(unsafe { BlockAddr::new(index, meta) }) + } + + /// Try to allocate the exact block specified, making all necessary splits. + /// Returns [`None`] if this some (or all) of this block is already allocated. + /// + /// Note that [`BlockAddr`] encodes the blocks location _and_ level. + pub fn allocate_exact(&mut self, exact_addr: BlockAddr) -> Option { + // This function only supports level 0 right now + assert_eq!(exact_addr.level().0, 0); + let exact_index = exact_addr.index(); + + let mut index_opt = None; + + // Go from the highest to the lowest level + for level in (0..self.levels.len()).rev() { + let level_size = 1 << level; + + // Split higher block if found + if let Some(index) = index_opt.take() { + self.levels[level].insert(index); + self.levels[level].insert(index + level_size); + } + + // Look for matching block and remove it + for &start in self.levels[level].iter() { + if start <= exact_index { + let end = start + level_size; + if end > exact_index { + self.levels[level].remove(&start); + index_opt = Some(start); + break; + } + } + } + } + + Some(unsafe { BlockAddr::new(index_opt?, exact_addr.meta()) }) + } + + /// Deallocate the given block, marking it "free" so that it can be re-used later. + pub fn deallocate(&mut self, addr: BlockAddr) { + // When we deallocate, we check if block we're deallocating has a free sibling. + // If it does, we join the two to create one free block in the next (higher) level. + // + // We repeat this until we no longer have a sibling to join. + let mut index = addr.index(); + let mut level = addr.level().0; + loop { + while level >= self.levels.len() { + self.levels.push(BTreeSet::new()); + } + + let level_size = 1 << level; + let next_size = level_size << 1; + + let mut found = false; + // look at all free blocks in the current level... + for &level_index in self.levels[level].iter() { + // - the block we just freed aligns with the next largest block, and + // - the second block we're looking at is the right sibling of this block + if index % next_size == 0 && index + level_size == level_index { + // "alloc" the next highest block, repeat deallocation process. + self.levels[level].remove(&level_index); + found = true; + break; + // - the index of this block doesn't align with the next largest block, and + // - the block we're looking at is the left neighbor of this block + } else if level_index % next_size == 0 && level_index + level_size == index { + // "alloc" the next highest block, repeat deallocation process. + self.levels[level].remove(&level_index); + index = level_index; // index moves to left block + found = true; + break; + } + } + + // We couldn't find a higher block, + // deallocate this one and finish + if !found { + self.levels[level].insert(index); + return; + } + + // repeat deallocation process on the + // higher-level block we just created. + level += 1; + } + } +} + +#[repr(C, packed)] +#[derive(Clone, Copy, Default, Debug)] +pub struct AllocEntry { + /// The index of the first block this [`AllocEntry`] refers to + index: Le, + + /// The number of blocks after (and including) `index` that are are free or used. + /// If negative, they are used; if positive, they are free. + count: Le, +} + +impl AllocEntry { + pub fn new(index: u64, count: i64) -> Self { + Self { + index: index.into(), + count: count.into(), + } + } + + pub fn allocate(addr: BlockAddr) -> Self { + Self::new(addr.index(), -addr.level().blocks::()) + } + + pub fn deallocate(addr: BlockAddr) -> Self { + Self::new(addr.index(), addr.level().blocks::()) + } + + pub fn index(&self) -> u64 { + self.index.to_ne() + } + + pub fn count(&self) -> i64 { + self.count.to_ne() + } + + pub fn is_null(&self) -> bool { + self.count() == 0 + } +} + +/// A node in the allocation chain. +#[repr(C, packed)] +pub struct AllocList { + /// A pointer to the previous AllocList. + /// If this is the null pointer, this is the first element of the chain. + pub prev: BlockPtr, + + /// Allocation entries. + pub entries: [AllocEntry; ALLOC_LIST_ENTRIES], +} + +unsafe impl BlockTrait for AllocList { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self { + prev: BlockPtr::default(), + entries: [AllocEntry::default(); ALLOC_LIST_ENTRIES], + }) + } else { + None + } + } +} + +impl fmt::Debug for AllocList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let prev = self.prev; + let entries: Vec<&AllocEntry> = self + .entries + .iter() + .filter(|entry| entry.count() > 0) + .collect(); + f.debug_struct("AllocList") + .field("prev", &prev) + .field("entries", &entries) + .finish() + } +} + +impl ops::Deref for AllocList { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + self as *const AllocList as *const u8, + mem::size_of::(), + ) as &[u8] + } + } +} + +impl ops::DerefMut for AllocList { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut( + self as *mut AllocList as *mut u8, + mem::size_of::(), + ) as &mut [u8] + } + } +} + +/// A list of nodes pending release. +#[repr(C, packed)] +pub struct ReleaseList { + /// A pointer to the previous ReleaseList. + /// If this is the null pointer, this is the first element of the chain. + pub prev: BlockPtr, + + /// Allocation entries. + pub entries: [TreePtr; RELEASE_LIST_ENTRIES], +} + +unsafe impl BlockTrait for ReleaseList { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self { + prev: BlockPtr::default(), + entries: [TreePtr::::default(); RELEASE_LIST_ENTRIES], + }) + } else { + None + } + } +} + +impl fmt::Debug for ReleaseList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let prev = self.prev; + let entries: Vec<_> = self + .entries + .iter() + .filter(|entry| !entry.is_null()) + .map(|entry| entry.id()) + .collect(); + f.debug_struct("ReleaseList") + .field("prev", &prev) + .field("entries", &entries) + .finish() + } +} + +impl ops::Deref for ReleaseList { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + self as *const ReleaseList as *const u8, + mem::size_of::(), + ) as &[u8] + } + } +} + +impl ops::DerefMut for ReleaseList { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut( + self as *mut ReleaseList as *mut u8, + mem::size_of::(), + ) as &mut [u8] + } + } +} + +#[test] +fn alloc_node_size_test() { + assert_eq!(mem::size_of::(), crate::BLOCK_SIZE as usize); +} + +#[test] +fn release_node_size_test() { + assert_eq!(mem::size_of::(), crate::BLOCK_SIZE as usize); +} + +#[test] +fn allocator_test() { + let mut alloc = Allocator::default(); + + assert_eq!(alloc.allocate(BlockMeta::default()), None); + + alloc.deallocate(unsafe { BlockAddr::new(1, BlockMeta::default()) }); + assert_eq!( + alloc.allocate(BlockMeta::default()), + Some(unsafe { BlockAddr::new(1, BlockMeta::default()) }) + ); + assert_eq!(alloc.allocate(BlockMeta::default()), None); + + for addr in 1023..2048 { + alloc.deallocate(unsafe { BlockAddr::new(addr, BlockMeta::default()) }); + } + + assert_eq!(alloc.levels.len(), 11); + for level in 0..alloc.levels.len() { + if level == 0 { + assert_eq!(alloc.levels[level], [1023].into()); + } else if level == 10 { + assert_eq!(alloc.levels[level], [1024].into()); + } else { + assert_eq!(alloc.levels[level], [0u64; 0].into()); + } + } + + for addr in 1023..2048 { + assert_eq!( + alloc.allocate(BlockMeta::default()), + Some(unsafe { BlockAddr::new(addr, BlockMeta::default()) }) + ); + } + assert_eq!(alloc.allocate(BlockMeta::default()), None); + + assert_eq!(alloc.levels.len(), 11); + for level in 0..alloc.levels.len() { + assert_eq!(alloc.levels[level], [0u64; 0].into()); + } +} diff --git a/src/archive.rs b/src/archive.rs new file mode 100644 index 0000000000..2b258d94a0 --- /dev/null +++ b/src/archive.rs @@ -0,0 +1,146 @@ +use std::fs; +use std::io; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::MetadataExt; +use std::path::Path; + +use crate::{Disk, FileSystem, Node, Transaction, TreePtr, BLOCK_SIZE}; + +fn syscall_err(err: syscall::Error) -> io::Error { + io::Error::from_raw_os_error(err.errno) +} + +pub fn archive_at>( + tx: &mut Transaction, + parent_path: P, + parent_ptr: TreePtr, +) -> io::Result<()> { + for entry_res in fs::read_dir(parent_path)? { + let entry = entry_res?; + + let metadata = entry.metadata()?; + let file_type = metadata.file_type(); + + let name = entry.file_name().into_string().map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "filename is not valid UTF-8") + })?; + + let mode_type = if file_type.is_dir() { + Node::MODE_DIR + } else if file_type.is_file() { + Node::MODE_FILE + } else if file_type.is_symlink() { + Node::MODE_SYMLINK + } else { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Does not support parsing {:?}", file_type), + )); + }; + + let node_ptr; + { + let mode = mode_type | (metadata.mode() as u16 & Node::MODE_PERM); + let mut node = tx + .create_node( + parent_ptr, + &name, + mode, + metadata.ctime() as u64, + metadata.ctime_nsec() as u32, + ) + .map_err(syscall_err)?; + + node_ptr = node.ptr(); + + if node.data().uid() != metadata.uid() || node.data().gid() != metadata.gid() { + node.data_mut().set_uid(metadata.uid()); + node.data_mut().set_gid(metadata.gid()); + tx.sync_tree(node).map_err(syscall_err)?; + } + } + + let path = entry.path(); + if file_type.is_dir() { + archive_at(tx, path, node_ptr)?; + } else if file_type.is_file() { + let data = fs::read(path)?; + let count = tx + .write_node( + node_ptr, + 0, + &data, + metadata.mtime() as u64, + metadata.mtime_nsec() as u32, + ) + .map_err(syscall_err)?; + if count != data.len() { + panic!("file write count {} != {}", count, data.len()); + } + } else if file_type.is_symlink() { + let destination = fs::read_link(path)?; + let data = destination.as_os_str().as_bytes(); + let count = tx + .write_node( + node_ptr, + 0, + data, + metadata.mtime() as u64, + metadata.mtime_nsec() as u32, + ) + .map_err(syscall_err)?; + if count != data.len() { + panic!("symlink write count {} != {}", count, data.len()); + } + } else { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Does not support creating {:?}", file_type), + )); + } + } + + Ok(()) +} + +pub fn archive>(fs: &mut FileSystem, parent_path: P) -> io::Result { + let end_block = fs + .tx(|tx| { + // Archive_at root node + archive_at(tx, parent_path, TreePtr::root()) + .map_err(|err| syscall::Error::new(err.raw_os_error().unwrap()))?; + + // Squash alloc log + tx.sync(true)?; + + let end_block = tx.header.size() / BLOCK_SIZE; + /* TODO: Cut off any free blocks at the end of the filesystem + let mut end_changed = true; + while end_changed { + end_changed = false; + + let allocator = fs.allocator(); + let levels = allocator.levels(); + for level in 0..levels.len() { + let level_size = 1 << level; + for &block in levels[level].iter() { + if block < end_block && block + level_size >= end_block { + end_block = block; + end_changed = true; + } + } + } + } + */ + + // Update header + tx.header.size = (end_block * BLOCK_SIZE).into(); + tx.header_changed = true; + tx.sync(false)?; + + Ok(end_block) + }) + .map_err(syscall_err)?; + + Ok((fs.block + end_block) * BLOCK_SIZE) +} diff --git a/src/bin/ar.rs b/src/bin/ar.rs new file mode 100644 index 0000000000..9549997f1c --- /dev/null +++ b/src/bin/ar.rs @@ -0,0 +1,108 @@ +extern crate redoxfs; +extern crate syscall; +extern crate uuid; + +use std::io::Read; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{env, fs, process}; + +use redoxfs::{archive, DiskFile, FileSystem}; +use uuid::Uuid; + +fn main() { + env_logger::init(); + + let mut args = env::args().skip(1); + + let disk_path = if let Some(path) = args.next() { + path + } else { + println!("redoxfs-ar: no disk image provided"); + println!("redoxfs-ar DISK FOLDER [BOOTLOADER]"); + process::exit(1); + }; + + let folder_path = if let Some(path) = args.next() { + path + } else { + println!("redoxfs-ar: no folder provided"); + println!("redoxfs-ar DISK FOLDER [BOOTLOADER]"); + process::exit(1); + }; + + let bootloader_path_opt = args.next(); + + let disk = match DiskFile::open(&disk_path) { + Ok(disk) => disk, + Err(err) => { + println!("redoxfs-ar: failed to open image {}: {}", disk_path, err); + process::exit(1); + } + }; + + let mut bootloader = vec![]; + if let Some(bootloader_path) = bootloader_path_opt { + match fs::File::open(&bootloader_path) { + Ok(mut file) => match file.read_to_end(&mut bootloader) { + Ok(_) => (), + Err(err) => { + println!( + "redoxfs-ar: failed to read bootloader {}: {}", + bootloader_path, err + ); + process::exit(1); + } + }, + Err(err) => { + println!( + "redoxfs-ar: failed to open bootloader {}: {}", + bootloader_path, err + ); + process::exit(1); + } + } + }; + + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + match FileSystem::create_reserved( + disk, + None, + &bootloader, + ctime.as_secs(), + ctime.subsec_nanos(), + ) { + Ok(mut fs) => { + let size = match archive(&mut fs, &folder_path) { + Ok(ok) => ok, + Err(err) => { + println!("redoxfs-ar: failed to archive {}: {}", folder_path, err); + process::exit(1); + } + }; + + if let Err(err) = fs.disk.file.set_len(size) { + println!( + "redoxfs-ar: failed to truncate {} to {}: {}", + disk_path, size, err + ); + process::exit(1); + } + + let uuid = Uuid::from_bytes(fs.header.uuid()); + println!( + "redoxfs-ar: created filesystem on {}, reserved {} blocks, size {} MB, uuid {}", + disk_path, + fs.block, + fs.header.size() / 1000 / 1000, + uuid.hyphenated() + ); + } + Err(err) => { + println!( + "redoxfs-ar: failed to create filesystem on {}: {}", + disk_path, err + ); + process::exit(1); + } + }; +} diff --git a/src/bin/clone.rs b/src/bin/clone.rs new file mode 100644 index 0000000000..c29f97220e --- /dev/null +++ b/src/bin/clone.rs @@ -0,0 +1,153 @@ +extern crate redoxfs; +extern crate syscall; +extern crate uuid; + +use std::io::Read; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{env, fs, process}; + +use redoxfs::{clone, DiskFile, FileSystem}; +use uuid::Uuid; + +fn main() { + env_logger::init(); + + let mut args = env::args().skip(1); + + let disk_path_old = if let Some(path) = args.next() { + path + } else { + println!("redoxfs-clone: no old disk image provided"); + println!("redoxfs-clone NEW-DISK OLD-DISK [BOOTLOADER]"); + process::exit(1); + }; + + let disk_path = if let Some(path) = args.next() { + path + } else { + println!("redoxfs-clone: no new disk image provided"); + println!("redoxfs-clone NEW-DISK OLD-DISK [BOOTLOADER]"); + process::exit(1); + }; + + let bootloader_path_opt = args.next(); + + // Open old disk in readonly mode + let disk_old = match fs::OpenOptions::new() + .read(true) + .write(false) + .open(&disk_path_old) + .map(DiskFile::from) + { + Ok(disk) => disk, + Err(err) => { + println!( + "redoxfs-clone: failed to open old disk image {}: {}", + disk_path_old, err + ); + process::exit(1); + } + }; + + let mut fs_old = match FileSystem::open(disk_old, None, None, false) { + Ok(fs) => fs, + Err(err) => { + println!( + "redoxfs-clone: failed to open filesystem on {}: {}", + disk_path_old, err + ); + process::exit(1); + } + }; + + let disk = match DiskFile::open(&disk_path) { + Ok(disk) => disk, + Err(err) => { + println!( + "redoxfs-clone: failed to open new disk image {}: {}", + disk_path, err + ); + process::exit(1); + } + }; + + let mut bootloader = vec![]; + if let Some(bootloader_path) = bootloader_path_opt { + match fs::File::open(&bootloader_path) { + Ok(mut file) => match file.read_to_end(&mut bootloader) { + Ok(_) => (), + Err(err) => { + println!( + "redoxfs-clone: failed to read bootloader {}: {}", + bootloader_path, err + ); + process::exit(1); + } + }, + Err(err) => { + println!( + "redoxfs-clone: failed to open bootloader {}: {}", + bootloader_path, err + ); + process::exit(1); + } + } + }; + + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let mut fs = match FileSystem::create_reserved( + disk, + None, + &bootloader, + ctime.as_secs(), + ctime.subsec_nanos(), + ) { + Ok(fs) => fs, + Err(err) => { + println!( + "redoxfs-clone: failed to create filesystem on {}: {}", + disk_path, err + ); + process::exit(1); + } + }; + + let size_old = fs_old.header.size(); + let free_old = fs_old.allocator().free() * redoxfs::BLOCK_SIZE; + let used_old = size_old - free_old; + let mut last_percent = 0; + let clone_res = clone(&mut fs_old, &mut fs, move |used| { + let percent = (used * 100) / used_old; + if percent != last_percent { + eprint!( + "\r{}%: {} MB/{} MB", + percent, + used / 1000 / 1000, + used_old / 1000 / 1000 + ); + last_percent = percent; + } + }); + eprintln!(); + match clone_res { + Ok(()) => (), + Err(err) => { + println!( + "redoxfs-clone: failed to clone {} to {}: {}", + disk_path_old, disk_path, err + ); + process::exit(1); + } + } + + let uuid = Uuid::from_bytes(fs.header.uuid()); + let size = fs.header.size(); + let free = fs.allocator().free() * redoxfs::BLOCK_SIZE; + let used = size - free; + println!("redoxfs-clone: created filesystem on {}", disk_path,); + println!("\treserved: {} blocks", fs.block); + println!("\tuuid: {}", uuid.hyphenated()); + println!("\tsize: {} MB", size / 1000 / 1000); + println!("\tused: {} MB", used / 1000 / 1000); + println!("\tfree: {} MB", free / 1000 / 1000); +} diff --git a/src/bin/mkfs.rs b/src/bin/mkfs.rs new file mode 100644 index 0000000000..b8689409a2 --- /dev/null +++ b/src/bin/mkfs.rs @@ -0,0 +1,121 @@ +extern crate redoxfs; +extern crate uuid; + +use std::io::Read; +use std::{env, fs, io, process, time}; + +use redoxfs::{DiskFile, FileSystem}; +use termion::input::TermRead; +use uuid::Uuid; + +fn usage() -> ! { + eprintln!("redoxfs-mkfs [--encrypt] DISK [BOOTLOADER]"); + process::exit(1); +} + +fn main() { + env_logger::init(); + + let mut encrypt = false; + let mut disk_path_opt = None; + let mut bootloader_path_opt = None; + for arg in env::args().skip(1) { + if arg == "--encrypt" { + encrypt = true; + } else if disk_path_opt.is_none() { + disk_path_opt = Some(arg); + } else if bootloader_path_opt.is_none() { + bootloader_path_opt = Some(arg); + } else { + eprintln!("redoxfs-mkfs: too many arguments provided"); + usage(); + } + } + + let disk_path = if let Some(path) = disk_path_opt { + path + } else { + eprintln!("redoxfs-mkfs: no disk image provided"); + usage(); + }; + + let disk = match DiskFile::open(&disk_path) { + Ok(disk) => disk, + Err(err) => { + eprintln!("redoxfs-mkfs: failed to open image {}: {}", disk_path, err); + process::exit(1); + } + }; + + let mut bootloader = vec![]; + if let Some(bootloader_path) = bootloader_path_opt { + match fs::File::open(&bootloader_path) { + Ok(mut file) => match file.read_to_end(&mut bootloader) { + Ok(_) => (), + Err(err) => { + eprintln!( + "redoxfs-mkfs: failed to read bootloader {}: {}", + bootloader_path, err + ); + process::exit(1); + } + }, + Err(err) => { + eprintln!( + "redoxfs-mkfs: failed to open bootloader {}: {}", + bootloader_path, err + ); + process::exit(1); + } + } + }; + + let password_opt = if encrypt { + eprint!("redoxfs-mkfs: password: "); + + let password = io::stdin() + .read_passwd(&mut io::stderr()) + .unwrap() + .unwrap_or_default(); + + eprintln!(); + + if password.is_empty() { + eprintln!("redoxfs-mkfs: empty password, giving up"); + process::exit(1); + } + + Some(password) + } else { + None + }; + + let ctime = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap(); + match FileSystem::create_reserved( + disk, + password_opt.as_ref().map(|x| x.as_bytes()), + &bootloader, + ctime.as_secs(), + ctime.subsec_nanos(), + ) { + Ok(filesystem) => { + let uuid = Uuid::from_bytes(filesystem.header.uuid()); + eprintln!( + "redoxfs-mkfs: created filesystem on {}, reserved {} blocks, size {} MB, uuid {}", + disk_path, + filesystem.block, + filesystem.header.size() / 1000 / 1000, + uuid.hyphenated() + ); + } + Err(err) => { + eprintln!( + "redoxfs-mkfs: failed to create filesystem on {}: {}", + disk_path, err + ); + process::exit(1); + } + } +} diff --git a/src/bin/mount.rs b/src/bin/mount.rs new file mode 100644 index 0000000000..dba7f3c6be --- /dev/null +++ b/src/bin/mount.rs @@ -0,0 +1,409 @@ +extern crate libc; +extern crate redoxfs; +#[cfg(target_os = "redox")] +extern crate syscall; +extern crate uuid; + +use std::env; +use std::fs::File; +use std::io::{self, Read, Write}; +use std::os::unix::io::{FromRawFd, RawFd}; +use std::process; + +#[cfg(target_os = "redox")] +use std::{mem::MaybeUninit, ptr::addr_of_mut, sync::atomic::Ordering}; + +use redoxfs::{mount, DiskCache, DiskFile, FileSystem}; +use termion::input::TermRead; +use uuid::Uuid; + +#[cfg(target_os = "redox")] +extern "C" fn unmount_handler(_s: usize) { + redoxfs::IS_UMT.store(1, Ordering::SeqCst); +} + +#[cfg(target_os = "redox")] +//set up a signal handler on redox, this implements unmounting. I have no idea what sa_flags is +//for, so I put 2. I don't think 0,0 is a valid sa_mask. I don't know what i'm doing here. When u +//send it a sigkill, it shuts off the filesystem +fn setsig() { + // TODO: High-level wrapper like the nix crate? + unsafe { + let mut action = MaybeUninit::::uninit(); + + assert_eq!( + libc::sigemptyset(addr_of_mut!((*action.as_mut_ptr()).sa_mask)), + 0 + ); + addr_of_mut!((*action.as_mut_ptr()).sa_flags).write(0); + addr_of_mut!((*action.as_mut_ptr()).sa_sigaction).write(unmount_handler as usize); + + assert_eq!( + libc::sigaction(libc::SIGTERM, action.as_ptr(), core::ptr::null_mut()), + 0 + ); + } +} + +#[cfg(not(target_os = "redox"))] +// on linux, this is implemented properly, so no need for this unscrupulous nonsense! +fn setsig() {} + +fn fork() -> isize { + unsafe { libc::fork() as isize } +} + +fn pipe(pipes: &mut [i32; 2]) -> isize { + unsafe { libc::pipe(pipes.as_mut_ptr()) as isize } +} + +#[cfg(not(target_os = "redox"))] +fn capability_mode() {} + +#[cfg(not(target_os = "redox"))] +fn bootloader_password() -> Option> { + None +} + +#[cfg(target_os = "redox")] +fn capability_mode() { + libredox::call::setrens(0, 0).expect("redoxfs: failed to enter null namespace"); +} + +#[cfg(target_os = "redox")] +fn bootloader_password() -> Option> { + use libredox::call::MmapArgs; + + let addr_env = env::var_os("REDOXFS_PASSWORD_ADDR")?; + let size_env = env::var_os("REDOXFS_PASSWORD_SIZE")?; + + let addr = usize::from_str_radix( + addr_env.to_str().expect("REDOXFS_PASSWORD_ADDR not valid"), + 16, + ) + .expect("failed to parse REDOXFS_PASSWORD_ADDR"); + + let size = usize::from_str_radix( + size_env.to_str().expect("REDOXFS_PASSWORD_SIZE not valid"), + 16, + ) + .expect("failed to parse REDOXFS_PASSWORD_SIZE"); + + let mut password = Vec::with_capacity(size); + unsafe { + let aligned_size = size.next_multiple_of(syscall::PAGE_SIZE); + + let fd = libredox::Fd::open("memory:physical", libredox::flag::O_CLOEXEC, 0) + .expect("failed to open physical memory file"); + + let password_map = libredox::call::mmap(MmapArgs { + addr: core::ptr::null_mut(), + length: aligned_size, + prot: libredox::flag::PROT_READ, + flags: libredox::flag::MAP_SHARED, + fd: fd.raw(), + offset: addr as u64, + }) + .expect("failed to map REDOXFS_PASSWORD") + .cast::(); + + for i in 0..size { + password.push(password_map.add(i).read()); + } + + let _ = libredox::call::munmap(password_map.cast(), aligned_size); + } + Some(password) +} + +fn print_err_exit(err: impl AsRef) -> ! { + eprintln!("redoxfs: {}", err.as_ref()); + usage(); + process::exit(1) +} + +fn print_usage_exit() -> ! { + usage(); + process::exit(1) +} + +fn usage() { + eprintln!("redoxfs [--no-daemon|-d] [--uuid] [disk or uuid] [mountpoint] [block in hex]"); +} + +enum DiskId { + Path(String), + Uuid(Uuid), +} + +fn filesystem_by_path( + path: &str, + block_opt: Option, + log_errors: bool, +) -> Option<(String, FileSystem>)> { + log::debug!("opening {}", path); + let attempts = 10; + for attempt in 0..=attempts { + let password_opt = if attempt > 0 { + eprint!("redoxfs: password: "); + + let password = io::stdin() + .read_passwd(&mut io::stderr()) + .unwrap() + .unwrap_or_default(); + + eprintln!(); + + if password.is_empty() { + eprintln!("redoxfs: empty password, giving up"); + + // Password is empty, exit loop + break; + } + + Some(password.into_bytes()) + } else { + bootloader_password() + }; + + match DiskFile::open(path).map(DiskCache::new) { + Ok(disk) => { + match redoxfs::FileSystem::open(disk, password_opt.as_deref(), block_opt, true) { + Ok(filesystem) => { + log::debug!( + "opened filesystem on {} with uuid {}", + path, + Uuid::from_bytes(filesystem.header.uuid()).hyphenated() + ); + + return Some((path.to_string(), filesystem)); + } + Err(err) => match err.errno { + syscall::ENOKEY => { + if password_opt.is_some() { + eprintln!("redoxfs: incorrect password ({}/{})", attempt, attempts); + } + } + _ => { + if log_errors { + log::error!("failed to open filesystem {}: {}", path, err); + } + break; + } + }, + } + } + Err(err) => { + if log_errors { + log::error!("failed to open image {}: {}", path, err); + } + break; + } + } + } + None +} + +#[cfg(not(target_os = "redox"))] +fn filesystem_by_uuid( + _uuid: &Uuid, + _block_opt: Option, +) -> Option<(String, FileSystem>)> { + None +} + +#[cfg(target_os = "redox")] +fn filesystem_by_uuid( + uuid: &Uuid, + block_opt: Option, +) -> Option<(String, FileSystem>)> { + use std::fs; + + use redox_path::RedoxPath; + + match fs::read_dir("/scheme") { + Ok(entries) => { + for entry_res in entries { + if let Ok(entry) = entry_res { + if let Some(disk) = entry.path().to_str() { + if RedoxPath::from_absolute(disk) + .unwrap_or(RedoxPath::from_absolute("/")?) + .is_scheme_category("disk") + { + log::debug!("found scheme {}", disk); + match fs::read_dir(disk) { + Ok(entries) => { + for entry_res in entries { + if let Ok(entry) = entry_res { + if let Ok(path) = + entry.path().into_os_string().into_string() + { + log::debug!("found path {}", path); + if let Some((path, filesystem)) = + filesystem_by_path(&path, block_opt, false) + { + if &filesystem.header.uuid() == uuid.as_bytes() + { + log::debug!( + "filesystem on {} matches uuid {}", + path, + uuid.hyphenated() + ); + return Some((path, filesystem)); + } else { + log::debug!( + "filesystem on {} does not match uuid {}", + path, + uuid.hyphenated() + ); + } + } + } + } + } + } + Err(err) => { + log::debug!("failed to list '{}': {}", disk, err); + } + } + } + } + } + } + } + Err(err) => { + log::error!("failed to list schemes: {}", err); + } + } + + None +} + +fn daemon( + disk_id: &DiskId, + mountpoint: &str, + block_opt: Option, + mut write: Option, +) -> ! { + setsig(); + + let filesystem_opt = match *disk_id { + DiskId::Path(ref path) => filesystem_by_path(path, block_opt, true), + DiskId::Uuid(ref uuid) => filesystem_by_uuid(uuid, block_opt), + }; + + if let Some((path, filesystem)) = filesystem_opt { + match mount(filesystem, mountpoint, |mounted_path| { + capability_mode(); + + log::info!( + "mounted filesystem on {} to {}", + path, + mounted_path.display() + ); + + if let Some(ref mut write) = write { + let _ = write.write(&[0]); + } + }) { + Ok(()) => { + process::exit(0); + } + Err(err) => { + log::error!("failed to mount {} to {}: {}", path, mountpoint, err); + } + } + } + + match *disk_id { + DiskId::Path(ref path) => { + log::error!("not able to mount path {}", path); + } + DiskId::Uuid(ref uuid) => { + log::error!("not able to mount uuid {}", uuid.hyphenated()); + } + } + + if let Some(ref mut write) = write { + let _ = write.write(&[1]); + } + + process::exit(1); +} + +fn main() { + env_logger::init(); + + let mut args = env::args().skip(1); + + let mut daemonise = true; + let mut disk_id: Option = None; + let mut mountpoint: Option = None; + let mut block_opt: Option = None; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--no-daemon" | "-d" => daemonise = false, + + "--uuid" if disk_id.is_none() => { + disk_id = Some(DiskId::Uuid( + match args.next().as_deref().map(Uuid::parse_str) { + Some(Ok(uuid)) => uuid, + Some(Err(err)) => { + print_err_exit(format!("invalid uuid '{}': {}", arg, err)) + } + None => print_err_exit("no uuid provided"), + }, + )); + } + + disk if disk_id.is_none() => disk_id = Some(DiskId::Path(disk.to_owned())), + + mnt if disk_id.is_some() && mountpoint.is_none() => mountpoint = Some(mnt.to_owned()), + + opts if mountpoint.is_some() => match u64::from_str_radix(opts, 16) { + Ok(block) => block_opt = Some(block), + Err(err) => print_err_exit(format!("invalid block '{}': {}", opts, err)), + }, + + _ => print_usage_exit(), + } + } + + let Some(disk_id) = disk_id else { + print_err_exit("no disk provided"); + }; + + let Some(mountpoint) = mountpoint else { + print_err_exit("no mountpoint provided"); + }; + + if daemonise { + let mut pipes = [0; 2]; + if pipe(&mut pipes) == 0 { + let mut read = unsafe { File::from_raw_fd(pipes[0] as RawFd) }; + let write = unsafe { File::from_raw_fd(pipes[1] as RawFd) }; + + let pid = fork(); + if pid == 0 { + drop(read); + + daemon(&disk_id, &mountpoint, block_opt, Some(write)); + } else if pid > 0 { + drop(write); + + let mut res = [0]; + read.read_exact(&mut res).unwrap(); + + process::exit(res[0] as i32); + } else { + panic!("redoxfs: failed to fork"); + } + } else { + panic!("redoxfs: failed to create pipe"); + } + } else { + log::info!("running in foreground"); + daemon(&disk_id, &mountpoint, block_opt, None); + } +} diff --git a/src/bin/resize.rs b/src/bin/resize.rs new file mode 100644 index 0000000000..4ebb88b1eb --- /dev/null +++ b/src/bin/resize.rs @@ -0,0 +1,206 @@ +use std::{env, process}; + +use humansize::{format_size, BINARY, DECIMAL}; +use redoxfs::{BlockAddr, BlockMeta, Disk, DiskFile, FileSystem}; +use uuid::Uuid; + +fn resize(fs: &mut FileSystem, size_arg: String) -> Result<(), String> { + let disk_size = fs + .disk + .size() + .map_err(|err| format!("failed to read disk size: {}", err))?; + + // Find contiguous free region + //TODO: better error management + let mut last_free = None; + let mut last_end = 0; + fs.tx(|tx| { + let mut alloc_ptr = tx.header.alloc; + while !alloc_ptr.is_null() { + let alloc = tx.read_block(alloc_ptr)?; + alloc_ptr = alloc.data().prev; + for entry in alloc.data().entries.iter() { + let count = entry.count(); + if count <= 0 { + continue; + } + let end = entry.index() + count as u64; + if end > last_end { + last_free = Some(*entry); + last_end = end; + } + } + } + Ok(()) + }) + .map_err(|err| format!("failed to read alloc log: {}", err))?; + + let old_size = fs.header.size(); + let min_size = if let Some(entry) = last_free { + entry.index() * redoxfs::BLOCK_SIZE + } else { + old_size + }; + let max_size = disk_size - (fs.block * redoxfs::BLOCK_SIZE); + + let new_size = match size_arg.to_lowercase().as_str() { + "min" | "minimum" => min_size, + "" | "max" | "maximum" => max_size, + _ => match parse_size::parse_size(&size_arg) { + Ok(new_size) => { + if new_size < min_size { + return Err(format!( + "requested size {} is smaller than {} by {}", + new_size, + min_size, + min_size - new_size + )); + } + + if new_size > max_size { + return Err(format!( + "requested size {} is larger than {} by {}", + new_size, + max_size, + new_size - max_size + )); + } + + new_size + } + Err(err) => { + return Err(format!( + "failed to parse size argument {:?}: {}", + size_arg, err + )); + } + }, + }; + + println!( + "minimum size: {} ({})", + format_size(min_size, DECIMAL), + format_size(min_size, BINARY) + ); + println!( + "maximum size: {} ({})", + format_size(max_size, DECIMAL), + format_size(max_size, BINARY) + ); + println!( + "new size: {} ({})", + format_size(new_size, DECIMAL), + format_size(new_size, BINARY) + ); + + let old_blocks = old_size / redoxfs::BLOCK_SIZE; + let new_blocks = new_size / redoxfs::BLOCK_SIZE; + let (start, end, shrink) = if new_size == old_size { + println!("already requested size"); + return Ok(()); + } else if new_size < old_size { + println!("shrinking by {}", old_size - new_size); + (new_blocks, old_blocks, true) + } else { + println!("growing by {}", new_size - old_size); + (old_blocks, new_blocks, false) + }; + + // Allocate or deallocate blocks as needed + unsafe { + let allocator = fs.allocator_mut(); + for index in start..end { + if shrink { + //TODO: replace assert with error? + let addr = BlockAddr::new(index as u64, BlockMeta::default()); + assert_eq!(allocator.allocate_exact(addr), Some(addr)); + } else { + let addr = BlockAddr::new(index as u64, BlockMeta::default()); + allocator.deallocate(addr); + } + } + } + + fs.tx(|tx| { + // Update header + tx.header.size = new_size.into(); + tx.header_changed = true; + + // Sync with squash + tx.sync(true)?; + + Ok(()) + }) + .map_err(|err| format!("transaction failed: {}", err)) +} + +fn main() { + env_logger::init(); + + let mut args = env::args().skip(1); + + let disk_path = if let Some(path) = args.next() { + path + } else { + eprintln!("redoxfs-resize: no new disk image provided"); + eprintln!("redoxfs-resize NEW-DISK [SIZE]"); + process::exit(1); + }; + + let size_arg = args.next().unwrap_or_default(); + + let disk = match DiskFile::open(&disk_path) { + Ok(disk) => disk, + Err(err) => { + eprintln!( + "redoxfs-resize: failed to open disk image {}: {}", + disk_path, err + ); + process::exit(1); + } + }; + + let mut fs = match FileSystem::open(disk, None, None, true) { + Ok(fs) => fs, + Err(err) => { + eprintln!( + "redoxfs-resize: failed to open filesystem on {}: {}", + disk_path, err + ); + process::exit(1); + } + }; + + match resize(&mut fs, size_arg) { + Ok(()) => {} + Err(err) => { + eprintln!( + "redoxfs-resize: failed to resize filesystem on {}: {}", + disk_path, err + ); + process::exit(1); + } + } + + let uuid = Uuid::from_bytes(fs.header.uuid()); + let size = fs.header.size(); + let free = fs.allocator().free() * redoxfs::BLOCK_SIZE; + let used = size - free; + println!("redoxfs-resize: resized filesystem on {}", disk_path); + println!("\tuuid: {}", uuid.hyphenated()); + println!( + "\tsize: {} ({})", + format_size(size, DECIMAL), + format_size(size, BINARY) + ); + println!( + "\tused: {} ({})", + format_size(used, DECIMAL), + format_size(used, BINARY) + ); + println!( + "\tfree: {} ({})", + format_size(free, DECIMAL), + format_size(free, BINARY) + ); +} diff --git a/src/block.rs b/src/block.rs new file mode 100644 index 0000000000..2410692f8d --- /dev/null +++ b/src/block.rs @@ -0,0 +1,393 @@ +use core::{fmt, marker::PhantomData, mem, ops, slice}; +use endian_num::Le; + +use crate::BLOCK_SIZE; + +const BLOCK_LIST_ENTRIES: usize = BLOCK_SIZE as usize / mem::size_of::>(); + +/// An address of a data block. +/// +/// This encodes a block's position _and_ [`BlockLevel`]: +/// the first four bits of this `u64` encode the block's level, +/// the next four bits indicates decompression level, +/// the rest encode its index. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct BlockAddr(u64); + +impl BlockAddr { + const INDEX_SHIFT: u64 = 8; + const DECOMP_LEVEL_MASK: u64 = 0xF0; + const DECOMP_LEVEL_SHIFT: u64 = 4; + const LEVEL_MASK: u64 = 0xF; + + // Unsafe because this can create invalid blocks + pub unsafe fn new(index: u64, meta: BlockMeta) -> Self { + // Level must fit within LEVEL_MASK + if meta.level.0 > Self::LEVEL_MASK as usize { + panic!("block level too large"); + } + + // Decomp level must fit within DECOMP_LEVEL_MASK + let decomp_level = meta.decomp_level.unwrap_or_default(); + if (decomp_level.0 << Self::DECOMP_LEVEL_SHIFT) > Self::DECOMP_LEVEL_MASK as usize { + panic!("decompressed block level too large"); + } + + // Index must not use the metadata bits + let inner = index + .checked_shl(Self::INDEX_SHIFT as u32) + .expect("block index too large") + | ((decomp_level.0 as u64) << Self::DECOMP_LEVEL_SHIFT) + | (meta.level.0 as u64); + Self(inner) + } + + pub fn null(meta: BlockMeta) -> Self { + unsafe { Self::new(0, meta) } + } + + pub fn index(&self) -> u64 { + // The first four bits store the level + self.0 >> Self::INDEX_SHIFT + } + + pub fn level(&self) -> BlockLevel { + // The first four bits store the level + BlockLevel((self.0 & Self::LEVEL_MASK) as usize) + } + + pub fn decomp_level(&self) -> Option { + let value = (self.0 & Self::DECOMP_LEVEL_MASK) >> Self::DECOMP_LEVEL_SHIFT; + if value != 0 { + Some(BlockLevel(value as usize)) + } else { + None + } + } + + pub fn meta(&self) -> BlockMeta { + BlockMeta { + level: self.level(), + decomp_level: self.decomp_level(), + } + } + + pub fn is_null(&self) -> bool { + self.index() == 0 + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub struct BlockMeta { + pub(crate) level: BlockLevel, + pub(crate) decomp_level: Option, +} + +impl BlockMeta { + pub fn new(level: BlockLevel) -> Self { + Self { + level, + decomp_level: None, + } + } + + pub fn new_compressed(level: BlockLevel, decomp_level: BlockLevel) -> Self { + Self { + level, + decomp_level: Some(decomp_level), + } + } +} + +/// The size of a block. +/// +/// Level 0 blocks are blocks of [`BLOCK_SIZE`] bytes. +/// A level 1 block consists of two consecutive level 0 blocks. +/// A level n block consists of two consecutive level n-1 blocks. +/// +/// See [`crate::Allocator`] docs for more details. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct BlockLevel(pub(crate) usize); + +impl BlockLevel { + /// Returns the smallest block level that can contain + /// the given number of bytes. + pub(crate) fn for_bytes(bytes: u64) -> Self { + if bytes == 0 { + return BlockLevel(0); + } + let level = bytes + .div_ceil(BLOCK_SIZE) + .next_power_of_two() + .trailing_zeros() as usize; + BlockLevel(level) + } + + /// The number of [`BLOCK_SIZE`] blocks (i.e, level 0 blocks) + /// in a block of this level + pub fn blocks>(self) -> T { + T::from(1u32 << self.0) + } + + /// The number of bytes in a block of this level + pub fn bytes(self) -> u64 { + BLOCK_SIZE << self.0 + } +} + +pub unsafe trait BlockTrait { + /// Create an empty block of this type. + fn empty(level: BlockLevel) -> Option + where + Self: Sized; +} + +/// A [`BlockAddr`] and the data it points to. +#[derive(Clone, Copy, Debug, Default)] +pub struct BlockData { + addr: BlockAddr, + data: T, +} + +impl BlockData { + pub fn new(addr: BlockAddr, data: T) -> Self { + Self { addr, data } + } + + pub fn addr(&self) -> BlockAddr { + self.addr + } + + pub fn data(&self) -> &T { + &self.data + } + + pub fn data_mut(&mut self) -> &mut T { + &mut self.data + } + + pub(crate) unsafe fn into_parts(self) -> (BlockAddr, T) { + (self.addr, self.data) + } + + /// Set the address of this [`BlockData`] to `addr`, returning this + /// block's old address. This method does not update block data. + /// + /// `addr` must point to a block with the same level as this block. + #[must_use = "don't forget to de-allocate old block address"] + pub fn swap_addr(&mut self, addr: BlockAddr) -> BlockAddr { + // Address levels must match + assert_eq!(self.addr.level(), addr.level()); + let old = self.addr; + self.addr = addr; + old + } +} + +impl BlockData { + pub fn empty(addr: BlockAddr) -> Option { + let empty = T::empty(addr.level())?; + Some(Self::new(addr, empty)) + } +} + +impl> BlockData { + pub fn create_ptr(&self) -> BlockPtr { + BlockPtr { + addr: self.addr.0.into(), + hash: seahash::hash(self.data.deref()).into(), + phantom: PhantomData, + } + } +} + +#[repr(C, packed)] +pub struct BlockList { + pub ptrs: [BlockPtr; BLOCK_LIST_ENTRIES], +} + +unsafe impl BlockTrait for BlockList { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self { + ptrs: [BlockPtr::default(); BLOCK_LIST_ENTRIES], + }) + } else { + None + } + } +} + +impl BlockList { + pub fn is_empty(&self) -> bool { + self.ptrs.iter().all(|ptr| ptr.is_null()) + } +} + +impl ops::Deref for BlockList { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + self as *const BlockList as *const u8, + mem::size_of::>(), + ) as &[u8] + } + } +} + +impl ops::DerefMut for BlockList { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut( + self as *mut BlockList as *mut u8, + mem::size_of::>(), + ) as &mut [u8] + } + } +} + +/// An address of a data block, along with a checksum of its data. +/// +/// This encodes a block's position _and_ [`BlockLevel`]. +/// the first four bits of `addr` encode the block's level, +/// the rest encode its index. +/// +/// Also see [`BlockAddr`]. +#[repr(C, packed)] +pub struct BlockPtr { + addr: Le, + hash: Le, + phantom: PhantomData, +} + +impl BlockPtr { + pub fn null(meta: BlockMeta) -> Self { + Self { + addr: BlockAddr::null(meta).0.into(), + hash: 0.into(), + phantom: PhantomData, + } + } + + pub fn addr(&self) -> BlockAddr { + BlockAddr(self.addr.to_ne()) + } + + pub fn hash(&self) -> u64 { + self.hash.to_ne() + } + + pub fn is_null(&self) -> bool { + self.addr().is_null() + } + + pub fn marker(level: u8) -> Self { + assert!(level <= 0xF); + Self { + addr: (0xFFFF_FFFF_FFFF_FFF0 | (level as u64)).into(), + hash: u64::MAX.into(), + phantom: PhantomData, + } + } + + pub fn is_marker(&self) -> bool { + (self.addr.to_ne() | 0xF) == u64::MAX && self.hash.to_ne() == u64::MAX + } + + /// Cast BlockPtr to another type + /// + /// # Safety + /// Unsafe because it can be used to transmute types + pub unsafe fn cast(self) -> BlockPtr { + BlockPtr { + addr: self.addr, + hash: self.hash, + phantom: PhantomData, + } + } + + #[must_use = "the returned pointer should usually be deallocated"] + pub fn clear(&mut self) -> BlockPtr { + let mut ptr = Self::default(); + mem::swap(self, &mut ptr); + ptr + } +} + +impl Clone for BlockPtr { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for BlockPtr {} + +impl Default for BlockPtr { + fn default() -> Self { + Self { + addr: 0.into(), + hash: 0.into(), + phantom: PhantomData, + } + } +} + +impl fmt::Debug for BlockPtr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let addr = self.addr(); + let hash = self.hash(); + f.debug_struct("BlockPtr") + .field("addr", &addr) + .field("hash", &hash) + .finish() + } +} + +#[repr(C, packed)] +#[derive(Clone)] +pub struct BlockRaw([u8; BLOCK_SIZE as usize]); + +unsafe impl BlockTrait for BlockRaw { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self([0; BLOCK_SIZE as usize])) + } else { + None + } + } +} + +impl ops::Deref for BlockRaw { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl ops::DerefMut for BlockRaw { + fn deref_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +#[test] +fn block_list_size_test() { + assert_eq!(mem::size_of::>(), BLOCK_SIZE as usize); +} + +#[test] +fn block_raw_size_test() { + assert_eq!(mem::size_of::(), BLOCK_SIZE as usize); +} + +#[test] +fn block_ptr_marker_test() { + let ptr = BlockPtr::::marker(0); + assert_eq!(ptr.addr().level().0, 0); + assert!(ptr.is_marker()); + + let ptr = BlockPtr::::marker(2); + assert_eq!(ptr.addr().level().0, 2); + assert!(ptr.is_marker()); +} diff --git a/src/clone.rs b/src/clone.rs new file mode 100644 index 0000000000..18fd9a02a1 --- /dev/null +++ b/src/clone.rs @@ -0,0 +1,90 @@ +use crate::{Disk, FileSystem, Node, Transaction, TreePtr, BLOCK_SIZE}; + +fn tx_progress(tx: &mut Transaction, progress: &mut F) { + let size = tx.header.size(); + let free = tx.allocator.free() * BLOCK_SIZE; + progress(size - free); +} + +//TODO: handle hard links +fn clone_at( + tx_old: &mut Transaction, + parent_ptr_old: TreePtr, + tx: &mut Transaction, + parent_ptr: TreePtr, + buf: &mut [u8], + progress: &mut F, +) -> syscall::Result<()> { + let mut entries = Vec::new(); + tx_old.child_nodes(parent_ptr_old, &mut entries)?; + for entry in entries { + //TODO: return error instead? + let Some(name) = entry.name() else { + continue; + }; + let node_ptr_old = entry.node_ptr(); + let node_old = tx_old.read_tree(node_ptr_old)?; + + //TODO: this slows down the clone, but Redox has issues without this (Linux is fine) + if tx.write_cache.len() > 64 { + tx.sync(false)?; + } + + let node_ptr = { + let mode = node_old.data().mode(); + let (ctime, ctime_nsec) = node_old.data().ctime(); + let (mtime, mtime_nsec) = node_old.data().mtime(); + let mut node = tx.create_node(parent_ptr, &name, mode, ctime, ctime_nsec)?; + node.data_mut().set_uid(node_old.data().uid()); + node.data_mut().set_gid(node_old.data().gid()); + node.data_mut().set_mtime(mtime, mtime_nsec); + + if !node_old.data().is_dir() { + let mut offset = 0; + loop { + let count = tx_old.read_node_inner(&node_old, offset, buf)?; + if count == 0 { + break; + } + tx.write_node_inner(&mut node, &mut offset, &buf[..count])?; + } + } + + let node_ptr = node.ptr(); + tx.sync_tree(node)?; + node_ptr + }; + + tx_progress(tx, progress); + + if node_old.data().is_dir() { + clone_at(tx_old, node_ptr_old, tx, node_ptr, buf, progress)?; + } + } + + Ok(()) +} + +pub fn clone( + fs_old: &mut FileSystem, + fs: &mut FileSystem, + mut progress: F, +) -> syscall::Result<()> { + fs_old.tx(|tx_old| { + let mut tx = Transaction::new(fs); + + // Clone at root node + let mut buf = vec![0; 4 * 1024 * 1024]; + clone_at( + tx_old, + TreePtr::root(), + &mut tx, + TreePtr::root(), + &mut buf, + &mut progress, + )?; + + // Commit and squash alloc log + tx.commit(true) + }) +} diff --git a/src/dir.rs b/src/dir.rs new file mode 100644 index 0000000000..b8e8b095b2 --- /dev/null +++ b/src/dir.rs @@ -0,0 +1,300 @@ +use core::{mem, ops, slice, str}; + +use crate::{BlockLevel, BlockTrait, Node, TreePtr, BLOCK_SIZE, DIR_ENTRY_MAX_LENGTH}; + +#[repr(C, packed)] +#[derive(Clone, Copy)] +pub struct DirEntry { + node_ptr: TreePtr, + name: [u8; DIR_ENTRY_MAX_LENGTH], +} + +impl DirEntry { + pub fn new(node_ptr: TreePtr, name: &str) -> DirEntry { + let mut entry = DirEntry { + node_ptr, + ..Default::default() + }; + + entry.name[..name.len()].copy_from_slice(name.as_bytes()); + + entry + } + + pub fn node_ptr(&self) -> TreePtr { + self.node_ptr + } + + fn name_len(&self) -> usize { + let mut len = 0; + while len < self.name.len() { + if self.name[len] == 0 { + break; + } + len += 1; + } + len + } + + pub fn name(&self) -> Option<&str> { + let len = self.name_len(); + //TODO: report utf8 error? + str::from_utf8(&self.name[..len]).ok() + } + + // 4 bytes TreePtr + // 1 byte name_len + const SERIALIZED_PREFIX_SIZE: usize = mem::size_of::>() + 1; + + pub fn serialized_size(&self) -> usize { + DirEntry::SERIALIZED_PREFIX_SIZE + self.name_len() + } + + fn serialize_into(&self, buf: &mut [u8]) -> Option { + let required = self.serialized_size(); + if buf.len() < required { + return None; + } + + buf[0..4].copy_from_slice(&self.node_ptr().to_bytes()); + buf[4] = self.name_len() as u8; + buf[5..5 + self.name_len()].copy_from_slice(&self.name[..self.name_len()]); + + Some(required) + } + + fn deserialize_from(buf: &[u8]) -> Result<(Self, usize), &'static str> { + if buf.len() <= DirEntry::SERIALIZED_PREFIX_SIZE { + return Err("Buffer too small"); + } + + let node_ptr: TreePtr = + TreePtr::from_bytes(buf[0..4].try_into().expect("Slice must be 4 bytes long")); + let name_len = buf[4] as usize; + + if name_len < 1 || name_len > DIR_ENTRY_MAX_LENGTH { + return Err("Invalid name length"); + } + + if buf.len() < DirEntry::SERIALIZED_PREFIX_SIZE + name_len { + return Err("Buffer too small"); + } + + let mut name = [0u8; DIR_ENTRY_MAX_LENGTH]; + name[..name_len].copy_from_slice( + &buf[DirEntry::SERIALIZED_PREFIX_SIZE..DirEntry::SERIALIZED_PREFIX_SIZE + name_len], + ); + + Ok(( + DirEntry { node_ptr, name }, + DirEntry::SERIALIZED_PREFIX_SIZE + name_len, + )) + } +} + +impl Default for DirEntry { + fn default() -> Self { + Self { + node_ptr: TreePtr::default(), + name: [0; DIR_ENTRY_MAX_LENGTH], + } + } +} + +pub struct DirList { + count: u16, + entry_bytes_len: u16, + entry_bytes: [u8; BLOCK_SIZE as usize - 4], +} + +unsafe impl BlockTrait for DirList { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self { + count: 0, + entry_bytes_len: 0, + entry_bytes: [0; BLOCK_SIZE as usize - 4], + }) + } else { + None + } + } +} + +impl DirList { + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + pub fn entries(&self) -> DirEntryIterator<'_> { + DirEntryIterator { + dir_list: self, + emit_count: 0, + position: 0, + } + } + + fn entry_position_for_name(&self, name: &str) -> Option { + let name_len = name.len(); + let mut position = 0; + let mut entry_id = 0; + + while entry_id < self.count { + let entry_name_len = self.entry_bytes[position + 4] as usize; + if entry_name_len == name_len { + let start = DirEntry::SERIALIZED_PREFIX_SIZE + position; + let entry_name = &self.entry_bytes[start..start + entry_name_len]; + if entry_name == name.as_bytes() { + return Some(position); + } + } + position += DirEntry::SERIALIZED_PREFIX_SIZE + entry_name_len; + entry_id += 1; + } + None + } + + pub fn find_entry(&self, name: &str) -> Option { + if let Some(position) = self.entry_position_for_name(name) { + let (entry, _) = DirEntry::deserialize_from(&self.entry_bytes[position..]).unwrap(); + return Some(entry); + } + None + } + + pub fn remove_entry(&mut self, name: &str) -> bool { + if let Some(position) = self.entry_position_for_name(name) { + let entry_size = + DirEntry::SERIALIZED_PREFIX_SIZE + self.entry_bytes[position + 4] as usize; + let remaining_size = self.entry_bytes_len as usize - position - entry_size; + if remaining_size > 0 { + self.entry_bytes.copy_within( + position + entry_size..self.entry_bytes_len as usize, + position, + ); + } + self.entry_bytes_len -= entry_size as u16; + self.count -= 1; + return true; + } + false + } + + pub fn for_each_entry(&self, mut f: F) + where + F: FnMut(&[u8; 4], &[u8]), + { + let mut position = 0; + let mut entry_id = 0; + + while entry_id < self.count { + let node_ptr_bytes = &self.entry_bytes[position..position + 4]; + //let node_ptr = TreePtr::::from_bytes(node_ptr_bytes.try_into().unwrap()); + let entry_name_len = self.entry_bytes[position + 4] as usize; + let start = DirEntry::SERIALIZED_PREFIX_SIZE + position; + let entry_name = &self.entry_bytes[start..start + entry_name_len]; + + f(node_ptr_bytes.try_into().unwrap(), entry_name); + + position += DirEntry::SERIALIZED_PREFIX_SIZE + entry_name_len; + entry_id += 1; + } + } + + pub fn append(&mut self, entry: &DirEntry) -> bool { + let entry_bytes_len = self.entry_bytes_len as usize; + if let Some(size) = entry.serialize_into(&mut self.entry_bytes[entry_bytes_len..]) { + self.count += 1; + self.entry_bytes_len += size as u16; + return true; + } + false + } + + pub fn entry_count(&self) -> usize { + self.count as usize + } +} + +impl ops::Deref for DirList { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + self as *const DirList as *const u8, + mem::size_of::(), + ) as &[u8] + } + } +} + +impl ops::DerefMut for DirList { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut(self as *mut DirList as *mut u8, mem::size_of::()) + as &mut [u8] + } + } +} + +pub struct DirEntryIterator<'a> { + dir_list: &'a DirList, + emit_count: usize, + position: usize, +} + +impl Iterator for DirEntryIterator<'_> { + type Item = DirEntry; + + fn next(&mut self) -> Option { + if self.emit_count < self.dir_list.entry_count() { + let position = self.position; + let (entry, bytes_read) = + DirEntry::deserialize_from(&self.dir_list.entry_bytes[position..]).unwrap(); + + self.emit_count += 1; + self.position += bytes_read; + + Some(entry) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::format; + + #[test] + fn dir_list_size_test() { + use core::ops::Deref; + assert_eq!( + DirList::empty(BlockLevel(0)).unwrap().deref().len(), + BLOCK_SIZE as usize + ); + } + + #[test] + fn test_append() { + let mut dir_list = DirList::empty(BlockLevel(0)).unwrap(); + let dirent = DirEntry::new(TreePtr::new(123), "test000"); + + assert!(dir_list.append(&dirent)); + assert_eq!(dir_list.entry_count(), 1); + assert_eq!(dir_list.entry_bytes_len as usize, dirent.serialized_size()); + + let max_entries = dir_list.entry_bytes.len() / dirent.serialized_size(); + for i in 1..max_entries { + let dirent = DirEntry::new(TreePtr::new(123), format!("test{i:03}").as_str()); + assert!(dir_list.append(&dirent), "Failed on iteration {i}"); + } + let dirent = DirEntry::new(TreePtr::new(123), format!("test{max_entries}").as_str()); + assert!(!dir_list.append(&dirent)); + + for (i, entry) in dir_list.entries().enumerate() { + assert_eq!(entry.name().unwrap(), format!("test{i:03}")); + } + } +} diff --git a/src/disk/cache.rs b/src/disk/cache.rs new file mode 100644 index 0000000000..ddeb336322 --- /dev/null +++ b/src/disk/cache.rs @@ -0,0 +1,110 @@ +use std::collections::{HashMap, VecDeque}; +use std::{cmp, ptr}; +use syscall::error::Result; + +use crate::disk::Disk; +use crate::BLOCK_SIZE; + +fn copy_memory(src: &[u8], dest: &mut [u8]) -> usize { + let len = cmp::min(src.len(), dest.len()); + unsafe { ptr::copy(src.as_ptr(), dest.as_mut_ptr(), len) }; + len +} + +pub struct DiskCache { + inner: T, + cache: HashMap, + order: VecDeque, + size: usize, +} + +impl DiskCache { + pub fn new(inner: T) -> Self { + // 16 MB cache + let size = 16 * 1024 * 1024 / BLOCK_SIZE as usize; + DiskCache { + inner, + cache: HashMap::with_capacity(size), + order: VecDeque::with_capacity(size), + size, + } + } + + fn insert(&mut self, i: u64, data: [u8; BLOCK_SIZE as usize]) { + while self.order.len() >= self.size { + let removed = self.order.pop_front().unwrap(); + self.cache.remove(&removed); + } + + self.cache.insert(i, data); + self.order.push_back(i); + } +} + +impl Disk for DiskCache { + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result { + // println!("Cache read at {}", block); + + let mut read = 0; + let mut failed = false; + for i in 0..buffer.len().div_ceil(BLOCK_SIZE as usize) { + let block_i = block + i as u64; + + let buffer_i = i * BLOCK_SIZE as usize; + let buffer_j = cmp::min(buffer_i + BLOCK_SIZE as usize, buffer.len()); + let buffer_slice = &mut buffer[buffer_i..buffer_j]; + + if let Some(cache_buf) = self.cache.get_mut(&block_i) { + read += copy_memory(cache_buf, buffer_slice); + } else { + failed = true; + break; + } + } + + if failed { + self.inner.read_at(block, buffer)?; + + read = 0; + for i in 0..buffer.len().div_ceil(BLOCK_SIZE as usize) { + let block_i = block + i as u64; + + let buffer_i = i * BLOCK_SIZE as usize; + let buffer_j = cmp::min(buffer_i + BLOCK_SIZE as usize, buffer.len()); + let buffer_slice = &buffer[buffer_i..buffer_j]; + + let mut cache_buf = [0; BLOCK_SIZE as usize]; + read += copy_memory(buffer_slice, &mut cache_buf); + self.insert(block_i, cache_buf); + } + } + + Ok(read) + } + + unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result { + //TODO: Write only blocks that have changed + // println!("Cache write at {}", block); + + self.inner.write_at(block, buffer)?; + + let mut written = 0; + for i in 0..buffer.len().div_ceil(BLOCK_SIZE as usize) { + let block_i = block + i as u64; + + let buffer_i = i * BLOCK_SIZE as usize; + let buffer_j = cmp::min(buffer_i + BLOCK_SIZE as usize, buffer.len()); + let buffer_slice = &buffer[buffer_i..buffer_j]; + + let mut cache_buf = [0; BLOCK_SIZE as usize]; + written += copy_memory(buffer_slice, &mut cache_buf); + self.insert(block_i, cache_buf); + } + + Ok(written) + } + + fn size(&mut self) -> Result { + self.inner.size() + } +} diff --git a/src/disk/file.rs b/src/disk/file.rs new file mode 100644 index 0000000000..78d51bce05 --- /dev/null +++ b/src/disk/file.rs @@ -0,0 +1,84 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Seek, SeekFrom}; +use std::os::unix::fs::FileExt; +use std::path::Path; + +use syscall::error::{Error, Result, EIO}; + +use crate::disk::Disk; +use crate::BLOCK_SIZE; + +pub struct DiskFile { + pub file: File, +} + +trait ResultExt { + type T; + fn or_eio(self) -> Result; +} +impl ResultExt for Result { + type T = T; + fn or_eio(self) -> Result { + match self { + Ok(t) => Ok(t), + Err(err) => { + eprintln!("RedoxFS: IO ERROR: {err}"); + Err(Error::new(EIO)) + } + } + } +} +impl ResultExt for std::io::Result { + type T = T; + fn or_eio(self) -> Result { + match self { + Ok(t) => Ok(t), + Err(err) => { + eprintln!("RedoxFS: IO ERROR: {err}"); + Err(Error::new(EIO)) + } + } + } +} + +impl DiskFile { + pub fn open(path: impl AsRef) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .open(path) + .or_eio()?; + Ok(DiskFile { file }) + } + + pub fn create(path: impl AsRef, size: u64) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path) + .or_eio()?; + file.set_len(size).or_eio()?; + Ok(DiskFile { file }) + } +} + +impl Disk for DiskFile { + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result { + self.file.read_at(buffer, block * BLOCK_SIZE).or_eio() + } + + unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result { + self.file.write_at(buffer, block * BLOCK_SIZE).or_eio() + } + + fn size(&mut self) -> Result { + self.file.seek(SeekFrom::End(0)).or_eio() + } +} + +impl From for DiskFile { + fn from(file: File) -> Self { + Self { file } + } +} diff --git a/src/disk/io.rs b/src/disk/io.rs new file mode 100644 index 0000000000..5b20a8f3f6 --- /dev/null +++ b/src/disk/io.rs @@ -0,0 +1,38 @@ +use std::io::{Read, Seek, SeekFrom, Write}; +use syscall::error::{Error, Result, EIO}; + +use crate::disk::Disk; +use crate::BLOCK_SIZE; + +macro_rules! try_disk { + ($expr:expr) => { + match $expr { + Ok(val) => val, + Err(err) => { + eprintln!("Disk I/O Error: {}", err); + return Err(Error::new(EIO)); + } + } + }; +} + +pub struct DiskIo(pub T); + +impl Disk for DiskIo { + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result { + try_disk!(self.0.seek(SeekFrom::Start(block * BLOCK_SIZE))); + let count = try_disk!(self.0.read(buffer)); + Ok(count) + } + + unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result { + try_disk!(self.0.seek(SeekFrom::Start(block * BLOCK_SIZE))); + let count = try_disk!(self.0.write(buffer)); + Ok(count) + } + + fn size(&mut self) -> Result { + let size = try_disk!(self.0.seek(SeekFrom::End(0))); + Ok(size) + } +} diff --git a/src/disk/memory.rs b/src/disk/memory.rs new file mode 100644 index 0000000000..37eec6c865 --- /dev/null +++ b/src/disk/memory.rs @@ -0,0 +1,42 @@ +use syscall::error::{Error, Result, EIO}; + +use crate::disk::Disk; +use crate::BLOCK_SIZE; + +pub struct DiskMemory { + data: Vec, +} + +impl DiskMemory { + pub fn new(size: u64) -> DiskMemory { + DiskMemory { + data: vec![0; size as usize], + } + } +} + +impl Disk for DiskMemory { + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result { + let offset = (block * BLOCK_SIZE) as usize; + let end = offset + buffer.len(); + if end > self.data.len() { + return Err(Error::new(EIO)); + } + buffer.copy_from_slice(&self.data[offset..end]); + Ok(buffer.len()) + } + + unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result { + let offset = (block * BLOCK_SIZE) as usize; + let end = offset + buffer.len(); + if end > self.data.len() { + return Err(Error::new(EIO)); + } + self.data[offset..end].copy_from_slice(buffer); + Ok(buffer.len()) + } + + fn size(&mut self) -> Result { + Ok(self.data.len() as u64) + } +} diff --git a/src/disk/mod.rs b/src/disk/mod.rs new file mode 100644 index 0000000000..9c51cbac26 --- /dev/null +++ b/src/disk/mod.rs @@ -0,0 +1,41 @@ +use syscall::error::Result; + +#[cfg(feature = "std")] +pub use self::cache::DiskCache; +#[cfg(feature = "std")] +pub use self::file::DiskFile; +#[cfg(feature = "std")] +pub use self::io::DiskIo; +#[cfg(feature = "std")] +pub use self::memory::DiskMemory; +#[cfg(feature = "std")] +pub use self::sparse::DiskSparse; + +#[cfg(feature = "std")] +mod cache; +#[cfg(feature = "std")] +mod file; +#[cfg(feature = "std")] +mod io; +#[cfg(feature = "std")] +mod memory; +#[cfg(feature = "std")] +mod sparse; + +/// A disk +pub trait Disk { + /// Read blocks from disk + /// + /// # Safety + /// Unsafe to discourage use, use filesystem wrappers instead + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result; + + /// Write blocks from disk + /// + /// # Safety + /// Unsafe to discourage use, use filesystem wrappers instead + unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result; + + /// Get size of disk in bytes + fn size(&mut self) -> Result; +} diff --git a/src/disk/sparse.rs b/src/disk/sparse.rs new file mode 100644 index 0000000000..11997af717 --- /dev/null +++ b/src/disk/sparse.rs @@ -0,0 +1,53 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::Path; +use syscall::error::{Error, Result, EIO}; + +use crate::disk::Disk; +use crate::BLOCK_SIZE; + +macro_rules! try_disk { + ($expr:expr) => { + match $expr { + Ok(val) => val, + Err(err) => { + eprintln!("Disk I/O Error: {}", err); + return Err(Error::new(EIO)); + } + } + }; +} + +pub struct DiskSparse { + pub file: File, + pub max_size: u64, +} + +impl DiskSparse { + pub fn create>(path: P, max_size: u64) -> Result { + let file = try_disk!(OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path)); + Ok(DiskSparse { file, max_size }) + } +} + +impl Disk for DiskSparse { + unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result { + try_disk!(self.file.seek(SeekFrom::Start(block * BLOCK_SIZE))); + let count = try_disk!(self.file.read(buffer)); + Ok(count) + } + + unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result { + try_disk!(self.file.seek(SeekFrom::Start(block * BLOCK_SIZE))); + let count = try_disk!(self.file.write(buffer)); + Ok(count) + } + + fn size(&mut self) -> Result { + Ok(self.max_size) + } +} diff --git a/src/filesystem.rs b/src/filesystem.rs new file mode 100644 index 0000000000..5baf6ea5be --- /dev/null +++ b/src/filesystem.rs @@ -0,0 +1,331 @@ +use aes::Aes128; +use alloc::{ + boxed::Box, + collections::{BTreeMap, VecDeque}, + vec, +}; +use syscall::error::{Error, Result, EKEYREJECTED, ENOENT, ENOKEY}; +use xts_mode::{get_tweak_default, Xts128}; + +#[cfg(feature = "std")] +use crate::{AllocEntry, AllocList, BlockData, BlockTrait, Key, KeySlot, Node, Salt, TreeList}; +use crate::{ + Allocator, BlockAddr, BlockLevel, BlockMeta, Disk, Header, Transaction, BLOCK_SIZE, + HEADER_RING, RECORD_SIZE, +}; + +fn compress_cache() -> Box<[u8]> { + vec![0; lz4_flex::block::get_maximum_output_size(RECORD_SIZE as usize)].into_boxed_slice() +} + +/// A file system +pub struct FileSystem { + //TODO: make private + pub disk: D, + //TODO: make private + pub block: u64, + //TODO: make private + pub header: Header, + pub(crate) allocator: Allocator, + pub(crate) cipher_opt: Option>, + pub(crate) compress_cache: Box<[u8]>, + pub node_usages: BTreeMap, +} + +impl FileSystem { + /// Open a file system on a disk + pub fn open( + mut disk: D, + password_opt: Option<&[u8]>, + block_opt: Option, + cleanup: bool, + ) -> Result { + for ring_block in block_opt.map_or(0..65536, |x| x..x + 1) { + let mut header = Header::default(); + unsafe { disk.read_at(ring_block, &mut header)? }; + + // Skip invalid headers + if !header.valid() { + continue; + } + + let block = ring_block - (header.generation() % HEADER_RING); + for i in 0..HEADER_RING { + let mut other_header = Header::default(); + unsafe { disk.read_at(block + i, &mut other_header)? }; + + // Skip invalid headers + if !other_header.valid() { + continue; + } + + // If this is a newer header, use it + if other_header.generation() > header.generation() { + header = other_header; + } + } + + let cipher_opt = match password_opt { + Some(password) => { + if !header.encrypted() { + // Header not encrypted but password provided + return Err(Error::new(EKEYREJECTED)); + } + match header.cipher(password) { + Some(cipher) => Some(cipher), + None => { + // Header encrypted with a different password + return Err(Error::new(ENOKEY)); + } + } + } + None => { + if header.encrypted() { + // Header encrypted but no password provided + return Err(Error::new(ENOKEY)); + } + None + } + }; + + let mut fs = FileSystem { + disk, + block, + header, + allocator: Allocator::default(), + cipher_opt, + compress_cache: compress_cache(), + node_usages: BTreeMap::new(), + }; + + unsafe { fs.reset_allocator()? }; + + if cleanup { + fs.cleanup()? + } + + return Ok(fs); + } + + Err(Error::new(ENOENT)) + } + + /// Create a file system on a disk + #[cfg(feature = "std")] + pub fn create( + disk: D, + password_opt: Option<&[u8]>, + ctime: u64, + ctime_nsec: u32, + ) -> Result { + Self::create_reserved(disk, password_opt, &[], ctime, ctime_nsec) + } + + /// Create a file system on a disk, with reserved data at the beginning + /// Reserved data will be zero padded up to the nearest block + /// We need to pass ctime and ctime_nsec in order to initialize the unix timestamps + #[cfg(feature = "std")] + pub fn create_reserved( + mut disk: D, + password_opt: Option<&[u8]>, + reserved: &[u8], + ctime: u64, + ctime_nsec: u32, + ) -> Result { + let disk_size = disk.size()?; + let disk_blocks = disk_size / BLOCK_SIZE; + let block_offset = (reserved.len() as u64).div_ceil(BLOCK_SIZE); + if disk_blocks < (block_offset + HEADER_RING + 4) { + return Err(Error::new(syscall::error::ENOSPC)); + } + let fs_blocks = disk_blocks - block_offset; + + // Fill reserved data, pad with zeroes + for block in 0..block_offset as usize { + let mut data = [0; BLOCK_SIZE as usize]; + + let mut i = 0; + while i < data.len() && block * BLOCK_SIZE as usize + i < reserved.len() { + data[i] = reserved[block * BLOCK_SIZE as usize + i]; + i += 1; + } + + unsafe { + disk.write_at(block as u64, &data)?; + } + } + + let mut header = Header::new(fs_blocks * BLOCK_SIZE); + + let cipher_opt = match password_opt { + Some(password) => { + //TODO: handle errors + header.key_slots[0] = KeySlot::new( + password, + Salt::new().unwrap(), + (Key::new().unwrap(), Key::new().unwrap()), + ) + .unwrap(); + Some(header.key_slots[0].cipher(password).unwrap()) + } + None => None, + }; + + let mut fs = FileSystem { + disk, + block: block_offset, + header, + allocator: Allocator::default(), + cipher_opt, + compress_cache: compress_cache(), + node_usages: BTreeMap::new(), + }; + + // Write header generation zero + let count = unsafe { fs.disk.write_at(fs.block, &fs.header)? }; + if count != core::mem::size_of_val(&fs.header) { + // Wrote wrong number of bytes + #[cfg(feature = "log")] + log::error!("CREATE: WRONG NUMBER OF BYTES"); + return Err(Error::new(syscall::error::EIO)); + } + + // Set tree and alloc pointers and write header generation one + fs.tx(|tx| unsafe { + let tree = BlockData::new( + BlockAddr::new(HEADER_RING + 1, BlockMeta::default()), + TreeList::empty(BlockLevel::default()).unwrap(), + ); + + let mut alloc = BlockData::new( + BlockAddr::new(HEADER_RING + 2, BlockMeta::default()), + AllocList::empty(BlockLevel::default()).unwrap(), + ); + + let alloc_free = fs_blocks - (HEADER_RING + 4); + alloc.data_mut().entries[0] = AllocEntry::new(HEADER_RING + 4, alloc_free as i64); + + tx.header.tree = tx.write_block(tree)?; + tx.header.alloc = tx.write_block(alloc)?; + tx.header_changed = true; + + Ok(()) + })?; + + unsafe { + fs.reset_allocator()?; + } + + fs.tx(|tx| unsafe { + let mut root = BlockData::new( + BlockAddr::new(HEADER_RING + 3, BlockMeta::default()), + Node::new(Node::MODE_DIR | 0o755, 0, 0, ctime, ctime_nsec), + ); + root.data_mut().set_links(1); + let root_ptr = tx.write_block(root)?; + assert_eq!(tx.insert_tree(root_ptr)?.id(), 1); + Ok(()) + })?; + + fs.cleanup()?; + + Ok(fs) + } + + /// Release unused nodes and squash allocation log, happens on mount (with cleanup) and unmount + pub fn cleanup(&mut self) -> Result<()> { + let mut tx = Transaction::new(self); + tx.release_unused_nodes()?; + tx.commit(true) + } + + /// start a filesystem transaction, required for making any changes + pub fn tx) -> Result, T>(&mut self, f: F) -> Result { + let mut tx = Transaction::new(self); + let t = f(&mut tx)?; + tx.commit(false)?; + Ok(t) + } + + pub fn allocator(&self) -> &Allocator { + &self.allocator + } + + /// Unsafe as it can corrupt the filesystem + pub unsafe fn allocator_mut(&mut self) -> &mut Allocator { + &mut self.allocator + } + + /// Reset allocator to state stored on disk + /// + /// # Safety + /// Unsafe, it must only be called when opening the filesystem + unsafe fn reset_allocator(&mut self) -> Result<()> { + self.allocator = Allocator::default(); + + // To avoid having to update all prior alloc blocks, there is only a previous pointer + // This means we need to roll back all allocations. Currently we do this by reading the + // alloc log into a buffer to reverse it. + let mut allocs = VecDeque::new(); + self.tx(|tx| { + let mut alloc_ptr = tx.header.alloc; + while !alloc_ptr.is_null() { + let alloc = tx.read_block(alloc_ptr)?; + alloc_ptr = alloc.data().prev; + allocs.push_front(alloc); + } + Ok(()) + })?; + + for alloc in allocs { + for entry in alloc.data().entries.iter() { + let index = entry.index(); + let count = entry.count(); + if count < 0 { + for i in 0..-count { + //TODO: replace assert with error? + let addr = BlockAddr::new(index + i as u64, BlockMeta::default()); + assert_eq!(self.allocator.allocate_exact(addr), Some(addr)); + } + } else { + for i in 0..count { + let addr = BlockAddr::new(index + i as u64, BlockMeta::default()); + self.allocator.deallocate(addr); + } + } + } + } + + Ok(()) + } + + pub(crate) fn decrypt(&mut self, data: &mut [u8], addr: BlockAddr) -> bool { + if let Some(ref cipher) = self.cipher_opt { + cipher.decrypt_area( + data, + BLOCK_SIZE as usize, + addr.index().into(), + get_tweak_default, + ); + true + } else { + // Do nothing if encryption is disabled + false + } + } + + pub(crate) fn encrypt(&mut self, data: &mut [u8], addr: BlockAddr) -> bool { + if let Some(ref cipher) = self.cipher_opt { + cipher.encrypt_area( + data, + BLOCK_SIZE as usize, + addr.index().into(), + get_tweak_default, + ); + true + } else { + // Do nothing if encryption is disabled + false + } + } +} diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000000..b47d6ff159 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,242 @@ +use core::ops::{Deref, DerefMut}; +use core::{fmt, mem, slice}; +use endian_num::Le; + +use aes::Aes128; +use xts_mode::{get_tweak_default, Xts128}; + +use crate::{AllocList, BlockPtr, KeySlot, ReleaseList, Tree, BLOCK_SIZE, SIGNATURE, VERSION}; + +pub const HEADER_RING: u64 = 256; + +/// The header of the filesystem +#[derive(Clone, Copy)] +#[repr(C, packed)] +pub struct Header { + /// Signature, should be SIGNATURE + pub signature: [u8; 8], + /// Version, should be VERSION + pub version: Le, + /// Disk ID, a 128-bit unique identifier + pub uuid: [u8; 16], + /// Disk size, in number of BLOCK_SIZE sectors + pub size: Le, + /// Generation of header + pub generation: Le, + /// Block of first tree node + pub tree: BlockPtr, + /// Block of last alloc node + pub alloc: BlockPtr, + /// Key slots + pub key_slots: [KeySlot; 64], + /// Nodes pending release, may be null + pub release: BlockPtr, + /// Padding + pub padding: [u8; BLOCK_SIZE as usize - 3192], + /// encrypted hash of header data without hash, set to hash and padded if disk is not encrypted + pub encrypted_hash: [u8; 16], + /// hash of header data without hash + pub hash: Le, +} + +impl Header { + #[cfg(feature = "std")] + pub fn new(size: u64) -> Header { + let uuid = uuid::Uuid::new_v4(); + let mut header = Header { + signature: *SIGNATURE, + version: VERSION.into(), + uuid: *uuid.as_bytes(), + size: size.into(), + ..Default::default() + }; + header.update_hash(None); + header + } + + pub fn valid(&self) -> bool { + if &self.signature != SIGNATURE { + // Signature does not match + return false; + } + + if self.version.to_ne() != VERSION { + // Version does not match + return false; + } + + if self.hash.to_ne() != self.create_hash() { + // Hash does not match + return false; + } + + // All tests passed, header is valid + true + } + + pub fn uuid(&self) -> [u8; 16] { + self.uuid + } + + pub fn size(&self) -> u64 { + self.size.to_ne() + } + + pub fn generation(&self) -> u64 { + self.generation.to_ne() + } + + fn create_hash(&self) -> u64 { + // Calculate part of header to hash (everything before the hashes) + let end = mem::size_of_val(self) + - mem::size_of_val(&{ self.hash }) + - mem::size_of_val(&{ self.encrypted_hash }); + seahash::hash(&self[..end]) + } + + fn create_encrypted_hash(&self, cipher_opt: Option<&Xts128>) -> [u8; 16] { + let mut encrypted_hash = [0; 16]; + for (i, b) in self.hash.to_le_bytes().iter().enumerate() { + encrypted_hash[i] = *b; + } + if let Some(cipher) = cipher_opt { + let mut block = aes::Block::from(encrypted_hash); + cipher.encrypt_area( + &mut block, + BLOCK_SIZE as usize, + self.generation().into(), + get_tweak_default, + ); + encrypted_hash = block.into(); + } + encrypted_hash + } + + pub fn encrypted(&self) -> bool { + (self.encrypted_hash) != self.create_encrypted_hash(None) + } + + pub fn cipher(&self, password: &[u8]) -> Option> { + let hash = self.create_encrypted_hash(None); + for slot in self.key_slots.iter() { + //TODO: handle errors + let cipher = slot.cipher(password).unwrap(); + let mut block = aes::Block::from(self.encrypted_hash); + cipher.decrypt_area( + &mut block, + BLOCK_SIZE as usize, + self.generation().into(), + get_tweak_default, + ); + if block == aes::Block::from(hash) { + return Some(cipher); + } + } + None + } + + fn update_hash(&mut self, cipher_opt: Option<&Xts128>) { + self.hash = self.create_hash().into(); + // Make sure to do this second, it relies on the hash being up to date + self.encrypted_hash = self.create_encrypted_hash(cipher_opt); + } + + pub fn update(&mut self, cipher_opt: Option<&Xts128>) -> u64 { + let mut generation = self.generation(); + generation += 1; + self.generation = generation.into(); + self.update_hash(cipher_opt); + generation + } +} + +impl Default for Header { + fn default() -> Self { + Self { + signature: [0; 8], + version: 0.into(), + uuid: [0; 16], + size: 0.into(), + generation: 0.into(), + tree: BlockPtr::::default(), + alloc: BlockPtr::::default(), + key_slots: [KeySlot::default(); 64], + release: BlockPtr::::default(), + padding: [0; BLOCK_SIZE as usize - 3192], + encrypted_hash: [0; 16], + hash: 0.into(), + } + } +} + +impl fmt::Debug for Header { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let signature = self.signature; + let version = self.version; + let uuid = self.uuid; + let size = self.size; + let generation = self.generation; + let tree = self.tree; + let alloc = self.alloc; + let release = self.release; + let hash = self.hash; + f.debug_struct("Header") + .field("signature", &signature) + .field("version", &version) + .field("uuid", &uuid) + .field("size", &size) + .field("generation", &generation) + .field("tree", &tree) + .field("alloc", &alloc) + .field("release", &release) + .field("hash", &hash) + .finish() + } +} + +impl Deref for Header { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts(self as *const Header as *const u8, mem::size_of::
()) + as &[u8] + } + } +} + +impl DerefMut for Header { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut(self as *mut Header as *mut u8, mem::size_of::
()) + as &mut [u8] + } + } +} + +#[test] +fn header_not_valid_test() { + assert_eq!(Header::default().valid(), false); +} + +#[test] +fn header_size_test() { + assert_eq!(mem::size_of::
(), BLOCK_SIZE as usize); +} + +#[test] +fn header_hash_test() { + let mut header = Header::default(); + assert_eq!(header.create_hash(), 0xe81ffcb86026ff96); + header.update_hash(None); + assert_eq!(header.hash.to_ne(), 0xe81ffcb86026ff96); + assert_eq!( + header.encrypted_hash, + [0x96, 0xff, 0x26, 0x60, 0xb8, 0xfc, 0x1f, 0xe8, 0, 0, 0, 0, 0, 0, 0, 0] + ); +} + +#[cfg(feature = "std")] +#[test] +fn header_valid_test() { + assert_eq!(Header::new(0).valid(), true); +} diff --git a/src/htree.rs b/src/htree.rs new file mode 100644 index 0000000000..b41620f6c6 --- /dev/null +++ b/src/htree.rs @@ -0,0 +1,691 @@ +use alloc::string::String; +use alloc::vec::Vec; +use core::{fmt, mem, ops, slice}; +use endian_num::Le; +use syscall::error::{Error, Result, EEXIST, EIO}; + +use crate::{ + BlockLevel, BlockPtr, BlockRaw, BlockTrait, DirEntry, DirList, BLOCK_SIZE, RECORD_LEVEL, +}; + +pub const HTREE_IDX_ENTRIES: usize = BLOCK_SIZE as usize / mem::size_of::>(); +const HTREE_IDX_PADDING: usize = + BLOCK_SIZE as usize - mem::size_of::<[HTreePtr; HTREE_IDX_ENTRIES]>(); + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(C, packed)] +pub struct HTreeHash(Le); + +impl HTreeHash { + // Create a MAX constant populated iwth the maximum value of Le minus 1 + pub const MAX: HTreeHash = HTreeHash(Le(u32::MAX - 1)); + + #[cfg(not(test))] + pub fn from_name(name: &str) -> Self { + let hash = seahash::hash(name.as_bytes()) as u32; + // Don't allow the default hash value to be calculated for a real hash + if hash == u32::MAX { + return Self::MAX; + } + Self(hash.into()) + } + + #[cfg(test)] + pub fn from_name(name: &str) -> Self { + // Allow overriding the hashing function to something easily controled for testing. + let hash = if let Some(pos) = name.rfind("__") { + let number_str = &name[pos + 2..]; + number_str.parse::().unwrap() + } else { + seahash::hash(name.as_bytes()) as u32 + }; + + // Don't allow the default hash value to be calculated for a real hash + if hash == u32::MAX { + return Self::MAX; + } + Self(hash.into()) + } + + /// Returns the maximum of two `HTreeHash` values, ignoring the default hash value. + pub fn max_ignoring_default(&self, other: Self) -> Self { + let default = HTreeHash::default(); + if *self == default { + return other; + } + if other == default { + return *self; + } + if *self > other { + *self + } else { + other + } + } + + pub fn find_max(dir_list: &DirList) -> Option { + let mut max_hash = HTreeHash::default(); + dir_list.for_each_entry(|_ptr_bytes, name_bytes| { + let name = String::from_utf8_lossy(name_bytes); + let hash = HTreeHash::from_name(name.as_ref()); + max_hash = max_hash.max_ignoring_default(hash); + }); + + if max_hash == HTreeHash::default() { + None + } else { + Some(max_hash) + } + } +} + +impl Default for HTreeHash { + /// The default hash value is the maximum possible value to push it to the end of the list when sorting. + fn default() -> Self { + Self(u32::MAX.into()) + } +} + +#[repr(C, packed)] +pub struct HTreePtr { + pub htree_hash: HTreeHash, + pub ptr: BlockPtr, +} + +impl HTreePtr { + pub fn new(htree_hash: HTreeHash, ptr: BlockPtr) -> Self { + Self { htree_hash, ptr } + } + + /// Cast HTreePtr to another type + /// + /// # Safety + /// Unsafe because it can be used to transmute types + pub unsafe fn cast(self) -> HTreePtr { + HTreePtr { + htree_hash: self.htree_hash, + ptr: self.ptr.cast(), + } + } +} + +impl HTreePtr { + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } +} + +impl Clone for HTreePtr { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for HTreePtr {} + +impl Default for HTreePtr { + fn default() -> Self { + Self { + htree_hash: HTreeHash::default(), + ptr: BlockPtr::default(), + } + } +} + +impl fmt::Debug for HTreePtr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let htree_hash = self.htree_hash; + let ptr = self.ptr; + f.debug_struct("BlockPtr") + .field("htree_hash", &htree_hash) + .field("ptr", &ptr) + .finish() + } +} + +#[repr(C, packed)] +pub struct HTreeNode { + pub ptrs: [HTreePtr; HTREE_IDX_ENTRIES], + padding: [u8; HTREE_IDX_PADDING], +} + +impl HTreeNode { + pub fn find_max_htree_hash(&self) -> Option { + let mut hash = HTreeHash::default(); + for entry in self.ptrs.iter() { + hash = hash.max_ignoring_default(entry.htree_hash); + } + + if hash != HTreeHash::default() { + Some(hash) + } else { + None + } + } + + pub fn find_ptrs_for_read( + &self, + htree_hash: HTreeHash, + ) -> impl Iterator)> { + let mut last_hash = HTreeHash(0.into()); + self.ptrs + .iter() + .enumerate() + .filter(move |(_idx, entry)| entry.htree_hash >= htree_hash) + .take_while(move |(_idx, entry)| { + let should_take = !entry.is_null() && last_hash <= htree_hash; + last_hash = entry.htree_hash; + should_take + }) + } +} + +unsafe impl BlockTrait for HTreeNode { + fn empty(level: BlockLevel) -> Option { + if level.0 <= RECORD_LEVEL { + Some(Self { + ptrs: [HTreePtr::default(); HTREE_IDX_ENTRIES], + padding: [0; HTREE_IDX_PADDING], + }) + } else { + None + } + } +} + +impl ops::Deref for HTreeNode { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + self as *const HTreeNode as *const u8, + mem::size_of::>(), + ) as &[u8] + } + } +} + +impl ops::DerefMut for HTreeNode { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut( + self as *mut HTreeNode as *mut u8, + mem::size_of::>(), + ) as &mut [u8] + } + } +} + +pub fn add_inner_node( + parent: &mut HTreeNode, + new_ptr: HTreePtr, +) -> Result)>> { + // Update the input htree parameters in place + for ptr in parent.ptrs.iter_mut() { + if ptr.is_null() { + *ptr = new_ptr; + parent.ptrs.sort_by(|a, b| a.htree_hash.cmp(&b.htree_hash)); + return Ok(None); + } + } + + // The parent is full. We need to split it into two by half, ordered by the htree hash. + let mut all_ptrs = Vec::with_capacity(parent.ptrs.len() + 1); + for ptr in parent.ptrs.iter() { + all_ptrs.push(*ptr); + } + all_ptrs.push(new_ptr); + all_ptrs.sort_by(|a, b| a.htree_hash.cmp(&b.htree_hash)); + let half_idx = all_ptrs.len() / 2; + + // Find if there are duplicate name hashes on the boundary of where we want to split + let half_name_hash = all_ptrs[half_idx].htree_hash; + let mut first_idx = half_idx; + let mut last_idx = half_idx; + for (i, ptr) in all_ptrs.iter().enumerate() { + if ptr.htree_hash == half_name_hash { + if i < first_idx { + first_idx = i; + } + if i > last_idx { + last_idx = i; + } + } + } + + // Split the entries_with_name_hash list at the index that minimizes the number of entries in each list while keeping the duplicate name hashes together + let split = if (half_idx - first_idx) < (last_idx - half_idx) { + first_idx + } else { + last_idx + }; + + let (ptrs1, ptrs2) = all_ptrs.split_at(split); + + // Update the existing parent with the first half of the entries + let mut htree_idx1 = HTreeNode::empty(BlockLevel::default()).ok_or(Error::new(EIO))?; + htree_idx1.ptrs[..ptrs1.len()].copy_from_slice(ptrs1); + let _ = mem::replace(parent, htree_idx1); + + // Return the second half as a new sibling parent + let mut htree_idx2 = HTreeNode::empty(BlockLevel::default()).ok_or(Error::new(EIO))?; + htree_idx2.ptrs[..ptrs2.len()].copy_from_slice(ptrs2); + + let htree_hash2 = ptrs2[ptrs2.len() - 1].htree_hash; + Ok(Some((htree_hash2, htree_idx2))) +} + +pub fn add_dir_entry( + dir_list: &mut DirList, + htree_hash: &mut HTreeHash, + dirent: DirEntry, +) -> Result> { + if let Some(name) = dirent.name() { + if dir_list.find_entry(name).is_some() { + return Err(Error::new(EEXIST)); + } + } + + // Update the input htree parameters in place + let name = dirent.name().ok_or(Error::new(EIO))?; + if dir_list.append(&dirent) { + *htree_hash = HTreeHash::from_name(name).max_ignoring_default(*htree_hash); + return Ok(None); + } + + // The dir_list is full. We need to split it into two dir_lists by half, ordered by the name hash. + let mut entries_with_name_hash = Vec::with_capacity(dir_list.entry_count() + 1); + for entry in dir_list.entries() { + entries_with_name_hash.push(( + HTreeHash::from_name(entry.name().ok_or(Error::new(EIO))?), + entry, + )); + } + entries_with_name_hash.push((HTreeHash::from_name(dirent.name().unwrap()), dirent)); + entries_with_name_hash.sort_by(|a, b| a.0.cmp(&b.0)); + let half = entries_with_name_hash.len() / 2; + let half_name_hash = entries_with_name_hash[half].0; + + // Find if there are duplicate name hashes on the boundary of where we want to split + let mut first_idx = half; + let mut last_idx = half; + for (i, (name_hash, _)) in entries_with_name_hash.iter().enumerate() { + if *name_hash == half_name_hash { + if i < first_idx { + first_idx = i; + } + if i > last_idx { + last_idx = i; + } + } + } + last_idx += 1; + + // Split the entries_with_name_hash list at the index that minimizes the number of entries in each list while keeping the duplicate name hashes together + let split = if (half - first_idx) < (last_idx - half) { + first_idx + } else { + last_idx + }; + let split = split.max(1); + + let sorted_entries = entries_with_name_hash + .iter() + .map(|(_, entry)| *entry) + .collect::>(); + + let (entries1, entries2) = sorted_entries.split_at(split); + + // Update the existing dir_list with the first half of the entries + let mut new_dir_list = DirList::empty(BlockLevel::default()).ok_or(Error::new(EIO))?; + for entry in entries1.iter() { + new_dir_list.append(entry); + } + let _ = mem::replace(dir_list, new_dir_list); + *htree_hash = entries_with_name_hash[entries1.len() - 1].0; + + // Return the second half of the entries as a new dir_list + let mut new_dir_list = DirList::empty(BlockLevel::default()).ok_or(Error::new(EIO))?; + for entry in entries2.iter() { + new_dir_list.append(entry); + } + let new_name_hash = entries_with_name_hash[entries_with_name_hash.len() - 1].0; + Ok(Some((new_name_hash, new_dir_list))) +} + +// +// MARK: Unit Tests +// + +#[cfg(test)] +mod tests { + use super::*; + use crate::alloc::string::ToString; + use crate::TreePtr; + use alloc::format; + use alloc::string::String; + + #[test] + fn htree_ptr_size_test() { + assert_eq!(mem::size_of::>(), 20); + } + + #[test] + fn htree_node_size_test() { + assert_eq!(mem::size_of::>(), BLOCK_SIZE as usize); + } + + #[test] + fn htree_hash_max_test() { + assert_eq!(HTreeHash::MAX, HTreeHash((u32::MAX - 1).into())); + } + + #[test] + fn htree_hash_max_ignoring_default_test() { + let default = HTreeHash::default(); + let hash1 = HTreeHash(0.into()); + let hash2 = HTreeHash(1.into()); + + assert_eq!(hash1.max_ignoring_default(default), hash1); + assert_eq!(default.max_ignoring_default(hash1), hash1); + assert_eq!(hash1.max_ignoring_default(hash2), hash2); + } + + #[test] + fn htree_node_find_max_htree_hash() { + // In practice, the HTreeHash values should always be in sorted order + let mut htree_node: HTreeNode = HTreeNode::empty(BlockLevel::default()).unwrap(); + htree_node.ptrs[0] = HTreePtr::new(HTreeHash(0.into()), BlockPtr::marker(0)); + htree_node.ptrs[1] = HTreePtr::new(HTreeHash(1.into()), BlockPtr::marker(0)); + htree_node.ptrs[2] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0)); + + assert_eq!( + htree_node.find_max_htree_hash().unwrap(), + HTreeHash(2.into()) + ); + + htree_node.ptrs[2] = HTreePtr::default(); + assert_eq!( + htree_node.find_max_htree_hash().unwrap(), + HTreeHash(1.into()) + ); + + htree_node.ptrs[1] = HTreePtr::default(); + assert_eq!( + htree_node.find_max_htree_hash().unwrap(), + HTreeHash(0.into()) + ); + + htree_node.ptrs[0] = HTreePtr::default(); + assert!(htree_node.find_max_htree_hash().is_none()); + + // For thoroughness, test with HTreeHash out of order + htree_node.ptrs[2] = HTreePtr::new(HTreeHash(4.into()), BlockPtr::marker(0)); + htree_node.ptrs[4] = HTreePtr::new(HTreeHash(6.into()), BlockPtr::marker(0)); + htree_node.ptrs[6] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0)); + assert_eq!( + htree_node.find_max_htree_hash().unwrap(), + HTreeHash(6.into()) + ); + } + + #[test] + fn htree_node_find_for_read() { + let mut htree_node: HTreeNode = HTreeNode::empty(BlockLevel::default()).unwrap(); + htree_node.ptrs[0] = HTreePtr::new(HTreeHash(0.into()), BlockPtr::marker(0)); + htree_node.ptrs[1] = HTreePtr::new(HTreeHash(1.into()), BlockPtr::marker(0)); + htree_node.ptrs[2] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0)); + htree_node.ptrs[3] = HTreePtr::new(HTreeHash(2.into()), BlockPtr::marker(0)); + htree_node.ptrs[4] = HTreePtr::new(HTreeHash(3.into()), BlockPtr::marker(0)); + htree_node.ptrs[5] = HTreePtr::new(HTreeHash(3.into()), BlockPtr::marker(0)); + htree_node.ptrs[6] = HTreePtr::new(HTreeHash(5.into()), BlockPtr::marker(0)); + htree_node.ptrs[7] = HTreePtr::new(HTreeHash(6.into()), BlockPtr::marker(0)); + + // Confirm that a hash that does not exist, but is less than an existing hash results in a single entry + let mut iter = htree_node.find_ptrs_for_read(HTreeHash(4.into())); + let mut val = iter.next().unwrap(); + assert_eq!(val.0, 6); + assert_eq!(val.1.htree_hash, HTreeHash(5.into())); + assert!(iter.next().is_none()); + + // Confirm that a hash that equals an existing hash results in the match and one following entry + let mut iter = htree_node.find_ptrs_for_read(HTreeHash(1.into())); + val = iter.next().unwrap(); + assert_eq!(val.0, 1); + assert_eq!(val.1.htree_hash, HTreeHash(1.into())); + val = iter.next().unwrap(); + assert_eq!(val.0, 2); + assert_eq!(val.1.htree_hash, HTreeHash(2.into())); + assert!(iter.next().is_none()); + + // Confirm that multiple exact hash matches are all returned plus the next entry + let mut iter = htree_node.find_ptrs_for_read(HTreeHash(2.into())); + val = iter.next().unwrap(); + assert_eq!(val.0, 2); + assert_eq!(val.1.htree_hash, HTreeHash(2.into())); + val = iter.next().unwrap(); + assert_eq!(val.0, 3); + assert_eq!(val.1.htree_hash, HTreeHash(2.into())); + val = iter.next().unwrap(); + assert_eq!(val.0, 4); + assert_eq!(val.1.htree_hash, HTreeHash(3.into())); + assert!(iter.next().is_none()); + + // Confirm that if the last entry matches and the next entry is null, only the match is returned + let mut iter = htree_node.find_ptrs_for_read(HTreeHash(6.into())); + val = iter.next().unwrap(); + assert_eq!(val.0, 7); + assert_eq!(val.1.htree_hash, HTreeHash(6.into())); + assert!(iter.next().is_none()); + + // Confirm that if a hash that is larger than any existing entries, then no entries are returned + let mut iter = htree_node.find_ptrs_for_read(HTreeHash(7.into())); + assert!(iter.next().is_none()); + } + + #[test] + fn add_dir_entry_exists_test() { + let mut dir_list = DirList::empty(BlockLevel::default()).unwrap(); + let mut htree_hash = HTreeHash::default(); + let dirent = DirEntry::new(TreePtr::new(123), "test"); + let new_sibling = add_dir_entry(&mut dir_list, &mut htree_hash, dirent).unwrap(); + assert!(new_sibling.is_none()); + assert_eq!(htree_hash, HTreeHash::from_name("test")); + assert_eq!(dir_list.entries().next().unwrap().name(), Some("test")); + + // Add the same entry again, and it should fail with an appropriate IO error + let dirent = DirEntry::new(TreePtr::new(123), "test"); + let error_expected = add_dir_entry(&mut dir_list, &mut htree_hash, dirent); + assert!(error_expected.is_err()); + assert_eq!(error_expected.err().unwrap().errno, EEXIST); + } + + #[test] + fn add_dir_entry_many_test() { + let mut dir_list = DirList::empty(BlockLevel::default()).unwrap(); + let mut htree_hash = HTreeHash::default(); + let total_count = 16; + + // Fill up the dir_list + for i in 0..total_count { + let v: usize = i % 10; + let dirent = DirEntry::new(TreePtr::new(123), format!("test{v}_{i:0244}").as_str()); + let new_sibling = add_dir_entry(&mut dir_list, &mut htree_hash, dirent).unwrap(); + assert!(new_sibling.is_none()); + } + + // The maximum htree_hash should be retained + let max_tree_hash = + dir_list + .entries() + .enumerate() + .fold(HTreeHash::default(), |max, (i, _)| { + let v = i % 10; + let hash = HTreeHash::from_name(format!("test{v}_{i:0244}").as_str()); + max.max_ignoring_default(hash) + }); + assert_eq!(htree_hash, max_tree_hash); + + // Confirm all the entries exist. Note they happen to be in insert order + for (i, entry) in dir_list.entries().enumerate() { + let v = i % 10; + assert_eq!(entry.name(), Some(format!("test{v}_{i:0244}").as_str())); + } + + // Test a split by adding one more entry + let dirent = DirEntry::new(TreePtr::new(123), "test_split"); + let new_sibling = add_dir_entry(&mut dir_list, &mut htree_hash, dirent).unwrap(); + let (new_sibling_htree_hash, new_sibling_dir_list) = + new_sibling.expect("new_sibling should be created"); + // assert!(new_sibling_dir_list.entries.len() ); + assert!(new_sibling_htree_hash > htree_hash); + + // The htree_hash should be less than the minimum htree_hash in new_sibling_dir_list + let new_sibling_min_htree_hash = new_sibling_dir_list + .entries() + .filter(|entry| !entry.node_ptr().is_null()) + .fold(HTreeHash::default(), |min, entry| { + let hash = HTreeHash::from_name(entry.name().unwrap()); + min.min(hash) + }); + assert!(htree_hash < new_sibling_min_htree_hash); + + // Confirm all the entries exist across both dir_lists + let mut expected_names: Vec = (0..total_count) + .map(|i| { + let v = i % 10; + format!("test{v}_{i:0244}") + }) + .collect(); + expected_names.push("test_split".to_string()); + expected_names.sort(); + + let mut dir_list_entry_count = 0; + for entry in dir_list.entries() { + dir_list_entry_count += 1; + let name = entry.name().unwrap().to_string(); + let _ = expected_names.remove(expected_names.binary_search(&name).unwrap()); + } + + let mut new_sibling_entry_count = 0; + for entry in new_sibling_dir_list.entries() { + new_sibling_entry_count += 1; + let name = entry.name().unwrap().to_string(); + let _ = expected_names.remove(expected_names.binary_search(&name).unwrap()); + } + assert!(expected_names.is_empty()); + + // Confirm that the split is in half + assert!((dir_list_entry_count as i32 - new_sibling_entry_count).abs() <= 1); + } + + #[test] + fn add_inner_node_simple_test() { + let mut htree_node: HTreeNode<_> = HTreeNode::empty(BlockLevel::default()).unwrap(); + let htree_ptr: HTreePtr<_> = HTreePtr:: { + htree_hash: HTreeHash::from_name("test"), + ptr: BlockPtr::marker(0), + }; + let new_sibling = add_inner_node(&mut htree_node, htree_ptr).unwrap(); + assert!(new_sibling.is_none()); + assert_eq!(htree_node.ptrs[0].htree_hash, HTreeHash::from_name("test")); + } + + #[test] + fn add_inner_node_multiple_test() { + let mut htree_node: HTreeNode<_> = HTreeNode::empty(BlockLevel::default()).unwrap(); + + for i in 0..HTREE_IDX_ENTRIES { + let htree_ptr: HTreePtr<_> = HTreePtr:: { + htree_hash: HTreeHash(((100_000 + (i % 10) * 1000 + i) as u32).into()), + ptr: BlockPtr::marker(0), + }; + let new_sibling = add_inner_node(&mut htree_node, htree_ptr).unwrap(); + assert!(new_sibling.is_none()); + + // Confirm that the htree_ptrs are in sorted order at the start of the ptrs list + let mut prev_hash = HTreeHash::default(); + let mut count = 0; + for ptr in htree_node.ptrs.iter() { + if ptr.is_null() { + continue; + } + assert!( + ptr.htree_hash.max_ignoring_default(prev_hash) == ptr.htree_hash, + "index {i}: {:?} > {:?}", + ptr.htree_hash, + prev_hash + ); + prev_hash = ptr.htree_hash; + count += 1; + } + assert_eq!(count, i + 1); + } + + // Confirm all expected hashes are present + let mut expected_hashes: Vec = (0..HTREE_IDX_ENTRIES) + .map(|i| (100_000 + (i % 10) * 1000 + i) as u32) + .collect(); + expected_hashes.sort(); + + for ptr in htree_node.ptrs.iter() { + if ptr.is_null() { + break; + } + let idx = expected_hashes + .binary_search(&ptr.htree_hash.0.into()) + .unwrap(); + expected_hashes.remove(idx); + } + assert!(expected_hashes.is_empty()); + + // Force a split by adding one more entry + let htree_ptr: HTreePtr<_> = HTreePtr:: { + htree_hash: HTreeHash(130_000.into()), + ptr: BlockPtr::marker(0), + }; + + let mut expected_hashes: Vec = (0..HTREE_IDX_ENTRIES) + .map(|i| (100_000 + (i % 10) * 1000 + i) as u32) + .collect(); + expected_hashes.push(130_000); + expected_hashes.sort(); + + let new_sibling = add_inner_node(&mut htree_node, htree_ptr).unwrap(); + let new_sibling = new_sibling.expect("new_sibling should be created"); + + // Confirm all the entries exist across both htree_nodes + let mut htree_node_entry_count = 0; + for ptr in htree_node.ptrs.iter() { + if ptr.ptr.is_null() { + break; + } + htree_node_entry_count += 1; + let idx = expected_hashes + .binary_search(&ptr.htree_hash.0.into()) + .unwrap(); + expected_hashes.remove(idx); + } + + let mut new_sibling_entry_count = 0; + for ptr in new_sibling.1.ptrs.iter() { + if ptr.ptr.is_null() { + break; + } + new_sibling_entry_count += 1; + let idx = expected_hashes + .binary_search(&ptr.htree_hash.0.into()) + .unwrap(); + expected_hashes.remove(idx); + } + assert!( + expected_hashes.is_empty(), + "expected_hashes should be empty, but had length {}: {:?}", + expected_hashes.len(), + expected_hashes + ); + + // Confirm that the split is in half + assert!((htree_node_entry_count as i32 - new_sibling_entry_count).abs() <= 1); + } +} diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000000..8c44a356cc --- /dev/null +++ b/src/key.rs @@ -0,0 +1,104 @@ +use aes::{ + cipher::{BlockDecrypt, BlockEncrypt, KeyInit}, + Aes128, +}; +use xts_mode::Xts128; + +// The raw key, keep secret! +#[repr(transparent)] +pub struct Key([u8; 16]); + +impl Key { + /// Generate a random key + #[cfg(feature = "std")] + pub fn new() -> Result { + let mut bytes = [0; 16]; + getrandom::getrandom(&mut bytes)?; + Ok(Self(bytes)) + } + + pub fn encrypt(&self, password_aes: &Aes128) -> EncryptedKey { + let mut block = aes::Block::from(self.0); + password_aes.encrypt_block(&mut block); + EncryptedKey(block.into()) + } + + pub fn into_aes(self) -> Aes128 { + Aes128::new(&aes::Block::from(self.0)) + } +} + +/// The encrypted key, encrypted with AES using the salt and password +#[derive(Clone, Copy, Default)] +#[repr(transparent)] +pub struct EncryptedKey([u8; 16]); + +impl EncryptedKey { + pub fn decrypt(&self, password_aes: &Aes128) -> Key { + let mut block = aes::Block::from(self.0); + password_aes.decrypt_block(&mut block); + Key(block.into()) + } +} + +/// Salt used to prevent rainbow table attacks on the encryption password +#[derive(Clone, Copy, Default)] +#[repr(transparent)] +pub struct Salt([u8; 16]); + +impl Salt { + /// Generate a random salt + #[cfg(feature = "std")] + pub fn new() -> Result { + let mut bytes = [0; 16]; + getrandom::getrandom(&mut bytes)?; + Ok(Self(bytes)) + } +} + +/// The key slot, containing the salt and encrypted key that are used with one password +#[derive(Clone, Copy, Default)] +#[repr(C, packed)] +pub struct KeySlot { + salt: Salt, + // Two keys for AES XTS 128 + encrypted_keys: (EncryptedKey, EncryptedKey), +} + +impl KeySlot { + /// Get the password AES key (generated from the password and salt, encrypts the real key) + pub fn password_aes(password: &[u8], salt: &Salt) -> Result { + let mut key = Key([0; 16]); + + let mut params_builder = argon2::ParamsBuilder::new(); + params_builder.output_len(key.0.len())?; + + let argon2 = argon2::Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + params_builder.params()?, + ); + + argon2.hash_password_into(password, &salt.0, &mut key.0)?; + + Ok(key.into_aes()) + } + + /// Create a new key slot from a password, salt, and encryption key + pub fn new(password: &[u8], salt: Salt, keys: (Key, Key)) -> Result { + let password_aes = Self::password_aes(password, &salt)?; + Ok(Self { + salt, + encrypted_keys: (keys.0.encrypt(&password_aes), keys.1.encrypt(&password_aes)), + }) + } + + /// Get the encryption cipher from this key slot + pub fn cipher(&self, password: &[u8]) -> Result, argon2::Error> { + let password_aes = Self::password_aes(password, &self.salt)?; + Ok(Xts128::new( + self.encrypted_keys.0.decrypt(&password_aes).into_aes(), + self.encrypted_keys.1.decrypt(&password_aes).into_aes(), + )) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000..e85c5bbed7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,70 @@ +#![crate_name = "redoxfs"] +#![crate_type = "lib"] +#![cfg_attr(not(feature = "std"), no_std)] +// Used often in generating redox_syscall errors +#![allow(clippy::or_fun_call)] +#![allow(unexpected_cfgs)] + +extern crate alloc; + +use core::sync::atomic::AtomicUsize; + +// The alloc log grows by 1 block about every 21 generations +pub const ALLOC_GC_THRESHOLD: u64 = 1024; +pub const BLOCK_SIZE: u64 = 4096; +// A record is 4KiB << 5 = 128KiB +pub const RECORD_LEVEL: usize = 5; +pub const RECORD_SIZE: u64 = BLOCK_SIZE << RECORD_LEVEL; +pub const SIGNATURE: &[u8; 8] = b"RedoxFS\0"; +pub const VERSION: u64 = 8; +pub const DIR_ENTRY_MAX_LENGTH: usize = 252; + +pub static IS_UMT: AtomicUsize = AtomicUsize::new(0); + +pub use self::allocator::{AllocEntry, AllocList, Allocator, ReleaseList, ALLOC_LIST_ENTRIES}; +#[cfg(feature = "std")] +pub use self::archive::{archive, archive_at}; +pub use self::block::{ + BlockAddr, BlockData, BlockLevel, BlockList, BlockMeta, BlockPtr, BlockRaw, BlockTrait, +}; +#[cfg(feature = "std")] +pub use self::clone::clone; +pub use self::dir::{DirEntry, DirList}; +pub use self::disk::*; +pub use self::filesystem::FileSystem; +pub use self::header::{Header, HEADER_RING}; +pub use self::key::{Key, KeySlot, Salt}; +#[cfg(feature = "std")] +pub use self::mount::mount; +pub use self::node::{Node, NodeFlags, NodeLevel, NodeLevelData}; +pub use self::record::RecordRaw; +pub use self::transaction::Transaction; +pub use self::tree::{Tree, TreeData, TreeList, TreePtr}; +#[cfg(feature = "std")] +pub use self::unmount::unmount_path; + +mod allocator; +#[cfg(feature = "std")] +mod archive; +mod block; +#[cfg(feature = "std")] +mod clone; +mod dir; +mod disk; +mod filesystem; +mod header; +mod htree; +mod key; +#[cfg(all(feature = "std", not(fuzzing)))] +mod mount; +#[cfg(all(feature = "std", fuzzing))] +pub mod mount; +mod node; +mod record; +mod transaction; +mod tree; +#[cfg(feature = "std")] +mod unmount; + +#[cfg(all(feature = "std", test))] +mod tests; diff --git a/src/mount/fuse.rs b/src/mount/fuse.rs new file mode 100644 index 0000000000..415cc42dbe --- /dev/null +++ b/src/mount/fuse.rs @@ -0,0 +1,580 @@ +extern crate fuser; + +use std::cmp; +use std::ffi::OsStr; +use std::io; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; + +use self::fuser::MountOption; +use self::fuser::TimeOrNow; +use crate::mount::fuse::TimeOrNow::Now; +use crate::mount::fuse::TimeOrNow::SpecificTime; + +use crate::{filesystem, Disk, Node, TreeData, TreePtr, BLOCK_SIZE}; + +use self::fuser::{ + FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, + ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, Request, Session, +}; +use std::time::Duration; + +const TTL: Duration = Duration::new(1, 0); // 1 second + +const NULL_TIME: Duration = Duration::new(0, 0); + +pub fn mount( + mut filesystem: filesystem::FileSystem, + mountpoint: P, + callback: F, +) -> io::Result +where + D: Disk, + P: AsRef, + F: FnOnce(&Path) -> T, +{ + let mountpoint = mountpoint.as_ref(); + + // One of the uses of this redoxfs fuse wrapper is to populate a filesystem + // while building the Redox OS kernel. This means that we need to write on + // a filesystem that belongs to `root`, which in turn means that we need to + // be `root`, thus that we need to allow `root` to have access. + let defer_permissions = [MountOption::CUSTOM("defer_permissions".to_owned())]; + + let res = { + let mut session = Session::new( + Fuse { + fs: &mut filesystem, + }, + mountpoint, + if cfg!(target_os = "macos") { + &defer_permissions + } else { + &[] + }, + )?; + + let res = callback(mountpoint); + + session.run()?; + + res + }; + + // Cleanup on unmount + filesystem.cleanup()?; + + Ok(res) +} + +pub struct Fuse<'f, D: Disk> { + pub fs: &'f mut filesystem::FileSystem, +} + +fn node_attr(node: &TreeData) -> FileAttr { + FileAttr { + ino: node.id() as u64, + size: node.data().size(), + // Blocks is in 512 byte blocks, not in our block size + blocks: node.data().blocks() * (BLOCK_SIZE / 512), + blksize: 512, + atime: SystemTime::UNIX_EPOCH + Duration::new(node.data().atime().0, node.data().atime().1), + mtime: SystemTime::UNIX_EPOCH + Duration::new(node.data().mtime().0, node.data().mtime().1), + ctime: SystemTime::UNIX_EPOCH + Duration::new(node.data().ctime().0, node.data().ctime().1), + crtime: UNIX_EPOCH + NULL_TIME, + kind: if node.data().is_dir() { + FileType::Directory + } else if node.data().is_symlink() { + FileType::Symlink + } else { + FileType::RegularFile + }, + perm: node.data().mode() & Node::MODE_PERM, + nlink: node.data().links(), + uid: node.data().uid(), + gid: node.data().gid(), + rdev: 0, + flags: 0, + } +} + +impl Filesystem for Fuse<'_, D> { + fn lookup(&mut self, _req: &Request, parent_id: u64, name: &OsStr, reply: ReplyEntry) { + let parent_ptr = TreePtr::new(parent_id as u32); + match self + .fs + .tx(|tx| tx.find_node(parent_ptr, name.to_str().unwrap())) + { + Ok(node) => { + reply.entry(&TTL, &node_attr(&node), 0); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn getattr(&mut self, _req: &Request, node_id: u64, _fh: Option, reply: ReplyAttr) { + let node_ptr = TreePtr::::new(node_id as u32); + match self.fs.tx(|tx| tx.read_tree(node_ptr)) { + Ok(node) => { + reply.attr(&TTL, &node_attr(&node)); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn setattr( + &mut self, + _req: &Request, + node_id: u64, + mode: Option, + uid: Option, + gid: Option, + size: Option, + atime: Option, + mtime: Option, + _ctime: Option, + _fh: Option, + _crtime: Option, + _chgtime: Option, + _bkuptime: Option, + _flags: Option, + reply: ReplyAttr, + ) { + let node_ptr = TreePtr::::new(node_id as u32); + + let mut node = match self.fs.tx(|tx| tx.read_tree(node_ptr)) { + Ok(ok) => ok, + Err(err) => { + reply.error(err.errno); + return; + } + }; + let mut node_changed = false; + + if let Some(mode) = mode { + if node.data().mode() & Node::MODE_PERM != mode as u16 & Node::MODE_PERM { + let new_mode = + (node.data().mode() & Node::MODE_TYPE) | (mode as u16 & Node::MODE_PERM); + node.data_mut().set_mode(new_mode); + node_changed = true; + } + } + + if let Some(uid) = uid { + if node.data().uid() != uid { + node.data_mut().set_uid(uid); + node_changed = true; + } + } + + if let Some(gid) = gid { + if node.data().gid() != gid { + node.data_mut().set_gid(gid); + node_changed = true; + } + } + + if let Some(atime) = atime { + let atime_c = match atime { + SpecificTime(st) => st.duration_since(UNIX_EPOCH).unwrap(), + Now => SystemTime::now().duration_since(UNIX_EPOCH).unwrap(), + }; + node.data_mut() + .set_atime(atime_c.as_secs(), atime_c.subsec_nanos()); + node_changed = true; + } + + if let Some(mtime) = mtime { + let mtime_c = match mtime { + SpecificTime(st) => st.duration_since(UNIX_EPOCH).unwrap(), + Now => SystemTime::now().duration_since(UNIX_EPOCH).unwrap(), + }; + node.data_mut() + .set_mtime(mtime_c.as_secs(), mtime_c.subsec_nanos()); + node_changed = true; + } + + if let Some(size) = size { + match self.fs.tx(|tx| tx.truncate_node_inner(&mut node, size)) { + Ok(ok) => { + if ok { + node_changed = true; + } + } + Err(err) => { + reply.error(err.errno); + return; + } + } + } + + let attr = node_attr(&node); + + if node_changed { + if let Err(err) = self.fs.tx(|tx| tx.sync_tree(node)) { + reply.error(err.errno); + return; + } + } + + reply.attr(&TTL, &attr); + } + + fn open(&mut self, _req: &Request<'_>, node_id: u64, _flags: i32, reply: ReplyOpen) { + let node_ptr = TreePtr::::new(node_id as u32); + match self.fs.tx(|tx| tx.on_open_node(node_ptr)) { + Ok(()) => reply.opened(0, 0), + Err(err) => reply.error(err.errno), + } + } + + fn read( + &mut self, + _req: &Request, + node_id: u64, + _fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + reply: ReplyData, + ) { + let node_ptr = TreePtr::::new(node_id as u32); + + let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let mut data = vec![0; size as usize]; + match self.fs.tx(|tx| { + tx.read_node( + node_ptr, + cmp::max(0, offset) as u64, + &mut data, + atime.as_secs(), + atime.subsec_nanos(), + ) + }) { + Ok(count) => { + reply.data(&data[..count]); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn write( + &mut self, + _req: &Request, + node_id: u64, + _fh: u64, + offset: i64, + data: &[u8], + _write_flags: u32, + _flags: i32, + _lock_owner: Option, + reply: ReplyWrite, + ) { + let node_ptr = TreePtr::::new(node_id as u32); + + let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + match self.fs.tx(|tx| { + tx.write_node( + node_ptr, + cmp::max(0, offset) as u64, + data, + mtime.as_secs(), + mtime.subsec_nanos(), + ) + }) { + Ok(count) => { + reply.written(count as u32); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn flush(&mut self, _req: &Request, _ino: u64, _fh: u64, _lock_owner: u64, reply: ReplyEmpty) { + reply.ok(); + } + + fn release( + &mut self, + _req: &Request, + node_id: u64, + _fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: ReplyEmpty, + ) { + let node_ptr = TreePtr::new(node_id as u32); + match self.fs.tx(|tx| tx.on_close_node(node_ptr)) { + Ok(()) => reply.ok(), + Err(err) => reply.error(err.errno), + } + } + + fn fsync(&mut self, _req: &Request, _ino: u64, _fh: u64, _datasync: bool, reply: ReplyEmpty) { + reply.ok(); + } + + fn readdir( + &mut self, + _req: &Request, + parent_id: u64, + _fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + let parent_ptr = TreePtr::new(parent_id as u32); + let mut children = Vec::new(); + match self.fs.tx(|tx| tx.child_nodes(parent_ptr, &mut children)) { + Ok(()) => { + let mut i; + let skip; + if offset == 0 { + skip = 0; + i = 0; + let _full = reply.add(parent_id, i, FileType::Directory, "."); + + i += 1; + let _full = reply.add( + //TODO: get parent? + parent_id, + i, + FileType::Directory, + "..", + ); + i += 1; + } else { + i = offset + 1; + skip = offset as usize - 1; + } + + for child in children.iter().skip(skip) { + //TODO: make it possible to get file type from directory entry + let node = match self.fs.tx(|tx| tx.read_tree(child.node_ptr())) { + Ok(ok) => ok, + Err(err) => { + reply.error(err.errno); + return; + } + }; + + let full = reply.add( + child.node_ptr().id() as u64, + i, + if node.data().is_dir() { + FileType::Directory + } else { + FileType::RegularFile + }, + child.name().unwrap(), + ); + + if full { + break; + } + + i += 1; + } + reply.ok(); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn create( + &mut self, + _req: &Request, + parent_id: u64, + name: &OsStr, + mode: u32, + _umask: u32, + _flags: i32, + reply: ReplyCreate, + ) { + let parent_ptr = TreePtr::::new(parent_id as u32); + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + match self.fs.tx(|tx| { + let node = tx.create_node( + parent_ptr, + name.to_str().unwrap(), + Node::MODE_FILE | (mode as u16 & Node::MODE_PERM), + ctime.as_secs(), + ctime.subsec_nanos(), + )?; + tx.on_open_node(node.ptr())?; + Ok(node) + }) { + Ok(node) => { + // println!("Create {:?}:{:o}:{:o}", node.1.name(), node.1.mode, mode); + reply.created(&TTL, &node_attr(&node), 0, 0, 0); + } + Err(error) => { + reply.error(error.errno); + } + } + } + + fn mkdir( + &mut self, + _req: &Request, + parent_id: u64, + name: &OsStr, + mode: u32, + _umask: u32, + reply: ReplyEntry, + ) { + let parent_ptr = TreePtr::::new(parent_id as u32); + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + match self.fs.tx(|tx| { + tx.create_node( + parent_ptr, + name.to_str().unwrap(), + Node::MODE_DIR | (mode as u16 & Node::MODE_PERM), + ctime.as_secs(), + ctime.subsec_nanos(), + ) + }) { + Ok(node) => { + // println!("Mkdir {:?}:{:o}:{:o}", node.1.name(), node.1.mode, mode); + reply.entry(&TTL, &node_attr(&node), 0); + } + Err(error) => { + reply.error(error.errno); + } + } + } + + fn rmdir(&mut self, _req: &Request, parent_id: u64, name: &OsStr, reply: ReplyEmpty) { + let parent_ptr = TreePtr::::new(parent_id as u32); + match self + .fs + .tx(|tx| tx.remove_node(parent_ptr, name.to_str().unwrap(), Node::MODE_DIR)) + { + Ok(_) => { + reply.ok(); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn unlink(&mut self, _req: &Request, parent_id: u64, name: &OsStr, reply: ReplyEmpty) { + let parent_ptr = TreePtr::::new(parent_id as u32); + match self + .fs + .tx(|tx| tx.remove_node(parent_ptr, name.to_str().unwrap(), Node::MODE_FILE)) + { + Ok(_) => { + reply.ok(); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn statfs(&mut self, _req: &Request, _ino: u64, reply: ReplyStatfs) { + let bsize = BLOCK_SIZE; + let blocks = self.fs.header.size() / bsize; + let bfree = self.fs.allocator().free(); + reply.statfs(blocks, bfree, bfree, 0, 0, bsize as u32, 256, 0); + } + + fn symlink( + &mut self, + _req: &Request, + parent_id: u64, + name: &OsStr, + link: &Path, + reply: ReplyEntry, + ) { + let parent_ptr = TreePtr::::new(parent_id as u32); + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + match self.fs.tx(|tx| { + let node = tx.create_node( + parent_ptr, + name.to_str().unwrap(), + Node::MODE_SYMLINK | 0o777, + ctime.as_secs(), + ctime.subsec_nanos(), + )?; + + let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + tx.write_node( + node.ptr(), + 0, + link.as_os_str().as_bytes(), + mtime.as_secs(), + mtime.subsec_nanos(), + )?; + + Ok(node) + }) { + Ok(node) => { + reply.entry(&TTL, &node_attr(&node), 0); + } + Err(error) => { + reply.error(error.errno); + } + } + } + + fn readlink(&mut self, _req: &Request, node_id: u64, reply: ReplyData) { + let node_ptr = TreePtr::::new(node_id as u32); + let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let mut data = vec![0; 4096]; + match self.fs.tx(|tx| { + tx.read_node( + node_ptr, + 0, + &mut data, + atime.as_secs(), + atime.subsec_nanos(), + ) + }) { + Ok(count) => { + reply.data(&data[..count]); + } + Err(err) => { + reply.error(err.errno); + } + } + } + + fn rename( + &mut self, + _req: &Request, + orig_parent: u64, + orig_name: &OsStr, + new_parent: u64, + new_name: &OsStr, + _flags: u32, + reply: ReplyEmpty, + ) { + let orig_parent_ptr = TreePtr::::new(orig_parent as u32); + let orig_name = orig_name.to_str().expect("name is not utf-8"); + let new_parent_ptr = TreePtr::::new(new_parent as u32); + let new_name = new_name.to_str().expect("name is not utf-8"); + + // TODO: improve performance + match self + .fs + .tx(|tx| tx.rename_node(orig_parent_ptr, orig_name, new_parent_ptr, new_name)) + { + Ok(()) => reply.ok(), + Err(err) => reply.error(err.errno), + } + } +} diff --git a/src/mount/mod.rs b/src/mount/mod.rs new file mode 100644 index 0000000000..16948cf6c1 --- /dev/null +++ b/src/mount/mod.rs @@ -0,0 +1,23 @@ +#[cfg(all(not(target_os = "redox"), not(fuzzing), feature = "fuse"))] +mod fuse; + +#[cfg(all(not(target_os = "redox"), fuzzing, feature = "fuse"))] +pub mod fuse; + +#[cfg(all(not(target_os = "redox"), feature = "fuse"))] +pub use self::fuse::mount; + +#[cfg(all(not(target_os = "redox"), not(fuzzing), not(feature = "fuse")))] +mod stub; + +#[cfg(all(not(target_os = "redox"), fuzzing, not(feature = "fuse")))] +pub mod stub; + +#[cfg(all(not(target_os = "redox"), not(feature = "fuse")))] +pub use self::stub::mount; + +#[cfg(target_os = "redox")] +mod redox; + +#[cfg(target_os = "redox")] +pub use self::redox::mount; diff --git a/src/mount/redox/mod.rs b/src/mount/redox/mod.rs new file mode 100644 index 0000000000..cd1462c4db --- /dev/null +++ b/src/mount/redox/mod.rs @@ -0,0 +1,89 @@ +use redox_scheme::{ + scheme::{SchemeState, SchemeSync}, + RequestKind, Response, SignalBehavior, Socket, +}; +use std::io; +use std::path::Path; +use std::sync::atomic::Ordering; + +use crate::{Disk, FileSystem, IS_UMT}; + +use self::scheme::FileScheme; + +pub mod resource; +pub mod scheme; + +//FIXME: mut callback is not mut +#[allow(unused_mut)] + +pub fn mount(filesystem: FileSystem, mountpoint: P, mut callback: F) -> io::Result +where + D: Disk, + P: AsRef, + F: FnOnce(&Path) -> T, +{ + let mountpoint = mountpoint.as_ref(); + let socket = Socket::create()?; + + let scheme_name = format!("{}", mountpoint.display()); + let mounted_path = format!("/scheme/{}", mountpoint.display()); + + let mut state = SchemeState::new(); + let mut scheme = FileScheme::new(scheme_name, mounted_path.clone(), filesystem, &socket)?; + + redox_scheme::scheme::register_sync_scheme( + &socket, + &format!("{}", mountpoint.display()), + &mut scheme, + )?; + + let res = callback(Path::new(&mounted_path)); + + while IS_UMT.load(Ordering::SeqCst) == 0 { + let req = match socket.next_request(SignalBehavior::Restart)? { + None => break, + Some(req) => { + match req.kind() { + RequestKind::Call(r) => r, + RequestKind::SendFd(sendfd_request) => { + let result = scheme.on_sendfd(&sendfd_request); + let response = Response::new(result, sendfd_request); + + if !socket.write_response(response, SignalBehavior::Restart)? { + break; + } + continue; + } + RequestKind::OnClose { id } => { + scheme.on_close(id); + state.on_close(id); + continue; + } + RequestKind::OnDetach { id, pid } => { + let Ok(inode) = scheme.inode(id) else { + log::warn!("RequestKind::OnDetach with invalid `id`"); + continue; + }; + state.on_detach(id, inode, pid); + continue; + } + _ => { + // TODO: Redoxfs does not yet support asynchronous file IO. It might still make + // sense to implement cancellation for huge buffers, e.g. dd bs=1G + continue; + } + } + } + }; + let response = req.handle_sync(&mut scheme, &mut state); + + if !socket.write_response(response, SignalBehavior::Restart)? { + break; + } + } + + // Cleanup on unmount + scheme.fs.cleanup()?; + + Ok(res) +} diff --git a/src/mount/redox/resource.rs b/src/mount/redox/resource.rs new file mode 100644 index 0000000000..400e257aaa --- /dev/null +++ b/src/mount/redox/resource.rs @@ -0,0 +1,794 @@ +use std::slice; +use std::time::{SystemTime, UNIX_EPOCH}; + +use alloc::collections::BTreeMap; +use libredox::call::MmapArgs; +use range_tree::RangeTree; + +use syscall::data::{Stat, TimeSpec}; +use syscall::dirent::{DirEntry, DirentBuf, DirentKind}; +use syscall::error::{Error, Result, EBADF, EINVAL, EISDIR, ENOTDIR, EPERM}; +use syscall::flag::{ + MapFlags, F_GETFL, F_SETFL, MODE_PERM, O_ACCMODE, O_APPEND, O_RDONLY, O_RDWR, O_WRONLY, + PROT_READ, PROT_WRITE, +}; +use syscall::{EBADFD, ENOENT, PAGE_SIZE}; + +use crate::{Disk, Node, Transaction, TreePtr, BLOCK_SIZE}; + +pub type Fmaps = BTreeMap; + +pub trait Resource { + fn parent_ptr_opt(&self) -> Option>; + + fn node_ptr(&self) -> TreePtr; + + fn uid(&self) -> u32; + + fn set_path(&mut self, path: &str); + + fn read(&mut self, buf: &mut [u8], offset: u64, tx: &mut Transaction) -> Result; + + fn write(&mut self, buf: &[u8], offset: u64, tx: &mut Transaction) -> Result; + + fn fsize(&mut self, tx: &mut Transaction) -> Result; + + fn fmap( + &mut self, + fmaps: &mut Fmaps, + flags: MapFlags, + size: usize, + offset: u64, + tx: &mut Transaction, + ) -> Result; + + fn funmap( + &mut self, + fmaps: &mut Fmaps, + offset: u64, + size: usize, + tx: &mut Transaction, + ) -> Result<()>; + + fn fchmod(&mut self, mode: u16, tx: &mut Transaction) -> Result<()> { + let mut node = tx.read_tree(self.node_ptr())?; + + if node.data().uid() == self.uid() || self.uid() == 0 { + let old_mode = node.data().mode(); + let new_mode = (old_mode & !MODE_PERM) | (mode & MODE_PERM); + if old_mode != new_mode { + node.data_mut().set_mode(new_mode); + tx.sync_tree(node)?; + } + + Ok(()) + } else { + Err(Error::new(EPERM)) + } + } + + fn fchown(&mut self, uid: u32, gid: u32, tx: &mut Transaction) -> Result<()> { + let mut node = tx.read_tree(self.node_ptr())?; + + let old_uid = node.data().uid(); + if old_uid == self.uid() || self.uid() == 0 { + let mut node_changed = false; + + if uid as i32 != -1 { + if uid != old_uid { + node.data_mut().set_uid(uid); + node_changed = true; + } + } + + if gid as i32 != -1 { + let old_gid = node.data().gid(); + if gid != old_gid { + node.data_mut().set_gid(gid); + node_changed = true; + } + } + + if node_changed { + tx.sync_tree(node)?; + } + + Ok(()) + } else { + Err(Error::new(EPERM)) + } + } + + fn fcntl(&mut self, cmd: usize, arg: usize) -> Result; + + fn path(&self) -> &str; + + fn stat(&self, stat: &mut Stat, tx: &mut Transaction) -> Result<()> { + let node = tx.read_tree(self.node_ptr())?; + + let ctime = node.data().ctime(); + let mtime = node.data().mtime(); + let atime = node.data().atime(); + *stat = Stat { + st_dev: 0, // TODO + st_ino: node.id() as u64, + st_mode: node.data().mode(), + st_nlink: node.data().links(), + st_uid: node.data().uid(), + st_gid: node.data().gid(), + st_size: node.data().size(), + st_blksize: 512, + // Blocks is in 512 byte blocks, not in our block size + st_blocks: node.data().blocks() * (BLOCK_SIZE / 512), + st_mtime: mtime.0, + st_mtime_nsec: mtime.1, + st_atime: atime.0, + st_atime_nsec: atime.1, + st_ctime: ctime.0, + st_ctime_nsec: ctime.1, + }; + + Ok(()) + } + + fn sync(&mut self, fmaps: &mut Fmaps, tx: &mut Transaction) -> Result<()>; + + fn truncate(&mut self, len: u64, tx: &mut Transaction) -> Result<()>; + + fn utimens(&mut self, times: &[TimeSpec], tx: &mut Transaction) -> Result<()>; + + fn getdents<'buf>( + &mut self, + buf: DirentBuf<&'buf mut [u8]>, + opaque_offset: u64, + tx: &mut Transaction, + ) -> Result>; +} + +pub struct Entry { + pub node_ptr: TreePtr, + pub name: String, +} + +pub struct DirResource { + path: String, + parent_ptr_opt: Option>, + node_ptr: TreePtr, + data: Option>, + uid: u32, +} + +impl DirResource { + pub fn new( + path: String, + parent_ptr_opt: Option>, + node_ptr: TreePtr, + data: Option>, + uid: u32, + ) -> DirResource { + DirResource { + path, + parent_ptr_opt, + node_ptr, + data, + uid, + } + } +} + +impl Resource for DirResource { + fn parent_ptr_opt(&self) -> Option> { + self.parent_ptr_opt + } + + fn node_ptr(&self) -> TreePtr { + self.node_ptr + } + + fn uid(&self) -> u32 { + self.uid + } + + fn set_path(&mut self, path: &str) { + self.path = path.to_string(); + } + + fn read(&mut self, _buf: &mut [u8], _offset: u64, _tx: &mut Transaction) -> Result { + Err(Error::new(EISDIR)) + } + + fn write(&mut self, _buf: &[u8], _offset: u64, _tx: &mut Transaction) -> Result { + Err(Error::new(EBADF)) + } + + fn fsize(&mut self, _tx: &mut Transaction) -> Result { + Ok(self.data.as_ref().ok_or(Error::new(EBADF))?.len() as u64) + } + + fn fmap( + &mut self, + _fmaps: &mut Fmaps, + _flags: MapFlags, + _size: usize, + _offset: u64, + _tx: &mut Transaction, + ) -> Result { + Err(Error::new(EBADF)) + } + fn funmap( + &mut self, + _fmaps: &mut Fmaps, + _offset: u64, + _size: usize, + _tx: &mut Transaction, + ) -> Result<()> { + Err(Error::new(EBADF)) + } + + fn fcntl(&mut self, _cmd: usize, _arg: usize) -> Result { + Err(Error::new(EBADF)) + } + + fn path(&self) -> &str { + &self.path + } + + fn sync(&mut self, _fmaps: &mut Fmaps, _tx: &mut Transaction) -> Result<()> { + Err(Error::new(EBADF)) + } + + fn truncate(&mut self, _len: u64, _tx: &mut Transaction) -> Result<()> { + Err(Error::new(EBADF)) + } + + fn utimens(&mut self, times: &[TimeSpec], tx: &mut Transaction) -> Result<()> { + let mut node = tx.read_tree(self.node_ptr)?; + + if node.data().uid() == self.uid || self.uid == 0 { + if let &[atime, mtime] = times { + let mut node_changed = false; + + let old_mtime = node.data().mtime(); + let new_mtime = (mtime.tv_sec as u64, mtime.tv_nsec as u32); + if old_mtime != new_mtime { + node.data_mut().set_mtime(new_mtime.0, new_mtime.1); + node_changed = true; + } + + let old_atime = node.data().atime(); + let new_atime = (atime.tv_sec as u64, atime.tv_nsec as u32); + if old_atime != new_atime { + node.data_mut().set_atime(new_atime.0, new_atime.1); + node_changed = true; + } + + if node_changed { + tx.sync_tree(node)?; + } + } + Ok(()) + } else { + Err(Error::new(EPERM)) + } + } + + fn getdents<'buf>( + &mut self, + mut buf: DirentBuf<&'buf mut [u8]>, + opaque_offset: u64, + tx: &mut Transaction, + ) -> Result> { + match &self.data { + Some(data) => { + let opaque_offset = opaque_offset as usize; + for (idx, entry) in data.iter().enumerate().skip(opaque_offset) { + let child = match tx.read_tree(entry.node_ptr) { + Ok(r) => r, + Err(Error { errno: ENOENT }) => continue, + Err(err) => return Err(err), + }; + let result = buf.entry(DirEntry { + inode: child.id() as u64, + next_opaque_id: idx as u64 + 1, + name: &entry.name, + kind: match child.data().mode() & Node::MODE_TYPE { + Node::MODE_DIR => DirentKind::Directory, + Node::MODE_FILE => DirentKind::Regular, + Node::MODE_SYMLINK => DirentKind::Symlink, + //TODO: more types? + _ => DirentKind::Unspecified, + }, + }); + if let Err(err) = result { + if err.errno == EINVAL && idx > opaque_offset { + // POSIX allows partial result of getdents + break; + } else { + return Err(err); + } + } + } + Ok(buf) + } + None => Err(Error::new(EBADF)), + } + } +} + +#[derive(Debug)] +pub struct Fmap { + rc: usize, + flags: MapFlags, + last_page_tail: u16, +} + +impl Fmap { + pub unsafe fn new( + node_ptr: TreePtr, + flags: MapFlags, + unaligned_size: usize, + offset: u64, + base: *mut u8, + tx: &mut Transaction, + ) -> Result { + // Memory provided to fmap must be page aligned and sized + let aligned_size = unaligned_size.next_multiple_of(syscall::PAGE_SIZE); + + let address = base.add(offset as usize); + //println!("ADDR {:p} {:p}", base, address); + + // Read buffer from disk + let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + + let buf = slice::from_raw_parts_mut(address, unaligned_size); + + let count = match tx.read_node(node_ptr, offset, buf, atime.as_secs(), atime.subsec_nanos()) + { + Ok(ok) => ok, + Err(err) => { + let _ = libredox::call::munmap(address.cast(), aligned_size); + return Err(err); + } + }; + + // Make sure remaining data is zeroed + buf[count..].fill(0_u8); + + Ok(Self { + rc: 1, + flags, + last_page_tail: (unaligned_size % PAGE_SIZE) as u16, + }) + } + + pub unsafe fn sync( + &mut self, + node_ptr: TreePtr, + base: *mut u8, + offset: u64, + size: usize, + tx: &mut Transaction, + ) -> Result<()> { + if self.flags & PROT_WRITE == PROT_WRITE { + let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + tx.write_node( + node_ptr, + offset, + unsafe { core::slice::from_raw_parts(base.add(offset as usize), size) }, + mtime.as_secs(), + mtime.subsec_nanos(), + )?; + } + Ok(()) + } +} + +pub struct FileResource { + path: String, + parent_ptr_opt: Option>, + node_ptr: TreePtr, + flags: usize, + uid: u32, +} + +#[derive(Debug)] +pub struct FileMmapInfo { + base: *mut u8, + size: usize, + pub ranges: RangeTree, + pub open_fds: usize, +} + +impl FileMmapInfo { + pub fn new() -> Self { + Self { + base: core::ptr::null_mut(), + size: 0, + ranges: RangeTree::new(), + open_fds: 0, + } + } + + pub fn in_use(&self) -> bool { + self.open_fds > 0 || !self.ranges.is_empty() + } +} + +impl Drop for FileMmapInfo { + fn drop(&mut self) { + if self.in_use() { + log::error!("FileMmapInfo dropped while in use"); + } + } +} + +impl FileResource { + pub fn new( + path: String, + parent_ptr_opt: Option>, + node_ptr: TreePtr, + flags: usize, + uid: u32, + ) -> FileResource { + FileResource { + path, + parent_ptr_opt, + node_ptr, + flags, + uid, + } + } +} + +impl Resource for FileResource { + fn parent_ptr_opt(&self) -> Option> { + self.parent_ptr_opt + } + + fn node_ptr(&self) -> TreePtr { + self.node_ptr + } + + fn uid(&self) -> u32 { + self.uid + } + + fn set_path(&mut self, path: &str) { + self.path = path.to_string(); + } + + fn read(&mut self, buf: &mut [u8], offset: u64, tx: &mut Transaction) -> Result { + if self.flags & O_ACCMODE != O_RDWR && self.flags & O_ACCMODE != O_RDONLY { + return Err(Error::new(EBADF)); + } + let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + tx.read_node( + self.node_ptr, + offset, + buf, + atime.as_secs(), + atime.subsec_nanos(), + ) + } + + fn write(&mut self, buf: &[u8], offset: u64, tx: &mut Transaction) -> Result { + if self.flags & O_ACCMODE != O_RDWR && self.flags & O_ACCMODE != O_WRONLY { + return Err(Error::new(EBADF)); + } + let effective_offset = if self.flags & O_APPEND == O_APPEND { + let node = tx.read_tree(self.node_ptr)?; + node.data().size() + } else { + offset + }; + let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + tx.write_node( + self.node_ptr, + effective_offset, + buf, + mtime.as_secs(), + mtime.subsec_nanos(), + ) + } + + fn fsize(&mut self, tx: &mut Transaction) -> Result { + let node = tx.read_tree(self.node_ptr)?; + Ok(node.data().size()) + } + + fn fmap( + &mut self, + fmaps: &mut Fmaps, + flags: MapFlags, + unaligned_size: usize, + offset: u64, + tx: &mut Transaction, + ) -> Result { + //dbg!(&self.fmaps); + let accmode = self.flags & O_ACCMODE; + if flags.contains(PROT_READ) && !(accmode == O_RDWR || accmode == O_RDONLY) { + return Err(Error::new(EBADF)); + } + if flags.contains(PROT_WRITE) && !(accmode == O_RDWR || accmode == O_WRONLY) { + return Err(Error::new(EBADF)); + } + + let aligned_size = unaligned_size.next_multiple_of(PAGE_SIZE); + + // TODO: PROT_EXEC? It is however unenforcable without restricting anonymous mmap, since a + // program can always map anonymous RW-, read from a file, then remap as R-E. But it might + // be usable as a hint, prohibiting direct executable mmaps at least. + + // TODO: Pass entry directory to Resource trait functions, since the node_ptr can be + // obtained by the caller. + let fmap_info = fmaps + .get_mut(&self.node_ptr.id()) + .ok_or(Error::new(EBADFD))?; + + if !fmap_info.in_use() { + // Notify filesystem of open + tx.on_open_node(self.node_ptr)?; + } + + let new_size = (offset as usize + aligned_size).next_multiple_of(PAGE_SIZE); + if new_size > fmap_info.size { + fmap_info.base = if fmap_info.base.is_null() { + unsafe { + libredox::call::mmap(MmapArgs { + length: new_size, + // PRIVATE/SHARED doesn't matter once the pages are passed in the fmap + // handler. + prot: libredox::flag::PROT_READ | libredox::flag::PROT_WRITE, + flags: libredox::flag::MAP_PRIVATE, + + offset: 0, + fd: !0, + addr: core::ptr::null_mut(), + })? as *mut u8 + } + } else { + unsafe { + syscall::syscall5( + syscall::SYS_MREMAP, + fmap_info.base as usize, + fmap_info.size, + 0, + new_size, + syscall::MremapFlags::empty().bits() | (PROT_READ | PROT_WRITE).bits(), + )? as *mut u8 + } + }; + fmap_info.size = new_size; + } + + let affected_fmaps = fmap_info + .ranges + .remove_and_unused(offset..offset + aligned_size as u64); + + for (range, v_opt) in affected_fmaps { + //dbg!(&range); + if let Some(mut fmap) = v_opt { + fmap.rc += 1; + fmap.flags |= flags; + //FIXME: Use result? + let _ = fmap_info + .ranges + .insert(range.start, range.end - range.start, fmap); + } else { + let map = unsafe { + Fmap::new( + self.node_ptr, + flags, + unaligned_size, + offset, + fmap_info.base, + tx, + )? + }; + //FIXME: Use result? + let _ = fmap_info.ranges.insert(offset, aligned_size as u64, map); + } + } + //dbg!(&self.fmaps); + + Ok(fmap_info.base as usize + offset as usize) + } + + fn funmap( + &mut self, + fmaps: &mut Fmaps, + offset: u64, + size: usize, + tx: &mut Transaction, + ) -> Result<()> { + let fmap_info = fmaps + .get_mut(&self.node_ptr.id()) + .ok_or(Error::new(EBADFD))?; + + //dbg!(&self.fmaps); + //dbg!(self.fmaps.conflicts(offset..offset + size as u64).collect::>()); + #[allow(unused_mut)] + let mut affected_fmaps = fmap_info.ranges.remove(offset..offset + size as u64); + + for (range, mut fmap) in affected_fmaps { + fmap.rc = fmap.rc.checked_sub(1).unwrap(); + + //log::info!("SYNCING {}..{}", range.start, range.end); + unsafe { + fmap.sync( + self.node_ptr, + fmap_info.base, + range.start, + (range.end - range.start) as usize, + tx, + )?; + } + + if fmap.rc > 0 { + //FIXME: Use result? + let _ = fmap_info + .ranges + .insert(range.start, range.end - range.start, fmap); + } + } + //dbg!(&self.fmaps); + + // Allow release of node if not in use anymore + if !fmap_info.in_use() { + // Notify filesystem of close + tx.on_close_node(self.node_ptr)?; + + /*TODO: leaks memory, but why? + // Remove from fmaps list + fmaps.remove(&self.node_ptr.id()); + */ + } + + Ok(()) + } + + fn fcntl(&mut self, cmd: usize, arg: usize) -> Result { + match cmd { + F_GETFL => Ok(self.flags), + F_SETFL => { + self.flags = (self.flags & O_ACCMODE) | (arg & !O_ACCMODE); + Ok(0) + } + _ => Err(Error::new(EINVAL)), + } + } + + fn path(&self) -> &str { + &self.path + } + + fn sync(&mut self, fmaps: &mut Fmaps, tx: &mut Transaction) -> Result<()> { + if let Some(fmap_info) = fmaps.get_mut(&self.node_ptr.id()) { + for (range, fmap) in fmap_info.ranges.iter_mut() { + unsafe { + fmap.sync( + self.node_ptr, + fmap_info.base, + range.start, + (range.end - range.start) as usize, + tx, + )?; + } + } + } + + Ok(()) + } + + fn truncate(&mut self, len: u64, tx: &mut Transaction) -> Result<()> { + if self.flags & O_ACCMODE == O_RDWR || self.flags & O_ACCMODE == O_WRONLY { + let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + tx.truncate_node(self.node_ptr, len, mtime.as_secs(), mtime.subsec_nanos())?; + Ok(()) + } else { + Err(Error::new(EBADF)) + } + } + + fn utimens(&mut self, times: &[TimeSpec], tx: &mut Transaction) -> Result<()> { + let mut node = tx.read_tree(self.node_ptr)?; + + if node.data().uid() == self.uid || self.uid == 0 { + if let &[atime, mtime] = times { + let mut node_changed = false; + + let old_mtime = node.data().mtime(); + let new_mtime = (mtime.tv_sec as u64, mtime.tv_nsec as u32); + if old_mtime != new_mtime { + node.data_mut().set_mtime(new_mtime.0, new_mtime.1); + node_changed = true; + } + + let old_atime = node.data().atime(); + let new_atime = (atime.tv_sec as u64, atime.tv_nsec as u32); + if old_atime != new_atime { + node.data_mut().set_atime(new_atime.0, new_atime.1); + node_changed = true; + } + + if node_changed { + tx.sync_tree(node)?; + } + } + Ok(()) + } else { + Err(Error::new(EPERM)) + } + } + + fn getdents<'buf>( + &mut self, + _buf: DirentBuf<&'buf mut [u8]>, + _opaque_offset: u64, + _tx: &mut Transaction, + ) -> Result> { + Err(Error::new(ENOTDIR)) + } +} + +impl Drop for FileResource { + fn drop(&mut self) { + /* + if !self.fmaps.is_empty() { + eprintln!( + "redoxfs: file {} still has {} fmaps!", + self.path, + self.fmaps.len() + ); + } + */ + } +} + +impl range_tree::Value for Fmap { + type K = u64; + + fn try_merge_forward(self, other: &Self) -> core::result::Result { + if self.rc == other.rc && self.flags == other.flags && self.last_page_tail == 0 { + Ok(self) + } else { + Err(self) + } + } + fn try_merge_backwards(self, other: &Self) -> core::result::Result { + if self.rc == other.rc && self.flags == other.flags && other.last_page_tail == 0 { + Ok(self) + } else { + Err(self) + } + } + #[allow(unused_variables)] + fn split( + self, + prev_range: Option>, + range: core::ops::Range, + next_range: Option>, + ) -> (Option, Self, Option) { + ( + prev_range.map(|_range| Fmap { + rc: self.rc, + flags: self.flags, + last_page_tail: 0, + }), + Fmap { + rc: self.rc, + flags: self.flags, + last_page_tail: if next_range.is_none() { + self.last_page_tail + } else { + 0 + }, + }, + next_range.map(|_range| Fmap { + rc: self.rc, + flags: self.flags, + last_page_tail: self.last_page_tail, + }), + ) + } +} diff --git a/src/mount/redox/scheme.rs b/src/mount/redox/scheme.rs new file mode 100644 index 0000000000..8315cee4e0 --- /dev/null +++ b/src/mount/redox/scheme.rs @@ -0,0 +1,1253 @@ +use std::collections::BTreeMap; +use std::mem; +use std::str; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use libredox::Fd; +use redox_scheme::{scheme::SchemeSync, CallerCtx, OpenResult, SendFdRequest, Socket}; +use syscall::data::{Stat, StatVfs, StdFsCallMeta, TimeSpec}; +use syscall::dirent::DirentBuf; +use syscall::error::{ + Error, Result, EACCES, EBADF, EBUSY, EEXIST, EINVAL, EISDIR, ELOOP, ENOENT, ENOTDIR, ENOTEMPTY, + EOPNOTSUPP, EPERM, EXDEV, +}; +use syscall::flag::{ + EventFlags, MapFlags, StdFsCallKind, O_ACCMODE, O_CREAT, O_DIRECTORY, O_EXCL, O_NOFOLLOW, + O_RDONLY, O_RDWR, O_STAT, O_SYMLINK, O_TRUNC, O_WRONLY, +}; +use syscall::schemev2::NewFdFlags; +use syscall::FobtainFdFlags; +use syscall::FsCall; +use syscall::MunmapFlags; + +use redox_path::{ + canonicalize_to_standard, canonicalize_using_cwd, canonicalize_using_scheme, scheme_path, + RedoxPath, +}; + +use crate::{Disk, FileSystem, Node, Transaction, TreeData, TreePtr, BLOCK_SIZE}; + +use super::resource::{DirResource, Entry, FileMmapInfo, FileResource, Resource}; + +enum Handle { + Resource(Box>), + SchemeRoot, +} + +pub struct FileScheme<'sock, D: Disk> { + scheme_name: String, + mounted_path: String, + pub(crate) fs: FileSystem, + socket: &'sock Socket, + next_id: AtomicUsize, + handles: BTreeMap>, + fmap: super::resource::Fmaps, + + // Map of file id to other scheme's file descriptor. + other_scheme_fd_map: BTreeMap, + + proc_creds_capability: Fd, +} + +impl<'sock, D: Disk> FileScheme<'sock, D> { + pub fn new( + scheme_name: String, + mounted_path: String, + fs: FileSystem, + socket: &'sock Socket, + ) -> Result> { + Ok(FileScheme { + scheme_name, + mounted_path, + fs, + socket, + next_id: AtomicUsize::new(1), + handles: BTreeMap::new(), + fmap: BTreeMap::new(), + other_scheme_fd_map: BTreeMap::new(), + proc_creds_capability: { + libredox::Fd::open( + "/scheme/proc/proc-creds-capability", + libredox::flag::O_RDONLY, + 0, + )? + }, + }) + } + + fn resolve_symlink( + scheme_name: &str, + tx: &mut Transaction, + uid: u32, + gid: u32, + full_path: &str, + node: TreeData, + nodes: &mut Vec<(TreeData, String)>, + ) -> Result { + let atime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + + // symbolic link is relative to this part of the url + let mut working_dir = + dirname(full_path).unwrap_or(scheme_path(scheme_name).ok_or(Error::new(EINVAL))?); + // node of the link + let mut node = node; + + for _ in 0..32 { + // XXX What should the limit be? + assert!(node.data().is_symlink()); + let mut buf = [0; 4096]; + let count = tx.read_node( + node.ptr(), + 0, + &mut buf, + atime.as_secs(), + atime.subsec_nanos(), + )?; + + let target = canonicalize_to_standard( + Some(&working_dir), + str::from_utf8(&buf[..count]).or(Err(Error::new(EINVAL)))?, + ) + .ok_or(Error::new(EINVAL))?; + let target_as_path = RedoxPath::from_absolute(&target).ok_or(Error::new(EINVAL))?; + + let (scheme, reference) = target_as_path.as_parts().ok_or(Error::new(EINVAL))?; + if scheme.as_ref() != scheme_name { + return Err(Error::new(EXDEV)); + } + let target_reference = reference.to_string(); + + nodes.clear(); + if let Some((next_node, next_node_name)) = Self::path_nodes( + scheme_name, + tx, + TreePtr::root(), + &target_reference, + uid, + gid, + nodes, + )? { + if !next_node.data().is_symlink() { + nodes.push((next_node, next_node_name)); + return Ok(target_reference); + } + node = next_node; + working_dir = dirname(&target).ok_or(Error::new(EINVAL))?.to_string(); + } else { + return Err(Error::new(ENOENT)); + } + } + Err(Error::new(ELOOP)) + } + + fn handle_connect(&mut self, id: usize, payload: &mut [u8]) -> Result { + let Some(Handle::Resource(resource)) = self.handles.get(&id) else { + return Err(Error::new(EBADF)); + }; + let inode_id = resource.node_ptr().id(); + let target_fd = self + .other_scheme_fd_map + .get(&inode_id) + .ok_or(Error::new(EBADF))?; + let len = libredox::call::get_socket_token(target_fd.raw(), payload)?; + return Ok(len); + } + + fn open(&mut self, url: &str, flags: usize, ctx: &CallerCtx) -> Result { + self.open_internal(TreePtr::root(), url, flags, ctx) + } + + fn open_internal( + &mut self, + start_ptr: TreePtr, + url: &str, + flags: usize, + ctx: &CallerCtx, + ) -> Result { + let CallerCtx { uid, gid, .. } = *ctx; + + let path = url.trim_matches('/'); + + // println!("Open '{}' {:X}", &path, flags); + + //TODO: try to move things into one transaction + let scheme_name = &self.scheme_name; + let mut nodes = Vec::new(); + let node_opt = self + .fs + .tx(|tx| Self::path_nodes(scheme_name, tx, start_ptr, path, uid, gid, &mut nodes))?; + let parent_ptr_opt = nodes.last().map(|x| x.0.ptr()); + let resource: Box> = match node_opt { + Some((node, _node_name)) => { + if flags & (O_CREAT | O_EXCL) == O_CREAT | O_EXCL { + return Err(Error::new(EEXIST)); + } else if node.data().is_dir() { + if flags & O_ACCMODE == O_RDONLY { + if !node.data().permission(uid, gid, Node::MODE_READ) { + // println!("dir not readable {:o}", node.data().mode); + return Err(Error::new(EACCES)); + } + + let mut children = Vec::new(); + self.fs.tx(|tx| tx.child_nodes(node.ptr(), &mut children))?; + + let mut data = Vec::new(); + for child in children.iter() { + if let Some(child_name) = child.name() { + data.push(Entry { + node_ptr: child.node_ptr(), + name: child_name.to_string(), + }); + } + } + + Box::new(DirResource::new( + path.to_string(), + parent_ptr_opt, + node.ptr(), + Some(data), + uid, + )) + } else if flags & O_WRONLY == O_WRONLY { + // println!("{:X} & {:X}: EISDIR {}", flags, O_DIRECTORY, path); + return Err(Error::new(EISDIR)); + } else { + Box::new(DirResource::new( + path.to_string(), + parent_ptr_opt, + node.ptr(), + None, + uid, + )) + } + } else if node.data().is_symlink() + && !(flags & O_STAT == O_STAT && flags & O_NOFOLLOW == O_NOFOLLOW) + && flags & O_SYMLINK != O_SYMLINK + { + let mut resolve_nodes = Vec::new(); + let full_path = + canonicalize_using_scheme(scheme_name, url).ok_or(Error::new(EINVAL))?; + let resolved = self.fs.tx(|tx| { + Self::resolve_symlink( + scheme_name, + tx, + uid, + gid, + &full_path, + node, + &mut resolve_nodes, + ) + })?; + return self.open(&resolved, flags, ctx); + } else if !node.data().is_symlink() && flags & O_SYMLINK == O_SYMLINK { + return Err(Error::new(EINVAL)); + } else { + let node_ptr = node.ptr(); + + if flags & O_DIRECTORY == O_DIRECTORY { + // println!("{:X} & {:X}: ENOTDIR {}", flags, O_DIRECTORY, path); + return Err(Error::new(ENOTDIR)); + } + + if (flags & O_ACCMODE == O_RDONLY || flags & O_ACCMODE == O_RDWR) + && !node.data().permission(uid, gid, Node::MODE_READ) + { + // println!("file not readable {:o}", node.data().mode); + return Err(Error::new(EACCES)); + } + + if (flags & O_ACCMODE == O_WRONLY || flags & O_ACCMODE == O_RDWR) + && !node.data().permission(uid, gid, Node::MODE_WRITE) + { + // println!("file not writable {:o}", node.data().mode); + return Err(Error::new(EACCES)); + } + + if flags & O_TRUNC == O_TRUNC { + if !node.data().permission(uid, gid, Node::MODE_WRITE) { + // println!("file not writable {:o}", node.data().mode); + return Err(Error::new(EACCES)); + } + + let mtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + self.fs.tx(|tx| { + tx.truncate_node(node_ptr, 0, mtime.as_secs(), mtime.subsec_nanos()) + })?; + } + + Box::new(FileResource::new( + path.to_string(), + parent_ptr_opt, + node_ptr, + flags, + uid, + )) + } + } + None => { + if flags & O_CREAT == O_CREAT { + let mut last_part = String::new(); + for part in path.split('/') { + if !part.is_empty() { + last_part = part.to_string(); + } + } + if !last_part.is_empty() { + if let Some((parent, _parent_name)) = nodes.last() { + if !parent.data().permission(uid, gid, Node::MODE_WRITE) { + // println!("dir not writable {:o}", parent.1.mode); + return Err(Error::new(EACCES)); + } + + let dir = flags & O_DIRECTORY == O_DIRECTORY; + let mode_type = if dir { + Node::MODE_DIR + } else if flags & O_SYMLINK == O_SYMLINK { + Node::MODE_SYMLINK + } else { + Node::MODE_FILE + }; + + let node_ptr = self.fs.tx(|tx| { + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let mut node = tx.create_node( + parent.ptr(), + &last_part, + mode_type as u16 | (flags as u16 & Node::MODE_PERM), + ctime.as_secs(), + ctime.subsec_nanos(), + )?; + let node_ptr = node.ptr(); + if node.data().uid() != uid || node.data().gid() != gid { + node.data_mut().set_uid(uid); + node.data_mut().set_gid(gid); + tx.sync_tree(node)?; + } + Ok(node_ptr) + })?; + + if dir { + Box::new(DirResource::new( + path.to_string(), + parent_ptr_opt, + node_ptr, + None, + uid, + )) + } else { + Box::new(FileResource::new( + path.to_string(), + parent_ptr_opt, + node_ptr, + flags, + uid, + )) + } + } else { + return Err(Error::new(EPERM)); + } + } else { + return Err(Error::new(EPERM)); + } + } else { + return Err(Error::new(ENOENT)); + } + } + }; + + let node_ptr = resource.node_ptr(); + { + let fmap_info = self + .fmap + .entry(node_ptr.id()) + .or_insert_with(FileMmapInfo::new); + if !fmap_info.in_use() { + // Notify filesystem of open + self.fs.tx(|tx| tx.on_open_node(node_ptr))?; + } + fmap_info.open_fds += 1; + } + + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + self.handles.insert(id, Handle::Resource(resource)); + + Ok(OpenResult::ThisScheme { + number: id, + flags: NewFdFlags::POSITIONED, + }) + } + + fn unlink_internal( + &mut self, + start_ptr: TreePtr, + path: &str, + flags: usize, + uid: u32, + gid: u32, + ) -> Result<()> { + let scheme_name = &self.scheme_name; + + let unlink_result = self.fs.tx(|tx| { + let mut nodes = Vec::new(); + + let Some((child, child_name)) = + Self::path_nodes(scheme_name, tx, start_ptr, path, uid, gid, &mut nodes)? + else { + return Err(Error::new(ENOENT)); + }; + + let Some((parent, _parent_name)) = nodes.last() else { + return Err(Error::new(EPERM)); + }; + + if !parent.data().permission(uid, gid, Node::MODE_WRITE) { + // println!("dir not writable {:o}", parent.1.mode); + return Err(Error::new(EACCES)); + } + + // Check AT_REMOVEDIR + if flags & syscall::AT_REMOVEDIR == syscall::AT_REMOVEDIR { + // --- rmdir --- + if child.data().is_dir() { + if !child.data().permission(uid, gid, Node::MODE_WRITE) { + return Err(Error::new(EACCES)); + } + tx.remove_node(parent.ptr(), &child_name, Node::MODE_DIR) + } else { + Err(Error::new(ENOTDIR)) + } + } else { + // --- unlink --- + if !child.data().is_dir() { + if child.data().uid() != uid && uid != 0 { + // println!("file not owned by current user {}", parent.1.uid); + return Err(Error::new(EACCES)); + } + + let mode = if child.data().is_symlink() { + Node::MODE_SYMLINK + } else if child.data().is_sock() { + Node::MODE_SOCK + } else { + Node::MODE_FILE + }; + + tx.remove_node(parent.ptr(), &child_name, mode) + } else { + Err(Error::new(EISDIR)) + } + } + }); + + let Some(node_id) = unlink_result? else { + return Ok(()); + }; + + let _ = self.other_scheme_fd_map.remove(&node_id); + + Ok(()) + } + + fn path_nodes( + scheme_name: &str, + tx: &mut Transaction, + start_ptr: TreePtr, + path: &str, + uid: u32, + gid: u32, + nodes: &mut Vec<(TreeData, String)>, + ) -> Result, String)>> { + let mut parts = path + .split('/') + .filter(|part| !part.is_empty() && *part != "."); + let mut part_opt: Option<&str> = None; + let mut node_ptr = start_ptr; + let mut node_name = String::new(); + loop { + let node_res = match part_opt { + None => tx.read_tree(node_ptr), + Some(part) => { + node_name = part.to_string(); + tx.find_node(node_ptr, part) + } + }; + + part_opt = parts.next(); + if let Some(part) = part_opt { + let node = node_res?; + if !node.data().permission(uid, gid, Node::MODE_EXEC) { + return Err(Error::new(EACCES)); + } + if node.data().is_symlink() { + let mut url = String::new(); + url.push_str(scheme_name); + url.push(':'); + for (_parent, parent_name) in nodes.iter() { + url.push('/'); + url.push_str(&parent_name); + } + Self::resolve_symlink(scheme_name, tx, uid, gid, &url, node, nodes)?; + node_ptr = nodes.last().unwrap().0.ptr(); + } else if !node.data().is_dir() { + return Err(Error::new(ENOTDIR)); + } else { + node_ptr = node.ptr(); + nodes.push((node, part.to_string())); + } + } else { + match node_res { + Ok(node) => return Ok(Some((node, node_name))), + Err(err) => match err.errno { + ENOENT => return Ok(None), + _ => return Err(err), + }, + } + } + } + } +} + +/// given a path with a scheme, return the containing directory (or scheme) +fn dirname(path: &str) -> Option { + canonicalize_using_cwd(Some(path), "..") +} + +// TODO: Reimplement these using the scheme common path related crate +pub fn resolve_path(dir: &Box>, path: String) -> Option { + let max_upward_depth = 64; // Same value as MAX_LEVEL of sym loops in relibc + let (canon, _depth) = + canonicalize_using_cwd_with_max_upward_depth(dir.path(), &path, max_upward_depth)?; + + Some(canon) +} +pub fn canonicalize_using_cwd_with_max_upward_depth( + cwd: &str, + path: &str, + max_upward_depth: usize, +) -> Option<(String, usize)> { + let (canonical, upward_depth) = + canonicalize_with_max_upward_depth(&format!("{}/{}", cwd, path), max_upward_depth)?; + Some((canonical, upward_depth)) +} +pub fn canonicalize_with_max_upward_depth( + path: &str, + max_upward_depth: usize, +) -> Option<(String, usize)> { + let mut nskip = 0; + let mut parts = Vec::new(); + let mut upward_depth = 0; + + for part in path.split('/').rev() { + if part == "." || part.is_empty() { + continue; + } else if part == ".." { + nskip += 1; + upward_depth += 1; + if upward_depth > max_upward_depth { + return None; + } + } else if nskip > 0 { + nskip -= 1; + } else { + parts.push(part); + } + } + + let canonical = parts.iter().rev().fold(String::new(), |mut string, &part| { + if !string.is_empty() && !string.ends_with('/') { + string.push('/'); + } + string.push_str(part); + string + }); + + Some((canonical, upward_depth)) +} + +impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> { + fn scheme_root(&mut self) -> Result { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + self.handles.insert(id, Handle::SchemeRoot); + Ok(id) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + flags: usize, + _fcntl_flags: u32, + ctx: &CallerCtx, + ) -> Result { + let path = path.to_string(); + let path_to_open = match self.handles.get(&dirfd).ok_or(Error::new(EBADF))? { + // If pathname is absolute, then dirfd is ignored. + Handle::Resource(dir_resource) if !path.starts_with('/') => { + resolve_path(&dir_resource, path).ok_or(Error::new(ENOENT))? + } + _ => path.to_string(), + }; + self.open_internal(TreePtr::root(), &path_to_open, flags, ctx) + } + + fn unlinkat(&mut self, dirfd: usize, url: &str, flags: usize, ctx: &CallerCtx) -> Result<()> { + let uid = ctx.uid; + let gid = ctx.gid; + let url = url.to_string(); + + let path = match self.handles.get(&dirfd).ok_or(Error::new(EBADF))? { + Handle::Resource(dir_resource) => { + resolve_path(&dir_resource, url).ok_or(Error::new(ENOENT))? + } + Handle::SchemeRoot => url, + }; + let path = path.trim_matches('/'); + let start_ptr = TreePtr::root(); + + // println!("Unlinkat '{}' flags: {:X}", path, flags); + + self.unlink_internal(start_ptr, path, flags, uid, gid) + } + + /* Resource operations */ + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + // println!("Read {}, {:X} {}", id, buf.as_ptr() as usize, buf.len()); + let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else { + return Err(Error::new(EBADF)); + }; + self.fs.tx(|tx| file.read(buf, offset, tx)) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + offset: u64, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + // println!("Write {}, {:X} {}", id, buf.as_ptr() as usize, buf.len()); + let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else { + return Err(Error::new(EBADF)); + }; + self.fs.tx(|tx| file.write(buf, offset, tx)) + } + + fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result { + // println!("Seek {}, {} {}", id, pos, whence); + let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else { + return Err(Error::new(EBADF)); + }; + self.fs.tx(|tx| file.fsize(tx)) + } + + fn fchmod(&mut self, id: usize, mode: u16, _ctx: &CallerCtx) -> Result<()> { + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + self.fs.tx(|tx| file.fchmod(mode, tx)) + } else { + Err(Error::new(EBADF)) + } + } + + fn fchown(&mut self, id: usize, new_uid: u32, new_gid: u32, _ctx: &CallerCtx) -> Result<()> { + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + self.fs.tx(|tx| file.fchown(new_uid, new_gid, tx)) + } else { + Err(Error::new(EBADF)) + } + } + + fn fcntl(&mut self, id: usize, cmd: usize, arg: usize, _ctx: &CallerCtx) -> Result { + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + file.fcntl(cmd, arg) + } else { + Err(Error::new(EBADF)) + } + } + + fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result { + if let Some(Handle::Resource(_file)) = self.handles.get(&id) { + // EPERM is returned for handles that are always readable or writable + Err(Error::new(EPERM)) + } else { + Err(Error::new(EBADF)) + } + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + // println!("Fpath {}, {:X} {}", id, buf.as_ptr() as usize, buf.len()); + if let Some(Handle::Resource(file)) = self.handles.get(&id) { + let mounted_path = self.mounted_path.as_bytes(); + + let mut i = 0; + while i < buf.len() && i < mounted_path.len() { + buf[i] = mounted_path[i]; + i += 1; + } + + let path = file.path().as_bytes(); + if !path.is_empty() { + if i < buf.len() { + buf[i] = b'/'; + i += 1; + } + + let mut j = 0; + while i < buf.len() && j < path.len() { + buf[i] = path[j]; + i += 1; + j += 1; + } + } + + Ok(i) + } else { + Err(Error::new(EBADF)) + } + } + + //TODO: this function has too much code, try to simplify it + fn flink(&mut self, id: usize, url: &str, ctx: &CallerCtx) -> Result { + let new_path = url.trim_matches('/'); + let uid = ctx.uid; + let gid = ctx.gid; + + // println!("Flink {}, {} from {}, {}", id, new_path, uid, gid); + + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + //TODO: Check for EINVAL + // The new pathname contained a path prefix of the old, or, more generally, + // an attempt was made to make a directory a subdirectory of itself. + + let mut old_name = String::new(); + for part in file.path().split('/') { + if !part.is_empty() { + old_name = part.to_string(); + } + } + if old_name.is_empty() { + return Err(Error::new(EPERM)); + } + + let mut new_name = String::new(); + for part in new_path.split('/') { + if !part.is_empty() { + new_name = part.to_string(); + } + } + if new_name.is_empty() { + return Err(Error::new(EPERM)); + } + + let scheme_name = &self.scheme_name; + self.fs.tx(|tx| { + let _orig_parent_ptr = match file.parent_ptr_opt() { + Some(some) => some, + None => { + // println!("orig is root"); + return Err(Error::new(EBUSY)); + } + }; + + let orig_node = tx.read_tree(file.node_ptr())?; + + if !orig_node.data().owner(uid) { + // println!("orig_node not owned by caller {}", uid); + return Err(Error::new(EACCES)); + } + + let mut new_nodes = Vec::new(); + let new_node_opt = Self::path_nodes( + scheme_name, + tx, + TreePtr::root(), + new_path, + uid, + gid, + &mut new_nodes, + )?; + + if let Some((ref new_parent, _)) = new_nodes.last() { + if !new_parent.data().owner(uid) { + // println!("new_parent not owned by caller {}", uid); + return Err(Error::new(EACCES)); + } + + if let Some((ref new_node, _)) = new_node_opt { + if !new_node.data().owner(uid) { + // println!("new dir not owned by caller {}", uid); + return Err(Error::new(EACCES)); + } + + if new_node.data().is_dir() { + if !orig_node.data().is_dir() { + // println!("orig_node is file, new is dir"); + return Err(Error::new(EACCES)); + } + + let mut children = Vec::new(); + tx.child_nodes(new_node.ptr(), &mut children)?; + + if !children.is_empty() { + // println!("new dir not empty"); + return Err(Error::new(ENOTEMPTY)); + } + } else { + if orig_node.data().is_dir() { + // println!("orig_node is dir, new is file"); + return Err(Error::new(ENOTDIR)); + } + } + } + + tx.link_node(new_parent.ptr(), &new_name, orig_node.ptr())?; + + file.set_path(new_path); + Ok(0) + } else { + Err(Error::new(EPERM)) + } + }) + } else { + Err(Error::new(EBADF)) + } + } + + //TODO: this function has too much code, try to simplify it + fn frename(&mut self, id: usize, url: &str, ctx: &CallerCtx) -> Result { + let new_path = url.trim_matches('/'); + let uid = ctx.uid; + let gid = ctx.gid; + + // println!("Frename {}, {} from {}, {}", id, new_path, uid, gid); + + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + //TODO: Check for EINVAL + // The new pathname contained a path prefix of the old, or, more generally, + // an attempt was made to make a directory a subdirectory of itself. + + let mut old_name = String::new(); + for part in file.path().split('/') { + if !part.is_empty() { + old_name = part.to_string(); + } + } + if old_name.is_empty() { + return Err(Error::new(EPERM)); + } + + let mut new_name = String::new(); + for part in new_path.split('/') { + if !part.is_empty() { + new_name = part.to_string(); + } + } + if new_name.is_empty() { + return Err(Error::new(EPERM)); + } + + let scheme_name = &self.scheme_name; + self.fs.tx(|tx| { + let orig_parent_ptr = match file.parent_ptr_opt() { + Some(some) => some, + None => { + // println!("orig is root"); + return Err(Error::new(EBUSY)); + } + }; + + let orig_node = tx.read_tree(file.node_ptr())?; + + if !orig_node.data().owner(uid) { + // println!("orig_node not owned by caller {}", uid); + return Err(Error::new(EACCES)); + } + + let mut new_nodes = Vec::new(); + let new_node_opt = Self::path_nodes( + scheme_name, + tx, + TreePtr::root(), + new_path, + uid, + gid, + &mut new_nodes, + )?; + + if let Some((ref new_parent, _)) = new_nodes.last() { + if !new_parent.data().owner(uid) { + // println!("new_parent not owned by caller {}", uid); + return Err(Error::new(EACCES)); + } + + if let Some((ref new_node, _)) = new_node_opt { + if !new_node.data().owner(uid) { + // println!("new dir not owned by caller {}", uid); + return Err(Error::new(EACCES)); + } + + if new_node.data().is_dir() { + if !orig_node.data().is_dir() { + // println!("orig_node is file, new is dir"); + return Err(Error::new(EACCES)); + } + + let mut children = Vec::new(); + tx.child_nodes(new_node.ptr(), &mut children)?; + + if !children.is_empty() { + // println!("new dir not empty"); + return Err(Error::new(ENOTEMPTY)); + } + } else { + if orig_node.data().is_dir() { + // println!("orig_node is dir, new is file"); + return Err(Error::new(ENOTDIR)); + } + } + } + + tx.rename_node(orig_parent_ptr, &old_name, new_parent.ptr(), &new_name)?; + + file.set_path(new_path); + Ok(0) + } else { + Err(Error::new(EPERM)) + } + }) + } else { + Err(Error::new(EBADF)) + } + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { + // println!("Fstat {}, {:X}", id, stat as *mut Stat as usize); + if let Some(Handle::Resource(file)) = self.handles.get(&id) { + self.fs.tx(|tx| file.stat(stat, tx)) + } else { + Err(Error::new(EBADF)) + } + } + + fn fstatvfs(&mut self, id: usize, stat: &mut StatVfs, _ctx: &CallerCtx) -> Result<()> { + if let Some(Handle::Resource(_file)) = self.handles.get(&id) { + stat.f_bsize = BLOCK_SIZE as u32; + stat.f_blocks = self.fs.header.size() / (stat.f_bsize as u64); + stat.f_bfree = self.fs.allocator().free(); + stat.f_bavail = stat.f_bfree; + + Ok(()) + } else { + Err(Error::new(EBADF)) + } + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> { + // println!("Fsync {}", id); + let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else { + return Err(Error::new(EBADF)); + }; + let fmaps = &mut self.fmap; + + self.fs.tx(|tx| file.sync(fmaps, tx)) + } + + fn ftruncate(&mut self, id: usize, len: u64, _ctx: &CallerCtx) -> Result<()> { + // println!("Ftruncate {}, {}", id, len); + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + self.fs.tx(|tx| file.truncate(len, tx)) + } else { + Err(Error::new(EBADF)) + } + } + + fn futimens(&mut self, id: usize, times: &[TimeSpec], _ctx: &CallerCtx) -> Result<()> { + // println!("Futimens {}, {}", id, times.len()); + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + self.fs.tx(|tx| file.utimens(times, tx)) + } else { + Err(Error::new(EBADF)) + } + } + + fn getdents<'buf>( + &mut self, + id: usize, + buf: DirentBuf<&'buf mut [u8]>, + opaque_offset: u64, + ) -> Result> { + if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) { + self.fs.tx(|tx| file.getdents(buf, opaque_offset, tx)) + } else { + Err(Error::new(EBADF)) + } + } + + fn mmap_prep( + &mut self, + id: usize, + offset: u64, + size: usize, + flags: MapFlags, + _ctx: &CallerCtx, + ) -> Result { + let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else { + return Err(Error::new(EBADF)); + }; + let fmaps = &mut self.fmap; + + self.fs.tx(|tx| file.fmap(fmaps, flags, size, offset, tx)) + } + #[allow(unused_variables)] + fn munmap( + &mut self, + id: usize, + offset: u64, + size: usize, + flags: MunmapFlags, + _ctx: &CallerCtx, + ) -> Result<()> { + let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else { + return Err(Error::new(EBADF)); + }; + let fmaps = &mut self.fmap; + + self.fs.tx(|tx| file.funmap(fmaps, offset, size, tx)) + } + + fn on_close(&mut self, id: usize) { + // println!("Close {}", id); + let Some(Handle::Resource(file)) = self.handles.remove(&id) else { + return; + }; + let node_ptr = file.node_ptr(); + let Some(file_info) = self.fmap.get_mut(&node_ptr.id()) else { + return; + }; + + file_info.open_fds = file_info + .open_fds + .checked_sub(1) + .expect("open_fds not tracked correctly"); + + // Check if node no longer in use + if !file_info.in_use() { + // Notify filesystem of close + if let Err(err) = self.fs.tx(|tx| tx.on_close_node(node_ptr)) { + log::error!("failed to close node {}: {}", node_ptr.id(), err); + } + + /*TODO: leaks memory, but why? + // Remove from fmap list + self.fmap.remove(&node_ptr.id()); + */ + } + } + + fn on_sendfd(&mut self, sendfd_request: &SendFdRequest) -> Result { + let ctx = sendfd_request.caller(); + let uid = ctx.uid; + let gid = ctx.gid; + + let Some(Handle::Resource(parent_resource)) = self.handles.get(&sendfd_request.id()) else { + return Err(Error::new(EBADF)); + }; + + let mut new_fd = usize::MAX; + if let Err(e) = sendfd_request.obtain_fd( + &self.socket, + FobtainFdFlags::empty(), + std::slice::from_mut(&mut new_fd), + ) { + return Err(e); + } + let other_scheme_fd = Fd::new(new_fd); + + let parent_resource_ptr = parent_resource.node_ptr(); + + let parent_node = self.fs.tx(|tx| tx.read_tree(parent_resource_ptr))?; + if !parent_node.data().is_dir() { + return Err(Error::new(ENOTDIR)); + } + if !parent_node.data().permission(uid, gid, Node::MODE_WRITE) { + return Err(Error::new(EACCES)); + } + let parent_path = parent_resource.path(); + + // TODO: Move the PATH_MAX definition to a more appropriate place. + const PATH_MAX: usize = 4096; + let mut url_buf = [0u8; PATH_MAX]; + let url_len = other_scheme_fd.fpath(&mut url_buf)?; + let url_str = str::from_utf8(&url_buf[..url_len]).map_err(|_| Error::new(EINVAL))?; + let redox_path = RedoxPath::from_absolute(url_str).ok_or(Error::new(EINVAL))?; + let (_, path) = redox_path.as_parts().ok_or(Error::new(EINVAL))?; + + let mut last_part = String::new(); + for part in path.as_ref().split('/') { + if !part.is_empty() { + last_part = part.to_string(); + } + } + + let (resource, node_id): (Box>, u32) = if !last_part.is_empty() { + let stat = other_scheme_fd.stat()?; + let mode_type = stat.st_mode as u16 & Node::MODE_TYPE; + + let flags = 0o777; + let node_ptr = self.fs.tx(|tx| { + if tx.find_node(parent_resource_ptr, &last_part).is_ok() { + // If the file already exists, we cannot create it again + return Err(Error::new(EEXIST)); + } + + let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let mut node = tx.create_node( + parent_resource_ptr, + &last_part, + mode_type | (flags as u16 & Node::MODE_PERM), + ctime.as_secs(), + ctime.subsec_nanos(), + )?; + let node_ptr = node.ptr(); + if node.data().uid() != uid || node.data().gid() != gid { + node.data_mut().set_uid(uid); + node.data_mut().set_gid(gid); + tx.sync_tree(node)?; + } + Ok(node_ptr) + })?; + + let file_path = format!("{parent_path}/{last_part}"); + let node_id = node_ptr.id(); + + ( + Box::new(FileResource::new( + file_path, + Some(parent_resource_ptr), + node_ptr, + flags, + uid, + )), + node_id, + ) + } else { + return Err(Error::new(EINVAL)); + }; + + let node_ptr = resource.node_ptr(); + { + let fmap_info = self + .fmap + .entry(node_ptr.id()) + .or_insert_with(FileMmapInfo::new); + if !fmap_info.in_use() { + // Notify filesystem of open + self.fs.tx(|tx| tx.on_open_node(node_ptr))?; + } + fmap_info.open_fds += 1; + } + + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + self.handles.insert(id, Handle::Resource(resource)); + self.other_scheme_fd_map.insert(node_id, other_scheme_fd); + Ok(new_fd) + } + + fn call( + &mut self, + id: usize, + payload: &mut [u8], + metadata: &[u64], + _ctx: &CallerCtx, + ) -> Result { + let Some(verb) = FsCall::try_from_raw(metadata[0] as usize) else { + return Err(Error::new(EINVAL)); + }; + match verb { + FsCall::Connect => self.handle_connect(id, payload), + _ => Err(Error::new(EOPNOTSUPP)), + } + } + + fn std_fs_call( + &mut self, + id: usize, + kind: StdFsCallKind, + payload: &mut [u8], + metadata: StdFsCallMeta, + ctx: &CallerCtx, + ) -> Result { + match kind { + StdFsCallKind::Fchown => { + let (new_uid, new_gid) = (metadata.arg1 as u32, metadata.arg2 >> 32 as u32); + let (_pid, uid, gid) = get_uid_gid_from_pid(&self.proc_creds_capability, ctx.pid)?; + if uid != 0 && (uid != ctx.uid || gid != ctx.gid) { + return Err(Error::new(EPERM)); + } + self.fchown(id, new_uid, new_gid as u32, ctx).map(|_| 0) + } + /* TODO: Support Unlinkat using std_fs_call + Unlinkat => { + let path = unsafe { str::from_utf8_unchecked(payload) }; + let flags = metadata.arg1; + let dir_node_ptr = match self.handles.get(&id).ok_or(Error::new(EBADF))? { + // If pathname is absolute, then dirfd is ignored. + Handle::Resource(dir_resource) if !path.starts_with('/') => { + // only allow dirresource as base for openat + dir_resource.node_ptr() + } + _ => TreePtr::root(), + }; + let (_pid, uid, gid) = get_uid_gid_from_pid(&self.proc_creds_capability, ctx.pid)?; + self.unlink_internal(dir_node_ptr, path, *flags as usize, uid, gid) + .map(|_| 0) + } + */ + _ => Err(Error::new(EOPNOTSUPP)), + } + } + + fn inode(&self, id: usize) -> Result { + let Some(Handle::Resource(resource)) = self.handles.get(&id) else { + return Err(Error::new(EBADF)); + }; + Ok(resource.node_ptr().id() as usize) + } +} + +fn get_uid_gid_from_pid(cap_fd: &Fd, target_pid: usize) -> Result<(u32, u32, u32)> { + let mut buffer = [0u8; mem::size_of::()]; + let _ = libredox::call::get_proc_credentials(cap_fd.raw(), target_pid, &mut buffer).map_err( + |e| { + eprintln!( + "Failed to get process credentials for pid {}: {:?}", + target_pid, e + ); + Error::new(EINVAL) + }, + )?; + let mut cursor = 0; + let pid = read_u32(&buffer, cursor)?; + cursor += mem::size_of::() * 3; + let uid = read_u32(&buffer, cursor)?; + cursor += mem::size_of::() * 3; + let gid = read_u32(&buffer, cursor)?; + Ok((pid, uid, gid)) +} + +fn read_u32(buffer: &[u8], offset: usize) -> Result { + let bytes = buffer + .get(offset..offset + 4) + .and_then(|slice| slice.try_into().ok()) + .ok_or_else(|| Error::new(EINVAL))?; + + Ok(u32::from_le_bytes(bytes)) +} diff --git a/src/mount/stub.rs b/src/mount/stub.rs new file mode 100644 index 0000000000..ebf501a0ef --- /dev/null +++ b/src/mount/stub.rs @@ -0,0 +1,19 @@ +use std::{io, path::Path}; + +use crate::{filesystem, Disk}; + +pub fn mount( + mut _filesystem: filesystem::FileSystem, + _mountpoint: P, + _callback: F, +) -> io::Result +where + D: Disk, + P: AsRef, + F: FnOnce(&Path) -> T, +{ + Err(io::Error::new( + io::ErrorKind::Unsupported, + "FUSE mount feature is disabled", + )) +} diff --git a/src/node.rs b/src/node.rs new file mode 100644 index 0000000000..13d35b9c58 --- /dev/null +++ b/src/node.rs @@ -0,0 +1,584 @@ +use core::{fmt, mem, ops, slice}; +use endian_num::Le; + +use crate::{BlockLevel, BlockList, BlockPtr, BlockTrait, RecordRaw, BLOCK_SIZE, RECORD_LEVEL}; + +bitflags::bitflags! { + pub struct NodeFlags: u32 { + const INLINE_DATA = 0x1; + } +} + +/// An index into a [`Node`]'s block table. +pub enum NodeLevel { + L0(usize), + L1(usize, usize), + L2(usize, usize, usize), + L3(usize, usize, usize, usize), + L4(usize, usize, usize, usize, usize), +} + +impl NodeLevel { + // Warning: this uses constant record offsets, make sure to sync with Node + + /// Return the [`NodeLevel`] of the record with the given index. + /// - the first 128 are level 0, + /// - the next 64*256 are level 1, + /// - ...and so on. + pub fn new(mut record_offset: u64) -> Option { + // 1 << 8 = 256, this is the number of entries in a BlockList + const SHIFT: u64 = 8; + const NUM: u64 = 1 << SHIFT; + const MASK: u64 = NUM - 1; + + const L0: u64 = 128; + if record_offset < L0 { + return Some(Self::L0((record_offset & MASK) as usize)); + } else { + record_offset -= L0; + } + + const L1: u64 = 64 * NUM; + if record_offset < L1 { + return Some(Self::L1( + ((record_offset >> SHIFT) & MASK) as usize, + (record_offset & MASK) as usize, + )); + } else { + record_offset -= L1; + } + + const L2: u64 = 32 * NUM * NUM; + if record_offset < L2 { + return Some(Self::L2( + ((record_offset >> (2 * SHIFT)) & MASK) as usize, + ((record_offset >> SHIFT) & MASK) as usize, + (record_offset & MASK) as usize, + )); + } else { + record_offset -= L2; + } + + const L3: u64 = 16 * NUM * NUM * NUM; + if record_offset < L3 { + return Some(Self::L3( + ((record_offset >> (3 * SHIFT)) & MASK) as usize, + ((record_offset >> (2 * SHIFT)) & MASK) as usize, + ((record_offset >> SHIFT) & MASK) as usize, + (record_offset & MASK) as usize, + )); + } else { + record_offset -= L3; + } + + const L4: u64 = 12 * NUM * NUM * NUM * NUM; + if record_offset < L4 { + Some(Self::L4( + ((record_offset >> (4 * SHIFT)) & MASK) as usize, + ((record_offset >> (3 * SHIFT)) & MASK) as usize, + ((record_offset >> (2 * SHIFT)) & MASK) as usize, + ((record_offset >> SHIFT) & MASK) as usize, + (record_offset & MASK) as usize, + )) + } else { + None + } + } +} + +type BlockListL1 = BlockList; +type BlockListL2 = BlockList; +type BlockListL3 = BlockList; +type BlockListL4 = BlockList; + +#[repr(C, packed)] +pub struct NodeLevelData { + /// The first 128 blocks of this file. + /// + /// Total size: 128 * RECORD_SIZE (16 MiB, 128 KiB each) + pub level0: [BlockPtr; 128], + + /// The next 64 * 256 blocks of this file, + /// stored behind 64 level one tables. + /// + /// Total size: 64 * 256 * RECORD_SIZE (2 GiB, 32 MiB each) + pub level1: [BlockPtr; 64], + + /// The next 32 * 256 * 256 blocks of this file, + /// stored behind 32 level two tables. + /// Each level two table points to 256 level one tables. + /// + /// Total size: 32 * 256 * 256 * RECORD_SIZE (256 GiB, 8 GiB each) + pub level2: [BlockPtr; 32], + + /// The next 16 * 256 * 256 * 256 blocks of this file, + /// stored behind 16 level three tables. + /// + /// Total size: 16 * 256 * 256 * 256 * RECORD_SIZE (32 TiB, 2 TiB each) + pub level3: [BlockPtr; 16], + + /// The next 8 * 256 * 256 * 256 * 256 blocks of this file, + /// stored behind 8 level four tables. + /// + /// Total size: 8 * 256 * 256 * 256 * 256 * RECORD_SIZE (4 PiB, 512 TiB each) + pub level4: [BlockPtr; 8], +} + +impl Default for NodeLevelData { + fn default() -> Self { + Self { + level0: [BlockPtr::default(); 128], + level1: [BlockPtr::default(); 64], + level2: [BlockPtr::default(); 32], + level3: [BlockPtr::default(); 16], + level4: [BlockPtr::default(); 8], + } + } +} + +/// A file/folder node +#[repr(C, packed)] +pub struct Node { + /// This node's type & permissions. + /// - four most significant bits are the node's type + /// - next four bits are permissions for the node's user + /// - next four bits are permissions for the node's group + /// - four least significant bits are permissions for everyone else + pub mode: Le, + + /// The uid that owns this file + pub uid: Le, + + /// The gid that owns this file + pub gid: Le, + + /// The number of hard links to this file + pub links: Le, + + /// The length of this file, in bytes + pub size: Le, + /// The disk usage of this file, in blocks + pub blocks: Le, + + /// Creation time + pub ctime: Le, + pub ctime_nsec: Le, + + /// Modification time + pub mtime: Le, + pub mtime_nsec: Le, + + /// Access time + pub atime: Le, + pub atime_nsec: Le, + + /// Record level + pub record_level: Le, + + /// Flags + pub flags: Le, + + /// Padding + pub padding: [u8; BLOCK_SIZE as usize - 4042], + + /// Level data, should not be used directly so inline data can be supported + pub(crate) level_data: NodeLevelData, +} + +unsafe impl BlockTrait for Node { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self::default()) + } else { + None + } + } +} + +impl Default for Node { + fn default() -> Self { + Self { + mode: 0.into(), + uid: 0.into(), + gid: 0.into(), + links: 0.into(), + size: 0.into(), + // This node counts as a block + blocks: 1.into(), + ctime: 0.into(), + ctime_nsec: 0.into(), + mtime: 0.into(), + mtime_nsec: 0.into(), + atime: 0.into(), + atime_nsec: 0.into(), + record_level: 0.into(), + flags: 0.into(), + padding: [0; BLOCK_SIZE as usize - 4042], + level_data: NodeLevelData::default(), + } + } +} + +impl Node { + pub const MODE_TYPE: u16 = 0xF000; + pub const MODE_FILE: u16 = 0x8000; + pub const MODE_DIR: u16 = 0x4000; + pub const MODE_SYMLINK: u16 = 0xA000; + pub const MODE_SOCK: u16 = 0xC000; + + /// Mask for node permission bits + pub const MODE_PERM: u16 = 0x0FFF; + pub const MODE_EXEC: u16 = 0o1; + pub const MODE_WRITE: u16 = 0o2; + pub const MODE_READ: u16 = 0o4; + + /// Create a new, empty node with the given metadata + pub fn new(mode: u16, uid: u32, gid: u32, ctime: u64, ctime_nsec: u32) -> Self { + Self { + mode: mode.into(), + uid: uid.into(), + gid: gid.into(), + links: 0.into(), + ctime: ctime.into(), + ctime_nsec: ctime_nsec.into(), + mtime: ctime.into(), + mtime_nsec: ctime_nsec.into(), + atime: ctime.into(), + atime_nsec: ctime_nsec.into(), + record_level: if mode & Self::MODE_TYPE == Self::MODE_FILE { + // Files take on record level + RECORD_LEVEL as u32 + } else { + // Folders do not + 0 + } + .into(), + flags: if mode & Self::MODE_TYPE == Self::MODE_DIR { + // Directories must not use inline data (until h-tree supports it) + NodeFlags::empty() + } else { + NodeFlags::INLINE_DATA + } + .bits() + .into(), + ..Default::default() + } + } + + /// This node's type & permissions. + /// - four most significant bits are the node's type + /// - next four bits are permissions for the node's user + /// - next four bits are permissions for the node's group + /// - four least significant bits are permissions for everyone else + pub fn mode(&self) -> u16 { + self.mode.to_ne() + } + + /// The uid that owns this file + pub fn uid(&self) -> u32 { + self.uid.to_ne() + } + + /// The gid that owns this file + pub fn gid(&self) -> u32 { + self.gid.to_ne() + } + + /// The number of links to this file + /// (directory entries, symlinks, etc) + pub fn links(&self) -> u32 { + self.links.to_ne() + } + + /// The length of this file, in bytes. + pub fn size(&self) -> u64 { + self.size.to_ne() + } + + /// The disk usage of this file, in blocks. + pub fn blocks(&self) -> u64 { + self.blocks.to_ne() + } + + pub fn ctime(&self) -> (u64, u32) { + (self.ctime.to_ne(), self.ctime_nsec.to_ne()) + } + + pub fn mtime(&self) -> (u64, u32) { + (self.mtime.to_ne(), self.mtime_nsec.to_ne()) + } + + pub fn atime(&self) -> (u64, u32) { + (self.atime.to_ne(), self.atime_nsec.to_ne()) + } + + pub fn record_level(&self) -> BlockLevel { + BlockLevel(self.record_level.to_ne() as usize) + } + + pub fn flags(&self) -> NodeFlags { + NodeFlags::from_bits_retain(self.flags.to_ne()) + } + + pub fn set_mode(&mut self, mode: u16) { + self.mode = mode.into(); + } + + pub fn set_uid(&mut self, uid: u32) { + self.uid = uid.into(); + } + + pub fn set_gid(&mut self, gid: u32) { + self.gid = gid.into(); + } + + pub fn set_links(&mut self, links: u32) { + self.links = links.into(); + } + + pub fn set_size(&mut self, size: u64) { + self.size = size.into(); + } + + pub fn set_blocks(&mut self, blocks: u64) { + self.blocks = blocks.into(); + } + + pub fn set_mtime(&mut self, mtime: u64, mtime_nsec: u32) { + self.mtime = mtime.into(); + self.mtime_nsec = mtime_nsec.into(); + } + + pub fn set_atime(&mut self, atime: u64, atime_nsec: u32) { + self.atime = atime.into(); + self.atime_nsec = atime_nsec.into(); + } + + pub fn set_flags(&mut self, flags: NodeFlags) { + self.flags = flags.bits().into(); + } + + pub fn has_inline_data(&self) -> bool { + self.flags().contains(NodeFlags::INLINE_DATA) + } + + pub fn inline_data(&self) -> Option<&[u8]> { + if self.has_inline_data() { + Some(unsafe { + slice::from_raw_parts( + &self.level_data as *const NodeLevelData as *const u8, + mem::size_of::(), + ) + }) + } else { + None + } + } + + pub fn inline_data_mut(&mut self) -> Option<&mut [u8]> { + if self.has_inline_data() { + Some(unsafe { + slice::from_raw_parts_mut( + &mut self.level_data as *mut NodeLevelData as *mut u8, + mem::size_of::(), + ) + }) + } else { + None + } + } + + pub fn level_data(&self) -> Option<&NodeLevelData> { + if !self.has_inline_data() { + Some(&self.level_data) + } else { + None + } + } + + pub fn level_data_mut(&mut self) -> Option<&mut NodeLevelData> { + if !self.has_inline_data() { + Some(&mut self.level_data) + } else { + None + } + } + + pub fn is_dir(&self) -> bool { + self.mode() & Self::MODE_TYPE == Self::MODE_DIR + } + + pub fn is_file(&self) -> bool { + self.mode() & Self::MODE_TYPE == Self::MODE_FILE + } + + pub fn is_symlink(&self) -> bool { + self.mode() & Self::MODE_TYPE == Self::MODE_SYMLINK + } + + pub fn is_sock(&self) -> bool { + self.mode() & Self::MODE_SOCK == Self::MODE_SOCK + } + + /// Tests if UID is the owner of that file, only true when uid=0 or when the UID stored in metadata is equal to the UID you supply + pub fn owner(&self, uid: u32) -> bool { + uid == 0 || self.uid() == uid + } + + /// Tests if the current user has enough permissions to view the file, op is the operation, + /// like read and write, these modes are MODE_EXEC, MODE_READ, and MODE_WRITE + pub fn permission(&self, uid: u32, gid: u32, op: u16) -> bool { + let mut perm = self.mode() & 0o7; + if self.uid() == uid { + // If self.mode is 101100110, >> 6 would be 000000101 + // 0o7 is octal for 111, or, when expanded to 9 digits is 000000111 + perm |= (self.mode() >> 6) & 0o7; + // Since we erased the GID and OTHER bits when >>6'ing, |= will keep those bits in place. + } + if self.gid() == gid || gid == 0 { + perm |= (self.mode() >> 3) & 0o7; + } + if uid == 0 { + //set the `other` bits to 111 + perm |= 0o7; + } + perm & op == op + } +} + +impl fmt::Debug for Node { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mode = self.mode; + let uid = self.uid; + let gid = self.gid; + let links = self.links; + let size = self.size; + let blocks = self.blocks; + let ctime = self.ctime; + let ctime_nsec = self.ctime_nsec; + let mtime = self.mtime; + let mtime_nsec = self.mtime_nsec; + let atime = self.atime; + let atime_nsec = self.atime_nsec; + f.debug_struct("Node") + .field("mode", &mode) + .field("uid", &uid) + .field("gid", &gid) + .field("links", &links) + .field("size", &size) + .field("blocks", &blocks) + .field("ctime", &ctime) + .field("ctime_nsec", &ctime_nsec) + .field("mtime", &mtime) + .field("mtime_nsec", &mtime_nsec) + .field("atime", &atime) + .field("atime_nsec", &atime_nsec) + //TODO: level0/1/2/3 + .finish() + } +} + +impl ops::Deref for Node { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts(self as *const Node as *const u8, mem::size_of::()) + as &[u8] + } + } +} + +impl ops::DerefMut for Node { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut(self as *mut Node as *mut u8, mem::size_of::()) + as &mut [u8] + } + } +} + +#[test] +fn node_size_test() { + assert_eq!(mem::size_of::(), crate::BLOCK_SIZE as usize); +} + +#[test] +fn node_inline_data_test() { + let mut node = Node::default(); + assert!(!node.has_inline_data()); + assert!(node.inline_data().is_none()); + assert!(node.inline_data_mut().is_none()); + assert!(node.level_data().is_some()); + assert!(node.level_data_mut().is_some()); + + node.set_flags(NodeFlags::INLINE_DATA); + assert!(node.has_inline_data()); + assert!(node.level_data().is_none()); + assert!(node.level_data_mut().is_none()); + + let node_addr = &node as *const Node as usize; + let meta_size = 128; + { + let inline_data = node.inline_data().unwrap(); + let inline_data_addr = inline_data.as_ptr() as usize; + assert_eq!(node_addr + meta_size, inline_data_addr); + assert_eq!(inline_data.len(), (crate::BLOCK_SIZE as usize) - meta_size); + } + { + let inline_data = node.inline_data_mut().unwrap(); + let inline_data_addr = inline_data.as_ptr() as usize; + assert_eq!(node_addr + meta_size, inline_data_addr); + assert_eq!(inline_data.len(), (crate::BLOCK_SIZE as usize) - meta_size); + } +} + +#[cfg(kani)] +#[kani::proof] +fn check_node_level() { + let offset = kani::any(); + NodeLevel::new(offset); +} + +#[cfg(kani)] +#[kani::proof] +fn check_node_perms() { + let mode = 0o750; + + let uid = kani::any(); + let gid = kani::any(); + + let ctime = kani::any(); + let ctime_nsec = kani::any(); + + let node = Node::new(mode, uid, gid, ctime, ctime_nsec); + + let root_uid = 0; + let root_gid = 0; + + let other_uid = kani::any(); + kani::assume(other_uid != uid); + kani::assume(other_uid != root_uid); + let other_gid = kani::any(); + kani::assume(other_gid != gid); + kani::assume(other_gid != root_gid); + + assert!(node.owner(uid)); + assert!(node.permission(uid, gid, 0o7)); + assert!(node.permission(uid, gid, 0o5)); + assert!(node.permission(uid, other_gid, 0o7)); + assert!(node.permission(uid, other_gid, 0o5)); + assert!(!node.permission(other_uid, gid, 0o7)); + assert!(node.permission(other_uid, gid, 0o5)); + + assert!(node.owner(root_uid)); + assert!(node.permission(root_uid, root_gid, 0o7)); + assert!(node.permission(root_uid, root_gid, 0o5)); + assert!(node.permission(root_uid, other_gid, 0o7)); + assert!(node.permission(root_uid, other_gid, 0o5)); + assert!(!node.permission(other_uid, root_gid, 0o7)); + assert!(node.permission(other_uid, root_gid, 0o5)); + + assert!(!node.owner(other_uid)); + assert!(!node.permission(other_uid, other_gid, 0o7)); + assert!(!node.permission(other_uid, other_gid, 0o5)); +} diff --git a/src/record.rs b/src/record.rs new file mode 100644 index 0000000000..7586cd9aed --- /dev/null +++ b/src/record.rs @@ -0,0 +1,42 @@ +use alloc::{boxed::Box, vec}; +use core::ops; + +use crate::{BlockLevel, BlockTrait, RECORD_LEVEL}; + +//TODO: this is a box to prevent stack overflows +#[derive(Clone)] +pub struct RecordRaw(pub(crate) Box<[u8]>); + +unsafe impl BlockTrait for RecordRaw { + fn empty(level: BlockLevel) -> Option { + if level.0 <= RECORD_LEVEL { + Some(Self(vec![0; level.bytes() as usize].into_boxed_slice())) + } else { + None + } + } +} + +impl ops::Deref for RecordRaw { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl ops::DerefMut for RecordRaw { + fn deref_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +#[test] +fn record_raw_size_test() { + for level_i in 0..RECORD_LEVEL { + let level = BlockLevel(level_i); + assert_eq!( + RecordRaw::empty(level).unwrap().len(), + level.bytes() as usize + ); + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000000..0187a62b88 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,1104 @@ +use crate::{ + htree::{HTreeHash, HTreeNode, HTreePtr, HTREE_IDX_ENTRIES}, + transaction::{level_data, level_data_mut, FsCtx}, + BlockAddr, BlockData, BlockMeta, BlockPtr, DirEntry, DirList, DiskMemory, DiskSparse, + FileSystem, Node, TreePtr, ALLOC_GC_THRESHOLD, BLOCK_SIZE, +}; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering::Relaxed; +use std::{fs, time}; + +static IMAGE_SEQ: AtomicUsize = AtomicUsize::new(0); + +fn with_redoxfs(callback: F) -> T +where + T: Send + Sync + 'static, + F: FnOnce(FileSystem) -> T + Send + Sync + 'static, +{ + let disk_path = format!("image{}.bin", IMAGE_SEQ.fetch_add(1, Relaxed)); + + let res = { + let disk = DiskSparse::create(dbg!(&disk_path), 1024 * 1024 * 1024).unwrap(); + + let ctime = dbg!(time::SystemTime::now().duration_since(time::UNIX_EPOCH)).unwrap(); + let fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap(); + + callback(fs) + }; + + dbg!(fs::remove_file(dbg!(disk_path))).unwrap(); + + res +} + +#[test] +fn many_create_remove_should_not_increase_size() { + with_redoxfs(|mut fs| { + let initially_free = fs.allocator().free(); + let tree_ptr = TreePtr::::root(); + let name = "test"; + + // Iterate over 255 times to prove deleted files don't retain space within the node tree + // Iterate to an ALLOC_GC_THRESHOLD boundary to ensure the allocator GC reclaims space + let start = fs.header.generation.to_ne(); + let end = start + ALLOC_GC_THRESHOLD; + let end = end - (end % ALLOC_GC_THRESHOLD) + 1 + ALLOC_GC_THRESHOLD; + for i in start..end { + let _ = fs + .tx(|tx| { + tx.create_node( + tree_ptr, + &format!("{}{}", name, i), + Node::MODE_FILE | 0o644, + 1, + 0, + )?; + tx.remove_node(tree_ptr, &format!("{}{}", name, i), Node::MODE_FILE) + }) + .unwrap(); + } + + // Any value greater than 0 indicates a storage leak + let diff = initially_free - fs.allocator().free(); + assert_eq!(diff, 0); + }); +} + +#[test] +fn many_create_then_many_remove_should_not_increase_size() { + with_redoxfs(|mut fs| { + let tree_ptr = TreePtr::::root(); + let initially_free = fs.allocator().free(); + let initial_size = fs.tx(|tx| tx.read_tree(tree_ptr)).unwrap().data().size(); + + let end = 3000; + for i in 0..end { + let _ = fs + .tx(|tx| { + tx.create_node( + tree_ptr, + &format!("test{}", i), + Node::MODE_FILE | 0o644, + 1, + 0, + ) + }) + .unwrap(); + } + + for i in 0..end { + let result = + fs.tx(|tx| tx.remove_node(tree_ptr, &format!("test{}", i), Node::MODE_FILE)); + if result.is_err() { + println!("Failed to delete on iteration {i}"); + } + result.unwrap(); + } + + let final_size = fs.tx(|tx| tx.read_tree(tree_ptr)).unwrap().data().size(); + assert_eq!(initial_size, final_size); + + // Any value greater than 0 indicates a storage leak + let _ = fs.tx(|tx| tx.sync(true)); + let diff = initially_free - fs.allocator().free(); + assert_eq!(diff, 0); + }); +} + +#[test] +fn empty_dir() { + with_redoxfs(|mut fs| { + let root_ptr = TreePtr::root(); + let empty_dir = fs + .tx(|tx| tx.create_node(root_ptr, "my_dir", Node::MODE_DIR, 1, 0)) + .unwrap(); + + // List + let mut children = Vec::::new(); + fs.tx(|tx| tx.child_nodes(empty_dir.ptr(), &mut children)) + .unwrap(); + assert_eq!(children.len(), 0); + + // Find + let error = fs.tx(|tx| tx.find_node(empty_dir.ptr(), "does_not_exist")); + assert!(error.is_err()); + assert_eq!(error.unwrap_err().errno, syscall::error::ENOENT); + + // Remove + let error = fs.tx(|tx| tx.remove_node(empty_dir.ptr(), "does_not_exist", Node::MODE_FILE)); + assert!(error.is_err()); + assert_eq!(error.unwrap_err().errno, syscall::error::ENOENT); + }) +} + +// TODO: When increasing the total_count to 8000, the Allocator's deallocate() function surfaces as "slow" according to flamegraph. This +// appears to be the result of bulk deleting in this test, but I would bet that any filesystem that has lived for a long time would +// start to see degraded performance due to this. +#[test] +fn many_create_write_list_find_read_delete() { + let disk = DiskMemory::new(1024 * 1024 * 1024); + let ctime = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap(); + let mut fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap(); + let tree_ptr = TreePtr::::root(); + let total_count = 3000; + + // Create a bunch of files + for i in 0..total_count { + let result = fs.tx(|tx| { + tx.create_node( + tree_ptr, + &format!("file{i:05}"), + Node::MODE_FILE | 0o644, + 1, + 0, + ) + }); + if result.is_err() { + println!("Failure on create iteration {i}"); + } + + let file_node = result.unwrap(); + let result = fs.tx(|tx| { + tx.write_node( + file_node.ptr(), + 0, + format!("Hello World! #{i}").as_bytes(), + ctime.as_secs(), + ctime.subsec_nanos(), + ) + }); + if result.is_err() { + println!("Failure on write iteration {i}"); + } + assert!(result.unwrap() > 0) + } + + // Confirm that they can be listed + { + let mut children = Vec::::with_capacity(total_count); + fs.tx(|tx| tx.child_nodes(tree_ptr, &mut children)).unwrap(); + assert_eq!( + children.len(), + total_count, + "The list of children should match the number of files created." + ); + let mut children: Vec = children + .iter() + .map(|entry| entry.name().unwrap_or_default().to_string()) + .collect(); + children.sort(); + + for i in 0..total_count { + let expected = format!("file{i:05}"); + let idx = children.binary_search(&expected); + assert!(idx.is_ok(), "Children did not contain '{}'", expected); + } + } + + // Find and read the files + for i in 0..total_count { + let result = fs.tx(|tx| tx.find_node(tree_ptr, &format!("file{i:05}"))); + if result.is_err() { + println!("Failure on find node iteration {i}"); + } + + let file_node = result.unwrap(); + let offset = 0; + let mut buf = [0_u8; 32]; + let result = fs.tx(|tx| { + tx.read_node( + file_node.ptr(), + offset, + &mut buf, + ctime.as_secs(), + ctime.subsec_nanos(), + ) + }); + if result.is_err() { + println!("Failure on read iteration {i}"); + } + let size = result.unwrap(); + let body = std::str::from_utf8(&buf[..size]).unwrap(); + assert_eq!(body, format!("Hello World! #{i}")); + } + + // Delete all the files + for i in 0..total_count { + let file_name = format!("file{i:05}"); + if let Err(e) = fs.tx(|tx| tx.remove_node(tree_ptr, &file_name, Node::MODE_FILE)) { + println!("Failure on delete iteration {i}"); + panic!("{e}"); + } + let result = fs.tx(|tx| tx.find_node(tree_ptr, &file_name)); + if result.is_ok() || result.unwrap_err().errno != syscall::error::ENOENT { + println!("Failure on delete verification iteration {i}"); + panic!("Deletion appears to have failed"); + } + } +} + +// +// MARK: H-Tree tests +// +// Note that most of these tests use a test specific HTreeHash implementation that will simply parse the numeric +// value after two underscores in the name. So a name of `my_file__10` would have a HTreeHash value of 10. This +// allows for some explicit placement of test values into the H-tree. +// + +/// Create an unnaturally narrow but deep H-tree structure for efficient testing of the internal +/// algorithms used to change the H-tree state. +fn create_minimal_l2_htree( + child1_name: &str, + mut fs: FileSystem, +) -> (FileSystem, TreePtr) { + let parent_ptr = TreePtr::::root(); + let child_ptr = fs + .tx(|tx| { + let mut parent = tx.read_tree(parent_ptr).unwrap(); + + let child1_block_data = BlockData::new( + unsafe { tx.allocate(&mut FsCtx, BlockMeta::default()) }.unwrap(), + Node::new( + Node::MODE_FILE, + parent.data().uid(), + parent.data().gid(), + 1, + 0, + ), + ); + let child1_block_ptr = unsafe { tx.write_block(child1_block_data) }.unwrap(); + let child1_ptr = tx.insert_tree(child1_block_ptr).unwrap(); + let child1_dir_entry = DirEntry::new(child1_ptr, child1_name); + let child1_htree_hash = HTreeHash::from_name(child1_name); + + let mut dir_list = BlockData::::empty(BlockAddr::default()).unwrap(); + dir_list.data_mut().append(&child1_dir_entry); + let dir_ptr = tx.sync_block(&mut parent, dir_list).unwrap(); + + let mut l1 = BlockData::>::empty(BlockAddr::default()).unwrap(); + l1.data_mut().ptrs[0] = HTreePtr::new(child1_htree_hash, dir_ptr); + let l1_ptr = tx.sync_block(&mut parent, l1).unwrap(); + + let mut l2 = + BlockData::>>::empty(BlockAddr::default()).unwrap(); + l2.data_mut().ptrs[0] = HTreePtr::new(child1_htree_hash, l1_ptr); + let l2_ptr = tx.sync_block(&mut parent, l2).unwrap(); + let l2_ptr = unsafe { l2_ptr.cast() }; + + level_data_mut(&mut parent)?.level0[0] = BlockPtr::marker(2); + level_data_mut(&mut parent)?.level0[1] = l2_ptr; + let size = parent.data().size() + BLOCK_SIZE * 4; + parent.data_mut().size = size.into(); + tx.sync_tree(parent).unwrap(); + Ok(child1_ptr) + }) + .unwrap(); + (fs, child_ptr) +} + +#[test] +fn insert_dir_entry_without_hash_change() { + with_redoxfs(|fs| { + let parent_ptr = TreePtr::::root(); + + // GIVEN a directory with H-Tree populated to level 2 and a new entry that lands + // in the last existing DirList, but the hash sorts lower than the max hash in the DirList + let child1_name = "child1__9"; + let child2_name = "child2__1"; + let child1_htree_hash = HTreeHash::from_name(child1_name); + let (mut fs, child1_ptr) = create_minimal_l2_htree(child1_name, fs); + + let _ = fs.tx(|tx| { + // WHEN the new child node is added to the parent directory + let child2_node = tx + .create_node(parent_ptr, child2_name, Node::MODE_FILE, 2, 0) + .unwrap(); + + // THEN the child node is added, but the H-Tree retains its structure, and the updated nodes retain + // the old HTreeHash value + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2); + + let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() }; + let l2: BlockData>> = tx.read_block(l2_ptr).unwrap(); + + let l1_ptr = l2.data().ptrs[0]; + let l1 = tx.read_block(l1_ptr.ptr).unwrap(); + assert_eq!(l1_ptr.htree_hash, child1_htree_hash); + + let dir_list_ptr = l1.data().ptrs[0]; + let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap(); + assert_eq!(dir_list_ptr.htree_hash, child1_htree_hash); + + let mut entries: Vec = dir_list + .data() + .entries() + .map(|e| e.name().unwrap().to_string()) + .collect(); + entries.sort(); + + assert_eq!(entries.len(), 2); + assert_eq!(entries, vec![child1_name, child2_name]); + + // Validate listing child_nodes works + let mut children = Vec::new(); + tx.child_nodes(parent_ptr, &mut children).unwrap(); + let mut children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect(); + children.sort(); + assert_eq!(children, entries); + + // Validate find_node works + assert_eq!( + tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(), + child1_ptr.id() + ); + assert_eq!( + tx.find_node(parent_ptr, child2_name).unwrap().ptr().id(), + child2_node.ptr().id() + ); + + // WHEN the new child node is removed from the parent directory + tx.remove_node(parent_ptr, child2_name, Node::MODE_FILE) + .unwrap(); + + // THEN the child node is removed, the H-Tree retains its structure, and the updated nodes retain + // the old HTreeHash value + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2); + + let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() }; + let l2: BlockData>> = tx.read_block(l2_ptr).unwrap(); + + let l1_ptr = l2.data().ptrs[0]; + let l1 = tx.read_block(l1_ptr.ptr).unwrap(); + assert_eq!(l1_ptr.htree_hash, child1_htree_hash); + + let dir_list_ptr = l1.data().ptrs[0]; + let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap(); + assert_eq!(dir_list_ptr.htree_hash, child1_htree_hash); + + let entries: Vec = dir_list + .data() + .entries() + .map(|e| e.name().unwrap().to_string()) + .collect(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries, vec![child1_name]); + + // Validate listing child_nodes works + let mut children = Vec::new(); + tx.child_nodes(parent_ptr, &mut children).unwrap(); + let children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect(); + assert_eq!(children, entries); + + // Validate find_node works + assert_eq!( + tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(), + child1_ptr.id() + ); + assert_eq!( + tx.find_node(parent_ptr, child2_name).unwrap_err().errno, + syscall::error::ENOENT + ); + Ok(()) + }); + }); +} + +#[test] +fn insert_dir_entry_with_hash_change() { + with_redoxfs(|fs| { + let parent_ptr = TreePtr::::root(); + + // GIVEN a directory with H-Tree populated to level 2 and a new entry that lands + // in the last existing DirList, and the hash is sorted after the max hash in the DirList + let child1_name = "child1__1"; + let child2_name = "child2__9"; + let (mut fs, child1_ptr) = create_minimal_l2_htree(child1_name, fs); + + let _ = fs.tx(|tx| { + // WHEN the new child node is added to the parent directory + let child2_node = tx + .create_node(parent_ptr, child2_name, Node::MODE_FILE, 2, 0) + .unwrap(); + + // THEN the child node is added, the H-Tree retains its structure, and the updated nodes adopt + // the new HTreeHash value + let child2_htree_hash = HTreeHash::from_name(child2_name); + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2); + + let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() }; + let l2: BlockData>> = tx.read_block(l2_ptr).unwrap(); + + let l1_ptr = l2.data().ptrs[0]; + let l1 = tx.read_block(l1_ptr.ptr).unwrap(); + assert_eq!(l1_ptr.htree_hash, child2_htree_hash); + + let dir_list_ptr = l1.data().ptrs[0]; + let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap(); + assert_eq!(dir_list_ptr.htree_hash, child2_htree_hash); + + let mut entries: Vec = dir_list + .data() + .entries() + .map(|e| e.name().unwrap().to_string()) + .collect(); + entries.sort(); + + assert_eq!(entries.len(), 2); + assert_eq!(entries, vec![child1_name, child2_name]); + + // Validate listing child_nodes works + let mut children = Vec::new(); + tx.child_nodes(parent_ptr, &mut children).unwrap(); + let mut children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect(); + children.sort(); + assert_eq!(children, entries); + + // Validate find_node works + assert_eq!( + tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(), + child1_ptr.id() + ); + assert_eq!( + tx.find_node(parent_ptr, child2_name).unwrap().ptr().id(), + child2_node.ptr().id() + ); + + // WHEN the new child node is removed from the parent directory + tx.remove_node(parent_ptr, child2_name, Node::MODE_FILE) + .unwrap(); + + // THEN the child node is removed, the H-Tree retains its structure, and the updated nodes revert + // to child1's HTreeHash value + let child1_htree_hash = HTreeHash::from_name(child1_name); + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2); + + let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() }; + let l2: BlockData>> = tx.read_block(l2_ptr).unwrap(); + + let l1_ptr = l2.data().ptrs[0]; + let l1 = tx.read_block(l1_ptr.ptr).unwrap(); + assert_eq!(l1_ptr.htree_hash, child1_htree_hash); + + let dir_list_ptr = l1.data().ptrs[0]; + let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap(); + assert_eq!(dir_list_ptr.htree_hash, child1_htree_hash); + + let entries: Vec = dir_list + .data() + .entries() + .map(|e| e.name().unwrap().to_string()) + .collect(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries, vec![child1_name]); + + // Validate listing child_nodes works + let mut children = Vec::new(); + tx.child_nodes(parent_ptr, &mut children).unwrap(); + let children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect(); + assert_eq!(children, entries); + + // Validate find_node works + assert_eq!( + tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(), + child1_ptr.id() + ); + assert_eq!( + tx.find_node(parent_ptr, child2_name).unwrap_err().errno, + syscall::error::ENOENT + ); + Ok(()) + }); + }); +} + +#[test] +fn delete_to_empty() { + with_redoxfs(|fs| { + let parent_ptr = TreePtr::::root(); + + // GIVEN a nearly empty tree + let child_name = "child1__9"; + let (mut fs, _child_ptr) = create_minimal_l2_htree(child_name, fs); + + // WHEN the last directory entry is removed + fs.tx(|tx| tx.remove_node(parent_ptr, child_name, Node::MODE_FILE)) + .unwrap(); + + // THEN the directory entry is removed, as are all the H-tree nodes + fs.tx(|tx| { + assert_eq!( + tx.find_node(parent_ptr, child_name).unwrap_err().errno, + syscall::error::ENOENT + ); + + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(!level_data(&parent)?.level0[0].is_marker()); + assert!(level_data(&parent)?.level0[0].addr().is_null()); + + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn split_htree_level0_to_level1() { + with_redoxfs(|mut fs| { + let parent_ptr = TreePtr::::root(); + + // GIVEN a full root DirList + fs.tx(|tx| { + for i in 0..16 { + let child_name = format!("child__{i:0243}"); + tx.create_node(parent_ptr, child_name.as_str(), Node::MODE_FILE, 1, 0) + .unwrap(); + } + + // Confirm preconditions: the level 0 is full of the expected entries. + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 0); + assert!(!level_data(&parent)?.level0[0].addr().is_null()); + + let dir_ptr: BlockPtr = unsafe { level_data(&parent)?.level0[1].cast() }; + let dir_list = tx.read_block(dir_ptr).unwrap(); + for (i, entry) in dir_list.data().entries().enumerate() { + assert_eq!(entry.name().unwrap(), format!("child__{i:0243}")); + } + + Ok(()) + }) + .unwrap(); + + // WHEN one more entry is added + fs.tx(|tx| { + tx.create_node( + parent_ptr, + format!("child__{:0243}", 16).as_str(), + Node::MODE_FILE, + 1, + 0, + ) + }) + .unwrap(); + + // THEN the level is increased and the DirList is split + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 1); + assert!(!level_data(&parent)?.level0[1].addr().is_null()); + + let htree_ptr: BlockPtr> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 7).as_str()) + ); + assert!(!htree_node.data().ptrs[1].is_null()); + assert_eq!( + htree_node.data().ptrs[1].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 16).as_str()) + ); + + assert!(htree_node.data().ptrs[2].is_null()); + + let dir_list1 = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap(); + let dir_list2 = tx.read_block(htree_node.data().ptrs[1].ptr).unwrap(); + + assert_eq!(dir_list1.data().entry_count(), 8); + assert_eq!(dir_list2.data().entry_count(), 9); + + for (i, entry) in dir_list1.data().entries().enumerate() { + assert_eq!(entry.name().unwrap(), format!("child__{i:0243}")); + } + + for (i, entry) in dir_list2.data().entries().enumerate() { + let i = i + dir_list1.data().entry_count(); + assert_eq!(entry.name().unwrap(), format!("child__{i:0243}")); + } + + Ok(()) + }) + .unwrap(); + + // WHEN all entries in the first split are removed + fs.tx(|tx| { + for i in 0..8 { + tx.remove_node( + parent_ptr, + format!("child__{i:0243}").as_str(), + Node::MODE_FILE, + ) + .unwrap(); + } + Ok(()) + }) + .unwrap(); + + // THEN only the other split remains + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 1); + assert!(!level_data(&parent)?.level0[1].addr().is_null()); + + let htree_ptr: BlockPtr> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 16).as_str()) + ); + assert!(htree_node.data().ptrs[1].is_null()); + + Ok(()) + }) + .unwrap(); + + // WHEN all entries in the second split are removed + fs.tx(|tx| { + for i in 8..17 { + let name = format!("child__{i:0243}"); + let result = tx.remove_node(parent_ptr, name.as_str(), Node::MODE_FILE); + result.unwrap_or_else(|e| { + panic!( + "Failed to remove file {name} with hash {:?} error {:?}", + HTreeHash::from_name(&name), + e + ) + }); + } + Ok(()) + }) + .unwrap(); + + // THEN the level1 is collapsed back to an empty state + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(!level_data(&parent)?.level0[0].is_marker()); + assert!(level_data(&parent)?.level0[1].is_null()); + Ok(()) + }) + .unwrap(); + }); +} + +#[test] +fn split_htree_with_multiple_levels() { + with_redoxfs(|fs| { + let parent_ptr = TreePtr::::root(); + let (mut fs, _) = create_minimal_l2_htree(format!("child__{:0243}", 1000).as_str(), fs); + + // GIVEN a full root leaf node (DirList) with a full H-tree branch + fs.tx(|tx| { + for i in 1..16 { + let i = i + 1000; + let child_name = format!("child__{i:0243}"); + tx.create_node(parent_ptr, child_name.as_str(), Node::MODE_FILE, 1, 0) + .unwrap(); + } + + // Confirm preconditions: the level 0 is full of the expected entries. + let mut parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2); + + let l2_ptr: BlockPtr>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let mut l2_node = tx.read_block(l2_ptr).unwrap(); + for i in 0..HTREE_IDX_ENTRIES { + if i == 0 { + assert!(!l2_node.data().ptrs[i].is_null()); + } else { + assert!(l2_node.data().ptrs[i].is_null()); + l2_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15)) + } + } + + let l1_ptr = l2_node.data().ptrs[0]; + let mut l1_node = tx.read_block(l1_ptr.ptr).unwrap(); + for i in 0..HTREE_IDX_ENTRIES { + if i == 0 { + assert!(!l1_node.data().ptrs[i].is_null()); + } else { + assert!(l1_node.data().ptrs[i].is_null()); + l1_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15)) + } + } + + l2_node.data_mut().ptrs[0].ptr = unsafe { tx.write_block(l1_node) }.unwrap(); + let l2_record_ptr = unsafe { tx.write_block(l2_node) }.unwrap(); + level_data_mut(&mut parent)?.level0[1] = unsafe { l2_record_ptr.cast() }; + tx.sync_tree(parent).unwrap(); + + Ok(()) + }) + .unwrap(); + + // WHEN another entry is added to the full DirList + fs.tx(|tx| { + tx.create_node( + parent_ptr, + format!("child__{:0243}", 1).as_str(), + Node::MODE_FILE, + 1, + 0, + ) + }) + .unwrap(); + + // THEN the branch splits all the way to the root, increasing the level + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 3); + assert!(!level_data(&parent)?.level0[1].addr().is_null()); + + let htree_ptr: BlockPtr>>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + + // Note that while a split tries to evenly divide the H-tree entries between the new two sibling nodes, + // it tries to keep hash collisions together. This unnatural test scenario has a ton of the same max + // value hash, so those get grouped together, and all our varying named entries end up in the other. + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1015).as_str()) + ); + assert!(!htree_node.data().ptrs[1].is_null()); + assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX); + assert!(htree_node.data().ptrs[2].is_null()); + + let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap(); + let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap(); + assert_eq!( + l2_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1006).as_str()) + ); + assert_eq!( + l2_node.data().ptrs[1].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1015).as_str()) + ); + assert!(l2_node.data().ptrs[2].is_null()); + + Ok(()) + }) + .unwrap(); + + // WHEN the max HTreeHash is removed from the smaller sibling + fs.tx(|tx| { + tx.remove_node( + parent_ptr, + format!("child__{:0243}", 1015).as_str(), + Node::MODE_FILE, + ) + }) + .unwrap(); + + // THEN the HTreeHash values for that branch are updated + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + let htree_ptr: BlockPtr>>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1014).as_str()) + ); + assert!(!htree_node.data().ptrs[1].is_null()); + assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX); + assert!(htree_node.data().ptrs[2].is_null()); + + let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap(); + let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap(); + assert_eq!( + l2_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1006).as_str()) + ); + assert_eq!( + l2_node.data().ptrs[1].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1014).as_str()) + ); + assert!(l2_node.data().ptrs[2].is_null()); + + Ok(()) + }) + .unwrap(); + + // WHEN removing all of one DirList + fs.tx(|tx| { + for i in 7..15 { + let x = 1000 + i; + tx.remove_node( + parent_ptr, + format!("child__{x:0243}").as_str(), + Node::MODE_FILE, + ) + .unwrap(); + } + Ok(()) + }) + .unwrap(); + + // THEN that HTreeNode is returned to empty + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + let htree_ptr: BlockPtr>>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1006).as_str()) + ); + assert!(!htree_node.data().ptrs[1].is_null()); + assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX); + assert!(htree_node.data().ptrs[2].is_null()); + + let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap(); + let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap(); + assert_eq!( + l2_node.data().ptrs[0].htree_hash, + HTreeHash::from_name(format!("child__{:0243}", 1006).as_str()) + ); + assert!(l2_node.data().ptrs[1].is_null()); + assert!(l2_node.data().ptrs[2].is_null()); + + Ok(()) + }) + .unwrap(); + + // WHEN removing the other small DirList + fs.tx(|tx| { + tx.remove_node( + parent_ptr, + format!("child__{:0243}", 1).as_str(), + Node::MODE_FILE, + ) + .unwrap(); + for i in 0..7 { + let x = 1000 + i; + tx.remove_node( + parent_ptr, + format!("child__{x:0243}").as_str(), + Node::MODE_FILE, + ) + .unwrap(); + } + Ok(()) + }) + .unwrap(); + + // THEN that HTreeNode is returned to empty + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + let htree_ptr: BlockPtr>>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!(htree_node.data().ptrs[0].htree_hash, HTreeHash::MAX); + assert!(htree_node.data().ptrs[1].is_null()); + + Ok(()) + }) + .unwrap(); + }); +} + +/// Test a pathological case of many HTreeHash collisions. This should never happen in reality, +/// but the system can support it. +#[test] +fn split_htree_with_multiple_levels_using_duplicates() { + with_redoxfs(|fs| { + let parent_ptr = TreePtr::::root(); + let (mut fs, _) = create_minimal_l2_htree(format!("child{:0242}__0", 0).as_str(), fs); + + // GIVEN a full root leaf node (DirList) with a full H-tree branch + fs.tx(|tx| { + for i in 1..16 { + let child_name = format!("child{i:0242}__0"); + tx.create_node(parent_ptr, child_name.as_str(), Node::MODE_FILE, 1, 0) + .unwrap(); + } + + // Confirm preconditions: the level 0 is full of the expected entries. + let mut parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2); + + let l2_ptr: BlockPtr>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let mut l2_node = tx.read_block(l2_ptr).unwrap(); + for i in 0..HTREE_IDX_ENTRIES { + if i == 0 { + assert!(!l2_node.data().ptrs[i].is_null()); + } else { + assert!(l2_node.data().ptrs[i].is_null()); + l2_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15)) + } + } + + let l1_ptr = l2_node.data().ptrs[0]; + let mut l1_node = tx.read_block(l1_ptr.ptr).unwrap(); + for i in 0..HTREE_IDX_ENTRIES { + if i == 0 { + assert!(!l1_node.data().ptrs[i].is_null()); + } else { + assert!(l1_node.data().ptrs[i].is_null()); + l1_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15)) + } + } + + l2_node.data_mut().ptrs[0].ptr = unsafe { tx.write_block(l1_node) }.unwrap(); + let l2_record_ptr = unsafe { tx.write_block(l2_node) }.unwrap(); + level_data_mut(&mut parent)?.level0[1] = unsafe { l2_record_ptr.cast() }; + tx.sync_tree(parent).unwrap(); + + Ok(()) + }) + .unwrap(); + + // WHEN another entry is added to the full DirList + fs.tx(|tx| tx.create_node(parent_ptr, "child__0", Node::MODE_FILE, 1, 0)) + .unwrap(); + + // THEN the branch splits all the way to the root, increasing the level + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + assert!(level_data(&parent)?.level0[0].is_marker()); + assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 3); + assert!(!level_data(&parent)?.level0[1].addr().is_null()); + + let htree_ptr: BlockPtr>>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + + // Note that while a split tries to evenly divide the H-tree entries between the new two sibling nodes, + // it tries to keep hash collisions together. This unnatural test scenario has a ton of the same max + // value hash, so those get grouped together, and all our other entries are grouped with the same hash + // value of zero. + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name("__0") + ); + assert!(!htree_node.data().ptrs[1].is_null()); + assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX); + assert!(htree_node.data().ptrs[2].is_null()); + + let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap(); + let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap(); + assert_eq!( + l2_node.data().ptrs[0].htree_hash, + HTreeHash::from_name("__0") + ); + assert_eq!( + l2_node.data().ptrs[1].htree_hash, + HTreeHash::from_name("__0") + ); + assert!(l2_node.data().ptrs[2].is_null()); + + Ok(()) + }) + .unwrap(); + + // THEN all the colliding files can be listed + fs.tx(|tx| { + tx.find_node(parent_ptr, "child__0").unwrap(); + for i in 0..16 { + let name = format!("child{i:0242}__0"); + let result = tx.find_node(parent_ptr, name.as_str()); + assert!(result.is_ok(), "Could not read {name}"); + } + Ok(()) + }) + .unwrap(); + + // AND the first of the split DirLists has empty space while the second is full + fs.tx(|tx| { + let parent = tx.read_tree(parent_ptr).unwrap(); + let htree_ptr: BlockPtr>>> = + unsafe { level_data(&parent)?.level0[1].cast() }; + let htree_node = tx.read_block(htree_ptr).unwrap(); + + assert!(!htree_node.data().ptrs[0].is_null()); + assert_eq!( + htree_node.data().ptrs[0].htree_hash, + HTreeHash::from_name("__0") + ); + assert!(!htree_node.data().ptrs[1].is_null()); + assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX); + assert!(htree_node.data().ptrs[2].is_null()); + + let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap(); + let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap(); + assert_eq!( + l2_node.data().ptrs[0].htree_hash, + HTreeHash::from_name("__0") + ); + assert_eq!( + l2_node.data().ptrs[1].htree_hash, + HTreeHash::from_name("__0") + ); + assert!(l2_node.data().ptrs[2].is_null()); + + let dir1 = tx.read_block(l2_node.data().ptrs[0].ptr).unwrap(); + for (i, entry) in dir1.data().entries().enumerate() { + if i == 0 { + assert!( + !entry.node_ptr().is_null(), + "Entry {i} in dir1 should not be null" + ); + assert_eq!( + HTreeHash::from_name(entry.name().unwrap()), + HTreeHash::from_name("__0"), + "Entry {i} with name {}", + entry.name().unwrap() + ); + } else { + assert!( + entry.node_ptr().is_null(), + "Entry {i} in dir1 should be null" + ); + } + } + + let dir2 = tx.read_block(l2_node.data().ptrs[1].ptr).unwrap(); + for (i, entry) in dir2.data().entries().enumerate() { + assert!( + !entry.node_ptr().is_null(), + "Entry {i} in dir2 should not be null" + ); + assert_eq!( + HTreeHash::from_name(entry.name().unwrap()), + HTreeHash::from_name("__0"), + "Entry {i} with name {}", + entry.name().unwrap() + ); + } + Ok(()) + }) + .unwrap(); + }); +} diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000000..0abfd9961f --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,2053 @@ +use alloc::{ + boxed::Box, + collections::{BTreeMap, VecDeque}, + vec::Vec, +}; +use core::{ + cmp::min, + mem, + ops::{Deref, DerefMut}, +}; +use syscall::error::{ + Error, Result, EEXIST, EINVAL, EIO, EISDIR, ENOENT, ENOSPC, ENOTDIR, ENOTEMPTY, ERANGE, +}; + +use crate::{ + htree::{self, HTreeHash, HTreeNode, HTreePtr}, + AllocEntry, AllocList, Allocator, BlockAddr, BlockData, BlockLevel, BlockMeta, BlockPtr, + BlockTrait, DirEntry, DirList, Disk, FileSystem, Header, Node, NodeFlags, NodeLevel, + NodeLevelData, RecordRaw, ReleaseList, TreeData, TreePtr, ALLOC_GC_THRESHOLD, + ALLOC_LIST_ENTRIES, DIR_ENTRY_MAX_LENGTH, HEADER_RING, +}; + +pub(crate) fn level_data(node: &TreeData) -> Result<&NodeLevelData> { + node.data().level_data().ok_or_else(|| { + #[cfg(feature = "log")] + log::error!("LEVEL_DATA: NODE HAS INLINE DATA"); + Error::new(EIO) + }) +} + +pub(crate) fn level_data_mut(node: &mut TreeData) -> Result<&mut NodeLevelData> { + node.data_mut().level_data_mut().ok_or_else(|| { + #[cfg(feature = "log")] + log::error!("LEVEL_DATA_MUT: NODE HAS INLINE DATA"); + Error::new(EIO) + }) +} + +pub trait AllocCtx { + fn allocate(&mut self, _addr: BlockAddr) {} + fn deallocate(&mut self, _addr: BlockAddr) {} +} + +pub struct FsCtx; +impl AllocCtx for FsCtx {} + +impl AllocCtx for TreeData { + fn allocate(&mut self, addr: BlockAddr) { + let blocks = self.data().blocks(); + self.data_mut().set_blocks( + blocks + .checked_add(addr.level().blocks::()) + .expect("node block count overflow"), + ); + } + + fn deallocate(&mut self, addr: BlockAddr) { + let blocks = self.data().blocks(); + self.data_mut().set_blocks( + blocks + .checked_sub(addr.level().blocks::()) + .expect("node block count underflow"), + ); + } +} + +pub struct Transaction<'a, D: Disk> { + fs: &'a mut FileSystem, + //TODO: make private + pub header: Header, + //TODO: make private + pub header_changed: bool, + pub(crate) allocator: Allocator, + allocator_log: VecDeque, + deallocate: Vec, + pub(crate) write_cache: BTreeMap>, +} + +impl<'a, D: Disk> Transaction<'a, D> { + pub(crate) fn new(fs: &'a mut FileSystem) -> Self { + let header = fs.header; + let allocator = fs.allocator.clone(); + Self { + fs, + header, + header_changed: false, + allocator, + allocator_log: VecDeque::new(), + deallocate: Vec::new(), + write_cache: BTreeMap::new(), + } + } + + pub fn commit(mut self, squash: bool) -> Result<()> { + self.sync(squash)?; + self.fs.header = self.header; + self.fs.allocator = self.allocator; + Ok(()) + } + + // + // MARK: block operations + // + + /// Allocate a new block of size defined by `meta`, returning its address. + /// - returns `Err(ENOSPC)` if a block of this size could not be alloated. + /// - unsafe because order must be done carefully and changes must be flushed to disk + pub(crate) unsafe fn allocate( + &mut self, + ctx: &mut dyn AllocCtx, + meta: BlockMeta, + ) -> Result { + match self.allocator.allocate(meta) { + Some(addr) => { + self.allocator_log.push_back(AllocEntry::allocate(addr)); + ctx.allocate(addr); + Ok(addr) + } + None => Err(Error::new(ENOSPC)), + } + } + + /// Deallocate the given block. + /// - unsafe because order must be done carefully and changes must be flushed to disk + pub(crate) unsafe fn deallocate(&mut self, ctx: &mut dyn AllocCtx, addr: BlockAddr) { + //TODO: should we use some sort of not-null abstraction? + assert!(!addr.is_null()); + + // Remove from write_cache if it is there, since it no longer needs to be written + //TODO: for larger blocks do we need to check for sub-blocks in here? + self.write_cache.remove(&addr); + + // Search and remove the last matching entry in allocator_log + let mut found = false; + for i in (0..self.allocator_log.len()).rev() { + let entry = self.allocator_log[i]; + if entry.index() == addr.index() && entry.count() == -addr.level().blocks::() { + found = true; + self.allocator_log.remove(i); + break; + } + } + + if found { + // Deallocate immediately since it is an allocation that was not needed + self.allocator.deallocate(addr); + } else { + // Deallocate later when syncing filesystem, to avoid re-use + self.deallocate.push(addr); + } + ctx.deallocate(addr); + } + + unsafe fn deallocate_block( + &mut self, + ctx: &mut dyn AllocCtx, + ptr: BlockPtr, + ) -> bool { + if !ptr.is_null() { + self.deallocate(ctx, ptr.addr()); + true + } else { + false + } + } + + /// Drain `self.allocator_log` and `self.deallocate`, + /// updating the [`AllocList`] with the resulting state. + /// + /// This method does not write anything to disk, + /// all writes are cached. + /// + /// To keep the allocator log from growing excessively, it will + /// periodically be fully rebuilt using the state of `self.allocator`. + /// This rebuild can be forced by setting `force_squash` to `true`. + fn sync_allocator(&mut self, force_squash: bool) -> Result { + let mut prev_ptr = BlockPtr::default(); + let should_gc = self.header.generation() % ALLOC_GC_THRESHOLD == 0 + && self.header.generation() >= ALLOC_GC_THRESHOLD + && self.allocator.free() > 0; + if force_squash || should_gc { + // Clear and rebuild alloc log + self.allocator_log.clear(); + let levels = self.allocator.levels(); + for level in (0..levels.len()).rev() { + let count = (1 << level) as i64; + 'indexs: for &index in levels[level].iter() { + for entry in self.allocator_log.iter_mut() { + if index + count as u64 == entry.index() { + // New entry is at start of existing entry + *entry = AllocEntry::new(index, count + entry.count()); + continue 'indexs; + } else if entry.index() + entry.count() as u64 == index { + // New entry is at end of existing entry + *entry = AllocEntry::new(entry.index(), entry.count() + count); + continue 'indexs; + } + } + + self.allocator_log.push_back(AllocEntry::new(index, count)); + } + } + + // Prepare to deallocate old alloc blocks + let mut alloc_ptr = self.header.alloc; + while !alloc_ptr.is_null() { + let alloc = self.read_block(alloc_ptr)?; + self.deallocate.push(alloc.addr()); + alloc_ptr = alloc.data().prev; + } + } else { + // Return if there are no log changes + if self.allocator_log.is_empty() && self.deallocate.is_empty() { + return Ok(false); + } + + // Push old alloc block to front of allocator log + //TODO: just skip this if it is already full? + let alloc = self.read_block(self.header.alloc)?; + for i in (0..alloc.data().entries.len()).rev() { + let entry = alloc.data().entries[i]; + if !entry.is_null() { + self.allocator_log.push_front(entry); + } + } + + // Prepare to deallocate old alloc block + self.deallocate.push(alloc.addr()); + + // Link to previous alloc block + prev_ptr = alloc.data().prev; + } + + // Allocate required blocks, including CoW of current alloc tail + let mut new_blocks = Vec::new(); + while new_blocks.len() * ALLOC_LIST_ENTRIES + <= self.allocator_log.len() + self.deallocate.len() + { + new_blocks.push(unsafe { self.allocate(&mut FsCtx, BlockMeta::default())? }); + } + + // De-allocate old blocks (after allocation to prevent re-use) + //TODO: optimize allocator log in memory + while let Some(addr) = self.deallocate.pop() { + self.allocator.deallocate(addr); + self.allocator_log.push_back(AllocEntry::deallocate(addr)); + } + + for new_block in new_blocks { + let mut alloc = BlockData::::empty(new_block).unwrap(); + alloc.data_mut().prev = prev_ptr; + for entry in alloc.data_mut().entries.iter_mut() { + if let Some(log_entry) = self.allocator_log.pop_front() { + *entry = log_entry; + } else { + break; + } + } + prev_ptr = unsafe { self.write_block(alloc)? }; + } + + self.header.alloc = prev_ptr; + self.header_changed = true; + + Ok(true) + } + + /// Write all changes cached in this [`Transaction`] to disk. + pub fn sync(&mut self, force_squash: bool) -> Result { + // Make sure alloc is synced + self.sync_allocator(force_squash)?; + + // Write all items in write cache + for (addr, raw) in self.write_cache.iter_mut() { + // sync_alloc must have changed alloc block pointer + // if we have any blocks to write + assert!(self.header_changed); + + self.fs.encrypt(raw, *addr); + let count = unsafe { self.fs.disk.write_at(self.fs.block + addr.index(), raw)? }; + if count != raw.len() { + // Read wrong number of bytes + #[cfg(feature = "log")] + log::error!("SYNC WRITE_CACHE: WRONG NUMBER OF BYTES"); + return Err(Error::new(EIO)); + } + } + self.write_cache.clear(); + + // Do nothing if there are no changes to write. + // + // This only happens if `self.write_cache` was empty, + // and the fs header wasn't changed by another operation. + if !self.header_changed { + return Ok(false); + } + + // Update header to next generation + let gen = self.header.update(self.fs.cipher_opt.as_ref()); + let gen_block = gen % HEADER_RING; + + // Write header + let count = unsafe { + self.fs + .disk + .write_at(self.fs.block + gen_block, &self.header)? + }; + if count != mem::size_of_val(&self.header) { + // Read wrong number of bytes + #[cfg(feature = "log")] + log::error!("SYNC: WRONG NUMBER OF BYTES"); + return Err(Error::new(EIO)); + } + + self.header_changed = false; + Ok(true) + } + + pub fn read_block>( + &mut self, + ptr: BlockPtr, + ) -> Result> { + if ptr.is_null() { + // Pointer is invalid (should this return None?) + #[cfg(feature = "log")] + log::error!("READ_BLOCK: POINTER IS NULL"); + return Err(Error::new(ENOENT)); + } + + let mut data = match T::empty(ptr.addr().level()) { + Some(some) => some, + None => { + #[cfg(feature = "log")] + log::error!("READ_BLOCK: INVALID BLOCK LEVEL FOR TYPE"); + return Err(Error::new(ENOENT)); + } + }; + if let Some(raw) = self.write_cache.get(&ptr.addr()) { + data.copy_from_slice(raw); + } else { + let count = unsafe { + self.fs + .disk + .read_at(self.fs.block + ptr.addr().index(), &mut data)? + }; + if count != data.len() { + // Read wrong number of bytes + #[cfg(feature = "log")] + log::error!("READ_BLOCK: WRONG NUMBER OF BYTES"); + return Err(Error::new(EIO)); + } + self.fs.decrypt(&mut data, ptr.addr()); + } + + let block = BlockData::new(ptr.addr(), data); + let block_ptr = block.create_ptr(); + if block_ptr.hash() != ptr.hash() { + // Incorrect hash + #[cfg(feature = "log")] + log::error!( + "READ_BLOCK: INCORRECT HASH 0x{:X} != 0x{:X} for block 0x{:X}", + block_ptr.hash(), + ptr.hash(), + ptr.addr().index() + ); + return Err(Error::new(EIO)); + } + Ok(block) + } + + /// Read block data or, if pointer is null, return default block data + /// + /// # Safety + /// Unsafe because it creates strange BlockData types that must be swapped before use + unsafe fn read_block_or_empty>( + &mut self, + ptr: BlockPtr, + ) -> Result> { + if ptr.is_null() { + let addr = ptr.addr(); + match T::empty(addr.level()) { + Some(empty) => Ok(BlockData::new(addr, empty)), + None => { + #[cfg(feature = "log")] + log::error!("READ_BLOCK_OR_EMPTY: INVALID BLOCK LEVEL FOR TYPE"); + Err(Error::new(ENOENT)) + } + } + } else { + self.read_block(ptr) + } + } + + unsafe fn read_record>( + &mut self, + mut ptr: BlockPtr, + level: BlockLevel, + ) -> Result> { + // Set null pointers to correct size (reduces number of copies below) + if ptr.is_null() { + ptr = BlockPtr::::null(BlockMeta::new(level)); + } + + // Read record from disk, or construct empty one for null pointers + let mut record = unsafe { self.read_block_or_empty(ptr)? }; + + // Attempt to decompress if address metadata indicates compression + if let Some(decomp_level) = record.addr().decomp_level() { + // First 2 bytes store compressed data length + // This means only compressed record sizes up to 64 KiB are supported + let mut decomp = match T::empty(decomp_level) { + Some(empty) => empty, + None => { + #[cfg(feature = "log")] + log::error!("READ_RECORD: INVALID DECOMPRESSED BLOCK LEVEL FOR TYPE"); + return Err(Error::new(ENOENT)); + } + }; + let comp_len = record.data()[0] as usize | ((record.data()[1] as usize) << 8); + let total_len = comp_len + 2; + if let Err(err) = lz4_flex::decompress_into(&record.data()[2..total_len], &mut decomp) { + #[cfg(feature = "log")] + log::error!("READ_RECORD: FAILED TO DECOMPRESS: {:?}", err); + return Err(Error::new(EIO)); + } + record = BlockData::new(BlockAddr::null(BlockMeta::new(decomp_level)), decomp); + } + + // Return record if it is larger than or equal to requested level + if record.addr().level() >= level { + return Ok(record); + } + + // If a larger level was requested, + // create a fake record with the requested level + // and fill it with the data in the original record. + let (_old_addr, old_raw) = unsafe { record.into_parts() }; + let mut raw = match T::empty(level) { + Some(empty) => empty, + None => { + #[cfg(feature = "log")] + log::error!("READ_RECORD: INVALID BLOCK LEVEL FOR TYPE"); + return Err(Error::new(ENOENT)); + } + }; + let len = min(raw.len(), old_raw.len()); + raw[..len].copy_from_slice(&old_raw[..len]); + + Ok(BlockData::new(BlockAddr::null(BlockMeta::new(level)), raw)) + } + + /// Write block data to a new address, returning new address + pub fn sync_block>( + &mut self, + ctx: &mut dyn AllocCtx, + mut block: BlockData, + ) -> Result> { + // Swap block to new address + let meta = block.addr().meta(); + let old_addr = block.swap_addr(unsafe { self.allocate(ctx, meta)? }); + // Deallocate old address (will only take effect after sync_allocator, which helps to + // prevent re-use before a new header is written + if !old_addr.is_null() { + unsafe { + self.deallocate(ctx, old_addr); + } + } + // Write new block + unsafe { self.write_block(block) } + } + + /// Write block data, returning a calculated block pointer + /// + /// # Safety + /// Unsafe to encourage CoW semantics + pub(crate) unsafe fn write_block>( + &mut self, + block: BlockData, + ) -> Result> { + if block.addr().is_null() { + // Pointer is invalid + #[cfg(feature = "log")] + log::error!("WRITE_BLOCK: POINTER IS NULL"); + return Err(Error::new(ENOENT)); + } + + //TODO: do not convert to boxed slice if it already is one + self.write_cache.insert( + block.addr(), + block.data().deref().to_vec().into_boxed_slice(), + ); + + Ok(block.create_ptr()) + } + + // + // MARK: tree operations + // + + /// Walk the tree and return the contents and address + /// of the data block that `ptr` points too. + fn read_tree_and_addr>( + &mut self, + ptr: TreePtr, + ) -> Result<(TreeData, BlockAddr)> { + if ptr.is_null() { + // ID is invalid (should this return None?) + #[cfg(feature = "log")] + log::error!("READ_TREE: ID IS NULL"); + return Err(Error::new(ENOENT)); + } + + let (i3, i2, i1, i0) = ptr.indexes(); + let l3 = self.read_block(self.header.tree)?; + let l2 = self.read_block(l3.data().ptrs[i3])?; + let l1 = self.read_block(l2.data().ptrs[i2])?; + let l0 = self.read_block(l1.data().ptrs[i1])?; + let raw = self.read_block(l0.data().ptrs[i0])?; + + //TODO: transmute instead of copy? + let mut data = match T::empty(BlockLevel::default()) { + Some(some) => some, + None => { + #[cfg(feature = "log")] + log::error!("READ_TREE: INVALID BLOCK LEVEL FOR TYPE"); + return Err(Error::new(ENOENT)); + } + }; + data.copy_from_slice(raw.data()); + + Ok((TreeData::new(ptr.id(), data), raw.addr())) + } + + /// Walk the tree and return the contents of the data block that `ptr` points too. + pub fn read_tree>( + &mut self, + ptr: TreePtr, + ) -> Result> { + Ok(self.read_tree_and_addr(ptr)?.0) + } + + /// Insert `block_ptr` into the first free slot in the tree, + /// returning a pointer to that slot. + pub fn insert_tree>( + &mut self, + block_ptr: BlockPtr, + ) -> Result> { + // Remember that if there is a free block at any level it will always sync when it + // allocates at the lowest level, so we can save a write by not writing each level as it + // is allocated. + unsafe { + let mut l3 = self.read_block(self.header.tree)?; + for i3 in 0..l3.data().ptrs.len() { + if l3.data().branch_is_full(i3) { + continue; + } + let mut l2 = self.read_block_or_empty(l3.data().ptrs[i3])?; + for i2 in 0..l2.data().ptrs.len() { + if l2.data().branch_is_full(i2) { + continue; + } + let mut l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + for i1 in 0..l1.data().ptrs.len() { + if l1.data().branch_is_full(i1) { + continue; + } + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + for i0 in 0..l0.data().ptrs.len() { + if l0.data().branch_is_full(i0) { + continue; + } + + let pn = l0.data().ptrs[i0]; + assert!(pn.is_null()); + + let tree_ptr = TreePtr::from_indexes((i3, i2, i1, i0)); + + // Skip if this is a reserved node (null) + if tree_ptr.is_null() { + l0.data_mut().set_branch_full(i0, true); + continue; + } + + // Write updates to newly allocated blocks + l0.data_mut().set_branch_full(i0, true); + l0.data_mut().ptrs[i0] = block_ptr.cast(); + l1.data_mut() + .set_branch_full(i1, l0.data().tree_list_is_full()); + l1.data_mut().ptrs[i1] = self.sync_block(&mut FsCtx, l0)?; + l2.data_mut() + .set_branch_full(i2, l1.data().tree_list_is_full()); + l2.data_mut().ptrs[i2] = self.sync_block(&mut FsCtx, l1)?; + l3.data_mut() + .set_branch_full(i3, l2.data().tree_list_is_full()); + l3.data_mut().ptrs[i3] = self.sync_block(&mut FsCtx, l2)?; + self.header.tree = self.sync_block(&mut FsCtx, l3)?; + self.header_changed = true; + + return Ok(tree_ptr); + } + } + } + } + } + + Err(Error::new(ENOSPC)) + } + + /// Clear the previously claimed slot in the tree for the given `ptr`. Note that this + /// should only be called after the corresponding node block has already been deallocated. + fn remove_tree>( + &mut self, + ptr: TreePtr, + ) -> Result<()> { + if ptr.is_null() { + // ID is invalid (should this return None?) + #[cfg(feature = "log")] + log::error!("READ_TREE: ID IS NULL"); + return Err(Error::new(ENOENT)); + } + + let (i3, i2, i1, i0) = ptr.indexes(); + let mut l3 = self.read_block(self.header.tree)?; + let mut l2 = self.read_block(l3.data().ptrs[i3])?; + let mut l1 = self.read_block(l2.data().ptrs[i2])?; + let mut l0 = self.read_block(l1.data().ptrs[i1])?; + + // Clear the value in the tree, but do not deallocate the node block, as that should already + // have been done at the node level. The inner tree nodes can be deallocated if they are empty. + l0.data_mut().set_branch_full(i0, false); + l0.data_mut().ptrs[i0] = BlockPtr::default(); + let l0_ptr = if l0.data().tree_list_is_empty() { + unsafe { self.deallocate(&mut FsCtx, l0.addr()) }; + BlockPtr::default() + } else { + self.sync_block(&mut FsCtx, l0)? + }; + + l1.data_mut().set_branch_full(i1, false); + l1.data_mut().ptrs[i1] = l0_ptr; + let l1_ptr = if l1.data().tree_list_is_empty() { + unsafe { self.deallocate(&mut FsCtx, l1.addr()) }; + BlockPtr::default() + } else { + self.sync_block(&mut FsCtx, l1)? + }; + + l2.data_mut().set_branch_full(i2, false); + l2.data_mut().ptrs[i2] = l1_ptr; + let l2_ptr = if l2.data().tree_list_is_empty() { + unsafe { self.deallocate(&mut FsCtx, l2.addr()) }; + BlockPtr::default() + } else { + self.sync_block(&mut FsCtx, l2)? + }; + + l3.data_mut().set_branch_full(i3, false); + l3.data_mut().ptrs[i3] = l2_ptr; + let l3_ptr = if l3.data().tree_list_is_empty() { + unsafe { self.deallocate(&mut FsCtx, l3.addr()) }; + BlockPtr::default() + } else { + self.sync_block(&mut FsCtx, l3)? + }; + + self.header.tree = l3_ptr; + self.header_changed = true; + Ok(()) + } + + pub fn sync_trees>(&mut self, nodes: &[TreeData]) -> Result<()> { + for node in nodes.iter().rev() { + let ptr = node.ptr(); + if ptr.is_null() { + // ID is invalid + #[cfg(feature = "log")] + log::error!("SYNC_TREE: ID IS NULL"); + return Err(Error::new(ENOENT)); + } + } + + for node in nodes.iter().rev() { + let (i3, i2, i1, i0) = node.ptr().indexes(); + let mut l3 = self.read_block(self.header.tree)?; + let mut l2 = self.read_block(l3.data().ptrs[i3])?; + let mut l1 = self.read_block(l2.data().ptrs[i2])?; + let mut l0 = self.read_block(l1.data().ptrs[i1])?; + let mut raw = self.read_block(l0.data().ptrs[i0])?; + + // Return if data is equal + if raw.data().deref() == node.data().deref() { + continue; + } + + //TODO: transmute instead of copy? + raw.data_mut().copy_from_slice(node.data()); + + // Write updates to newly allocated blocks + l0.data_mut().ptrs[i0] = self.sync_block(&mut FsCtx, raw)?; + l1.data_mut().ptrs[i1] = self.sync_block(&mut FsCtx, l0)?; + l2.data_mut().ptrs[i2] = self.sync_block(&mut FsCtx, l1)?; + l3.data_mut().ptrs[i3] = self.sync_block(&mut FsCtx, l2)?; + self.header.tree = self.sync_block(&mut FsCtx, l3)?; + self.header_changed = true; + } + + Ok(()) + } + + pub fn sync_tree>(&mut self, node: TreeData) -> Result<()> { + self.sync_trees(&[node]) + } + + // + // MARK: node operations + // + + /// Write all children of `parent_ptr` to `children`. + /// `parent_ptr` must point to a directory node. + pub fn child_nodes( + &mut self, + parent_ptr: TreePtr, + children: &mut Vec, + ) -> Result<()> { + let parent = self.read_tree(parent_ptr)?; + if level_data(&parent)?.level0[0].is_marker() { + let htree_levels = level_data(&parent)?.level0[0].addr().level().0; + let htree_root = if htree_levels == 0 { + // Create a fake root to satisfy the recursive child_nodes_inner function signature + let mut fake_htree_node = + BlockData::>::empty(BlockAddr::default()).unwrap(); + let dir_ptr = level_data(&parent)?.level0[1]; + let htree_ptr = HTreePtr::new(HTreeHash::MAX, dir_ptr); + fake_htree_node.data_mut().ptrs[0] = htree_ptr; + fake_htree_node + } else { + let htree_record_ptr = level_data(&parent)?.level0[1]; + let htree_ptr: BlockPtr> = unsafe { htree_record_ptr.cast() }; + self.read_block(htree_ptr)? + }; + self.child_nodes_inner(htree_root.data(), children, htree_levels.max(1))?; + } + Ok(()) + } + + fn child_nodes_inner( + &mut self, + htree_node: &HTreeNode, + children: &mut Vec, + htree_levels: usize, + ) -> Result<()> { + assert!(htree_levels > 0); + if htree_levels == 1 { + for entry in htree_node.ptrs.iter().filter(|entry| !entry.is_null()) { + let dir_ptr: BlockPtr = unsafe { entry.ptr.cast() }; + let dir = self.read_block(dir_ptr)?; + for entry in dir.data().entries() { + children.push(entry); + } + } + } else { + for entry in htree_node.ptrs.iter().filter(|entry| !entry.is_null()) { + let htree_ptr: BlockPtr> = unsafe { entry.ptr.cast() }; + let htree_node = self.read_block(htree_ptr)?; + self.child_nodes_inner(htree_node.data(), children, htree_levels - 1)?; + } + } + + Ok(()) + } + + /// Find a node that is a child of the `parent_ptr` and is named `name`. + /// Returns ENOENT if this node is not found. + pub fn find_node(&mut self, parent_ptr: TreePtr, name: &str) -> Result> { + let parent = self.read_tree(parent_ptr)?; + if !level_data(&parent)?.level0[0].is_marker() { + return Err(Error::new(ENOENT)); + } + + let htree_levels = level_data(&parent)?.level0[0].addr().level().0; + + let root_htree_node = if htree_levels == 0 { + // Create a fake root to satisfy the recursive inner_find_node function signature + let mut fake_htree_node = + BlockData::>::empty(BlockAddr::default()).unwrap(); + let dir_ptr = level_data(&parent)?.level0[1]; + let htree_ptr = HTreePtr::new(HTreeHash::MAX, dir_ptr); + fake_htree_node.data_mut().ptrs[0] = htree_ptr; + fake_htree_node + } else { + let root_htree_ptr: BlockPtr> = + unsafe { level_data(&parent)?.level0[1].cast() }; + self.read_block(root_htree_ptr)? + }; + + let result = self.find_node_inner( + root_htree_node.data(), + name, + HTreeHash::from_name(name), + htree_levels.max(1), + )?; + result + .map(|(tree_node, _address)| tree_node) + .ok_or(Error::new(ENOENT)) + } + + fn find_node_inner( + &mut self, + parent_htree_node: &HTreeNode, + name: &str, + name_hash: HTreeHash, + htree_levels: usize, + ) -> Result, BlockAddr)>> { + assert!(htree_levels > 0); + if htree_levels == 1 { + // If we are at the leaf level, search for the name + for (_, htree_ptr) in parent_htree_node.find_ptrs_for_read(name_hash) { + let dir_ptr: BlockPtr = unsafe { htree_ptr.ptr.cast() }; + let dir = self.read_block(dir_ptr)?; + + if let Some(entry) = dir.data().find_entry(name) { + let node_ptr = entry.node_ptr(); + return Ok(Some(self.read_tree_and_addr(node_ptr)?)); + } + } + #[cfg(feature = "log")] + log::trace!("FIND_NODE: Node not found in leaf level 1"); + return Ok(None); + } + + // Otherwise, search the next level of the H-tree + for (_, entry) in parent_htree_node.find_ptrs_for_read(name_hash) { + let htree_ptr: BlockPtr> = unsafe { entry.ptr.cast() }; + let htree_node = self.read_block(htree_ptr)?; + let result = + self.find_node_inner(htree_node.data(), name, name_hash, htree_levels - 1)?; + if let Some(node) = result { + return Ok(Some(node)); + } + } + + #[cfg(feature = "log")] + log::trace!( + "FIND_NODE: Node not found in higher level: {}", + htree_levels + ); + Ok(None) + } + + /// Create a new node in the tree with the given parameters. + pub fn create_node( + &mut self, + parent_ptr: TreePtr, + name: &str, + mode: u16, + ctime: u64, + ctime_nsec: u32, + ) -> Result> { + self.check_name(&parent_ptr, name)?; + + unsafe { + let parent = self.read_tree(parent_ptr)?; + let node_block_data = BlockData::new( + self.allocate(&mut FsCtx, BlockMeta::default())?, + Node::new( + mode, + parent.data().uid(), + parent.data().gid(), + ctime, + ctime_nsec, + ), + ); + let node_block_ptr = self.write_block(node_block_data)?; + let node_ptr = self.insert_tree(node_block_ptr)?; + + self.link_node(parent_ptr, name, node_ptr)?; + + //TODO: do not re-read node + self.read_tree(node_ptr) + } + } + + pub fn link_node( + &mut self, + parent_ptr: TreePtr, + name: &str, + node_ptr: TreePtr, + ) -> Result<()> { + let mut parent = self.read_tree(parent_ptr)?; + let mut node = self.read_tree(node_ptr)?; + + // Increment node reference counter + let links = node.data().links(); + node.data_mut().set_links(links + 1); + + let dir_entry = DirEntry::new(node_ptr, name); + let dir_entry_htree_hash = HTreeHash::from_name(name); + let record_byte_size = parent.data().record_level().bytes(); + + // If this is a brand new directory, create the first DirList block + if !level_data(&parent)?.level0[0].is_marker() { + let marker: BlockPtr = BlockPtr::marker(0); + assert!(marker.is_marker()); + + level_data_mut(&mut parent)?.level0[0] = BlockPtr::marker(0); + assert!(level_data(&parent)?.level0[0].is_marker()); + + // Create the first DirList block + let dir = BlockData::::empty(BlockAddr::default()).unwrap(); + let dir_ptr = self.sync_block(&mut parent, dir)?; + + // Add the DirList directly to the parent directory + level_data_mut(&mut parent)?.level0[1] = unsafe { dir_ptr.cast() }; + let size = parent.data().size() + record_byte_size; + parent.data_mut().set_size(size); + } + + let mut htree_levels = level_data(&parent)?.level0[0].addr().level().0; + + let mut htree_root = if htree_levels == 0 { + // If we have no H-tree root, create a fake one to satisfy the recurisve inner_link_node function + let mut fake_htree_node = + BlockData::>::empty(BlockAddr::default()).unwrap(); + let dir_ptr = level_data(&parent)?.level0[1]; + let htree_ptr = HTreePtr::new(HTreeHash::MAX, dir_ptr); + fake_htree_node.data_mut().ptrs[0] = htree_ptr; + fake_htree_node + } else { + // Otherwise get the real H-tree root + let htree_root_ptr: BlockPtr> = + unsafe { level_data(&parent)?.level0[1].cast() }; + self.read_block(htree_root_ptr)? + }; + + let new_sibling = self.link_node_inner( + &mut parent, + htree_root.data_mut(), + dir_entry, + dir_entry_htree_hash, + htree_levels.max(1), + )?; + + // If we used a fake root, and we grew beyond a single DirList block, we need to create a real root + if htree_levels == 0 && !htree_root.data().ptrs[1].is_null() { + htree_levels = 1; + level_data_mut(&mut parent)?.level0[0] = BlockPtr::marker(1); + let size = parent.data().size() + record_byte_size; + parent.data_mut().set_size(size); + } + + // If the H-tree root was split, create a new root to hold the old root as a sibling along with the new sibling + if let Some((sibling_htree_hash, unallocated_sibling)) = new_sibling { + assert!(htree_levels > 0); + + // Prep the new sibling H-tree block to be added to the new root + let mut sibling = + BlockData::>::empty(BlockAddr::default()).unwrap(); + let _ = mem::replace(sibling.data_mut(), unallocated_sibling); + let sibling_block_ptr = self.sync_block(&mut parent, sibling)?; + let sibling_htree_ptr = HTreePtr::new(sibling_htree_hash, sibling_block_ptr); + let sibling_record_ptr: HTreePtr = unsafe { sibling_htree_ptr.cast() }; + + // Prep the existing H-tree root to become a sibling + let root_htree_hash = htree_root + .data() + .find_max_htree_hash() + .ok_or(Error::new(EIO))?; + let root_block_ptr = self.sync_block(&mut parent, htree_root)?; + let root_htree_ptr = HTreePtr::new(root_htree_hash, root_block_ptr); + let root_record_ptr: HTreePtr = unsafe { root_htree_ptr.cast() }; + + // Create the new root H-tree block + let mut new_root = + BlockData::>::empty(BlockAddr::default()).unwrap(); + new_root.data_mut().ptrs[0] = sibling_record_ptr; + let unexpected_sibling = htree::add_inner_node(new_root.data_mut(), root_record_ptr)?; + assert!(unexpected_sibling.is_none()); + let new_root_ptr = self.sync_block(&mut parent, new_root)?; + + // Add the parent node pointer, increase the level, and increase one block size per allocated block + level_data_mut(&mut parent)?.level0[0] = BlockPtr::marker(htree_levels as u8 + 1); + level_data_mut(&mut parent)?.level0[1] = unsafe { new_root_ptr.cast() }; + let size = parent.data().size() + 2 * record_byte_size; + parent.data_mut().set_size(size); + } else if htree_levels > 0 { + // Update the parent node with the new root pointer + let root_block_ptr = self.sync_block(&mut parent, htree_root)?; + level_data_mut(&mut parent)?.level0[1] = unsafe { root_block_ptr.cast() }; + } else { + // Update the parent with the DirList block, ignoring the fake htree_root + level_data_mut(&mut parent)?.level0[1] = htree_root.data().ptrs[0].ptr; + } + self.sync_trees(&[parent, node])?; + Ok(()) + } + + fn link_node_inner( + &mut self, + parent_dir_node: &mut TreeData, + parent_htree_node: &mut HTreeNode, + dir_entry: DirEntry, + dir_entry_htree_hash: HTreeHash, + htree_levels: usize, + ) -> Result)>> { + let record_byte_size = parent_dir_node.data().record_level().bytes(); + + // Find the entry to update + let mut htree_ptr = parent_htree_node.ptrs[0]; + let mut htree_ptr_idx = 0; + for (idx, entry) in parent_htree_node.ptrs.iter().enumerate() { + if entry.is_null() { + break; + } + htree_ptr = *entry; + htree_ptr_idx = idx; + if htree_ptr.htree_hash >= dir_entry_htree_hash { + break; + } + } + + // The recursion terminates by processing the last inner node + assert!(htree_levels > 0); + if htree_levels == 1 { + // Add the entry to the DirList block + let dir_ptr: BlockPtr = unsafe { htree_ptr.ptr.cast() }; + let mut dir = self.read_block(dir_ptr)?; + let unallocated_sibling = + htree::add_dir_entry(dir.data_mut(), &mut htree_ptr.htree_hash, dir_entry)?; + let dir_record_ptr = unsafe { self.sync_block(parent_dir_node, dir)?.cast() }; + parent_htree_node.ptrs[htree_ptr_idx] = + HTreePtr::new(htree_ptr.htree_hash, dir_record_ptr); + + if let Some((new_hash, new_unallocated_dir)) = unallocated_sibling { + // The DirList block was split, so we need to add it to the h-tree + let mut dir = BlockData::::empty(BlockAddr::default()).unwrap(); + let _ = mem::replace(dir.data_mut(), new_unallocated_dir); + let dir_ptr = self.sync_block(parent_dir_node, dir)?; + let dir_htree_ptr = HTreePtr::new(new_hash, dir_ptr); + let dir_record_ptr: HTreePtr = unsafe { dir_htree_ptr.cast() }; + let size = parent_dir_node.data().size() + record_byte_size; + parent_dir_node.data_mut().set_size(size); + + // We mutate the parent, but let the caller write the parent to disk + return htree::add_inner_node(parent_htree_node, dir_record_ptr); + } + return Ok(None); + } + + // Recursively insert the entry into the next H-tree level + let htree_block_ptr: BlockPtr> = unsafe { htree_ptr.ptr.cast() }; + let mut htree_block = self.read_block(htree_block_ptr)?; + let unallocated_sibling = self.link_node_inner( + parent_dir_node, + htree_block.data_mut(), + dir_entry, + dir_entry_htree_hash, + htree_levels - 1, + )?; + + // Write the muteated H-tree block back to disk and update the parent node's pointer + let htree_hash = htree_block.data().find_max_htree_hash().unwrap(); + let htree_block_ptr = self.sync_block(parent_dir_node, htree_block)?; + let htree_record_ptr: BlockPtr = unsafe { htree_block_ptr.cast() }; + parent_htree_node.ptrs[htree_ptr_idx] = HTreePtr::new(htree_hash, htree_record_ptr); + + // If the inner insert function returns a new H-tree sibling block, write it and add it to the parent H-tree node + if let Some((new_hash, new_unallocated_sibling)) = unallocated_sibling { + let mut sibling = + BlockData::>::empty(BlockAddr::default()).unwrap(); + let _ = mem::replace(sibling.data_mut(), new_unallocated_sibling); + let sibling_ptr = self.sync_block(parent_dir_node, sibling)?; + let sibling_htree_ptr = HTreePtr::new(new_hash, sibling_ptr); + let sibling_record_ptr: HTreePtr = unsafe { sibling_htree_ptr.cast() }; + let size = parent_dir_node.data().size() + record_byte_size; + parent_dir_node.data_mut().set_size(size); + + // We mutate the parent, but let the caller write the parent to disk + return htree::add_inner_node(parent_htree_node, sibling_record_ptr); + } + + Ok(None) + } + + pub fn remove_node( + &mut self, + parent_ptr: TreePtr, + name: &str, + mode: u16, + ) -> Result> { + #[cfg(feature = "log")] + log::debug!( + "REMOVE_NODE: name: {}, mode: {:x}, parent_ptr: {:?}", + name, + mode, + parent_ptr.indexes() + ); + + let mut parent = self.read_tree(parent_ptr)?; + if !level_data(&parent)?.level0[0].is_marker() { + #[cfg(feature = "log")] + log::error!("REMOVE_NODE: Parent has no htree marker set (not a directory or empty)"); + return Err(Error::new(ENOENT)); + } + + let htree_levels = level_data(&parent)?.level0[0].addr().level().0; + let name_hash = HTreeHash::from_name(name); + + let mut htree_root = if htree_levels == 0 { + // If we have no H-tree root, create a fake one to satisfy the recurisve inner_link_node function + let mut fake_htree_node = + BlockData::>::empty(BlockAddr::default()).unwrap(); + let dir_ptr = level_data(&parent)?.level0[1]; + let htree_ptr = HTreePtr::new(HTreeHash::MAX, dir_ptr); + fake_htree_node.data_mut().ptrs[0] = htree_ptr; + fake_htree_node + } else { + // Otherwise get the real H-tree root + let htree_root_record_ptr = level_data(&parent)?.level0[1]; + let htree_root_ptr: BlockPtr> = + unsafe { htree_root_record_ptr.cast() }; + self.read_block(htree_root_ptr)? + }; + + // Read node and test type against requested type + // TODO: Do this check as part of the removal tree processing, and get rid of this extra find + let (mut node, _node_addr) = self + .find_node_inner(htree_root.data(), name, name_hash, htree_levels.max(1))? + .ok_or(Error::new(ENOENT))?; + + if mode & Node::MODE_TYPE == Node::MODE_DIR { + if !node.data().is_dir() { + // Found a file instead of a directory + return Err(Error::new(ENOTDIR)); + } else if node.data().size() > 0 && node.data().links() == 1 { + // Tried to remove directory that still has entries + return Err(Error::new(ENOTEMPTY)); + } + // The directory will be removed. + } else { + if node.data().is_dir() { + // Found a directory instead of file + return Err(Error::new(EISDIR)); + } + // The non-directory entry will be removed. + } + + let links = node.data().links(); + let node_id = node.id(); + let remove_node = if links > 1 { + node.data_mut().set_links(links - 1); + false + } else { + node.data_mut().set_links(0); + true + }; + + // Recursively remove the node from the H-tree, removing empty H-tree nodes + self.remove_node_inner( + &mut parent, + htree_root.data_mut(), + name, + name_hash, + htree_levels.max(1), + )?; + + htree_root + .data_mut() + .ptrs + .sort_by(|a, b| a.htree_hash.cmp(&b.htree_hash)); + if htree_root.data().ptrs[0].is_null() { + // Dealocate the htree_root only if it was a real root node in the H-tree + if htree_levels > 0 { + unsafe { + self.deallocate(&mut parent, htree_root.addr()); + } + let record_byte_size = parent.data().record_level().bytes(); + let size = parent.data().size() - record_byte_size; + parent.data_mut().set_size(size); + } + level_data_mut(&mut parent)?.level0[0] = BlockPtr::default(); + level_data_mut(&mut parent)?.level0[1] = BlockPtr::default(); + } else if htree_levels > 0 { + // Update the real htree_root and update the ptr in the parent + let htree_root_block_ptr = self.sync_block(&mut parent, htree_root)?; + level_data_mut(&mut parent)?.level0[1] = unsafe { htree_root_block_ptr.cast() }; + } else { + // The htree_root is fake, so update the parent with the ptr to the one and only directory list + let dir_list_block_ptr = htree_root.data().ptrs[0].ptr; + level_data_mut(&mut parent)?.level0[1] = unsafe { dir_list_block_ptr.cast() }; + } + + if remove_node { + self.sync_tree(parent)?; + self.release_node(node.ptr())?; + Ok(Some(node_id)) + } else { + // Sync both parent and node at the same time + self.sync_trees(&[parent, node])?; + Ok(None) + } + } + + /// Notify of node open, for tracking node usage + pub fn on_open_node(&mut self, node_ptr: TreePtr) -> Result<()> { + let entry = self.fs.node_usages.entry(node_ptr.id()).or_insert(0); + *entry = entry.checked_add(1).ok_or_else(|| { + #[cfg(feature = "log")] + log::error!("node {} usage overflow", node_ptr.id()); + Error::new(EINVAL) + })?; + Ok(()) + } + + /// Notify of node close, for tracking node usage. Delete node if it is in + /// the release list. + /// + /// Returns `Ok(true)` if the node was deleted + pub fn on_close_node(&mut self, node_ptr: TreePtr) -> Result<()> { + // Subtract from usages and return if not zero + match self.fs.node_usages.get_mut(&node_ptr.id()) { + Some(entry) => { + *entry = entry.checked_sub(1).ok_or_else(|| { + #[cfg(feature = "log")] + log::error!("node {} usage underflow", node_ptr.id()); + Error::new(EINVAL) + })?; + if *entry > 0 { + // Node still in use + return Ok(()); + } + } + None => { + #[cfg(feature = "log")] + log::error!( + "tried to close node {} that is not already open", + node_ptr.id() + ); + return Ok(()); + } + } + + // Remove node usages entry + self.fs.node_usages.remove(&node_ptr.id()); + + // Check for node in release list and delete it + self.release_unused_nodes() + } + + /// Check for unused nodes in release list and delete them + pub fn release_unused_nodes(&mut self) -> Result<()> { + // Read current release lists (going forward through list) + let mut releases = VecDeque::>::new(); + { + let mut release_ptr = self.header.release; + while !release_ptr.is_null() { + let release = self.read_block(release_ptr)?; + release_ptr = release.data().prev; + releases.push_front(release); + } + } + + // Find unused nodes and remove them (going backwards through list) + let mut update_prev = None; + let mut release_nodes = Vec::new(); + while let Some(mut release) = releases.pop_back() { + if let Some(prev_ptr) = update_prev.take() { + release.data_mut().prev = prev_ptr; + } + + let mut changed = false; + let mut empty = true; + for entry in release.data_mut().entries.iter_mut() { + if !entry.is_null() { + let usages = self.fs.node_usages.get(&entry.id()).copied().unwrap_or(0); + if usages == 0 { + release_nodes.push(*entry); + *entry = TreePtr::default(); + changed = true; + } else { + empty = false; + } + } + } + + if empty { + // Deallocate this release list block + unsafe { + self.deallocate(&mut FsCtx, release.addr()); + } + // Skip this block in the list + update_prev = Some(release.data().prev); + } else if changed { + // Update this block + update_prev = Some(self.sync_block(&mut FsCtx, release)?); + } else { + update_prev = None; + } + } + if let Some(prev_ptr) = update_prev.take() { + self.header.release = prev_ptr; + self.header_changed = true; + } + + for node_ptr in release_nodes { + self.release_node(node_ptr)?; + } + + Ok(()) + } + + /// Removes node if usages are zero, adds it to release list if usages are + /// greater than zero. + pub fn release_node(&mut self, node_ptr: TreePtr) -> Result<()> { + let usages = self + .fs + .node_usages + .get(&node_ptr.id()) + .copied() + .unwrap_or(0); + if usages > 0 { + let mut release = unsafe { self.read_block_or_empty(self.header.release)? }; + + // Try to insert into current release block + let mut inserted = false; + for entry in release.data_mut().entries.iter_mut() { + if entry.is_null() { + *entry = node_ptr; + inserted = true; + break; + } + } + + // If not inserted, try to add another release block + if !inserted { + release = BlockData::empty(BlockAddr::null(BlockMeta::default())).unwrap(); + release.data_mut().prev = self.header.release; + release.data_mut().entries[0] = node_ptr; + } + + // Update header + self.header.release = self.sync_block(&mut FsCtx, release)?; + self.header_changed = true; + } else { + let (mut node, node_addr) = self.read_tree_and_addr(node_ptr)?; + self.truncate_node_inner(&mut node, 0)?; + self.remove_tree(node.ptr())?; + unsafe { + self.deallocate(&mut FsCtx, node_addr); + } + } + Ok(()) + } + + fn remove_node_inner( + &mut self, + parent_dir_node: &mut TreeData, + parent_htree_node: &mut HTreeNode, + dir_entry_name: &str, + dir_entry_htree_hash: HTreeHash, + htree_levels: usize, + ) -> Result<()> { + let record_byte_size = parent_dir_node.data().record_level().bytes(); + + // Process every node that could hold the entry + assert!(htree_levels > 0); + let relevant_entry_indexes: Vec = parent_htree_node + .find_ptrs_for_read(dir_entry_htree_hash) + .map(|x| x.0) + .collect(); + + for entry_idx in relevant_entry_indexes { + let entry_ptr = parent_htree_node.ptrs[entry_idx]; + if htree_levels == 1 { + let dir_ptr: BlockPtr = unsafe { entry_ptr.ptr.cast() }; + let mut dir_list = self.read_block(dir_ptr)?; + + // If we don't find the entry to remove, continue to the next relevant node + if !dir_list.data_mut().remove_entry(dir_entry_name) { + continue; + } + + // Determine if the htree_hash needs to be updated + let new_htree_hash = if dir_entry_htree_hash == HTreeHash::from_name(dir_entry_name) + { + HTreeHash::find_max(dir_list.data()) + } else { + Some(dir_entry_htree_hash) + }; + + if let Some(new_tree_hash) = new_htree_hash { + // The entry_ptr needs to be updated in the parent_htree_node + let dir_block_ptr = self.sync_block(parent_dir_node, dir_list)?; + let dir_record_ptr: BlockPtr = unsafe { dir_block_ptr.cast() }; + parent_htree_node.ptrs[entry_idx] = + HTreePtr::new(new_tree_hash, dir_record_ptr); + } else { + // The entry needs to be removed from the parent_htree_noce + parent_htree_node.ptrs[entry_idx] = HTreePtr::default(); + unsafe { self.deallocate(parent_dir_node, dir_list.addr()) }; + let size = parent_dir_node.data().size() - record_byte_size; + parent_dir_node.data_mut().set_size(size); + } + return Ok(()); + } else { + let htree_ptr: BlockPtr> = unsafe { entry_ptr.ptr.cast() }; + let mut htree_node = self.read_block(htree_ptr)?; + + let result = self.remove_node_inner( + parent_dir_node, + htree_node.data_mut(), + dir_entry_name, + dir_entry_htree_hash, + htree_levels - 1, + ); + + // If the removal attempt resulted in ENOENT, iterate to look at the next relevant node + if result.is_err() && result.err().unwrap().errno == ENOENT { + continue; + } + + // In case it is some other err + result?; + + // Sort entries, moving them to the start of the ptrs array in H-tree hash order + htree_node + .data_mut() + .ptrs + .sort_by(|a, b| a.htree_hash.cmp(&b.htree_hash)); + + if let Some(new_htree_hash) = htree_node.data().find_max_htree_hash() { + // The entry_ptr needs to be updated in the parent_htree_node + let htree_block_ptr = self.sync_block(parent_dir_node, htree_node)?; + let htree_record_ptr: BlockPtr = unsafe { htree_block_ptr.cast() }; + parent_htree_node.ptrs[entry_idx] = + HTreePtr::new(new_htree_hash, htree_record_ptr); + } else { + // The htree_node is now empty, so remove it + parent_htree_node.ptrs[entry_idx] = HTreePtr::default(); + unsafe { self.deallocate(parent_dir_node, htree_node.addr()) }; + let size = parent_dir_node.data().size() - record_byte_size; + parent_dir_node.data_mut().set_size(size); + } + return Ok(()); + } + } + Err(Error::new(ENOENT)) + } + + pub fn rename_node( + &mut self, + orig_parent_ptr: TreePtr, + orig_name: &str, + new_parent_ptr: TreePtr, + new_name: &str, + ) -> Result<()> { + let orig = self.find_node(orig_parent_ptr, orig_name)?; + + // TODO: only allow ENOENT as an error? + if let Ok(new) = self.find_node(new_parent_ptr, new_name) { + // Move to same name, return + if new.id() == orig.id() { + return Ok(()); + } + + // Remove new name + // (we renamed to a node that already exists, overwrite it.) + self.remove_node( + new_parent_ptr, + new_name, + new.data().mode() & Node::MODE_TYPE, + )?; + } + + // Link original file to new name + self.check_name(&new_parent_ptr, new_name)?; + self.link_node(new_parent_ptr, new_name, orig.ptr())?; + + // Remove original file + self.remove_node( + orig_parent_ptr, + orig_name, + orig.data().mode() & Node::MODE_TYPE, + )?; + + Ok(()) + } + + pub fn rename_node_no_replace( + &mut self, + orig_parent_ptr: TreePtr, + orig_name: &str, + new_parent_ptr: TreePtr, + new_name: &str, + ) -> Result<()> { + let orig = self.find_node(orig_parent_ptr, orig_name)?; + + // The target shouldn't exist. + if self.find_node(new_parent_ptr, new_name).is_ok() { + return Err(Error::new(EEXIST)); + } + + // The rest is the same as rename_node. + // Link original file to new name + self.check_name(&new_parent_ptr, new_name)?; + self.link_node(new_parent_ptr, new_name, orig.ptr())?; + + // Remove original file + self.remove_node( + orig_parent_ptr, + orig_name, + orig.data().mode() & Node::MODE_TYPE, + )?; + + Ok(()) + } + + fn check_name(&mut self, parent_ptr: &TreePtr, name: &str) -> Result<()> { + if name.contains(':') { + return Err(Error::new(EINVAL)); + } + + if name.len() > DIR_ENTRY_MAX_LENGTH { + return Err(Error::new(EINVAL)); + } + + // TODO: Can this be removed if link_node satisfies this check itself? + if self.find_node(*parent_ptr, name).is_ok() { + return Err(Error::new(EEXIST)); + } + + Ok(()) + } + + /// Get a pointer to a the record of `node` with the given offset. + /// (i.e, to the `n`th record of `node`.) + fn node_record_ptr( + &mut self, + node: &TreeData, + record_offset: u64, + ) -> Result> { + unsafe { + match NodeLevel::new(record_offset).ok_or(Error::new(ERANGE))? { + NodeLevel::L0(i0) => Ok(level_data(node)?.level0[i0]), + NodeLevel::L1(i1, i0) => { + let l0 = self.read_block_or_empty(level_data(node)?.level1[i1])?; + Ok(l0.data().ptrs[i0]) + } + NodeLevel::L2(i2, i1, i0) => { + let l1 = self.read_block_or_empty(level_data(node)?.level2[i2])?; + let l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + Ok(l0.data().ptrs[i0]) + } + NodeLevel::L3(i3, i2, i1, i0) => { + let l2 = self.read_block_or_empty(level_data(node)?.level3[i3])?; + let l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + let l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + Ok(l0.data().ptrs[i0]) + } + NodeLevel::L4(i4, i3, i2, i1, i0) => { + let l3 = self.read_block_or_empty(level_data(node)?.level4[i4])?; + let l2 = self.read_block_or_empty(l3.data().ptrs[i3])?; + let l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + let l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + Ok(l0.data().ptrs[i0]) + } + } + } + } + + fn remove_node_record_ptr( + &mut self, + node: &mut TreeData, + record_offset: u64, + ) -> Result<()> { + unsafe { + match NodeLevel::new(record_offset).ok_or(Error::new(ERANGE))? { + NodeLevel::L0(i0) => { + let ptr = level_data_mut(node)?.level0[i0].clear(); + self.deallocate_block(node, ptr); + } + NodeLevel::L1(i1, i0) => { + let mut l0 = self.read_block_or_empty(level_data(node)?.level1[i1])?; + self.deallocate_block(node, l0.data_mut().ptrs[i0].clear()); + if l0.data().is_empty() { + let ptr = level_data_mut(node)?.level1[i1].clear(); + self.deallocate_block(node, ptr); + } else { + level_data_mut(node)?.level1[i1] = self.sync_block(node, l0)?; + } + } + NodeLevel::L2(i2, i1, i0) => { + let mut l1 = self.read_block_or_empty(level_data(node)?.level2[i2])?; + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + self.deallocate_block(node, l0.data_mut().ptrs[i0].clear()); + if l0.data().is_empty() { + self.deallocate_block(node, l1.data_mut().ptrs[i1].clear()); + } else { + l1.data_mut().ptrs[i1] = self.sync_block(node, l0)?; + } + if l1.data().is_empty() { + let ptr = level_data_mut(node)?.level2[i2].clear(); + self.deallocate_block(node, ptr); + } else { + level_data_mut(node)?.level2[i2] = self.sync_block(node, l1)?; + } + } + NodeLevel::L3(i3, i2, i1, i0) => { + let mut l2 = self.read_block_or_empty(level_data(node)?.level3[i3])?; + let mut l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + self.deallocate_block(node, l0.data_mut().ptrs[i0].clear()); + if l0.data().is_empty() { + self.deallocate_block(node, l1.data_mut().ptrs[i1].clear()); + } else { + l1.data_mut().ptrs[i1] = self.sync_block(node, l0)?; + } + if l1.data().is_empty() { + self.deallocate_block(node, l2.data_mut().ptrs[i2].clear()); + } else { + l2.data_mut().ptrs[i2] = self.sync_block(node, l1)?; + } + if l2.data().is_empty() { + let ptr = level_data_mut(node)?.level3[i3].clear(); + self.deallocate_block(node, ptr); + } else { + level_data_mut(node)?.level3[i3] = self.sync_block(node, l2)?; + } + } + NodeLevel::L4(i4, i3, i2, i1, i0) => { + let mut l3 = self.read_block_or_empty(level_data(node)?.level4[i4])?; + let mut l2 = self.read_block_or_empty(l3.data().ptrs[i3])?; + let mut l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + self.deallocate_block(node, l0.data_mut().ptrs[i0].clear()); + if l0.data().is_empty() { + self.deallocate_block(node, l1.data_mut().ptrs[i1].clear()); + } else { + l1.data_mut().ptrs[i1] = self.sync_block(node, l0)?; + } + if l1.data().is_empty() { + self.deallocate_block(node, l2.data_mut().ptrs[i2].clear()); + } else { + l2.data_mut().ptrs[i2] = self.sync_block(node, l1)?; + } + if l2.data().is_empty() { + self.deallocate_block(node, l3.data_mut().ptrs[i3].clear()); + } else { + l3.data_mut().ptrs[i3] = self.sync_block(node, l2)?; + } + if l3.data().is_empty() { + let ptr = level_data_mut(node)?.level4[i4].clear(); + self.deallocate_block(node, ptr); + } else { + level_data_mut(node)?.level4[i4] = self.sync_block(node, l3)?; + } + } + } + + Ok(()) + } + } + + /// Set the record at `ptr` as the data at `record_offset` of `node`. + fn sync_node_record_ptr( + &mut self, + node: &mut TreeData, + record_offset: u64, + ptr: BlockPtr, + ) -> Result<()> { + unsafe { + match NodeLevel::new(record_offset).ok_or(Error::new(ERANGE))? { + NodeLevel::L0(i0) => { + level_data_mut(node)?.level0[i0] = ptr; + } + NodeLevel::L1(i1, i0) => { + let mut l0 = self.read_block_or_empty(level_data(node)?.level1[i1])?; + + l0.data_mut().ptrs[i0] = ptr; + level_data_mut(node)?.level1[i1] = self.sync_block(node, l0)?; + } + NodeLevel::L2(i2, i1, i0) => { + let mut l1 = self.read_block_or_empty(level_data(node)?.level2[i2])?; + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + + l0.data_mut().ptrs[i0] = ptr; + l1.data_mut().ptrs[i1] = self.sync_block(node, l0)?; + level_data_mut(node)?.level2[i2] = self.sync_block(node, l1)?; + } + NodeLevel::L3(i3, i2, i1, i0) => { + let mut l2 = self.read_block_or_empty(level_data(node)?.level3[i3])?; + let mut l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + + l0.data_mut().ptrs[i0] = ptr; + l1.data_mut().ptrs[i1] = self.sync_block(node, l0)?; + l2.data_mut().ptrs[i2] = self.sync_block(node, l1)?; + level_data_mut(node)?.level3[i3] = self.sync_block(node, l2)?; + } + NodeLevel::L4(i4, i3, i2, i1, i0) => { + let mut l3 = self.read_block_or_empty(level_data(node)?.level4[i4])?; + let mut l2 = self.read_block_or_empty(l3.data().ptrs[i3])?; + let mut l1 = self.read_block_or_empty(l2.data().ptrs[i2])?; + let mut l0 = self.read_block_or_empty(l1.data().ptrs[i1])?; + + l0.data_mut().ptrs[i0] = ptr; + l1.data_mut().ptrs[i1] = self.sync_block(node, l0)?; + l2.data_mut().ptrs[i2] = self.sync_block(node, l1)?; + l3.data_mut().ptrs[i3] = self.sync_block(node, l2)?; + level_data_mut(node)?.level4[i4] = self.sync_block(node, l3)?; + } + } + } + + Ok(()) + } + + pub fn read_node_inner( + &mut self, + node: &TreeData, + mut offset: u64, + buf: &mut [u8], + ) -> Result { + let node_size = node.data().size(); + + // Try reading from inline data + if let Some(inline_data) = node.data().inline_data() { + if offset >= node_size { + return Ok(0); + } + + // Read as much as possible from inline data + let mut i = 0; + if offset < inline_data.len() as u64 { + let len = min( + buf.len() as u64, + min(node_size - offset, inline_data.len() as u64 - offset), + ); + buf[i..len as usize] + .copy_from_slice(&inline_data[offset as usize..(offset + len) as usize]); + i += len as usize; + offset += len; + } + + // Handle sparse data (outside of inline data) + while i < buf.len() && offset < node_size { + buf[i] = 0; + i += 1; + offset += 1; + } + + return Ok(i); + } + + let record_level = node.data().record_level(); + + let mut bytes_read = 0; + while bytes_read < buf.len() && offset < node_size { + // How many bytes we've read into the next record + let j = (offset % record_level.bytes()) as usize; + + // Number of bytes to read in this iteration + let len = min( + buf.len() - bytes_read, // number of bytes we have left in `buf` + min( + record_level.bytes() - j as u64, // number of bytes we haven't read in this record + node_size - offset, // number of bytes left in this node + ) as usize, + ); + + let record_idx = offset / record_level.bytes(); + let record_ptr = self.node_record_ptr(node, record_idx)?; + + // The level of the record to read. + // This is at most `record_level` due to the way `len` is computed. + let level = BlockLevel::for_bytes((j + len) as u64); + + let record = unsafe { self.read_record(record_ptr, level)? }; + buf[bytes_read..bytes_read + len].copy_from_slice(&record.data()[j..j + len]); + + bytes_read += len; + offset += len as u64; + } + Ok(bytes_read) + } + + pub fn read_node( + &mut self, + node_ptr: TreePtr, + offset: u64, + buf: &mut [u8], + atime: u64, + atime_nsec: u32, + ) -> Result { + let mut node = self.read_tree(node_ptr)?; + let mut node_changed = false; + + let i = self.read_node_inner(&node, offset, buf)?; + if i > 0 { + let node_atime = node.data().atime(); + if atime > node_atime.0 || (atime == node_atime.0 && atime_nsec > node_atime.1) { + let is_old = atime - node_atime.0 > 3600; // Last read was more than a day ago + if is_old { + node.data_mut().set_atime(atime, atime_nsec); + node_changed = true; + } + } + } + + if node_changed { + self.sync_tree(node)?; + } + + Ok(i) + } + + pub fn truncate_node_inner(&mut self, node: &mut TreeData, size: u64) -> Result { + let old_size = node.data().size(); + let record_level = node.data().record_level(); + + // Size already matches, return + if old_size == size { + return Ok(false); + } + + if old_size < size { + // If we're "truncating" to a larger size, + // write zeroes until the size matches + let zeroes = RecordRaw::empty(record_level).unwrap(); + + let mut offset = old_size; + while offset < size { + let start = offset % record_level.bytes(); + if start == 0 { + // We don't have to write completely zero records as read will interpret + // null record pointers as zero records + offset = size; + break; + } + let end = if offset / record_level.bytes() == size / record_level.bytes() { + size % record_level.bytes() + } else { + record_level.bytes() + }; + self.write_node_inner(node, &mut offset, &zeroes[start as usize..end as usize])?; + } + assert_eq!(offset, size); + } else if !node.data().has_inline_data() { + // Deallocate records + for record in + (size.div_ceil(record_level.bytes())..old_size / record_level.bytes()).rev() + { + self.remove_node_record_ptr(node, record)?; + } + } + + // Update size + node.data_mut().set_size(size); + + Ok(true) + } + + /// Truncate the given node to the given size. + /// + /// If `size` is larger than the node's current size, + /// expand the node with zeroes. + pub fn truncate_node( + &mut self, + node_ptr: TreePtr, + size: u64, + mtime: u64, + mtime_nsec: u32, + ) -> Result<()> { + let mut node = self.read_tree(node_ptr)?; + if self.truncate_node_inner(&mut node, size)? { + let node_mtime = node.data().mtime(); + if mtime > node_mtime.0 || (mtime == node_mtime.0 && mtime_nsec > node_mtime.1) { + node.data_mut().set_mtime(mtime, mtime_nsec); + } + + self.sync_tree(node)?; + } + + Ok(()) + } + + fn write_node_inner_records( + &mut self, + node: &mut TreeData, + offset: &mut u64, + buf: &[u8], + ) -> Result { + let mut node_changed = false; + + let record_level = node.data().record_level(); + let node_size = node.data().size(); + let node_records = node_size.div_ceil(record_level.bytes()); + + let mut i = 0; + while i < buf.len() { + let j = (*offset % record_level.bytes()) as usize; + let len = min(buf.len() - i, record_level.bytes() as usize - j); + let level = BlockLevel::for_bytes((j + len) as u64); + + let mut record_ptr = if node_records > (*offset / record_level.bytes()) { + self.node_record_ptr(node, *offset / record_level.bytes())? + } else { + BlockPtr::null(BlockMeta::new(level)) + }; + let mut record = unsafe { self.read_record(record_ptr, level)? }; + + // If record has changed + if buf[i..i + len] != record.data()[j..j + len] { + // Update record in memory + record.data_mut()[j..j + len].copy_from_slice(&buf[i..i + len]); + + // Handle record compression, if record is larger than one block + let decomp_level = record.addr().level(); + if decomp_level.0 > 0 { + assert_eq!(decomp_level.bytes(), record.data().len() as u64); + match lz4_flex::compress_into(record.data(), &mut self.fs.compress_cache) { + Ok(comp_len) => { + let total_len = comp_len + 2; + // Maximum compressed record size is 64 KiB + if total_len <= 64 * 1024 { + let comp_level = BlockLevel::for_bytes(total_len as u64); + // Replace record with compressed record, if it saves space + if comp_level < decomp_level { + if let Some(mut comp) = RecordRaw::empty(comp_level) { + // First two bytes store compressed data length + comp[0] = comp_len as u8; + comp[1] = (comp_len >> 8) as u8; + comp[2..total_len] + .copy_from_slice(&self.fs.compress_cache[..comp_len]); + record = BlockData::new( + BlockAddr::null(BlockMeta::new_compressed( + comp_level, + decomp_level, + )), + comp, + ); + } + } + } + } + Err(_err) => { + // Failures to compress can be ignored, with the original record data used + } + } + } + + // CoW record using its current level + let new_addr = unsafe { self.allocate(node, record.addr().meta())? }; + let mut old_addr = record.swap_addr(new_addr); + + // If the record was resized we need to dealloc the original ptr + if old_addr.is_null() { + old_addr = record_ptr.addr(); + } + + // Write record to disk + //TODO: deallocate new_addr on failure? + record_ptr = unsafe { self.write_block(record)? }; + + // Update record pointer + self.sync_node_record_ptr(node, *offset / record_level.bytes(), record_ptr)?; + node_changed = true; + + // Deallocate old record + if !old_addr.is_null() { + unsafe { + self.deallocate(node, old_addr); + } + } + } + + i += len; + *offset += len as u64; + } + + if node.data().size() < *offset { + node.data_mut().set_size(*offset); + node_changed = true; + } + + Ok(node_changed) + } + + pub fn write_node_inner( + &mut self, + node: &mut TreeData, + offset: &mut u64, + buf: &[u8], + ) -> Result { + let mut node_changed = false; + + // Try writing to inline data + let node_size = node.data().size(); + let convert_inline = if let Some(inline_data) = node.data_mut().inline_data_mut() { + let end = *offset + (buf.len() as u64); + if end < inline_data.len() as u64 { + inline_data[*offset as usize..end as usize].copy_from_slice(buf); + *offset += buf.len() as u64; + if node.data().size() < *offset { + node.data_mut().set_size(*offset); + } + return Ok(true); + } else { + Some(inline_data[..min(node_size as usize, inline_data.len())].to_vec()) + } + } else { + None + }; + + if let Some(inline_data) = convert_inline { + // If inline data cannot fit, convert to records + let mut flags = node.data().flags(); + flags.remove(NodeFlags::INLINE_DATA); + node.data_mut().set_flags(flags); + node.data_mut().level_data = NodeLevelData::default(); + self.write_node_inner_records(node, &mut 0, &inline_data)?; + node_changed = true; + } + + if self.write_node_inner_records(node, offset, buf)? { + node_changed = true; + } + + Ok(node_changed) + } + + /// Write the bytes at `buf` to `node` starting at `offset`. + pub fn write_node( + &mut self, + node_ptr: TreePtr, + mut offset: u64, + buf: &[u8], + mtime: u64, + mtime_nsec: u32, + ) -> Result { + let mut node = self.read_tree(node_ptr)?; + + if self.write_node_inner(&mut node, &mut offset, buf)? { + let node_mtime = node.data().mtime(); + if mtime > node_mtime.0 || (mtime == node_mtime.0 && mtime_nsec > node_mtime.1) { + node.data_mut().set_mtime(mtime, mtime_nsec); + } + + self.sync_tree(node)?; + } + + Ok(buf.len()) + } +} diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000000..dad29f0b47 --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,280 @@ +use core::{marker::PhantomData, mem, ops, slice}; +use endian_num::Le; + +use crate::{BlockLevel, BlockPtr, BlockRaw, BlockTrait}; + +// 1 << 8 = 256, this is the number of entries in a TreeList +const TREE_LIST_SHIFT: u32 = 8; +const TREE_LIST_ENTRIES: usize = (1 << TREE_LIST_SHIFT) - 2; + +/// A tree with 4 levels +pub type Tree = TreeList>>>; + +/// A [`TreePtr`] and the contents of the block it references. +#[derive(Clone, Copy, Debug, Default)] +pub struct TreeData { + /// The value of the [`TreePtr`] + id: u32, + + // The data + data: T, +} + +impl TreeData { + pub fn new(id: u32, data: T) -> Self { + Self { id, data } + } + + pub fn id(&self) -> u32 { + self.id + } + + pub fn data(&self) -> &T { + &self.data + } + + pub fn data_mut(&mut self) -> &mut T { + &mut self.data + } + + pub fn into_data(self) -> T { + self.data + } + + pub fn ptr(&self) -> TreePtr { + TreePtr { + id: self.id.into(), + phantom: PhantomData, + } + } +} + +/// A list of pointers to blocks of type `T`. +/// This is one level of a [`Tree`], defined above. +#[repr(C, packed)] +pub struct TreeList { + pub ptrs: [BlockPtr; TREE_LIST_ENTRIES], + pub full_flags: [u128; 2], +} + +impl TreeList { + pub fn tree_list_is_full(&self) -> bool { + self.full_flags[1] == u128::MAX & !(3 << 126) && self.full_flags[0] == u128::MAX + } + + pub fn tree_list_is_empty(&self) -> bool { + for ptr in self.ptrs.iter() { + if !ptr.is_null() { + return false; + } + } + true + } + + pub fn branch_is_full(&self, index: usize) -> bool { + assert!(index < TREE_LIST_ENTRIES); + let shift = index % 128; + let full_flags_index = index / 128; + self.full_flags[full_flags_index] & (1 << shift) != 0 + } + + pub fn set_branch_full(&mut self, index: usize, full: bool) { + assert!(index < TREE_LIST_ENTRIES); + let shift = index % 128; + let full_flags_index = index / 128; + + if full { + self.full_flags[full_flags_index] |= 1 << shift; + } else { + self.full_flags[full_flags_index] &= !(1 << shift); + } + } +} + +unsafe impl BlockTrait for TreeList { + fn empty(level: BlockLevel) -> Option { + if level.0 == 0 { + Some(Self { + ptrs: [BlockPtr::default(); TREE_LIST_ENTRIES], + full_flags: [0; 2], + }) + } else { + None + } + } +} + +impl ops::Deref for TreeList { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + self as *const TreeList as *const u8, + mem::size_of::>(), + ) as &[u8] + } + } +} + +impl ops::DerefMut for TreeList { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + slice::from_raw_parts_mut( + self as *mut TreeList as *mut u8, + mem::size_of::>(), + ) as &mut [u8] + } + } +} + +/// A pointer to an entry in a [`Tree`]. +#[repr(C, packed)] +pub struct TreePtr { + id: Le, + phantom: PhantomData, +} + +impl TreePtr { + /// Get a [`TreePtr`] to the filesystem root + /// directory's node. + pub fn root() -> Self { + Self::new(1) + } + + pub fn new(id: u32) -> Self { + Self { + id: id.into(), + phantom: PhantomData, + } + } + + /// Create a [`TreePtr`] from [`Tree`] indices, + /// Where `indexes` is `(i3, i2, i1, i0)`. + /// - `i3` is the index into the level 3 table, + /// - `i2` is the index into the level 2 table at `i3` + /// - ...and so on. + pub fn from_indexes(indexes: (usize, usize, usize, usize)) -> Self { + const SHIFT: u32 = TREE_LIST_SHIFT; + let id = ((indexes.0 << (3 * SHIFT)) as u32) + | ((indexes.1 << (2 * SHIFT)) as u32) + | ((indexes.2 << SHIFT) as u32) + | (indexes.3 as u32); + Self { + id: id.into(), + phantom: PhantomData, + } + } + + pub fn id(&self) -> u32 { + self.id.to_ne() + } + + pub fn is_null(&self) -> bool { + self.id() == 0 + } + + /// Get this indices of this [`TreePtr`] in a [`Tree`]. + /// Returns `(i3, i2, i1, i0)`: + /// - `i3` is the index into the level 3 table, + /// - `i2` is the index into the level 2 table at `i3` + /// - ...and so on. + pub fn indexes(&self) -> (usize, usize, usize, usize) { + const SHIFT: u32 = TREE_LIST_SHIFT; + const NUM: u32 = 1 << SHIFT; + const MASK: u32 = NUM - 1; + let id = self.id(); + + let i3 = ((id >> (3 * SHIFT)) & MASK) as usize; + let i2 = ((id >> (2 * SHIFT)) & MASK) as usize; + let i1 = ((id >> SHIFT) & MASK) as usize; + let i0 = (id & MASK) as usize; + + (i3, i2, i1, i0) + } + + pub fn to_bytes(&self) -> [u8; 4] { + self.id.to_le_bytes() + } + + pub fn from_bytes(bytes: [u8; 4]) -> Self { + let val = u32::from_le_bytes(bytes); + Self { + id: Le(val), + phantom: PhantomData, + } + } +} + +impl Clone for TreePtr { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for TreePtr {} + +impl Default for TreePtr { + fn default() -> Self { + Self { + id: 0.into(), + phantom: PhantomData, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{BlockAddr, BlockData, BlockMeta}; + + use super::*; + + #[test] + fn tree_list_size_test() { + assert_eq!( + mem::size_of::>(), + crate::BLOCK_SIZE as usize + ); + } + + #[test] + fn tree_list_is_full_test() { + let mut tree_list = TreeList::::empty(BlockLevel::default()).unwrap(); + assert!(!tree_list.tree_list_is_full()); + + for i in 0..TREE_LIST_ENTRIES { + assert!(!tree_list.branch_is_full(i)); + tree_list.set_branch_full(i, true); + assert!(tree_list.branch_is_full(i)); + } + + assert!(tree_list.tree_list_is_full()); + + for i in 0..TREE_LIST_ENTRIES { + assert!(tree_list.branch_is_full(i)); + tree_list.set_branch_full(i, false); + assert!(!tree_list.branch_is_full(i)); + } + } + + fn mock_block(addr: u64) -> BlockPtr { + let block_addr = unsafe { BlockAddr::new(addr, BlockMeta::default()) }; + BlockData::empty(block_addr).unwrap().create_ptr() + } + + #[test] + fn tree_list_is_empty() { + let mut tree_list = TreeList::::empty(BlockLevel::default()).unwrap(); + assert!(tree_list.tree_list_is_empty()); + + tree_list.ptrs[3] = mock_block(123); + assert!(!tree_list.tree_list_is_empty()); + } + + #[test] + fn tree_ptr_to_and_from_bytes() { + let ptr: TreePtr = TreePtr::new(123456); + let bytes = ptr.to_bytes(); + let ptr2: TreePtr = TreePtr::from_bytes(bytes); + assert_eq!(ptr.id(), ptr2.id()); + } +} diff --git a/src/unmount.rs b/src/unmount.rs new file mode 100644 index 0000000000..1c109d97b4 --- /dev/null +++ b/src/unmount.rs @@ -0,0 +1,51 @@ +use std::{ + fs, + io::{self}, + process::{Command, ExitStatus}, +}; + +fn unmount_linux_path(mount_path: &str) -> io::Result { + // Different distributions can have various fusermount binaries. Try + // them all. + let commands = ["fusermount", "fusermount3"]; + + for command in commands { + let status = Command::new(command).arg("-u").arg(mount_path).status(); + if status.is_ok() { + return status; + } + if let Err(ref e) = status { + if e.kind() == io::ErrorKind::NotFound { + continue; + } + } + } + + // Unmounting failed since no suitable command was found + Err(std::io::Error::new( + io::ErrorKind::NotFound, + format!( + "Unable to locate any fusermount binaries. Tried {:?}. Is fuse installed?", + commands + ), + )) +} + +pub fn unmount_path(mount_path: &str) -> Result<(), io::Error> { + if cfg!(target_os = "redox") { + fs::remove_dir(format!("/scheme/{}", mount_path))? + } else { + let status_res = if cfg!(target_os = "linux") { + unmount_linux_path(mount_path) + } else { + Command::new("umount").arg(mount_path).status() + }; + + let status = status_res?; + if !status.success() { + return Err(io::Error::other("redoxfs umount failed")); + } + } + + Ok(()) +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000000..20d8f1ad78 --- /dev/null +++ b/test.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +CARGO_ARGS=(--release) +TARGET=target/release +export RUST_BACKTRACE=full +export RUST_LOG=info + +function cleanup { + sync + fusermount -u image || true + fusermount3 -u image || true +} + +trap 'cleanup' ERR + +set -eEx + +cleanup + +redoxer test -- --lib -- --nocapture +cargo test --lib --no-default-features -- --nocapture +cargo test --lib -- --nocapture +cargo build "${CARGO_ARGS[@]}" + +rm -f image.bin +fallocate -l 1G image.bin +time "${TARGET}/redoxfs-mkfs" image.bin + +mkdir -p image +"${TARGET}/redoxfs" image.bin image + +df -h image +ls -lah image + +mkdir image/test +time cp -r src image/test/src + +dd if=/dev/urandom of=image/test/random bs=1M count=256 +dd if=image/test/random of=/dev/null bs=1M count=256 + +time truncate --size=256M image/test/sparse +dd if=image/test/sparse of=/dev/null bs=1M count=256 + +dd if=/dev/zero of=image/test/zero bs=1M count=256 +dd if=image/test/zero of=/dev/null bs=1M count=256 + +ls -lah image/test + +df -h image + +rm image/test/random +rm image/test/sparse +rm image/test/zero +rm -rf image/test/src +rmdir image/test + +df -h image +ls -lah image + +cleanup + +"${TARGET}/redoxfs" image.bin image + +df -h image +ls -lah image + +cleanup diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000000..58ba58ab00 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,639 @@ +use core::panic::AssertUnwindSafe; +use redoxfs::{unmount_path, DirEntry, DiskMemory, DiskSparse, FileSystem, Node, TreePtr}; + +use std::io::{Read, Seek, SeekFrom, Write}; +use std::panic::catch_unwind; +use std::path::Path; +use std::process::Command; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering::Relaxed; +use std::thread::sleep; +use std::time::Duration; +use std::{env, fs, time}; + +static IMAGE_SEQ: AtomicUsize = AtomicUsize::new(0); + +fn with_redoxfs(callback: F) -> T +where + T: Send + Sync + 'static, + F: FnOnce(&str) -> T + Send + Sync + 'static, +{ + let disk_path = format!("image{}.bin", IMAGE_SEQ.fetch_add(1, Relaxed)); + + { + let disk = DiskSparse::create(dbg!(&disk_path), 1024 * 1024 * 1024).unwrap(); + let ctime = dbg!(time::SystemTime::now().duration_since(time::UNIX_EPOCH)).unwrap(); + FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap(); + } + let res = callback(&disk_path); + + dbg!(fs::remove_file(dbg!(disk_path))).unwrap(); + + res +} + +fn with_mounted(callback: F) -> T +where + T: Send + Sync + 'static, + F: FnOnce(&Path) -> T + Send + Sync + 'static, +{ + let mount_path_o = format!("image{}", IMAGE_SEQ.fetch_add(1, Relaxed)); + let mount_path = mount_path_o.clone(); + + let res = with_redoxfs(move |fs| { + // At redox, we mount on /scheme/ path, no need an empty dir + if cfg!(not(target_os = "redox")) { + if !Path::new(&mount_path).exists() { + dbg!(fs::create_dir(dbg!(&mount_path))).unwrap(); + } + } else { + //FIXME: cargo_bin is broken when cross compiling. This is redoxer specific workaround + env::set_var( + "CARGO_BIN_EXE_redoxfs", + "/root/target/x86_64-unknown-redox/debug/redoxfs", + ); + } + let mut mount_cmd = Command::new(assert_cmd::cargo_bin!("redoxfs")); + mount_cmd.arg("-d").arg(dbg!(&fs)).arg(dbg!(&mount_path)); + let mut child = mount_cmd.spawn().expect("mount failed to run"); + + let real_path = if cfg!(target_os = "redox") { + let real_path = dbg!(Path::new("/scheme").join(&mount_path)); + let mut tries = 0; + loop { + if real_path.exists() { + break; + } + tries += 1; + if tries == 10 { + panic!("Fail to wait for mount") + } + println!("{tries}"); + sleep(Duration::from_millis(500)); + } + real_path + } else { + sleep(Duration::from_millis(200)); + let r = Path::new(".").join(&mount_path); + r + }; + + let res = catch_unwind(AssertUnwindSafe(|| callback(&real_path))); + + sleep(Duration::from_millis(200)); + + child.kill().expect("Can't kill"); + let _ = child.wait(); + + if cfg!(target_os = "redox") { + unmount_path(&mount_path).unwrap(); + } else { + if !dbg!(Command::new("sync").status()).unwrap().success() { + panic!("sync failed"); + } + + if unmount_path(&mount_path).is_err() { + // There seems to be a race condition where the device can be busy when trying to unmount. + // So, we pause for a moment and retry. There will still be an error output to the logs + // for the first failed attempt. + sleep(Duration::from_millis(200)); + if unmount_path(&mount_path).is_err() { + panic!("umount failed"); + } + } + } + + res.expect("Test failed") + }); + + if cfg!(not(target_os = "redox")) { + dbg!(fs::remove_dir(dbg!(mount_path_o))).unwrap(); + } + + res +} + +#[test] +fn simple() { + with_mounted(|path| { + dbg!(fs::create_dir(path.join("test"))).unwrap(); + }) +} + +#[test] +fn create_and_remove_file() { + with_mounted(|path| { + let file_name = "test_file.txt"; + let file_path = path.join(file_name); + + // Create the file + fs::write(&file_path, "Hello, world!").unwrap(); + assert!(fs::exists(&file_path).unwrap()); + + // Read the file + let contents = fs::read_to_string(&file_path).unwrap(); + assert_eq!(contents, "Hello, world!"); + + // Remove the file + fs::remove_file(&file_path).unwrap(); + assert!(!fs::exists(&file_path).unwrap()); + }); +} + +#[test] +fn create_and_remove_directory() { + with_mounted(|path| { + let dir_name = "test_dir"; + let dir_path = path.join(dir_name); + + // Create the directory + fs::create_dir(&dir_path) + .unwrap_or_else(|_| panic!("cannot create dir {}", &dir_path.display())); + assert!(fs::exists(&dir_path).unwrap()); + + // Check that the directory is empty + let entries: Vec<_> = fs::read_dir(&dir_path) + .unwrap() + .map(|e| e.unwrap().file_name()) + .collect(); + assert!(entries.is_empty()); + + // Add a file to the directory + let file_name = "test_file.txt"; + let file_path = dir_path.join(file_name); + fs::write(&file_path, "Hello, world!").unwrap(); + + // Check that the dir cannot be removed when not empty + let error = fs::remove_dir(&dir_path); + assert!(error.is_err()); + assert_eq!( + error.unwrap_err().kind(), + std::io::ErrorKind::DirectoryNotEmpty + ); + + // Remove the file + fs::remove_file(&file_path).unwrap(); + + // Remove the directory + fs::remove_dir(&dir_path).unwrap(); + assert!(!fs::exists(&dir_path).unwrap()); + }); +} + +#[test] +fn create_and_remove_symlink() { + with_mounted(|path| { + let real_file = "real_file.txt"; + let real_path = path.join(real_file); + let symlink_file = "symlink_to_real_file.txt"; + let symlink_path = path.join(symlink_file); + + // Create the real file + fs::write(&real_path, "Hello, world!").unwrap(); + + // Create the symmlink according to the platform + #[cfg(unix)] + std::os::unix::fs::symlink(real_file, &symlink_path).unwrap(); + + #[cfg(windows)] + std::os::windows::fs::symlink_file(&real_file, &symlink_path).unwrap(); + + // Check that the symlink exists and points to the correct target + let exists = fs::exists(&symlink_path); + assert!( + exists.is_ok() && exists.unwrap(), + "Symlink should exist but was: {:?}", + fs::exists(&symlink_path) + ); + let symlink_metadata = fs::symlink_metadata(&symlink_path).unwrap(); + assert!(symlink_metadata.file_type().is_symlink()); + let target = fs::read_link(&symlink_path).unwrap(); + assert_eq!(target.to_str().unwrap(), real_file); + assert_eq!(fs::read(&symlink_path).unwrap(), b"Hello, world!"); + + // Confirm the symlink cannot be removed as a directory + let error = fs::remove_dir(&symlink_path); + assert!(error.is_err()); + assert_eq!(error.unwrap_err().kind(), std::io::ErrorKind::NotADirectory); + + // Remove the symlink + fs::remove_file(&symlink_path).unwrap(); + assert!(!fs::exists(&symlink_path).unwrap()); + }); +} + +#[cfg(target_os = "redox")] +#[test] +fn mmap() { + //TODO + with_mounted(|path| { + use std::slice; + + let path = dbg!(path.join("test")); + + let mmap_inner = |write: bool| { + let fd = dbg!(libredox::call::open( + path.to_str().unwrap(), + libredox::flag::O_CREAT | libredox::flag::O_RDWR | libredox::flag::O_CLOEXEC, + 0, + )) + .unwrap(); + + let map = unsafe { + slice::from_raw_parts_mut( + dbg!(libredox::call::mmap(libredox::call::MmapArgs { + fd, + offset: 0, + length: 128, + prot: libredox::flag::PROT_READ | libredox::flag::PROT_WRITE, + flags: libredox::flag::MAP_SHARED, + addr: core::ptr::null_mut(), + })) + .unwrap() as *mut u8, + 128, + ) + }; + + // Maps should be available after closing + assert_eq!(dbg!(libredox::call::close(fd)), Ok(())); + + for i in 0..128 { + if write { + map[i as usize] = i; + } + assert_eq!(map[i as usize], i); + } + + //TODO: add msync + unsafe { + assert_eq!( + dbg!(libredox::call::munmap(map.as_mut_ptr().cast(), map.len())), + Ok(()) + ); + } + }; + + mmap_inner(true); + mmap_inner(false); + }) +} + +// TODO: When increasing the total_count to 8000, the Allocator's deallocate() function surfaces as "slow" according to flamegraph. This +// appears to be the result of bulk deleting in this test, but I would bet that any filesystem that has lived for a long time would +// start to see degraded performance due to this. +#[test] +fn many_create_write_list_find_read_delete() { + let disk = DiskMemory::new(1024 * 1024 * 1024); + let ctime = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap(); + let mut fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap(); + let tree_ptr = TreePtr::::root(); + let total_count = 3000; + + // Create a bunch of files + for i in 0..total_count { + let result = fs.tx(|tx| { + tx.create_node( + tree_ptr, + &format!("file{i:05}"), + Node::MODE_FILE | 0o644, + 1, + 0, + ) + }); + if result.is_err() { + println!("Failure on create iteration {i}"); + } + + let file_node = result.unwrap(); + let result = fs.tx(|tx| { + tx.write_node( + file_node.ptr(), + 0, + format!("Hello World! #{i}").as_bytes(), + ctime.as_secs(), + ctime.subsec_nanos(), + ) + }); + if result.is_err() { + println!("Failure on write iteration {i}"); + } + assert!(result.unwrap() > 0) + } + + // Confirm that they can be listed + { + let mut children = Vec::::with_capacity(total_count); + fs.tx(|tx| tx.child_nodes(tree_ptr, &mut children)).unwrap(); + assert_eq!( + children.len(), + total_count, + "The list of children should match the number of files created." + ); + let mut children: Vec = children + .iter() + .map(|entry| entry.name().unwrap_or_default().to_string()) + .collect(); + children.sort(); + + for i in 0..total_count { + let expected = format!("file{i:05}"); + let idx = children.binary_search(&expected); + assert!(idx.is_ok(), "Children did not contain '{}'", expected); + } + } + + // Find and read the files + for i in 0..total_count { + let result = fs.tx(|tx| tx.find_node(tree_ptr, &format!("file{i:05}"))); + if result.is_err() { + println!("Failure on find node iteration {i}"); + } + + let file_node = result.unwrap(); + let offset = 0; + let mut buf = [0_u8; 32]; + let result = fs.tx(|tx| { + tx.read_node( + file_node.ptr(), + offset, + &mut buf, + ctime.as_secs(), + ctime.subsec_nanos(), + ) + }); + if result.is_err() { + println!("Failure on read iteration {i}"); + } + let size = result.unwrap(); + let body = std::str::from_utf8(&buf[..size]).unwrap(); + assert_eq!(body, format!("Hello World! #{i}")); + } + + // Delete all the files + for i in 0..total_count { + let file_name = format!("file{i:05}"); + if let Err(e) = fs.tx(|tx| tx.remove_node(tree_ptr, &file_name, Node::MODE_FILE)) { + println!("Failure on delete iteration {i}"); + panic!("{e}"); + } + + let result = fs.tx(|tx| tx.find_node(tree_ptr, &file_name)); + if result.is_ok() || result.unwrap_err().errno != syscall::error::ENOENT { + println!("Failure on delete verification iteration {i}"); + panic!("Deletion appears to have failed"); + } + } +} + +#[test] +fn many_write_read_delete_mounted() { + with_mounted(|path| { + let total_count = 500; + + for i in 0..total_count { + fs::write( + path.join(format!("file{}", i)), + format!("Hello, number {i}!"), + ) + .unwrap(); + } + + // Confirm each of the created files can be found and read + for i in 0..total_count { + let contents = fs::read_to_string(path.join(format!("file{}", i))).unwrap(); + assert_eq!(contents, format!("Hello, number {i}!")); + } + + // Remove all the files + for i in 0..total_count { + let file_path = path.join(format!("file{i}")); + assert!(fs::exists(&file_path).unwrap()); + fs::remove_file(&file_path).unwrap(); + assert!(!fs::exists(&file_path).unwrap()); + } + }); +} + +#[test] +fn rename_no_replace() { + let disk = DiskMemory::new(1024 * 1024 * 1024); + let mut fs = FileSystem::create(disk, None, 0, 0) + .expect("Creating in memory file system should succeed"); + + let root = TreePtr::root(); + let dir = fs + .tx(|tx| tx.create_node(root, "dir", Node::MODE_DIR, 0, 0)) + .expect("Creating a directory should succeed"); + let source_file = fs + .tx(|tx| tx.create_node(root, "source", Node::MODE_FILE, 0, 0)) + .expect("Creating source file to copy should succeed"); + let no_clobber_file = fs + .tx(|tx| tx.create_node(root, "no_clobber", Node::MODE_FILE, 0, 0)) + .expect("Creating second file to not clobber should succeed"); + + // Rename /source to /target + fs.tx(|tx| tx.rename_node_no_replace(root, "source", root, "target")) + .expect("Renaming existing 'source' to non-existing 'target' should succeed"); + let target_file = fs + .tx(|tx| tx.find_node(root, "target")) + .expect("'target' should exist because we just renamed 'source' to 'target'"); + assert_eq!( + source_file.id(), + target_file.id(), + "source and target are most definitely the same file" + ); + + // Don't rename /target to /no_clobber + let err = fs + .tx(|tx| tx.rename_node_no_replace(root, "target", root, "no_clobber")) + .expect_err("Renaming 'target' to existing 'no_clobber' should fail"); + assert_eq!( + syscall::EEXIST, + err.errno, + "Renaming to existing file should fail with EEXIST" + ); + assert_ne!( + no_clobber_file.id(), + target_file.id(), + "'target' and 'no_clobber' should be distinct files" + ); + + // Don't rename /target to /dir + let err = fs + .tx(|tx| tx.rename_node_no_replace(root, "target", root, "dir")) + .expect_err("Renaming 'target' to existing directory 'dir' should fail"); + assert_eq!( + syscall::EEXIST, + err.errno, + "Renaming to existing file should fail with EEXIST" + ); + assert_ne!( + dir.id(), + target_file.id(), + "'target' and 'dir' should be distinct nodes" + ); + + // Don't rename /dir to /target + let err = fs + .tx(|tx| tx.rename_node_no_replace(root, "dir", root, "target")) + .expect_err("Renaming 'dir' to existing file 'target' should fail"); + assert_eq!( + syscall::EEXIST, + err.errno, + "Renaming to existing file should fail with EEXIST" + ); + assert_ne!( + target_file.id(), + dir.id(), + "'dir' and 'target' should be distinct nodes" + ); + + // Don't rename /target to /target + let err = fs + .tx(|tx| tx.rename_node_no_replace(root, "target", root, "target")) + .expect_err("Renaming 'target' to itself should fail"); + assert_eq!( + syscall::EEXIST, + err.errno, + "Renaming file to itself should fail with EEXIST" + ); + + // Rename /target to /dir/target + fs.tx(|tx| tx.rename_node_no_replace(root, "target", dir.ptr(), "target")) + .expect("Renaming /target to /dir/target should succeed"); + let moved_target = fs + .tx(|tx| tx.find_node(dir.ptr(), "target")) + .expect("'target' should have moved to /dir/target"); + assert_eq!(target_file.id(), moved_target.id()); + + // Rename /dir to /newdir + fs.tx(|tx| tx.rename_node_no_replace(root, "dir", root, "newdir")) + .expect("Renaming 'dir' to 'newdir' should succeed"); +} + +#[test] +fn rename_works() { + let disk = DiskMemory::new(1024 * 1024 * 1024); + let mut fs = FileSystem::create(disk, None, 0, 0) + .expect("Creating in memory file system should succeed"); + + let root = TreePtr::root(); + let dir = fs + .tx(|tx| tx.create_node(root, "dir", Node::MODE_DIR, 0, 0)) + .expect("Creating a directory should succeed"); + let source_file = fs + .tx(|tx| tx.create_node(root, "source", Node::MODE_FILE, 0, 0)) + .expect("Creating source file should succeed"); + let target_file_orig = fs + .tx(|tx| tx.create_node(root, "target", Node::MODE_FILE, 0, 0)) + .expect("Creating target file should succeed"); + + // Rename /source to /source2 + fs.tx(|tx| tx.rename_node(root, "source", root, "source2")) + .expect("Renaming existing 'source' to non-existing 'source2' should succeed"); + let source2_file = fs + .tx(|tx| tx.find_node(root, "source2")) + .expect("'source2' should exist because we just renamed 'source' to 'source2'"); + assert_eq!(source_file.id(), source2_file.id()); + let err = fs + .tx(|tx| tx.find_node(root, "source")) + .expect_err("'source' should not exist because it was moved"); + assert_eq!(syscall::ENOENT, err.errno); + + // Rename /source2 to /target + fs.tx(|tx| tx.rename_node(root, "source2", root, "target")) + .expect("Renaming existing 'source2' to existing 'target' should succeed"); + let target_file_mv = fs + .tx(|tx| tx.find_node(root, "target")) + .expect("'target' should exist because the rename succeeded"); + assert_ne!( + target_file_orig.id(), + target_file_mv.id(), + "Move failed because 'target' is still the same" + ); + assert_eq!( + source2_file.id(), + target_file_mv.id(), + "Move failed because 'source2' != 'target'" + ); + + // Don't rename /target to /dir + // XXX: A similar test fails on Linux using rename(). Not sure if the discrepancy matters. + // let err = fs + // .tx(|tx| tx.rename_node(root, "target", root, "dir")) + // .expect_err("Renaming 'target' to existing directory 'dir' should fail"); + // assert_eq!( + // syscall::EEXIST, + // err.errno, + // "Renaming to existing file should fail with EEXIST" + // ); + // assert_ne!( + // dir.id(), + // target_file_mv.id(), + // "'target' and 'dir' should be distinct nodes" + // ); + + // Don't rename /dir to /target + // XXX: A similar test fails on Linux using rename(). + // let err = fs + // .tx(|tx| tx.rename_node(root, "dir", root, "target")) + // .expect_err("Renaming 'dir' to existing file 'target' should fail"); + // assert_eq!( + // syscall::EEXIST, + // err.errno, + // "Renaming to existing file should fail with EEXIST" + // ); + // assert_ne!( + // target_file_mv.id(), + // dir.id(), + // "'dir' and 'target' should be distinct nodes" + // ); + + // Rename /target to /target + fs.tx(|tx| tx.rename_node(root, "target", root, "target")) + .expect("Renaming 'target' to itself should succeed"); + let target_self_mv = fs + .tx(|tx| tx.find_node(root, "target")) + .expect("'target' should exist because rename succeeded"); + assert_eq!( + target_file_mv.id(), + target_self_mv.id(), + "'target' shouldn't have changed during a move to self" + ); + + // Rename /target to /dir/target + fs.tx(|tx| tx.rename_node(root, "target", dir.ptr(), "target")) + .expect("Renaming /target to /dir/target should succeed"); + let moved_target = fs + .tx(|tx| tx.find_node(dir.ptr(), "target")) + .expect("'target' should have moved to /dir/target"); + assert_eq!(target_file_mv.id(), moved_target.id()); + + // Rename /dir to /newdir + fs.tx(|tx| tx.rename_node(root, "dir", root, "newdir")) + .expect("Renaming 'dir' to 'newdir' should succeed"); +} + +#[test] +fn temporary_file() { + with_mounted(|path| { + let file_path = path.join("temp"); + let mut file = fs::File::create_new(&file_path).expect("failed to create temp file"); + + fs::remove_file(&file_path).expect("failed to unlink temp file"); + + let write_data = "Test\n"; + file.write_all(write_data.as_bytes()) + .expect("failed to write temp file"); + + let mut read_data = String::new(); + file.seek(SeekFrom::Start(0)) + .expect("failed to seek temp file"); + file.read_to_string(&mut read_data) + .expect("failed to read temp file"); + + assert_eq!(read_data, write_data); + }); +}