diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..f0ccbc9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d096107 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: weekly + - package-ecosystem: cargo + directory: "/eck/fuzz" + schedule: + interval: weekly \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e0fce30 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + push: + branches: [dev, main] + paths-ignore: + - "doc/**" + - "**.md" + pull_request: + branches: [dev, main] + paths-ignore: + - "doc/**" + - "**.md" + +env: + CARGO_TERM_COLOR: always + +jobs: + # Static checks, tests, rustdoc, audit + checks: + name: fmt · clippy · test · doc · audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Clippy (all targets, warnings as errors) + run: cargo clippy --all-targets -- -D warnings + + - name: Run tests (eck_tests harness) + run: cargo xtask test + + - name: Build rustdoc (warnings as errors) + run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps + + - name: Install cargo-audit + run: cargo install cargo-audit --locked + + - name: cargo audit (allowlist in audit.toml) + run: cargo audit + + # Bounded fuzz smoke (blocking) + fuzz: + name: fuzz smoke (${{ matrix.target }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [decompress, compress_roundtrip, metadata] + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@nightly + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: eck/fuzz + + - name: Install cargo-fuzz + run: cargo install cargo-fuzz --locked + + - name: Fuzz smoke — ${{ matrix.target }} + run: cargo +nightly xtask fuzz ${{ matrix.target }} -- -max_total_time=60 + + # OS keyring roundtrip (macOS only) + keyring: + name: OS keyring roundtrip (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Keyring roundtrip (#[ignore]d test) + run: cargo test --test=eck_tests encrypt_decrypt_os_keytype_roundtrip -- --ignored \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b92a84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,108 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: linux-x86_64 + os: ubuntu-latest + target: x86_64-unknown-linux-gnu + bin: eck + suffix: "" + - name: linux-aarch64 + os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + bin: eck + suffix: "" + - name: macos-x86_64 + os: macos-13 + target: x86_64-apple-darwin + bin: eck + suffix: "" + - name: macos-arm64 + os: macos-latest + target: aarch64-apple-darwin + bin: eck + suffix: "" + - name: windows-x86_64 + os: windows-latest + target: x86_64-pc-windows-msvc + bin: eck.exe + suffix: ".exe" + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + + - name: Build release binary + run: cargo build --release --target ${{ matrix.target }} + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: eck-${{ matrix.target }}${{ matrix.suffix }} + path: target/${{ matrix.target }}/release/${{ matrix.bin }} + if-no-files-found: error + + # ---- Draft GitHub release with checksums ---- + release: + name: Draft release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Generate checksums + run: | + cd artifacts + for f in *; do + sha256sum "$f" > "$f.sha256" + done + ls -la + + - name: Create draft release + uses: softprops/action-gh-release@v2 + with: + draft: true + generate_release_notes: true + files: | + artifacts/* + body: | + ## Enkryptit! ${{ github.ref_name }} + + > [!WARNING] + > Gatekeeper note (macOS) + > Release binaries are **not notarized/signed**. On first launch macOS may + > warn "cannot be opened because the developer cannot be verified". + > Workarounds: right-click => *Open*, or from a terminal: + > `xattr -dr com.apple.quarantine eck` + > + > Windows: SmartScreen may show an "unrecognized app" warning — click + > *More info : Run anyway*. + + Checksums are provided as `*.sha256` files next to each binary. \ No newline at end of file diff --git a/.gitignore b/.gitignore index ada61f8..bb18ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,20 @@ # Rust : /target +/supply-chain + +# eck (Enkryptit!) : +/eck/target + +# xtask : +/xtask/target + +# eck fuzz (cargo-fuzz) : +/eck/fuzz/target +/eck/fuzz/corpus +/eck/fuzz/artifacts # MacOS : -.DS_Store \ No newline at end of file +.DS_Store + +# My personal checklist +TODO.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbdab2d..cbad510 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,15 +72,17 @@ Contributors should be able to explain: For Enkryptit!, human review and security awareness take precedence over development speed. -## Code Style +## Code General expectations: -* Follow idiomatic Rust practices. +* Follow idiomatic Rust practices (`cargo fmt` and `cargo clippy`) * Prefer readable and maintainable code. * Avoid unnecessary complexity. * Add comments when the intent is not obvious. +* Document your contribution with **RustDoc** comments. * Keep public APIs consistent and well documented. +* Launch `cargo audit` before and after coding. Additional coding guidelines may be added as the project evolves. diff --git a/Cargo.lock b/Cargo.lock index bac3e08..4e55184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -25,9 +40,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -139,7 +154,7 @@ checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.4.1", + "fastrand 2.5.0", "futures-lite 2.6.1", "pin-project-lite", "slab", @@ -190,7 +205,7 @@ dependencies = [ "futures-lite 2.6.1", "parking", "polling 3.11.0", - "rustix 1.1.4", + "rustix 1.1.5", "slab", "windows-sys 0.61.2", ] @@ -210,7 +225,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "event-listener-strategy", "pin-project-lite", ] @@ -240,7 +255,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -255,7 +270,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.4", + "rustix 1.1.5", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -269,13 +284,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -299,6 +314,30 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -313,9 +352,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blake2" @@ -344,11 +383,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "blocking" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" dependencies = [ "async-channel", "async-task", @@ -359,9 +407,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", @@ -391,9 +439,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ "find-msvc-tools", "jobserver", @@ -402,16 +450,21 @@ dependencies = [ ] [[package]] -name = "cfg-if" -version = "1.0.4" +name = "cfb" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] [[package]] -name = "cfg_aliases" -version = "0.2.1" +name = "cfg-if" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chacha20" @@ -450,9 +503,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -460,9 +513,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -472,21 +525,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cobs" @@ -512,6 +565,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "comfy-table" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136c8c4c3823846e8ba6d4bda011b4e5d5827b8bb0ac26c31f9ab31abe9f54f2" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width 0.2.2", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -523,13 +587,13 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" dependencies = [ "encode_unicode", "libc", - "unicode-width", + "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -573,11 +637,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" @@ -585,13 +658,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "crossterm_winapi", "derive_more", "document-features", "mio", "parking_lot", - "rustix 1.1.4", + "rustix 1.1.5", "signal-hook", "signal-hook-mio", "winapi", @@ -624,9 +697,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "derivative" version = "2.2.0" @@ -657,7 +736,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -707,6 +786,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "document-features" version = "0.2.12" @@ -716,6 +827,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -724,33 +841,44 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "eck" -version = "0.1.0" +version = "0.0.4" dependencies = [ "argon2", "assert_cmd", "chacha20poly1305", "clap", "colored", + "comfy-table", "ctor", "directories", "dirs", + "fmodeparser", + "gradient-bar", "hex", "indicatif", + "infer", "inquire", "keyring", "libc", "lz4_flex", - "nix 0.31.3", + "miette", "oxiarc-core", "postcard", "predicates", "rand", + "rfd", "rpassword", "serde", "serde_json", + "sysinfo", "tempfile", - "text_io", + "terminal_size", "thiserror", + "tracing", + "tracing-appender", + "tracing-subscriber", + "unicode-segmentation", + "unicode-width 0.2.2", "walkdir", "xz2", "zeroize", @@ -793,7 +921,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -831,11 +959,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -846,7 +973,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "pin-project-lite", ] @@ -861,15 +988,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" [[package]] name = "float-cmp" @@ -880,17 +1007,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fmodeparser" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342312c958816266bf5f3638d2727c243cc21e668173745982c96a86e44aa63" +dependencies = [ + "regex", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -913,7 +1055,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand 2.4.1", + "fastrand 2.5.0", "futures-core", "futures-io", "parking", @@ -922,32 +1064,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -1000,6 +1142,21 @@ dependencies = [ "r-efi", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gradient-bar" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3608c4e710985c879e80f38f991e95e5c83ff4e003ec1a6f279bc862f9b5dcb" +dependencies = [ + "indicatif", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1043,9 +1200,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -1071,11 +1228,98 @@ dependencies = [ "digest", ] +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" + [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown", @@ -1083,17 +1327,26 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", - "unicode-width", + "unicode-width 0.2.2", "unit-prefix", "web-time", ] +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.4" @@ -1110,12 +1363,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "crossterm", "dyn-clone", "fuzzy-matcher", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -1138,6 +1391,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1162,9 +1421,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -1193,15 +1452,25 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" dependencies = [ "libc", ] @@ -1212,7 +1481,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "libc", ] @@ -1234,6 +1503,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "1.0.0" @@ -1251,9 +1526,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lz4_flex" @@ -1275,11 +1550,20 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -1299,11 +1583,50 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -1324,22 +1647,28 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.31.3" +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "ntapi" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "cfg_aliases", - "libc", + "winapi", ] [[package]] -name = "normalize-line-endings" -version = "0.3.0" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] name = "num" @@ -1357,9 +1686,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -1374,22 +1703,27 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -1414,6 +1748,85 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1449,10 +1862,16 @@ dependencies = [ ] [[package]] -name = "oxiarc-core" -version = "0.3.3" +name = "owo-colors" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870de52f96c9df5474080d34ca0c32f5ca1db3e3bd9d6b614436090e86ae7f0c" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + +[[package]] +name = "oxiarc-core" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119a925483e4917b645fd77acef86859164e166faa4cce3353c1d7cecfd3665a" dependencies = [ "thiserror", ] @@ -1497,6 +1916,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1510,15 +1935,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", - "fastrand 2.4.1", + "fastrand 2.5.0", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "polling" @@ -1544,12 +1969,18 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.2", + "hermit-abi 0.5.3", "pin-project-lite", - "rustix 1.1.4", + "rustix 1.1.5", "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "poly1305" version = "0.8.0" @@ -1563,9 +1994,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "postcard" @@ -1580,6 +2011,23 @@ dependencies = [ "serde", ] +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1631,18 +2079,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1655,9 +2112,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha", @@ -1683,31 +2140,36 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", ] [[package]] name = "redox_users" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" dependencies = [ - "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -1717,9 +2179,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1732,6 +2194,33 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "percent-encoding", + "pollster", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rpassword" version = "7.5.4" @@ -1745,14 +2234,20 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1782,7 +2277,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1791,11 +2286,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.12.1", @@ -1804,9 +2299,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "same-file" @@ -1817,6 +2312,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1848,7 +2349,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -1873,9 +2374,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1883,29 +2384,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1916,20 +2417,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures", @@ -1947,6 +2448,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -1992,9 +2502,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" @@ -2008,9 +2518,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -2039,6 +2549,33 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -2052,25 +2589,72 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sysinfo" +version = "0.39.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows", +] + [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand 2.4.1", + "fastrand 2.5.0", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix 1.1.5", "windows-sys 0.61.2", ] @@ -2081,40 +2665,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] -name = "text_io" -version = "0.1.13" +name = "textwrap" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d8d3ca3b06292094e03841d8995e910712d2a10b5869c8f9725385b29761115" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "icu_segmenter", + "unicode-width 0.2.2", +] [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2143,6 +2772,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -2151,7 +2793,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2161,13 +2803,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" [[package]] name = "typenum" @@ -2188,9 +2860,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-segmentation" @@ -2198,6 +2870,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" @@ -2220,12 +2898,34 @@ dependencies = [ "subtle", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -2265,9 +2965,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -2276,11 +2976,21 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2288,26 +2998,96 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2349,12 +3129,107 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2422,6 +3297,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2521,6 +3405,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + [[package]] name = "xdg-home" version = "1.3.0" @@ -2531,6 +3421,10 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xtask" +version = "0.0.0" + [[package]] name = "xz2" version = "0.1.7" @@ -2540,6 +3434,29 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + [[package]] name = "zbus" version = "3.15.2" @@ -2564,7 +3481,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.26.4", + "nix", "once_cell", "ordered-stream", "rand", @@ -2608,22 +3525,43 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", + "synstructure", ] [[package]] @@ -2632,11 +3570,44 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" @@ -2649,18 +3620,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 473a165..56637aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,45 +1,18 @@ -[package] -name = "eck" -version = "0.1.0" -edition = "2024" +[workspace] +resolver = "3" +rust-version = "1.85" -[dependencies] -argon2 = "0.5.3" -chacha20poly1305 = { version = "0.10.1", features = ["alloc"] } -clap = { version = "4.6.1", features = ["derive"] } -colored = "3.1.1" -directories = "6.0.0" -dirs = "6.0.0" -hex = "0.4.3" -indicatif = "0.18.4" -inquire = "0.9.4" -keyring = "2.0" -libc = "0.2.186" -lz4_flex = "0.13.1" -nix = "0.31.3" -oxiarc-core = "0.3.0" -postcard = { version = "1", features = ["alloc"] } -rand = "0.8" -rpassword = "7.5.4" -serde = { version = "1", features = ["derive"] } -serde_json = "1.0.150" -text_io = "0.1.13" -thiserror = "2.0.18" -walkdir = "2.5.0" -xz2 = "0.1.7" -zeroize = "1.8.2" -zstd = "0.13.3" +members = [ + "eck", + "xtask", +] -[dev-dependencies] -tempfile = "3" -assert_cmd = "2" -predicates = "3" -ctor = "0.2" +exclude = [ + "eck/fuzz", +] -[[test]] -name = "eck_tests" -path = "test/mod.rs" -harness = true +[profile.dev] +opt-level = 1 [profile.test] opt-level = 1 @@ -51,4 +24,5 @@ codegen-units = 1 panic = "abort" strip = "symbols" debug = false -incremental = false \ No newline at end of file +incremental = false + diff --git a/PROGRESSION.md b/PROGRESSION.md index 40a9754..3b2bcc8 100644 --- a/PROGRESSION.md +++ b/PROGRESSION.md @@ -75,4 +75,598 @@ test result: ok. 89 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; fin I also modified README. ---- \ No newline at end of file +--- + +## DAY-4 Fixed an issue with folder encryption + +I fixed an issue with the folder encryption, in `/treatment/folder_case.rs`. +\ +The fact was that if you wanted to encrypt `path/to/my/folder/`, it created `path/to/my/folder/.encky`. +\ +It is not a huge issue, but I added a `strip_suffix()`, so that now it creates `path/to/my/folder.encky` correctly. + +Test results : + +```bash +test result: ok. 89 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 6.71s +``` + +--- + +## DAY-5 Refactorization & addon + +I did a huge refactorization in the code. Now, the frontend doesn't resolves the `key`. +\ +It passes an `EnkryptitContext` (*mutable*) to the backend, so that the backend resolves both `Key` and `KeyType` easily : + +```rust +let enkryptit_key = EnkryptitKey::resolve(Mode::Decrypting, &metadatas.key_type, context, path)?; +// Or +let enkryptit_key = EnkryptitKey::resolve(Mode::Encrypting, &key_type, context, path)?; +``` + +The `Key` and `KeyType` are now, in the backend, wrapped in `EnkryptitKey`, a more secure structure (`Zeroize` on drop, and calls `EnkryptitContext::resolve` wich contains the password as `Zeroizing<>`. Also, the key is *Locked* (`mlock`)). +\ +Because of these refactorizations, the code is now easier to read (and to maintain), but also allows the user to do : +```bash +eck myfile.txt myfile2.txt +eck myfolder/* +``` + +> Many files can be encrypted with one command now. + +This is possible because of `pub fn treat_objects_with_multiple_paths()` and the modification of the Cli's arguments (`Vec` for paths instead of `Option`.) +\ +Files added : +- frontend/params_helper.rs +\ +frontend/mod.rs +\ +frontend/treat_output.rs +\ +frontend/treatment.rs +\ +frontend/tui.rs (moved) +\ +frontend/cli.rs (moved) + +- context.rs +- key/derivation.rs +\ +key/generation.rs +\ +key/mod.rs +\ +key/resolve.rs +\ +key/storage.rs + +- Files modified : Almost all, except `compression`, `metadatas`, `parameters` and `encryption_primitives`. + +Test results : + +```bash +test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 7.63s +``` + +(But many tests are deprecated) + +--- + +## DAY-6 Ergonomic addon + +Added a `Browse` section in the TUI that uses `rfd` (Rusty Files Dialogs), and allows the users to now browse their **files** and **folders**. + +- Files modified : frontend/tui.rs +- Functions added : + - pub fn launch_browser() -> Result<(), EnkryptitError> {} (launches the browser menu) + - fn browse_files() -> Result<(), EnkryptitError> {} (opens the `Browse Files` pop-up, and treats the selected objects) + - fn browse_folders() -> Result<(), EnkryptitError> {} (opens the `Browse Folders` pop-up, and treats the selected objects) + - fn browse_files_then_folders() -> Result<(), EnkryptitError> {} (opens the `Browse Files` pop-up, then the `Browse Files` pop-up, combines the two results, and treats the selected objects) + +Test results : + +```bash +test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 5.21s +``` + +But **too many** tests are deprecated, and I need to add test for : +- testing `eck /myfile.rs /myfolder/ /myfolder2/*` +- tests for the `EnkryptitContext` (password resolution, essentially) +- tests for `EnkryptitKey` +- tests for the `Browse` section + +--- + +## DAY-7 Frontend refactorization + Added tests + +Modified `/frontend/`. It is now compound of `/cli`, `/tui` and `treat_output.rs`. +\ +All the **Cli** code has been grouped in `/cli`, and all the **Tui** code has been grouped in `/tui`. +\ +Added `EnkryptitTuiAction` enum, used by `launch_ui()`. It makes the code cleaner and will help for future tests. +\ +Added `9` tests that cover the *tui actions*, and cover more the tests related to *cli* (`cli_tests.rs`). + +Test results : + +```bash +test result: ok. 97 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 6.94s +``` + +--- + +## DAY-8 Added Multithreading when encrypting a file, updated the format and added tests + +Added `ParallelismType` in `types.rs`, and a `parallelism` field in *parameters*. +\ +Also added a new argument in Cli and a new `Select` in Tui for choosing Parallelism type (and possibly threads number). +\ +Added `/parallelism/`, that defines the *workers*, *jobs*, *workers pool*... for treating task with multithreading. +\ +Added `encrypt_chunk_job`, a new abstraction (same level as `encryption_flow`) used by multithreading. +\ +Added `/encryption/file_encryption/multithread.rs` that allows us to encrypt / decrypt file using this *pool*. +The architecture is : + +```mermaid +flowchart TD; + id1{{"Principal Thread"}} + id2("Reads the file, and split it in chunks.") + id3("Worker 1 [Treats the chunk's data]") + id4("Worker 2 [Treats the chunk's data]") + id5("Worker N [Treats the chunk's data]") + id6[["Result Vector"]] + id7(["Sorts the results"]) + id8(["Write the results in the output file, in the correct order"]) + + id1 --> id2 + id2 --> id3 + id2 --> id4 + id2 --> id5 + id3 --> id6 + id4 --> id6 + id5 --> id6 + id6 --> id7 + id7 --> id8 +``` +\ +Also, updated the format : `ENK1END` is now outside a chunk. +\ +Added `integration` tests (`encrypt_chunk_job`) and `unit` tests (`parallelism`). +\ +Test results : +```bash +test result: ok. 130 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 8.58s +``` + +I also did a little benchmark : +\ +**Setup**: Same 2 GB incompressible random file (/dev/urandom), NoComp compression, password key, isolated configs. 3 runs each, means reported. +\ +Modes : `Single` and `Multi` (8 threads) +\ +Measures : +- Encryption: 8.43s --> 2.67s (3.16x faster) +- Decryption: 8.08s --> 2.52s (3.21x faster) +- Overall: 16.5s --> 5.18s on the 2 GB file + +> [!NOTE] +> Multithreading gives ~3.2x on 8 threads rather than 8x. This is expected : the per-chunk XChaCha20 encryption work is small relative to the total 2 GB of disk I/O (read + write), which is the bottleneck and stays single-stream. The thread pool also serializes merging. Still, a solid ~3x win for both directions. + +We need, now, to : +- update READMEs (tests + repo's README) +- benchmark +- extend parallelism (for folder archives) + +--- + +## DAY-9 + +Modified frontend to enable the creation of tests (mock tests) : + +- `src/frontend/tui/input.rs` (new): public `TuiInput` trait + `RealTuiInput` (real inquire/rfd). All interactive reads route through it. +- `tui/mod.rs`: launch_ui(input) — main loop now drives input.select(); dispatch Encrypt/Params/Help/Browse/Exit. +- `tui/action.rs`: execute(input) threads input through; from_str unchanged. +- `tui/treatment.rs`: handle_object_treatment(input) + handle_object_treatment_with_password(input, pwd) seam (password bypasses the prompt for headless roundtrips). +- `tui/parameters.rs`: launch_params(input) using input.select/input.custom_counter for compression/key/parallelism/threads. +- `tui/browse.rs`: launch_browser(input) + public browse_files/browse_folders/browse_files_then_folders(input, password) using input.pick_files/pick_folders; shared treat_objects helper. +- `main.rs`: passes &RealTuiInput to launch_ui. + +Of course, addded new tests (*16*) : + +- `/mocks/tui_input.rs` +- `integration/tui_test.rs` +- `integration/tui_flow.rs` + +Also modified the `show_parameters` macro (now a function that also prints the *Parallelism Type*). +\ +\ +Created `/doc` (empty for now). + +> [!NOTE] +> I created a new / stylish progress bar for Enkryptit... but I found it so useful that I decided to make it an entire crate. +> The crate is [`gradient-bar`](github.com/VRAM-RAM/gradient-bar) + +Tests result : +```bash +test result: ok. 146 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.05s +``` + +Todo : Modify how folder encryption works (especially error handling & progress bar) + +--- + +## DAY-10 Added `Auto` compression type : Enkryptit resolves the compression to use. + +Added an `Auto` *CompressionType* : Enkryptit can infer the *CompressionType* to use, basing on : + +- The size of the file +- It's Mime extension + +Created `/context` (`context.rs` --> `/context`) +\ +Added tests & modified existing ones. +\ +Added the possibility to choose `Auto` in both *Cli* and *Tui*. +\ +Also added an `Auto` *ParallelismType*. +\ +Next steps : + +- Also add *Parallelism* inference, but for folders +- Implement *multithreading* in intern entry encryption +- Implement *multithreading* in folder encryption itself (entries in parallel) +- Added tests + +Test results : +```bash +test result: FAILED. 155 passed; 4 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.67s +``` + +--- + +## DAY-11 Fixed and issue with `Auto` CompressionType, added multithreading when encrypting/decrypting an entry of a folder + +Modifications : +- Fixed an issue with `CompressionType::Auto` resolution in `folder_encryption/` +- To fix it, and to make the architecture cleaner, created `/encryption/folder_encryption/collect_entry.rs` +- Refactorization of `folder_encryption/` : + - created a directory for entries (`entry/`) + - created `intern_archive_encryption` that will contain the code for both multithreading and singlethread encryption / decryption. + - simplified `decrypt_folder()` and `encrypt_folder()` : those functions will only resolve if the folder has to be treated with multithreading or singlethread, and then delegate the treatment to `de/encrypt_folder_single` or `de/encrypt_folder_multi` +- Added mulithreading when encrypting a folder (inside the entry encryption) : Now, `de/encrypt_folder_single` resolves the parallelism type and chooses between `encrypt_single_file_into_archive()/encrypt_multithreading_file_into_archive()` and `decrypt_single_file_from_archive()/decrypt_multithreading_file_from_archive()`. + +Tests results : +```bash +test result: ok. 159 passed; 2 failed; 1 ignored; 0 measured; 0 filtered out; finished in 12.36s +``` + +Todo : + +- Add a smooth skipping message / error system for folder encryption +- Add a logging system +- Implement folder multithreading encryption (multiple files at the same time) + +--- + +## DAY-12 Architectural changes of encryption, including modification of `intern_archive_encrytption`, creation of `encryption/chunk_job/` + +- Modified `encryption/folder_encryption/intern_archive_encryption/` : now contains, in `mod.rs`, the code for treating a `FileEntry`. It makes `folder_encryption/single.rs` much more simpler : + +```rust +use crate::{context::EnkryptitContext, encryption::{encrypt_chunk_job::{DecryptChunkJob, EncryptChunkJob}, folder_encryption::intern_archive_encryption::{treat_entry_decryption, treat_entry_encryption}}, errors::EnkryptitError, key::EnkryptitKey, metadatas::FileEntry, parallelism::pool::EnkryptitPool}; + +pub fn encrypt_folder_single(folder_path: &str, key: EnkryptitKey, entries: &mut Vec, offset: u64, context: &mut EnkryptitContext, archive_path: &str) -> Result<(), EnkryptitError> { + let mut current_offset = offset; + let mut pool: Option> = None; + + for entry in entries { + current_offset += treat_entry_encryption(&mut pool, folder_path, entry, current_offset, context, archive_path, &key)?; + } + + Ok(()) +} + +pub fn decrypt_folder_single(archive_path: &str, dest_folder: &str, entries: &Vec, key: EnkryptitKey, payload_offset: u64, version: u8, context: &mut EnkryptitContext) -> Result<(), EnkryptitError> { + let mut pool: Option> = None; + + for entry in entries { + treat_entry_decryption(&mut pool, version, dest_folder, &entry, context, archive_path, &key, payload_offset)?; + } + + Ok(()) +} +``` + +- Splitted `encryption/encrypt_chunk_job.rs` into `encryption/chunk_job/` that contains : + - `decrypt.rs` : `DecryptChunkJob` and its implementation. + - `encrypt.rs` : `EncryptChunkJob` and its implementation. + - `mod.rs` : **Module** file, that exposes two helpers for submitting a decryption job or an encryption job. + - `result.rs` : `ChunkResult` structure + +- Created `encryption/file.rs`, that contains an helper for reading & treating files. +- Modified almost every file in `/encryption/file_encryption/` and `/encryption/folder_encryption/` to match the new architectural changes. + +Test results: + +```bash +test result: ok. 159 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.96s +``` + +Todo : + +- Add a smooth skipping message / error system for folder encryption +- Add a logging system +- Continue implementing folder multithreading encryption (multiple files at the same time) + +--- + +## DAY-13 Added tests for testing all the architectural changes done since DAY-9 & changed the progress bar display + +Added : +1. **Auto parallelism** - test/unit/parallelism.rs: boundary tests for size-based inference (Single → MT 4/6/8/cpus at the 4 size gates), explicit non-Auto short-circuit, and path-based resolution on real/sparse files. +2. **chunk_job helpers** - submit_encrypt_chunk to submit_decrypt_chunk roundtrip through a real pool. +3. **collect_entry** - test/unit/folder_entries.rs (new): relative path + perms, root → DirectoryIsFolder, subdir → FileIsASymLink, symlink-to-file accepted. +4. **collect_folder_entries** - concrete per-entry compression (XML needs to return Zstd, WAV => Lz4, PNG => NoComp), empty folder, nested relative paths. +5. **read_file** - test/unit/file.rs (new): len, estimated_steps, zero-length, missing path. +6. **Folder metadata** — test/unit/metadatas.rs: FileEntry/FolderMetadata pack-unpack preserving the new per-entry compression field (field-by-field, since eq skips it). +7. **Auto** + **Auto** end-to-end regression — test/integration/folder_encryption.rs: mixed-content folder (XML/WAV/PNG + 55 MiB sparse file) roundtrips byte-exact; asserts offsets start at 65 and no raw Auto leaks into entry metadata. +8. **CLI** — `--compression auto` and `--parallelism` auto persist to config and display. + +Moved the `GradientProgressBar` creation from `encryption_flow` to outside of it. Now, it checks if we have a bar (`.is_some()`) and in that case, updates it. It allows us not to create a bar when encrypting / decrypting an entry of a folder in `intern_archive_encryption`. +\ +\ +Changed version from `0.0.2` to `0.0.3`. +\ +\ +Tests results : +```bash +test result: ok. 187 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.86s +``` + +I also choosed what to do next : I'll give up with multithreading folder encryption, because I think the I/O & RAM & Complexity (of the code) costs will be too high for a little of performance gains. Instead, we'll create : +- Cleanup & Polish (no more *eprintln!()*, logging system, smooth skipping, `--json` mode...) +- Ergonomic add-ons (`eck verify `, `eck inspect `, `eck recover `) +- Doc restructure (README + /doc, and later *mkdocs* maybe) +- cargo clippy / fuzzy testing +- benchmarks + +--- + +## DAY-14 ParallelismType maximum cpus modification and `eck inspect ` implementation + +- Modified `ParallelismType::Auto` min cpus (`8, 12 and 16` instead of `4, 6 and 8`) +- Moved `Output` enum from `frontend::cli` to `frontend` +- Fully implemented `eck inspect ` backend. +- Added a new `Output` : `Output::InspectionReport(InspectionReport)` +- Modified tests (parallelism tests) to match with the new max cpus in `ParallelismType::Auto` resolution. +- Added `frontend/cli/inspection.rs`, that contains an helper for inspecting object(s). +- Wired the cli with the **inspection**. + +Tests results : +```bash +test result: ok. 187 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 13.35s +``` + +Next things to do : +- Wire **inspection** to the *TUI* (and refactor a little the TUI) +- Cleanup `diplay()` for *InspectionReport* (raw ansi colors to constants (`const LABEL: u8 = ...`)) +- Cleanup & Polish (no more *eprintln!()*, logging system, smooth skipping, `--json` mode...) +- Ergonomic add-ons (`eck verify `, `eck recover `) +- Doc restructure (README + /doc, and later *mkdocs* maybe) +- cargo clippy / fuzzy testing +- benchmarks + +--- + +## DAY-15 Created `src/diagnostic` directory that will contain all the code for *reports*, *errors & errors rendering* and *logging* ; Modified frontend + +- Created `src/diagnostic` +- Created a new `Report` struct that uses `comfy-table` to render a clean table. +- Created `ReportArgument`, `EnkryptitStyle` and the display for the `Report` +- Implemented the usage of `Report` in the `display()` method of `InspectionReport` +- Modified **Frontend** (Now, we only have `Browse` instead of `Encrypt/Decrypt` & `Browse`. Also, `Browse` now redirects to `launch_treatment()` where the user chooses what to to with the file (*Encryption/Decryption* or *Inspection* for now)) +\ +\ +Concretely: +\ +\ +**Before :** +```txt +Inspection Report + +Type : Folder (Archive) encrypted with Enkryptit! +Version : 3 +Name : saves.encky +Directory : /Users/vram/Downloads +Size : 3346.58 KiB +Entries number : 9 +Key Type : hashed password, with the following salt : f0730000c8daf8de752f9fc10526bed5 +``` +\ +\ +**After :** +```txt +┌───────────────────┬─────────────────────────────────────────────────────────────────────────────┐ +│ Inspection Report ┆ │ +╞═══════════════════╪═════════════════════════════════════════════════════════════════════════════╡ +│ Type ┆ Folder (Archive) encrypted with Enkryptit! │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ Version ┆ 3 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ Name ┆ saves.encky │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ Directory ┆ /Users/vram/Downloads │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ Size ┆ 3346.58 KiB │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ Entries number ┆ 9 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ Key Type ┆ hashed password, with the following salt : f0730000c8daf8de752f9fc10526bed5 │ +└───────────────────┴─────────────────────────────────────────────────────────────────────────────┘ +``` +\ +\ +And in the code, conceptually: +\ +\ +**Before :** +```rust +println!("Inspection Report"); +println!(); +println!("{}{}", ...); +println!("{}{}", ...); +println!("{}{}", ...); +``` +\ +\ +**After :** +```rust +let mut report = Report::new(...) + .field(...) + .field(...) + ... + .display() +``` +\ +\ +Everything works, except tests (*error: could not compile `eck` (test "eck_tests") due to 6 previous errors*). This is perfectly normal since I removed direct encryption/decryption in `EnkryptitTuiAction`. +\ +\ +Next things to do : +- fix tests and add new ones +- Cleanup & Polish (no more *eprintln!()*, logging system, smooth skipping, `--json` mode...) in `diagnostic/`, using `comfy-table` and `tracing` +- Ergonomic add-ons (`eck verify `, `eck recover `) +- Doc restructure (README + /doc, and later *mkdocs* maybe) +- cargo clippy / fuzzy testing +- benchmarks + +--- + +## DAY-16 Updated TUI tests, huge frontend and error / outputs handling refactorization + +- Modified `tui_flow.rs` and `tui_tests.rs` that now use directly `treat_object_encryption()` +- Progress on **RustDoc** +- Fixed an issue with rfd `0.17.2` (read the issue for more infos) : [rfd and hyprland + NixOS issue](https://github.com/VRAM-RAM/Enkryptit/issues/1) +- Created `src/directory.rs` that contains two helpers that return the project's config directory & config path +- Added an `EnkryptitLogger` structure that initializes `tracing` logging, allows the program to write logs in `config_dir/log.txt`, and rolls daily. +- Call of `EnkryptitLogger::init()` in `main()` +- Added `Option` in **InspectionReport** so that it doesn't fail on error and just return an empty field (for name for example) +- Modified `display()` implementation for **InspectionReport** : it now checks if at least one field isn't empty. If not, it displays an error, and if yes, it displays the report. +- Updated `inspect_encrypted_archive()`, `inspect_plain_file()`, `inspect_plain_folder()` and `inspect_encrypted_file()` to match the new **InspectionReport** format. +- Added `tracing::warn!(...)` in some functions to log backend informations (in `log.txt`) for advanced users +- Splitted `type.rs` in `type/` (to make navigating in the code easier). +- Frontend refactorization : + - Suppressed `Output` + - Created `EnkryptitOutput`, a new structure that handles *successes*, *errors*, *warnings* and *infos*. + - Created `EnkryptitOutputKind`, an enum for `EnkryptitOutput`s + - Implemented `display()` method for `EnkryptitOutput` that uses `miette` for *errors* and *warnings*, creates a **badge** with borders for *successes*, and a simple *info* displaying. + - Created `Snippet` : A context to draw a miette source frame around an error. + - Created `EnkryptitDiagnostic` : A **diagnostic** structure that implements `miette::Diagnostic` and allows use to render arrows around the offending token when `Snippet` is provided. + - Wired all the frontend : **TUI** & **CLI** with the new architecture. + - Modified `object_treatment()`, `en/decrypt_file_case()` and `en/decrypt_folder_case()` to adapt to the new **EnkryptitOutput** + - Modified `read_file()` function from `treatment/` : it now returns an **EnkryptitOutput** and maps the intern *EnkryptitError*s + +> [!NOTE] +> This Day wasn't the most significative for Enkryptit!'s backend, but was really significative for frontend, and took me a long time (especially to wire everything). + +**New Success message:** +```txt +╭──────────────────────────────────────────────────────────╮ +│ ✔ File was encrypted at │ +│ /home/vram/Downloads/secret.zip.encky │ +╰──────────────────────────────────────────────────────────╯ +``` +\ +\ +**New Error message:** +```txt +io::misc_io_error (link) + + ✖ io error: No such file or directory (os error 2) + ╭─[eck:1:5] + 1 │ eck this_directory_is_absurd + · ────────────┬─────────── + · ╰── I/O + ╰──── + help: Check that the path exists and that you have the required permissions. +``` +\ +\ +New tests: +- `/unit/error_display.rs` : Error display & formatting tests +- `/unit/types.rs` : Regression tests for the `Display`/`String` conversions of the enum types. +Test results : +```bash +test result: FAILED. 199 passed; 2 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.53s +``` +\ +\ +Next things to do : +- In priority : + - update tests + - update the rust doc +- Ergonomic add-ons (`eck verify `, `eck recover `) +- Doc restructure (README + /doc, and later *mkdocs* maybe) +- cargo clippy / fuzzy testing +- benchmarks + +--- + +## DAY-17 Updated tests + +- Updated tests (replaced the `stderr` with `stdout` essentially) +- Progressed on **RustDoc** +- Modified `EnkryptitContext`. It doesn't need `Interface` anymore, so I deleted `Interface`. +- Fixed warnings and made the code slightly better (calling `cargo clippy` for report) +- Audited dependencies using `cargo audit` (see results in [/dev/audit.md](./dev/audit.md)) +- Created `/dev` instead, that contains `audit.md` (the audit result) +- Updated dependencies +- Created fuzzy testing for compression and metadata. +- Updated `Cargo.lock` for every binary +- Added tests for warning and error displaying +- Created `cargo xtask` and `eck`. Enkryptit! is now a workspace. +- `cargo flamegraph` on an intense Enkryptit! usage (inspection + encryption of 13.5 GiB file with `Multithreading(48)` and `NoComp`). The **graph** is available at [`/dev/flamegraph-intense_usage-release.svg`](./dev/flamegraph-intense-usage-release.svg). +- Updated **Inspection** reports, which are now much more compacts : +```txt +┌───────────────────┬─────────────────────────────────────────────────────────────────────────────┐ +│ Inspection Report ┆ │ +╞═══════════════════╪═════════════════════════════════════════════════════════════════════════════╡ +│ Type ┆ Folder (Archive) encrypted with Enkryptit! │ +│ Version ┆ 3 │ +│ Name ┆ saves.encky │ +│ Directory ┆ /Users/axel/Downloads │ +│ Size ┆ 3346 KiB │ +│ Entries number ┆ 9 │ +│ Key Type ┆ hashed password, with the following salt : f0730000c8daf8de752f9fc10526bed5 │ +└───────────────────┴─────────────────────────────────────────────────────────────────────────────┘ +``` +\ +\ +Test results: +```bash +test result: ok. 204 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.49s +``` +\ +\ +Next things to do : +- Doc restructure (*mkdocs* maybe) +- finish fuzzy testing +- benchmarks + +--- + +## DAY-18 Updated doc, cargo clippy, added audit.toml + +- Added `/doc` in french and english +- Updated `README` +- added `audit.toml` +- added `.github` for dependabot and **workflows** +- Fixed and issue with the **success** border. +- Added new tests + +Test results : +```bash +test result: ok. 213 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.95s +``` \ No newline at end of file diff --git a/README.md b/README.md index 284ede1..f1c9784 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,217 @@ # Enkryptit! -\ + 🔵 **Human-written** -\ -\ -![Enkryptit](https://img.shields.io/badge/enkryptit-rust-orange?style=for-the-badge&logo=rust) -![Version](https://img.shields.io/github/v/release/VRAM-RAM/enkryptit?style=for-the-badge) -![License](https://img.shields.io/badge/license-CeCILL--B%20%2F%20Apache%202.0-blue?style=for-the-badge) -**Enkryptit!** is a Rust-written cli-tool / interactive manager for file and folder encryption. +![Enkryptit](https://img.shields.io/badge/enkryptit-rust-orange?style=for-the-badge\&logo=rust) ![License](https://img.shields.io/badge/license-CeCILL--B%20%2F%20Apache%202.0-blue?style=for-the-badge) + +**Enkryptit! - A fast, simple file & folder encryption manager, written in Rust** + + +
+ Enkryptit! gif as example +
> [!WARNING] > This project is currently a work in progress. It is **not audited** for production security. +> Os' keyring usage is unstable for now. + +## Table of contents + +* [Quick Start](#quick-start) +* [Usage & Commands](#usage--commands) + * [CLI Tool](#cli-tool) + * [TUI](#tui) + * [Examples](#examples) +* [Why Enkryptit! ?](#why-enkryptit-) +* [Documentation](#documentation) +* [Development](#development) +* [License](#license) ## Quick Start ### Prerequisites -* **Rust:** `1.75+` is recommended. -* A working native credential store (e.g., `libsecret` on Linux, native Keychain on macOS). +* **Rust:** `1.85+` is recommended. +* A working native credential store (e.g. `libsecret` on Linux, native Keychain on macOS). -### Build and Launch +### Using **cargo** + +If you have `cargo` installed on your system, run : +```bash +cargo install eck +``` + +### Building and Launching using the source code ```bash # Clone the repository git clone https://github.com/VRAM-RAM/Enkryptit -cd Enkryptit -#Build : -cargo build --release +cd Enkryptit/ + +# Build +cargo xtask build --release -#Install in your current path : -cargo install --path . +# Or Install +cargo xtask install ``` > [!NOTE] -> The compilation time may be long, since I activated an agressive release profile. +> Compilation may take some time because Enkryptit uses an aggressive release profile. ## Usage & Commands -Enkryptit! can be run by two ways : +Enkryptit! can be used in two ways: as a CLI tool or as a TUI. ### CLI Tool -First, Enkryptit! can be run as a CLI tool. This are available commands : - - -| Command | Syntax | Description | -| --- | --- | --- | -| **Help** | `eck help` | Displays the available commands menu. | -| **Encrypt / Decrypt** | `eck /path/to/file` | Toggles encryption or decryption for the specified file. | -| **Encrypt / Decrypt with a password** | `eck /path/to/file -p mypassword` | Toggles encryption or decryption for the specified file, with a password. | -| **Open TUI** | `eck ui` or `eck` | Opens the TUI | -| **Show current params** | `eck parameters` or `eck params` | Shows current params | -| **Change compression** | `eck params (or parameters) -c (or --compression) compressiontype` | Changes compression algorithm. Available : zstd, lz4 and xz | -| **Change key type** | `eck params (or parameters) -kt (or --keytype) keytype` | Changes key type. Available : os, file and pwd (or password) | +These are the available commands: + +| Command | Syntax | Description | +| ------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | +| **Encrypt / Decrypt** | `eck ` | Automatically encrypts plaintext or decrypts `.encky` files. | +| **Encrypt / Decrypt with a password** | `eck -p ` | Encrypts or decrypts using the specified password. | +| **Open TUI** | `eck ui` or `eck` | Opens the interactive TUI. | +| **Show current parameters** | `eck parameters` or `eck params` | Shows the current parameters. | +| **Change compression** | `eck params -c ` | Changes the compression algorithm. Available: `zstd`, `lz4`, `xz`, `none`, and `auto`. | +| **Change key type** | `eck params -k ` | Changes the key type. Available: `os`, `file`, and `pwd` / `password`. | +| **Change parallelism** | `eck params -p ` | Changes the parallelism mode. Available: `single`, `multi`, or `multi:`. | +| **Inspect** | `eck inspect ` | Inspects a file or archive without encrypting or decrypting it. | + +> [!TIP] +> `eck ` and `eck inspect ` support multiple paths: +> +> `eck ` +> +> `eck inspect myfolder/*` ### TUI -Enkryptit! can also be run as a TUI. It offers a more "friendly" interface. To open the TUI, run : +Enkryptit! also provides an interactive terminal user interface. + +Launch it with: ```bash eck ``` -or + +or: ```bash eck ui ``` -You will see a menu, in which one you will be able to navigate : - +It will open : ```bash Enkryptit - Fast & Secure File Encryption Manager v1 + Fast & Simple File Encryption Manager v0.0.3 ? What do you want to do? -> Encrypt/Decrypt file +> Browse Parameters Help Exit [↑↓ to move, enter to select, type to filter] ``` +The Tui allows you to directly **browse** your files / folders. + ### Examples -For example, if you want to encrypt a file named `secrets.txt` in `/home/user/secrets/`, you just have to run : +Encrypt a file: + ```bash eck /home/user/secrets/secrets.txt ``` -Then, to decrypt the encrypted file as `secrets.txt.encky`, you run barely the same command : + +This creates: + +```text +secrets.txt.encky +``` + +To decrypt it: ```bash eck /home/user/secrets/secrets.txt.encky ``` -To encrypt a folder, you need to use the same `eck` command, but to specify a folder path : +Encrypt a folder: ```bash eck /home/user/secrets/ ``` -This will encrypt your folder and the files it contains inside a file named `secrets.encky`. To decrypt it, simply run : +This creates: + +```text +secrets.encky +``` + +To decrypt it: ```bash eck /home/user/secrets.encky ``` -## Cryptographic Stack +Multiple files and folders can be processed at once: + +```bash +eck /home/user/secrets/* /home/user/secret.txt /lib/secret.bin +``` + +Inspect a file without decrypting it: + +```bash +eck inspect /home/user/my_secret.txt +``` + +## Why Enkryptit! ? -Enkryptit! relies on those primitives for its architecture: +* **Modern primitives** — XChaCha20-Poly1305 provides authenticated encryption, protecting encrypted data against unauthorized modification as well as unauthorized access. -* **Encryption:** `XChaCha20-Poly1305` — Symmetric authenticated encryption (AEAD) with a 192-bit nonce. -* **Key Derivation:** `Argon2id` — The industry-standard password hashing algorithm, built to resist GPU/ASIC brute-force attacks. -* **Key Storage:** `Keyring` — Securely delegates key management to your operating system's native credential store (Keychain, Secret Service, etc.). -* **Data Serialization:** `Postcard` — A lightweight, efficient binary serialization format optimized for Rust. +* **Flexible key management** — use a password, the OS keyring, or a dedicated key file depending on your workflow. -## Change parameters +* **Built for large files** — chunked streaming encryption avoids requiring the entire file to be loaded into memory. -You have the ability to choose your compression, key type, encryption algorithm, Argon2id parameters with **Enkryptit!**. By default, the choosen parameters are : +* **Automatic performance optimization** — compression and parallelism can be infered automatically based on the file and its size. -- Keytype : Password -- Compression : Zstd +* **First-class folder encryption** - encrypt an entire directory into a single `.encky` archive while preserving its structure and Unix permissions. -## RoadMap +* **Security-conscious secret handling** — keys are memory-locked and zeroized when no longer needed. -Next steps would be : +* **CLI + TUI** — use a fast, scriptable CLI or an interactive terminal interface without needing separate applications. -- parallelization -- benchmarks -- Doc + Dev Doc -- Make the interface more ergonomic (add a `Browse Files & Folders`, modify how metadata works...) +* **Transparent inspection** — `eck inspect` can examine encrypted files and archives without decrypting their contents. + +* **Extensively tested** — 199+ tests cover cryptography, key handling, tamper detection, folder encryption, multithreading, CLI behavior, and TUI flows. + +**In short:** Enkryptit combines **strong authenticated encryption, flexible key management, efficient large-file processing, automatic compression and parallelism, and a usable CLI/TUI** in one native Rust application. + +## Documentation + +The full documentation lives in the [`doc/`](./doc/) folder: + +* **User guide** — bilingual English & French: + * [Installation](./doc/user-guide/en/installation.md) · [Command line](./doc/user-guide/en/cli.md) · [Terminal UI](./doc/user-guide/en/tui.md) + * [Errors & output](./doc/user-guide/en/errors.md) · [`.encky` format](./doc/user-guide/en/format.md) + * Français : [installation](./doc/user-guide/fr/installation.md), [ligne de commande](./doc/user-guide/fr/cli.md), [interface](./doc/user-guide/fr/tui.md), [erreurs](./doc/user-guide/fr/errors.md), [format](./doc/user-guide/fr/format.md) +* **Developer guide** (English): [start here](./doc/dev/README.md) — architecture, modules, testing, fuzzing, security. + +## Development + +**Enkryptit!** contains : +- Its binary, in `eck/` +- `cargo xtask` for launching tests, fuzzing and rustdoc + +You can follow the development in [**PROGRESSION.md**](./PROGRESSION.md). + +You can also help the development by [**contributing**](CONTRIBUTING.md)! ## License This project is dual-licensed under: -* **CeCILL-B License** (French law compliant, fully compatible with GNU GPL/Apache) +* **CeCILL-B License** (French-law compliant) * **Apache License, Version 2.0** -Choose the one that best fits your needs. - -## Contact - -* **Developer:** Olruix ([VRAM-RAM](https://github.com/VRAM-RAM)) +Choose the license that best fits your needs. diff --git a/assets/eck.gif b/assets/eck.gif new file mode 100644 index 0000000..a84b4e8 Binary files /dev/null and b/assets/eck.gif differ diff --git a/audit.toml b/audit.toml new file mode 100644 index 0000000..e1a6edf --- /dev/null +++ b/audit.toml @@ -0,0 +1,8 @@ +[advisories] +# Allowlist (mirrors dev/audit.md). All three are transitive, unmaintained, +# non-crypto dependencies and are not in the security-critical path. +ignore = [ + "RUSTSEC-2023-0089", # atomic-polyfill (unmaintained) + "RUSTSEC-2024-0384", # instant (unmaintained) + "RUSTSEC-2024-0388", # derivative (unmaintained) +] \ No newline at end of file diff --git a/dev/audit.md b/dev/audit.md new file mode 100644 index 0000000..04d86b5 --- /dev/null +++ b/dev/audit.md @@ -0,0 +1,49 @@ +# Dependency audit + +`cargo audit` is run in CI (`ci.yml`). The allowlist lives at the repo root in +**`audit.toml`** (git-tracked) and **must be kept in sync with this page**. + +## Current advisories allowed + +| ID | Crate | Kind | Reason to allow | +| -- | ----- | ---- | --------------- | +| `RUSTSEC-2023-0089` | `atomic-polyfill` | unmaintained | Transitive dep, no exploitable path used by the crate. | +| `RUSTSEC-2024-0384` | `instant` | unmaintained | Transitive dep, no security impact on the crypto or file handling. | +| `RUSTSEC-2024-0388` | `derivative` | unmaintained | Transitive dev/tooling dep only. | + +Rules: + +* Do **not** extend the allowlist for direct or security-relevant dependencies. +* Remove an entry as soon as the dependency is no longer pulled in. +* Re-run `cargo audit` locally after any dependency change: + `cargo audit` (uses `./audit.toml` automatically). + +## Reference output (audited with the allowlist applied) + +```bash +Crate: atomic-polyfill +Version: 1.0.3 +Warning: unmaintained +Title: atomic-polyfill is unmaintained +Date: 2023-07-11 +ID: RUSTSEC-2023-0089 +URL: https://rustsec.org/advisories/RUSTSEC-2023-0089 + +Crate: derivative +Version: 2.2.0 +Warning: unmaintained +Title: `derivative` is unmaintained; consider using an alternative +Date: 2024-06-26 +ID: RUSTSEC-2024-0388 +URL: https://rustsec.org/advisories/RUSTSEC-2024-0388 + +Crate: instant +Version: 0.1.13 +Warning: unmaintained +Title: `instant` is unmaintained +Date: 2024-09-01 +ID: RUSTSEC-2024-0384 +URL: https://rustsec.org/advisories/RUSTSEC-2024-0384 + +warning: 3 allowed warnings found +``` \ No newline at end of file diff --git a/dev/flamegraph-intense-usage-release.svg b/dev/flamegraph-intense-usage-release.svg new file mode 100644 index 0000000..abc905e --- /dev/null +++ b/dev/flamegraph-intense-usage-release.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch ext4_ext_rm_idx (5,343,514 samples, 0.02%)ext4_free_blocks (5,343,514 samples, 0.02%)ext4_mb_clear_bb (5,343,514 samples, 0.02%)__mark_inode_dirty (5,343,514 samples, 0.02%)ext4_dirty_inode (5,343,514 samples, 0.02%)__ext4_mark_inode_dirty (5,343,514 samples, 0.02%)[eck] (10,766,708 samples, 0.03%)__libc_open64 (10,766,708 samples, 0.03%)entry_SYSCALL_64_after_hwframe (10,766,708 samples, 0.03%)do_syscall_64 (10,766,708 samples, 0.03%)x64_sys_call (10,766,708 samples, 0.03%)__x64_sys_openat (10,766,708 samples, 0.03%)do_sys_openat2 (10,766,708 samples, 0.03%)do_file_open (10,766,708 samples, 0.03%)path_openat (10,766,708 samples, 0.03%)do_open (10,766,708 samples, 0.03%)do_truncate (10,766,708 samples, 0.03%)notify_change (10,766,708 samples, 0.03%)ext4_setattr (10,766,708 samples, 0.03%)ext4_truncate (10,766,708 samples, 0.03%)ext4_ext_truncate (10,766,708 samples, 0.03%)ext4_ext_remove_space (10,766,708 samples, 0.03%)ext4_ext_rm_leaf (10,766,708 samples, 0.03%)ext4_remove_blocks (5,423,194 samples, 0.02%)ext4_free_blocks (5,423,194 samples, 0.02%)ext4_mb_clear_bb (5,423,194 samples, 0.02%)ext4_mb_mark_context (5,423,194 samples, 0.02%)mb_clear_bits (5,423,194 samples, 0.02%)_copy_to_iter (36,917,174 samples, 0.12%)alloc_anon_folio (4,926,605 samples, 0.02%)vma_alloc_folio_noprof (4,926,605 samples, 0.02%)alloc_pages_mpol (4,926,605 samples, 0.02%)asm_exc_page_fault (9,853,210 samples, 0.03%)exc_page_fault (9,853,210 samples, 0.03%)do_user_addr_fault (9,853,210 samples, 0.03%)handle_mm_fault (9,853,210 samples, 0.03%)__handle_mm_fault (9,853,210 samples, 0.03%)handle_pte_fault (9,853,210 samples, 0.03%)do_anonymous_page (9,853,210 samples, 0.03%)folio_add_lru_vma (4,926,605 samples, 0.02%)folio_add_lru (4,926,605 samples, 0.02%)__folio_batch_add_and_move (4,926,605 samples, 0.02%)folio_batch_move_lru (4,926,605 samples, 0.02%)lru_add (4,926,605 samples, 0.02%)lru_gen_add_folio (4,926,605 samples, 0.02%)mod_memcg_lruvec_state (4,926,605 samples, 0.02%)_raw_spin_lock_irqsave (5,309,103 samples, 0.02%)tick_do_update_jiffies64 (4,615,752 samples, 0.01%)update_wall_time (4,615,752 samples, 0.01%)__timekeeping_advance (4,615,752 samples, 0.01%)ntp_tick_length (4,615,752 samples, 0.01%)perf_event_task_tick (5,265,068 samples, 0.02%)perf_adjust_freq_unthr_context (5,265,068 samples, 0.02%)perf_adjust_freq_unthr_events (5,265,068 samples, 0.02%)perf_ibs_stop (5,265,068 samples, 0.02%)task_tick_fair (5,261,422 samples, 0.02%)update_curr (5,261,422 samples, 0.02%)__hrtimer_run_queues (25,999,537 samples, 0.08%)tick_nohz_handler (20,557,160 samples, 0.07%)update_process_times (15,941,408 samples, 0.05%)sched_tick (15,941,408 samples, 0.05%)update_cfs_group (5,414,918 samples, 0.02%)_copy_to_iter (9,413,183,149 samples, 30.18%)_copy_to_iterasm_sysvec_apic_timer_interrupt (36,741,668 samples, 0.12%)sysvec_apic_timer_interrupt (36,741,668 samples, 0.12%)__sysvec_apic_timer_interrupt (36,741,668 samples, 0.12%)hrtimer_interrupt (31,432,565 samples, 0.10%)tick_program_event (5,433,028 samples, 0.02%)clockevents_program_event (5,433,028 samples, 0.02%)copy_page_to_iter (9,688,401,244 samples, 31.06%)copy_page_to_iterpage_copy_sane (5,250,786 samples, 0.02%)filemap_get_pages (21,383,976 samples, 0.07%)filemap_get_read_batch (21,383,976 samples, 0.07%)xas_load (5,409,799 samples, 0.02%)[eck] (9,799,717,762 samples, 31.42%)[eck]__libc_read (9,788,951,054 samples, 31.39%)__libc_read__libc_read (9,788,951,054 samples, 31.39%)__libc_readentry_SYSCALL_64_after_hwframe (9,788,951,054 samples, 31.39%)entry_SYSCALL_64_after_hwframedo_syscall_64 (9,788,951,054 samples, 31.39%)do_syscall_64x64_sys_call (9,788,949,006 samples, 31.39%)x64_sys_call__x64_sys_read (9,788,949,006 samples, 31.39%)__x64_sys_readksys_read (9,788,949,006 samples, 31.39%)ksys_readvfs_read (9,788,949,006 samples, 31.39%)vfs_readext4_file_read_iter (9,788,949,006 samples, 31.39%)ext4_file_read_itergeneric_file_read_iter (9,788,949,006 samples, 31.39%)generic_file_read_iterfilemap_read (9,788,949,006 samples, 31.39%)filemap_readsrso_alias_safe_ret (16,296,048 samples, 0.05%)bio_split_rw (5,262,525 samples, 0.02%)asm_common_interrupt (5,262,525 samples, 0.02%)common_interrupt (5,262,525 samples, 0.02%)__common_interrupt (5,262,525 samples, 0.02%)handle_edge_irq (5,262,525 samples, 0.02%)handle_irq_event (5,262,525 samples, 0.02%)__handle_irq_event_percpu (5,262,525 samples, 0.02%)nvme_irq (5,262,525 samples, 0.02%)nvme_pci_complete_batch (5,262,525 samples, 0.02%)nvme_unmap_data (5,262,525 samples, 0.02%)dma_iova_destroy (5,262,525 samples, 0.02%)__iommu_dma_iova_unlink (5,262,525 samples, 0.02%)iommu_unmap_fast (5,262,525 samples, 0.02%)__iommu_unmap (5,262,525 samples, 0.02%)amdv1_unmap_range (5,262,525 samples, 0.02%)__unmap_range (5,262,525 samples, 0.02%)__unmap_range (5,262,525 samples, 0.02%)_pt_iter_first.constprop.0 (5,262,525 samples, 0.02%)asm_common_interrupt (4,706,845 samples, 0.02%)common_interrupt (4,706,845 samples, 0.02%)__common_interrupt (4,706,845 samples, 0.02%)handle_edge_irq (4,706,845 samples, 0.02%)handle_irq_event (4,706,845 samples, 0.02%)__handle_irq_event_percpu (4,706,845 samples, 0.02%)nvme_irq (4,706,845 samples, 0.02%)nvme_pci_complete_batch (4,706,845 samples, 0.02%)blk_mq_end_request_batch (4,706,845 samples, 0.02%)ktime_get (4,706,845 samples, 0.02%)__blk_mq_get_tag (5,857,082 samples, 0.02%)__sbitmap_queue_get (5,857,082 samples, 0.02%)sbitmap_get (5,857,082 samples, 0.02%)sbitmap_find_bit (5,857,082 samples, 0.02%)__blk_mq_alloc_requests (15,173,717 samples, 0.05%)blk_mq_get_tag (10,466,872 samples, 0.03%)__sbitmap_queue_get (4,609,790 samples, 0.01%)asm_sysvec_apic_timer_interrupt (6,374,757 samples, 0.02%)sysvec_apic_timer_interrupt (6,374,757 samples, 0.02%)__sysvec_apic_timer_interrupt (6,374,757 samples, 0.02%)hrtimer_interrupt (6,374,757 samples, 0.02%)__hrtimer_run_queues (6,374,757 samples, 0.02%)tick_nohz_handler (6,374,757 samples, 0.02%)irq_work_tick (6,374,757 samples, 0.02%)__bio_clone (10,768,411 samples, 0.03%)bio_clone_blkg_association (4,393,654 samples, 0.01%)bio_associate_blkg_from_css (4,393,654 samples, 0.01%)bio_associate_blkg (5,231,920 samples, 0.02%)__rcu_read_unlock (5,231,920 samples, 0.02%)mempool_alloc_noprof (11,999,938 samples, 0.04%)mempool_alloc_slab (5,133,368 samples, 0.02%)kmem_cache_alloc_noprof (5,133,368 samples, 0.02%)bio_alloc_bioset (26,541,595 samples, 0.09%)mempool_alloc_slab (4,359,832 samples, 0.01%)bio_split (41,781,419 samples, 0.13%)srso_alias_safe_ret (4,471,413 samples, 0.01%)bio_split_rw (45,823,065 samples, 0.15%)bio_submit_split (45,823,065 samples, 0.15%)bio_submit_split_bioset (45,823,065 samples, 0.15%)submit_bio_noacct_nocheck (4,041,646 samples, 0.01%)__rcu_read_lock (4,392,659 samples, 0.01%)dma_iova_link (4,990,903 samples, 0.02%)iommu_map_nosync (4,990,903 samples, 0.02%)dma_iova_sync (4,590,445 samples, 0.01%)blk_rq_dma_map_iter_start (20,313,073 samples, 0.07%)blk_dma_map_iter_start (14,357,852 samples, 0.05%)dma_iova_try_alloc (4,776,504 samples, 0.02%)iommu_dma_alloc_iova (4,776,504 samples, 0.02%)alloc_iova_fast (4,776,504 samples, 0.02%)nvme_map_data (41,744,044 samples, 0.13%)nvme_pci_setup_data_prp (21,430,971 samples, 0.07%)blk_add_rq_to_plug (53,066,922 samples, 0.17%)blk_mq_flush_plug_list (53,066,922 samples, 0.17%)blk_mq_dispatch_queue_requests (53,066,922 samples, 0.17%)nvme_queue_rqs (48,674,263 samples, 0.16%)nvme_prep_rq.part.0 (48,674,263 samples, 0.16%)srso_alias_safe_ret (6,930,219 samples, 0.02%)ext4_io_submit (125,019,775 samples, 0.40%)submit_bio (125,019,775 samples, 0.40%)submit_bio_noacct (125,019,775 samples, 0.40%)submit_bio_noacct_nocheck (125,019,775 samples, 0.40%)__submit_bio (125,019,775 samples, 0.40%)blk_mq_submit_bio (119,757,250 samples, 0.38%)blk_mq_flush_plug_list (5,693,546 samples, 0.02%)bio_add_folio (14,340,487 samples, 0.05%)__folio_start_writeback (5,828,939 samples, 0.02%)bh_get_inode_and_lblk_num (3,906,487 samples, 0.01%)bio_add_folio (5,212,778 samples, 0.02%)bio_add_page (5,212,778 samples, 0.02%)bh_get_inode_and_lblk_num (19,348,391 samples, 0.06%)folio_mapping (9,590,045 samples, 0.03%)folio_mapping (5,212,778 samples, 0.02%)fscrypt_mergeable_bio (5,612,450 samples, 0.02%)fscrypt_mergeable_bio_bh (49,690,671 samples, 0.16%)srso_alias_safe_ret (4,567,728 samples, 0.01%)srso_alias_safe_ret (5,147,820 samples, 0.02%)ext4_bio_write_folio (185,327,185 samples, 0.59%)wbc_account_cgroup_owner (9,869,109 samples, 0.03%)fscrypt_mergeable_bio_bh (8,616,818 samples, 0.03%)mpage_map_and_submit_buffers (246,334,495 samples, 0.79%)mpage_submit_folio (224,766,462 samples, 0.72%)wbc_account_cgroup_owner (16,481,972 samples, 0.05%)asm_common_interrupt (3,943,870 samples, 0.01%)common_interrupt (3,943,870 samples, 0.01%)__common_interrupt (3,943,870 samples, 0.01%)handle_edge_irq (3,943,870 samples, 0.01%)handle_irq_event (3,943,870 samples, 0.01%)fast_mix (3,943,870 samples, 0.01%)ext4_mb_mark_diskspace_used (5,192,628 samples, 0.02%)ext4_mb_mark_context (5,192,628 samples, 0.02%)mpage_map_and_submit_extent (256,756,658 samples, 0.82%)mpage_map_one_extent (10,422,163 samples, 0.03%)ext4_map_blocks (10,422,163 samples, 0.03%)ext4_map_create_blocks (10,422,163 samples, 0.03%)ext4_ext_map_blocks (10,422,163 samples, 0.03%)ext4_mb_new_blocks (10,422,163 samples, 0.03%)ext4_mb_regular_allocator (5,229,535 samples, 0.02%)ext4_mb_find_by_goal (5,229,535 samples, 0.02%)ext4_mb_load_buddy_gfp (5,229,535 samples, 0.02%)__filemap_get_folio_mpol (5,229,535 samples, 0.02%)filemap_get_entry (5,229,535 samples, 0.02%)xas_load (5,229,535 samples, 0.02%)__blk_flush_plug (4,041,646 samples, 0.01%)blk_mq_flush_plug_list (4,041,646 samples, 0.01%)blk_mq_dispatch_list (4,041,646 samples, 0.01%)blk_mq_insert_requests (4,041,646 samples, 0.01%)blk_mq_run_hw_queue (4,041,646 samples, 0.01%)blk_mq_delay_run_hw_queue (4,041,646 samples, 0.01%)kblockd_mod_delayed_work_on (4,041,646 samples, 0.01%)mod_delayed_work_on (4,041,646 samples, 0.01%)__queue_delayed_work (4,041,646 samples, 0.01%)__queue_work (4,041,646 samples, 0.01%)__queue_work.part.0 (4,041,646 samples, 0.01%)kick_pool (4,041,646 samples, 0.01%)wake_up_process (4,041,646 samples, 0.01%)try_to_wake_up (4,041,646 samples, 0.01%)ttwu_do_activate (4,041,646 samples, 0.01%)wakeup_preempt (4,041,646 samples, 0.01%)wakeup_preempt_fair (4,041,646 samples, 0.01%)srso_alias_safe_ret (4,041,646 samples, 0.01%)pick_next_task (5,446,960 samples, 0.02%)pick_next_task_fair (5,446,960 samples, 0.02%)sched_balance_newidle (5,446,960 samples, 0.02%)__sched_balance_update_blocked_averages (5,446,960 samples, 0.02%)__folio_lock (19,028,526 samples, 0.06%)folio_wait_bit_common (19,028,526 samples, 0.06%)io_schedule (19,028,526 samples, 0.06%)schedule (14,986,880 samples, 0.05%)__schedule (14,986,880 samples, 0.05%)try_to_block_task.constprop.0.isra.0 (9,539,920 samples, 0.03%)dequeue_task (9,539,920 samples, 0.03%)dequeue_task_fair (4,933,714 samples, 0.02%)dequeue_entities (4,933,714 samples, 0.02%)dequeue_entity (4,933,714 samples, 0.02%)update_curr (4,933,714 samples, 0.02%)dl_server_update (4,933,714 samples, 0.02%)__close (510,397,211 samples, 1.64%)entry_SYSCALL_64_after_hwframe (510,397,211 samples, 1.64%)do_syscall_64 (510,397,211 samples, 1.64%)x64_sys_call (510,397,211 samples, 1.64%)__x64_sys_close (510,397,211 samples, 1.64%)fput_close_sync (510,397,211 samples, 1.64%)__fput (510,397,211 samples, 1.64%)ext4_release_file (510,397,211 samples, 1.64%)ext4_alloc_da_blocks (510,397,211 samples, 1.64%)filemap_flush (510,397,211 samples, 1.64%)filemap_writeback (510,397,211 samples, 1.64%)do_writepages (510,397,211 samples, 1.64%)ext4_writepages (510,397,211 samples, 1.64%)ext4_do_writepages (510,397,211 samples, 1.64%)mpage_prepare_extent_to_map (128,620,778 samples, 0.41%)mpage_process_page_bufs (109,592,252 samples, 0.35%)asm_common_interrupt (4,747,997 samples, 0.02%)common_interrupt (4,747,997 samples, 0.02%)__common_interrupt (4,747,997 samples, 0.02%)handle_edge_irq (4,747,997 samples, 0.02%)handle_irq_event (4,747,997 samples, 0.02%)__handle_irq_event_percpu (4,747,997 samples, 0.02%)nvme_irq (4,747,997 samples, 0.02%)nvme_pci_complete_batch (4,747,997 samples, 0.02%)nvme_unmap_data (4,747,997 samples, 0.02%)dma_iova_destroy (4,747,997 samples, 0.02%)__iommu_dma_iova_unlink (4,747,997 samples, 0.02%)iommu_dma_free_iova (4,747,997 samples, 0.02%)_raw_spin_lock_irqsave (4,747,997 samples, 0.02%)__raw_spin_lock_irqsave (4,747,997 samples, 0.02%)fpregs_assert_state_consistent (5,158,572 samples, 0.02%)_copy_from_iter (9,647,510 samples, 0.03%)down_read (5,202,187 samples, 0.02%)asm_common_interrupt (5,202,187 samples, 0.02%)common_interrupt (5,202,187 samples, 0.02%)__common_interrupt (5,202,187 samples, 0.02%)handle_edge_irq (5,202,187 samples, 0.02%)handle_irq_event (5,202,187 samples, 0.02%)__handle_irq_event_percpu (5,202,187 samples, 0.02%)nvidia_isr_msix (5,202,187 samples, 0.02%)nvidia_isr (5,202,187 samples, 0.02%)rm_isr (5,202,187 samples, 0.02%)osIsr (5,202,187 samples, 0.02%)osInterruptPending.constprop.0 (5,202,187 samples, 0.02%)kdispServiceLowLatencyIntrs_KERNEL (5,202,187 samples, 0.02%)kheadReadPendingRgSemIntr_v04_01 (5,202,187 samples, 0.02%)_regRead.constprop.0 (5,202,187 samples, 0.02%)osDevReadReg032 (5,202,187 samples, 0.02%)__libc_write (27,908,658 samples, 0.09%)__libc_write (27,908,658 samples, 0.09%)entry_SYSCALL_64_after_hwframe (27,908,658 samples, 0.09%)do_syscall_64 (27,908,658 samples, 0.09%)x64_sys_call (22,750,086 samples, 0.07%)__x64_sys_write (22,750,086 samples, 0.07%)ksys_write (22,750,086 samples, 0.07%)vfs_write (22,750,086 samples, 0.07%)tty_write (21,530,746 samples, 0.07%)file_tty_write.isra.0 (21,530,746 samples, 0.07%)iterate_tty_write (21,530,746 samples, 0.07%)n_tty_write (10,663,122 samples, 0.03%)process_output_block (5,460,935 samples, 0.02%)asm_common_interrupt (5,131,313 samples, 0.02%)common_interrupt (5,131,313 samples, 0.02%)__common_interrupt (5,131,313 samples, 0.02%)handle_edge_irq (5,131,313 samples, 0.02%)handle_irq_event (5,131,313 samples, 0.02%)__handle_irq_event_percpu (5,131,313 samples, 0.02%)nvidia_isr_msix (5,131,313 samples, 0.02%)nvidia_isr (5,131,313 samples, 0.02%)rm_isr (5,131,313 samples, 0.02%)osIsr (5,131,313 samples, 0.02%)osInterruptPending.constprop.0 (5,131,313 samples, 0.02%)kdispServiceLowLatencyIntrs_KERNEL (5,131,313 samples, 0.02%)kdispServiceAwakenIntr_v03_00 (5,131,313 samples, 0.02%)_regRead.constprop.0 (5,131,313 samples, 0.02%)osDevReadReg032 (5,131,313 samples, 0.02%)__rcu_read_unlock (5,364,393 samples, 0.02%)__folio_throttle_swaprate (5,538,188 samples, 0.02%)blk_cgroup_congested (5,538,188 samples, 0.02%)__rcu_read_lock (5,431,878 samples, 0.02%)memcg1_commit_charge (10,927,565 samples, 0.04%)count_memcg_events (10,927,565 samples, 0.04%)__css_rstat_updated (5,460,625 samples, 0.02%)__mem_cgroup_charge (21,594,670 samples, 0.07%)charge_memcg (16,162,792 samples, 0.05%)try_charge_memcg (5,235,227 samples, 0.02%)post_alloc_hook (10,698,527 samples, 0.03%)kernel_init_pages (10,698,527 samples, 0.03%)alloc_anon_folio (75,225,675 samples, 0.24%)vma_alloc_folio_noprof (48,092,817 samples, 0.15%)alloc_pages_mpol (42,855,500 samples, 0.14%)__alloc_frozen_pages_noprof (42,855,500 samples, 0.14%)get_page_from_freelist (37,533,697 samples, 0.12%)rmqueue.isra.0 (21,364,344 samples, 0.07%)__rmqueue_pcplist (15,952,855 samples, 0.05%)rmqueue_bulk (10,613,575 samples, 0.03%)folio_add_lru_vma (5,040,879 samples, 0.02%)folio_add_lru (5,040,879 samples, 0.02%)__folio_batch_add_and_move (5,040,879 samples, 0.02%)folio_batch_move_lru (5,040,879 samples, 0.02%)lru_add (5,040,879 samples, 0.02%)lru_gen_add_folio (5,040,879 samples, 0.02%)__mod_zone_page_state (5,040,879 samples, 0.02%)folio_add_new_anon_rmap (5,449,593 samples, 0.02%)srso_alias_safe_ret (5,449,593 samples, 0.02%)pte_offset_map_lock (5,401,424 samples, 0.02%)__pte_offset_map (5,401,424 samples, 0.02%)do_anonymous_page (96,587,614 samples, 0.31%)vma_alloc_folio_noprof (5,470,043 samples, 0.02%)pfn_pte (5,200,627 samples, 0.02%)__handle_mm_fault (117,945,107 samples, 0.38%)handle_pte_fault (117,945,107 samples, 0.38%)pte_offset_map_rw_nolock (10,803,302 samples, 0.03%)__pte_offset_map (5,390,882 samples, 0.02%)handle_mm_fault (128,725,950 samples, 0.41%)handle_pte_fault (5,390,103 samples, 0.02%)do_user_addr_fault (149,994,870 samples, 0.48%)lock_vma_under_rcu (5,339,947 samples, 0.02%)mas_walk (5,339,947 samples, 0.02%)asm_exc_page_fault (155,446,848 samples, 0.50%)exc_page_fault (155,446,848 samples, 0.50%)irqentry_exit (5,451,978 samples, 0.02%)arch_exit_to_user_mode_prepare.isra.0 (5,451,978 samples, 0.02%)fpregs_assert_state_consistent (5,451,978 samples, 0.02%)ktime_get (5,451,835 samples, 0.02%)sched_balance_trigger (5,402,906 samples, 0.02%)nohz_balancer_kick (5,402,906 samples, 0.02%)sched_use_asym_prio.isra.0 (5,402,906 samples, 0.02%)__sysvec_apic_timer_interrupt (16,323,041 samples, 0.05%)hrtimer_interrupt (16,323,041 samples, 0.05%)__hrtimer_run_queues (16,323,041 samples, 0.05%)tick_nohz_handler (16,323,041 samples, 0.05%)update_process_times (10,871,206 samples, 0.03%)sched_tick (10,871,206 samples, 0.03%)task_tick_fair (5,468,300 samples, 0.02%)asm_sysvec_apic_timer_interrupt (21,588,503 samples, 0.07%)sysvec_apic_timer_interrupt (21,588,503 samples, 0.07%)irq_exit_rcu (5,265,462 samples, 0.02%)__irq_exit_rcu (5,265,462 samples, 0.02%)handle_softirqs (5,265,462 samples, 0.02%)rcu_core_si (5,265,462 samples, 0.02%)rcu_core (5,265,462 samples, 0.02%)rcu_do_batch (5,265,462 samples, 0.02%)tlb_remove_table_rcu (5,265,462 samples, 0.02%)__free_pages (5,265,462 samples, 0.02%)___free_pages (5,265,462 samples, 0.02%)__free_frozen_pages (5,265,462 samples, 0.02%)__memcg_kmem_uncharge_page (5,265,462 samples, 0.02%)[eck] (15,311,682,391 samples, 49.09%)[eck]__memmove_avx512_unaligned_erms (4,481,474,621 samples, 14.37%)__memmove_avx512_unali..error_return (10,963,086 samples, 0.04%)[unknown] (10,839,705 samples, 0.03%)__memmove_avx512_unaligned_erms (10,839,705 samples, 0.03%)__pthread_create_2_1 (5,465,613 samples, 0.02%)create_thread (5,465,613 samples, 0.02%)__clone3 (5,465,613 samples, 0.02%)entry_SYSCALL_64_after_hwframe (5,465,613 samples, 0.02%)do_syscall_64 (5,465,613 samples, 0.02%)x64_sys_call (5,465,613 samples, 0.02%)__x64_sys_clone3 (5,465,613 samples, 0.02%)__do_sys_clone3 (5,465,613 samples, 0.02%)kernel_clone (5,465,613 samples, 0.02%)copy_process (5,465,613 samples, 0.02%)alloc_thread_stack_node (5,465,613 samples, 0.02%)__vmalloc_node_noprof (5,465,613 samples, 0.02%)__vmalloc_node_range_noprof (5,465,613 samples, 0.02%)__get_vm_area_node (5,465,613 samples, 0.02%)_raw_spin_lock (5,465,613 samples, 0.02%)__pthread_getattr_np (3,411,515 samples, 0.01%)_IO_new_fopen (3,411,515 samples, 0.01%)__fopen_internal (3,411,515 samples, 0.01%)__libc_malloc (3,411,515 samples, 0.01%)tcache_init (3,411,515 samples, 0.01%)tcache_init (3,411,515 samples, 0.01%)_int_malloc (3,411,515 samples, 0.01%)sysmalloc (3,411,515 samples, 0.01%)asm_exc_page_fault (3,411,515 samples, 0.01%)exc_page_fault (3,411,515 samples, 0.01%)do_user_addr_fault (3,411,515 samples, 0.01%)__rcu_read_unlock (3,411,515 samples, 0.01%)finish_task_switch.isra.0 (5,435,647 samples, 0.02%)__perf_event_task_sched_in (5,435,647 samples, 0.02%)perf_event_sched_in (5,433,151 samples, 0.02%)ctx_sched_in (5,433,151 samples, 0.02%)visit_groups_merge.constprop.0.isra.0 (5,433,151 samples, 0.02%)futex_wait (5,467,738 samples, 0.02%)__futex_wait (5,467,738 samples, 0.02%)futex_do_wait (5,467,738 samples, 0.02%)schedule (5,467,738 samples, 0.02%)__schedule (5,467,738 samples, 0.02%)select_task_rq (5,266,717 samples, 0.02%)[eck] (15,360,768,294 samples, 49.25%)[eck]syscall (16,247,743 samples, 0.05%)entry_SYSCALL_64_after_hwframe (16,247,743 samples, 0.05%)do_syscall_64 (16,247,743 samples, 0.05%)x64_sys_call (16,247,743 samples, 0.05%)__x64_sys_futex (16,247,743 samples, 0.05%)do_futex (16,247,743 samples, 0.05%)futex_wake (10,780,005 samples, 0.03%)wake_up_q (10,780,005 samples, 0.03%)try_to_wake_up (10,780,005 samples, 0.03%)ttwu_queue_wakelist (5,513,288 samples, 0.02%)__smp_call_single_queue (5,513,288 samples, 0.02%)native_send_call_func_single_ipi (5,513,288 samples, 0.02%)default_send_IPI_single_phys (5,513,288 samples, 0.02%)[libdbus-1.so.3.32.4] (4,328,654 samples, 0.01%)[eck] (5,569,457 samples, 0.02%)__mem_cgroup_uncharge_folios (5,431,846 samples, 0.02%)__page_cache_release.part.0 (5,420,640 samples, 0.02%)lru_gen_del_folio.constprop.0 (5,420,640 samples, 0.02%)mod_lruvec_state (5,420,640 samples, 0.02%)__free_one_page (5,452,780 samples, 0.02%)tlb_flush_mmu (27,192,814 samples, 0.09%)__tlb_batch_free_encoded_pages (27,192,814 samples, 0.09%)free_pages_and_swap_cache (27,192,814 samples, 0.09%)folios_put_refs (21,738,157 samples, 0.07%)free_unref_folios (10,885,671 samples, 0.03%)free_frozen_page_commit.isra.0 (10,885,671 samples, 0.03%)free_pcppages_bulk (5,432,891 samples, 0.02%)__libc_free (54,385,274 samples, 0.17%)__munmap (54,385,274 samples, 0.17%)entry_SYSCALL_64_after_hwframe (54,385,274 samples, 0.17%)do_syscall_64 (54,385,274 samples, 0.17%)x64_sys_call (54,385,274 samples, 0.17%)__x64_sys_munmap (54,385,274 samples, 0.17%)__vm_munmap (54,385,274 samples, 0.17%)do_vmi_munmap (54,385,274 samples, 0.17%)do_vmi_align_munmap (54,385,274 samples, 0.17%)vms_complete_munmap_vmas (54,385,274 samples, 0.17%)unmap_region (54,385,274 samples, 0.17%)unmap_vmas (48,917,219 samples, 0.16%)unmap_single_vma (48,917,219 samples, 0.16%)unmap_page_range (48,917,219 samples, 0.16%)zap_pmd_range.isra.0 (48,917,219 samples, 0.16%)zap_pte_range (48,917,219 samples, 0.16%)zap_present_ptes.constprop.0 (16,282,831 samples, 0.05%)__rcu_read_unlock (5,681,821 samples, 0.02%)_raw_spin_lock (11,502,858 samples, 0.04%)count_memcg_events (5,030,240 samples, 0.02%)__mem_cgroup_charge (14,037,300 samples, 0.05%)charge_memcg (14,037,300 samples, 0.05%)memcg1_commit_charge (14,037,300 samples, 0.05%)memcg1_check_events (9,007,060 samples, 0.03%)__alloc_frozen_pages_noprof (9,558,706 samples, 0.03%)post_alloc_hook (17,009,426 samples, 0.05%)kernel_init_pages (17,009,426 samples, 0.05%)alloc_anon_folio (84,477,912 samples, 0.27%)vma_alloc_folio_noprof (60,775,287 samples, 0.19%)alloc_pages_mpol (45,786,394 samples, 0.15%)__alloc_frozen_pages_noprof (45,786,394 samples, 0.15%)get_page_from_freelist (45,786,394 samples, 0.15%)rmqueue.isra.0 (28,776,968 samples, 0.09%)__rmqueue_pcplist (28,776,968 samples, 0.09%)rmqueue_bulk (28,776,968 samples, 0.09%)folio_add_lru_vma (17,743,486 samples, 0.06%)folio_add_lru (17,743,486 samples, 0.06%)__folio_batch_add_and_move (17,743,486 samples, 0.06%)folio_batch_move_lru (13,414,832 samples, 0.04%)lru_add (13,414,832 samples, 0.04%)lru_gen_add_folio (5,933,459 samples, 0.02%)mod_lruvec_state (5,933,459 samples, 0.02%)mod_memcg_lruvec_state (5,933,459 samples, 0.02%)handle_mm_fault (165,339,764 samples, 0.53%)__handle_mm_fault (154,051,018 samples, 0.49%)handle_pte_fault (147,717,242 samples, 0.47%)do_anonymous_page (129,696,835 samples, 0.42%)folio_add_new_anon_rmap (15,972,579 samples, 0.05%)__folio_mod_stat (15,972,579 samples, 0.05%)srso_alias_safe_ret (10,508,059 samples, 0.03%)do_user_addr_fault (176,778,059 samples, 0.57%)srso_alias_safe_ret (5,756,474 samples, 0.02%)__memset_avx512_unaligned_erms (188,950,906 samples, 0.61%)asm_exc_page_fault (188,950,906 samples, 0.61%)exc_page_fault (183,567,344 samples, 0.59%)handle_mm_fault (6,789,285 samples, 0.02%)asm_sysvec_apic_timer_interrupt (5,396,980 samples, 0.02%)sysvec_apic_timer_interrupt (5,396,980 samples, 0.02%)__sysvec_apic_timer_interrupt (5,396,980 samples, 0.02%)hrtimer_interrupt (5,396,980 samples, 0.02%)__hrtimer_run_queues (5,396,980 samples, 0.02%)tick_nohz_handler (5,396,980 samples, 0.02%)update_process_times (5,396,980 samples, 0.02%)sched_tick (5,396,980 samples, 0.02%)perf_event_task_tick (5,396,980 samples, 0.02%)perf_adjust_freq_unthr_context (5,396,980 samples, 0.02%)[eck] (282,761,007 samples, 0.91%)__mem_cgroup_uncharge_folios (5,574,357 samples, 0.02%)uncharge_folio (5,574,357 samples, 0.02%)__page_cache_release.part.0 (16,666,036 samples, 0.05%)lru_gen_del_folio.constprop.0 (11,095,408 samples, 0.04%)mod_lruvec_state (5,575,360 samples, 0.02%)__nr_to_section (5,536,475 samples, 0.02%)free_pages_and_swap_cache (83,281,481 samples, 0.27%)folios_put_refs (72,171,065 samples, 0.23%)free_unref_folios (44,383,858 samples, 0.14%)free_frozen_page_commit.isra.0 (44,383,858 samples, 0.14%)free_pcppages_bulk (38,847,383 samples, 0.12%)__free_one_page (11,050,736 samples, 0.04%)tlb_finish_mmu (88,834,793 samples, 0.28%)__tlb_batch_free_encoded_pages (88,834,793 samples, 0.28%)free_swap_cache (5,553,312 samples, 0.02%)folio_remove_rmap_ptes (11,135,776 samples, 0.04%)lruvec_stat_mod_folio (5,564,631 samples, 0.02%)__munmap (149,930,097 samples, 0.48%)entry_SYSCALL_64_after_hwframe (149,930,097 samples, 0.48%)do_syscall_64 (149,930,097 samples, 0.48%)x64_sys_call (149,930,097 samples, 0.48%)__x64_sys_munmap (149,930,097 samples, 0.48%)__vm_munmap (149,930,097 samples, 0.48%)do_vmi_munmap (149,930,097 samples, 0.48%)do_vmi_align_munmap (149,930,097 samples, 0.48%)vms_complete_munmap_vmas (149,930,097 samples, 0.48%)unmap_region (149,930,097 samples, 0.48%)unmap_vmas (61,095,304 samples, 0.20%)unmap_single_vma (61,095,304 samples, 0.20%)unmap_page_range (61,095,304 samples, 0.20%)zap_pmd_range.isra.0 (61,095,304 samples, 0.20%)zap_pte_range (61,095,304 samples, 0.20%)zap_present_ptes.constprop.0 (49,976,003 samples, 0.16%)vm_normal_page (11,072,397 samples, 0.04%)__tlb_remove_folio_pages (5,478,383 samples, 0.02%)__tlb_remove_folio_pages_size.isra.0 (5,478,383 samples, 0.02%)get_free_pages_noprof (5,478,383 samples, 0.02%)alloc_pages_noprof (5,478,383 samples, 0.02%)alloc_pages_mpol (5,478,383 samples, 0.02%)__alloc_frozen_pages_noprof (5,478,383 samples, 0.02%)get_page_from_freelist (5,478,383 samples, 0.02%)rmqueue.isra.0 (5,478,383 samples, 0.02%)__rmqueue_pcplist (5,478,383 samples, 0.02%)rmqueue_bulk (5,478,383 samples, 0.02%)madvise_do_behavior (55,048,027 samples, 0.18%)madvise_walk_vmas (55,048,027 samples, 0.18%)madvise_vma_behavior (55,048,027 samples, 0.18%)zap_page_range_single_batched (55,048,027 samples, 0.18%)unmap_single_vma (55,048,027 samples, 0.18%)unmap_page_range (55,048,027 samples, 0.18%)zap_pmd_range.isra.0 (55,048,027 samples, 0.18%)zap_pte_range (55,048,027 samples, 0.18%)zap_present_ptes.constprop.0 (55,048,027 samples, 0.18%)folio_remove_rmap_ptes (5,478,402 samples, 0.02%)__rcu_read_unlock (5,478,402 samples, 0.02%)__mem_cgroup_uncharge_folios (21,692,029 samples, 0.07%)uncharge_folio (16,183,214 samples, 0.05%)__page_cache_release.part.0 (22,018,520 samples, 0.07%)lru_gen_del_folio.constprop.0 (16,529,504 samples, 0.05%)free_unref_folios (10,530,053 samples, 0.03%)free_frozen_page_commit.isra.0 (5,485,351 samples, 0.02%)free_pcppages_bulk (5,485,351 samples, 0.02%)__free_one_page (5,485,351 samples, 0.02%)__libc_free (270,041,013 samples, 0.87%)_int_free (120,110,916 samples, 0.39%)_int_free_maybe_consolidate (120,110,916 samples, 0.39%)heap_trim (120,110,916 samples, 0.39%)shrink_heap (120,110,916 samples, 0.39%)__madvise (120,110,916 samples, 0.39%)entry_SYSCALL_64_after_hwframe (120,110,916 samples, 0.39%)do_syscall_64 (120,110,916 samples, 0.39%)x64_sys_call (120,110,916 samples, 0.39%)__x64_sys_madvise (120,110,916 samples, 0.39%)do_madvise (120,110,916 samples, 0.39%)tlb_finish_mmu (65,062,889 samples, 0.21%)__tlb_batch_free_encoded_pages (65,062,889 samples, 0.21%)free_pages_and_swap_cache (65,062,889 samples, 0.21%)folios_put_refs (65,062,889 samples, 0.21%)uncharge_folio (5,518,843 samples, 0.02%)__libc_read (5,095,893 samples, 0.02%)__libc_read (5,095,893 samples, 0.02%)entry_SYSCALL_64_after_hwframe (5,095,893 samples, 0.02%)do_syscall_64 (5,095,893 samples, 0.02%)x64_sys_call (5,095,893 samples, 0.02%)__x64_sys_read (5,095,893 samples, 0.02%)ksys_read (5,095,893 samples, 0.02%)vfs_read (5,095,893 samples, 0.02%)ext4_file_read_iter (5,095,893 samples, 0.02%)generic_file_read_iter (5,095,893 samples, 0.02%)filemap_read (5,095,893 samples, 0.02%)copy_page_to_iter (5,095,893 samples, 0.02%)_copy_to_iter (5,095,893 samples, 0.02%)asm_exc_page_fault (5,095,893 samples, 0.02%)exc_page_fault (5,095,893 samples, 0.02%)do_user_addr_fault (5,095,893 samples, 0.02%)handle_mm_fault (5,095,893 samples, 0.02%)__handle_mm_fault (5,095,893 samples, 0.02%)arch_exit_to_user_mode_prepare.isra.0 (5,479,983 samples, 0.02%)fpregs_assert_state_consistent (5,479,983 samples, 0.02%)exit_to_user_mode_loop (5,482,626 samples, 0.02%)schedule (5,482,626 samples, 0.02%)__schedule (5,482,626 samples, 0.02%)pick_next_task (5,482,626 samples, 0.02%)pick_next_task_fair (5,482,626 samples, 0.02%)put_prev_entity (5,482,626 samples, 0.02%)__update_load_avg_se (5,482,626 samples, 0.02%)fanotify_alloc_event (5,380,973 samples, 0.02%)fanotify_alloc_name_event.constprop.0 (5,380,973 samples, 0.02%)fanotify_encode_fh (5,380,973 samples, 0.02%)exportfs_encode_inode_fh (5,380,973 samples, 0.02%)generic_encode_ino32_fh (5,380,973 samples, 0.02%)__fsnotify_parent (16,261,813 samples, 0.05%)fsnotify (16,261,813 samples, 0.05%)send_to_group (16,261,813 samples, 0.05%)fanotify_handle_event (16,261,813 samples, 0.05%)fsnotify_insert_event (10,880,840 samples, 0.03%)__wake_up (5,424,523 samples, 0.02%)__wake_up_common (5,424,523 samples, 0.02%)pollwake (5,424,523 samples, 0.02%)default_wake_function (5,424,523 samples, 0.02%)try_to_wake_up (5,424,523 samples, 0.02%)ttwu_queue_wakelist (5,424,523 samples, 0.02%)__smp_call_single_queue (5,424,523 samples, 0.02%)native_send_call_func_single_ipi (5,424,523 samples, 0.02%)default_send_IPI_single_phys (5,424,523 samples, 0.02%)ext4_da_write_begin (5,477,322 samples, 0.02%)balance_dirty_pages_ratelimited (10,978,819 samples, 0.04%)balance_dirty_pages_ratelimited_flags (10,978,819 samples, 0.04%)domain_dirty_limits (5,479,135 samples, 0.02%)copy_folio_from_iter_atomic (2,900,079,461 samples, 9.30%)copy_folio_fr..asm_sysvec_apic_timer_interrupt (5,474,683 samples, 0.02%)sysvec_apic_timer_interrupt (5,474,683 samples, 0.02%)__sysvec_apic_timer_interrupt (5,474,683 samples, 0.02%)hrtimer_interrupt (5,474,683 samples, 0.02%)__hrtimer_run_queues (5,474,683 samples, 0.02%)tick_nohz_handler (5,474,683 samples, 0.02%)update_process_times (5,474,683 samples, 0.02%)sched_tick (5,474,683 samples, 0.02%)task_tick_fair (5,474,683 samples, 0.02%)update_curr (5,474,683 samples, 0.02%)update_deadline (5,474,683 samples, 0.02%)__calc_delta.constprop.0 (5,474,683 samples, 0.02%)__filemap_add_folio (5,474,791 samples, 0.02%)lruvec_stat_mod_folio (11,031,767 samples, 0.04%)mod_node_page_state (5,538,201 samples, 0.02%)workingset_update_node (5,488,058 samples, 0.02%)xas_find_conflict (5,097,352 samples, 0.02%)__filemap_add_folio (43,287,386 samples, 0.14%)xas_store (10,965,916 samples, 0.04%)xas_create (10,965,916 samples, 0.04%)xas_alloc (5,483,018 samples, 0.02%)kmem_cache_alloc_lru_noprof (5,483,018 samples, 0.02%)__memcg_slab_post_alloc_hook (5,483,018 samples, 0.02%)__rcu_read_lock (10,123,976 samples, 0.03%)memcg1_commit_charge (21,156,239 samples, 0.07%)memcg1_check_events (21,156,239 samples, 0.07%)__rcu_read_unlock (11,032,263 samples, 0.04%)charge_memcg (32,085,947 samples, 0.10%)try_charge_memcg (5,455,297 samples, 0.02%)page_counter_try_charge (5,455,297 samples, 0.02%)__mem_cgroup_charge (37,623,275 samples, 0.12%)get_mem_cgroup_from_mm (5,537,328 samples, 0.02%)filemap_add_folio (108,265,842 samples, 0.35%)folio_add_lru (16,355,033 samples, 0.05%)__folio_batch_add_and_move (10,962,974 samples, 0.04%)folio_batch_move_lru (10,962,974 samples, 0.04%)lru_add (10,962,974 samples, 0.04%)__mod_zone_page_state (5,490,540 samples, 0.02%)post_alloc_hook (81,304,903 samples, 0.26%)kernel_init_pages (81,304,903 samples, 0.26%)prep_compound_page (5,274,949 samples, 0.02%)isolate_migratepages (53,627,929 samples, 0.17%)isolate_migratepages_block (53,627,929 samples, 0.17%)lru_gen_del_folio.constprop.0 (16,390,871 samples, 0.05%)__mod_zone_page_state (5,476,231 samples, 0.02%)drain_local_pages (10,955,724 samples, 0.04%)drain_pages_zone (10,955,724 samples, 0.04%)free_pcppages_bulk (10,955,724 samples, 0.04%)__free_one_page (10,955,724 samples, 0.04%)lru_add_drain_cpu_zone (16,505,384 samples, 0.05%)lru_add_drain_cpu (5,549,660 samples, 0.02%)folio_batch_move_lru (5,549,660 samples, 0.02%)lru_add (5,549,660 samples, 0.02%)lru_gen_add_folio (5,549,660 samples, 0.02%)mod_lruvec_state (5,549,660 samples, 0.02%)mod_memcg_lruvec_state (5,549,660 samples, 0.02%)folio_unlock (10,919,069 samples, 0.04%)folio_lruvec_lock_irqsave (5,476,665 samples, 0.02%)_raw_spin_lock_irqsave (5,476,665 samples, 0.02%)__raw_spin_lock_irqsave (5,476,665 samples, 0.02%)native_queued_spin_lock_slowpath (5,476,665 samples, 0.02%)folios_put_refs (5,273,344 samples, 0.02%)__mod_zone_page_state (5,085,552 samples, 0.02%)mod_lruvec_state (5,472,737 samples, 0.02%)folio_add_lru (53,254,948 samples, 0.17%)__folio_batch_add_and_move (53,254,948 samples, 0.17%)folio_batch_move_lru (42,423,319 samples, 0.14%)lru_add (26,198,652 samples, 0.08%)lru_gen_add_folio (26,198,652 samples, 0.08%)mod_memcg_lruvec_state (5,057,377 samples, 0.02%)__free_one_page (27,188,231 samples, 0.09%)__free_frozen_pages (65,109,448 samples, 0.21%)free_frozen_page_commit.isra.0 (65,109,448 samples, 0.21%)free_pcppages_bulk (54,582,046 samples, 0.18%)_raw_spin_lock_irqsave (5,480,500 samples, 0.02%)__raw_spin_lock_irqsave (5,480,500 samples, 0.02%)native_queued_spin_lock_slowpath (5,480,500 samples, 0.02%)migrate_folio_done (70,585,608 samples, 0.23%)__folio_put (70,585,608 samples, 0.23%)free_frozen_pages (70,585,608 samples, 0.23%)__nr_to_section (5,476,160 samples, 0.02%)__folio_migrate_mapping (5,478,593 samples, 0.02%)__folio_migrate_mapping (27,283,262 samples, 0.09%)folio_mc_copy (49,206,105 samples, 0.16%)copy_mc_enhanced_fast_string (49,206,105 samples, 0.16%)__migrate_folio.isra.0 (123,799,033 samples, 0.40%)folio_migrate_flags (41,839,375 samples, 0.13%)_raw_spin_lock_irq (10,330,932 samples, 0.03%)__folio_migrate_mapping (42,870,194 samples, 0.14%)xas_store (10,955,049 samples, 0.04%)xas_create (5,477,155 samples, 0.02%)_raw_spin_lock_irq (4,118,224 samples, 0.01%)copy_mc_enhanced_fast_string (76,434,477 samples, 0.25%)folio_mc_copy (81,910,178 samples, 0.26%)srso_alias_safe_ret (5,475,701 samples, 0.02%)buffer_migrate_folio (161,587,125 samples, 0.52%)__buffer_migrate_folio (161,587,125 samples, 0.52%)__migrate_folio.isra.0 (156,106,660 samples, 0.50%)folio_migrate_flags (16,433,418 samples, 0.05%)__folio_migrate_mapping (25,386,194 samples, 0.08%)xas_store (5,317,651 samples, 0.02%)__migrate_folio.isra.0 (41,107,067 samples, 0.13%)folio_mc_copy (15,720,873 samples, 0.05%)copy_mc_enhanced_fast_string (15,720,873 samples, 0.05%)invalidate_bh_lrus (21,882,478 samples, 0.07%)on_each_cpu_cond_mask (21,882,478 samples, 0.07%)smp_call_function_many_cond (21,882,478 samples, 0.07%)has_bh_in_lru (5,476,795 samples, 0.02%)move_to_new_folio (364,031,798 samples, 1.17%)buffer_migrate_folio_norefs (73,167,047 samples, 0.23%)__buffer_migrate_folio (73,167,047 samples, 0.23%)unlock_buffer (4,706,825 samples, 0.02%)native_set_pte (10,961,162 samples, 0.04%)page_vma_mapped_walk (5,475,765 samples, 0.02%)__rcu_read_unlock (5,485,721 samples, 0.02%)folio_add_file_rmap_ptes (21,876,731 samples, 0.07%)lruvec_stat_mod_folio (5,477,160 samples, 0.02%)remove_migration_pte (156,568,877 samples, 0.50%)page_vma_mapped_walk (75,588,309 samples, 0.24%)map_pte (16,424,160 samples, 0.05%)_raw_spin_lock (5,479,068 samples, 0.02%)up_read (5,483,130 samples, 0.02%)vma_interval_tree_iter_first (5,479,889 samples, 0.02%)vma_interval_tree_subtree_search (5,479,889 samples, 0.02%)__rmap_walk_file (211,347,008 samples, 0.68%)vma_interval_tree_iter_next (10,945,012 samples, 0.04%)vma_interval_tree_subtree_search (10,945,012 samples, 0.04%)remove_migration_pte (32,828,742 samples, 0.11%)page_vma_mapped_walk (16,445,154 samples, 0.05%)check_pte (5,482,891 samples, 0.02%)softleaf_from_pte (5,482,891 samples, 0.02%)rmap_walk (260,195,808 samples, 0.83%)rmap_walk_anon (48,848,800 samples, 0.16%)up_read (10,804,523 samples, 0.03%)migrate_folio_move (775,435,552 samples, 2.49%)mi..srso_alias_safe_ret (5,468,143 samples, 0.02%)isolate_freepages (38,345,594 samples, 0.12%)fast_isolate_freepages (38,345,594 samples, 0.12%)__isolate_free_page (5,479,476 samples, 0.02%)move_freepages_block (5,479,476 samples, 0.02%)__move_freepages_block (5,479,476 samples, 0.02%)compaction_alloc_noprof (97,279,011 samples, 0.31%)post_alloc_hook (54,690,205 samples, 0.18%)kernel_init_pages (54,690,205 samples, 0.18%)compaction_alloc (102,772,829 samples, 0.33%)post_alloc_hook (5,493,818 samples, 0.02%)folio_get_anon_vma (10,741,463 samples, 0.03%)rmap_walk (5,479,472 samples, 0.02%)_find_next_bit (10,959,700 samples, 0.04%)srso_alias_safe_ret (5,479,589 samples, 0.02%)__bitmap_or (10,950,711 samples, 0.04%)folio_remove_rmap_ptes (10,930,634 samples, 0.04%)lruvec_stat_mod_folio (5,456,014 samples, 0.02%)mod_memcg_lruvec_state (5,456,014 samples, 0.02%)_raw_spin_lock (5,474,351 samples, 0.02%)check_pte (5,472,400 samples, 0.02%)map_pte (32,909,420 samples, 0.11%)page_vma_mapped_walk (71,251,406 samples, 0.23%)pte_offset_map_rw_nolock (5,478,315 samples, 0.02%)__rmap_walk_file (180,513,275 samples, 0.58%)try_to_migrate_one (136,814,790 samples, 0.44%)set_tlb_ubc_flush_pending.isra.0 (32,723,886 samples, 0.10%)anon_vma_interval_tree_iter_first (5,474,803 samples, 0.02%)set_tlb_ubc_flush_pending.isra.0 (16,395,115 samples, 0.05%)__bitmap_or (5,220,275 samples, 0.02%)__mmu_notifier_invalidate_range_start (5,364,859 samples, 0.02%)rmap_walk_anon (86,652,746 samples, 0.28%)try_to_migrate_one (53,846,540 samples, 0.17%)page_vma_mapped_walk (16,030,782 samples, 0.05%)try_to_migrate_one (5,477,212 samples, 0.02%)migrate_folio_unmap (451,408,038 samples, 1.45%)try_to_migrate (289,098,903 samples, 0.93%)rmap_walk (283,598,047 samples, 0.91%)vma_interval_tree_iter_next (10,954,814 samples, 0.04%)rmap_walk (5,479,969 samples, 0.02%)srso_alias_safe_ret (5,481,659 samples, 0.02%)native_send_call_func_ipi (10,926,471 samples, 0.04%)default_send_IPI_mask_sequence_phys (10,926,471 samples, 0.04%)__alloc_pages_direct_compact (1,465,367,905 samples, 4.70%)__all..try_to_compact_pages (1,373,329,656 samples, 4.40%)try_t..compact_zone_order (1,373,329,656 samples, 4.40%)compa..compact_zone (1,373,329,656 samples, 4.40%)compa..migrate_pages (1,303,196,343 samples, 4.18%)migra..migrate_pages_batch (1,303,196,343 samples, 4.18%)migra..try_to_unmap_flush (43,661,587 samples, 0.14%)arch_tlbbatch_flush (43,661,587 samples, 0.14%)native_flush_tlb_multi (43,661,587 samples, 0.14%)on_each_cpu_cond_mask (43,661,587 samples, 0.14%)smp_call_function_many_cond (43,661,587 samples, 0.14%)should_flush_tlb (5,469,125 samples, 0.02%)__drain_all_pages (10,963,960 samples, 0.04%)drain_pages_zone (10,963,960 samples, 0.04%)free_pcppages_bulk (10,963,960 samples, 0.04%)__free_one_page (10,963,960 samples, 0.04%)isolate_folios (5,474,392 samples, 0.02%)scan_folios (5,474,392 samples, 0.02%)lru_gen_del_folio (5,474,392 samples, 0.02%)mod_lruvec_state (5,474,392 samples, 0.02%)__remove_mapping (5,457,421 samples, 0.02%)__filemap_remove_folio (5,457,421 samples, 0.02%)srso_alias_safe_ret (5,457,421 samples, 0.02%)__alloc_pages_slowpath.constprop.0 (1,492,737,722 samples, 4.79%)__allo..try_to_free_pages (16,405,857 samples, 0.05%)do_try_to_free_pages (16,405,857 samples, 0.05%)shrink_zones.constprop.0 (16,405,857 samples, 0.05%)shrink_node (16,405,857 samples, 0.05%)shrink_many (16,405,857 samples, 0.05%)shrink_one (16,405,857 samples, 0.05%)try_to_shrink_lruvec (16,405,857 samples, 0.05%)evict_folios (16,405,857 samples, 0.05%)shrink_folio_list (10,931,465 samples, 0.04%)filemap_release_folio (5,474,044 samples, 0.02%)ext4_release_folio (5,474,044 samples, 0.02%)jbd2_journal_try_to_free_buffers (5,474,044 samples, 0.02%)irqentry_enter (5,474,893 samples, 0.02%)post_alloc_hook (1,806,612,622 samples, 5.79%)post_al..kernel_init_pages (1,790,045,821 samples, 5.74%)kernel_..asm_sysvec_apic_timer_interrupt (5,477,378 samples, 0.02%)sysvec_apic_timer_interrupt (5,477,378 samples, 0.02%)prep_compound_page (21,879,345 samples, 0.07%)__alloc_frozen_pages_noprof (3,354,135,103 samples, 10.75%)__alloc_frozen_p..get_page_from_freelist (1,855,894,624 samples, 5.95%)get_page..rmqueue.isra.0 (27,402,657 samples, 0.09%)__rmqueue_pcplist (21,977,246 samples, 0.07%)rmqueue_bulk (5,529,498 samples, 0.02%)filemap_alloc_folio_noprof (3,369,697,318 samples, 10.80%)filemap_alloc_fo..folio_alloc_noprof (3,369,697,318 samples, 10.80%)folio_alloc_nopr..alloc_pages_mpol (3,359,581,035 samples, 10.77%)alloc_pages_mpolget_page_from_freelist (5,445,932 samples, 0.02%)filemap_get_entry (10,951,858 samples, 0.04%)xas_load (10,951,858 samples, 0.04%)__filemap_get_folio_mpol (3,510,895,465 samples, 11.26%)__filemap_get_fol..folio_add_lru (5,488,839 samples, 0.02%)__memcg_slab_post_alloc_hook (26,346,448 samples, 0.08%)__pi_memset (206,168,699 samples, 0.66%)asm_sysvec_apic_timer_interrupt (5,479,764 samples, 0.02%)sysvec_apic_timer_interrupt (5,479,764 samples, 0.02%)__sysvec_apic_timer_interrupt (5,479,764 samples, 0.02%)hrtimer_interrupt (5,479,764 samples, 0.02%)__hrtimer_run_queues (5,479,764 samples, 0.02%)tick_nohz_handler (5,479,764 samples, 0.02%)tick_do_update_jiffies64 (5,479,764 samples, 0.02%)update_wall_time (5,479,764 samples, 0.02%)__timekeeping_advance (5,479,764 samples, 0.02%)timekeeping_update_from_shadow (5,479,764 samples, 0.02%)raw_notifier_call_chain (5,479,764 samples, 0.02%)pvclock_gtod_notify (5,479,764 samples, 0.02%)__refill_objects_node (5,481,083 samples, 0.02%)alloc_slab_obj_exts (16,502,783 samples, 0.05%)__kmalloc_node_noprof (16,502,783 samples, 0.05%)__pcs_replace_empty_main (11,017,219 samples, 0.04%)refill_objects (11,017,219 samples, 0.04%)new_slab (5,536,136 samples, 0.02%)allocate_slab (5,536,136 samples, 0.02%)__alloc_frozen_pages_noprof (5,536,136 samples, 0.02%)get_page_from_freelist (5,536,136 samples, 0.02%)post_alloc_hook (5,536,136 samples, 0.02%)kernel_init_pages (5,536,136 samples, 0.02%)__account_obj_stock (16,283,087 samples, 0.05%)mod_memcg_lruvec_state (5,414,898 samples, 0.02%)obj_cgroup_charge_pages (5,484,067 samples, 0.02%)mod_memcg_state (5,484,067 samples, 0.02%)refill_obj_stock (16,392,014 samples, 0.05%)drain_obj_stock (10,909,701 samples, 0.03%)mod_memcg_lruvec_state (5,412,720 samples, 0.02%)__memcg_slab_post_alloc_hook (382,078,097 samples, 1.23%)obj_cgroup_charge_account (98,158,765 samples, 0.31%)srso_alias_safe_ret (5,548,150 samples, 0.02%)cpuset_current_node_allowed (5,483,045 samples, 0.02%)__refill_objects_any (21,549,270 samples, 0.07%)mempolicy_slab_node (5,456,318 samples, 0.02%)__slab_free (16,136,926 samples, 0.05%)slab_update_freelist.isra.0 (16,136,926 samples, 0.05%)get_partial_node_bulk.isra.0 (5,482,535 samples, 0.02%)_raw_spin_lock_irqsave (5,482,535 samples, 0.02%)__raw_spin_lock_irqsave (5,482,535 samples, 0.02%)__refill_objects_node (64,996,656 samples, 0.21%)slab_update_freelist.isra.0 (5,542,722 samples, 0.02%)alloc_from_new_slab (43,797,393 samples, 0.14%)cpuset_current_node_allowed (5,497,743 samples, 0.02%)asm_sysvec_apic_timer_interrupt (5,485,824 samples, 0.02%)sysvec_apic_timer_interrupt (5,485,824 samples, 0.02%)__sysvec_apic_timer_interrupt (5,485,824 samples, 0.02%)hrtimer_interrupt (5,485,824 samples, 0.02%)__hrtimer_run_queues (5,485,824 samples, 0.02%)tick_nohz_handler (5,485,824 samples, 0.02%)update_process_times (5,485,824 samples, 0.02%)sched_tick (5,485,824 samples, 0.02%)sched_balance_trigger (5,485,824 samples, 0.02%)nohz_balancer_kick (5,485,824 samples, 0.02%)_find_next_bit (5,485,824 samples, 0.02%)post_alloc_hook (10,851,477 samples, 0.03%)kernel_init_pages (10,851,477 samples, 0.03%)__alloc_frozen_pages_noprof (47,225,194 samples, 0.15%)get_page_from_freelist (41,740,017 samples, 0.13%)rmqueue.isra.0 (19,901,756 samples, 0.06%)__rmqueue_pcplist (19,901,756 samples, 0.06%)rmqueue_bulk (8,928,002 samples, 0.03%)__get_random_u32_below (5,483,615 samples, 0.02%)setup_object (5,541,602 samples, 0.02%)__get_random_u32_below (10,899,059 samples, 0.03%)get_random_u32 (10,899,059 samples, 0.03%)_get_random_bytes (5,408,899 samples, 0.02%)crng_make_state (5,408,899 samples, 0.02%)crng_fast_key_erasure (5,408,899 samples, 0.02%)chacha_block_generic (5,408,899 samples, 0.02%)chacha_permute (5,408,899 samples, 0.02%)new_slab (205,965,356 samples, 0.66%)allocate_slab (205,965,356 samples, 0.66%)shuffle_freelist (147,714,945 samples, 0.47%)setup_object (21,949,480 samples, 0.07%)__pcs_replace_empty_main (363,610,592 samples, 1.17%)refill_objects (363,610,592 samples, 1.17%)srso_alias_return_thunk (5,486,594 samples, 0.02%)__rcu_read_unlock (5,510,621 samples, 0.02%)asm_sysvec_apic_timer_interrupt (5,390,483 samples, 0.02%)alloc_buffer_head (1,343,050,950 samples, 4.31%)alloc..kmem_cache_alloc_noprof (963,416,891 samples, 3.09%)kme..srso_alias_safe_ret (5,478,475 samples, 0.02%)create_empty_buffers (1,429,610,101 samples, 4.58%)creat..folio_alloc_buffers (1,396,664,502 samples, 4.48%)folio..kmem_cache_alloc_noprof (5,502,306 samples, 0.02%)down_write (22,028,520 samples, 0.07%)_raw_read_lock (109,774,968 samples, 0.35%)_raw_read_unlock (342,404,081 samples, 1.10%)asm_sysvec_apic_timer_interrupt (5,522,616 samples, 0.02%)sysvec_apic_timer_interrupt (5,522,616 samples, 0.02%)__sysvec_apic_timer_interrupt (5,522,616 samples, 0.02%)hrtimer_interrupt (5,522,616 samples, 0.02%)hrtimer_update_next_event (5,522,616 samples, 0.02%)__hrtimer_next_event_base (5,522,616 samples, 0.02%)down_write (64,614,821 samples, 0.21%)rwsem_down_write_slowpath (27,466,658 samples, 0.09%)rwsem_spin_on_owner (27,466,658 samples, 0.09%)ext4_es_insert_delayed_extent (5,575,809 samples, 0.02%)ext4_es_lookup_extent (290,007,738 samples, 0.93%)percpu_counter_add_batch (27,376,397 samples, 0.09%)__dquot_alloc_space (10,952,304 samples, 0.04%)__es_insert_extent (16,436,567 samples, 0.05%)_raw_spin_lock (60,196,278 samples, 0.19%)_raw_spin_unlock (5,555,867 samples, 0.02%)_raw_write_lock (5,476,472 samples, 0.02%)_raw_write_unlock (5,476,880 samples, 0.02%)ext4_claim_free_clusters (21,943,061 samples, 0.07%)__dquot_alloc_space (86,697,107 samples, 0.28%)_raw_spin_unlock (16,461,042 samples, 0.05%)asm_sysvec_apic_timer_interrupt (5,474,735 samples, 0.02%)sysvec_apic_timer_interrupt (5,474,735 samples, 0.02%)__sysvec_apic_timer_interrupt (5,474,735 samples, 0.02%)hrtimer_interrupt (5,474,735 samples, 0.02%)__hrtimer_run_queues (5,474,735 samples, 0.02%)tick_nohz_handler (5,474,735 samples, 0.02%)update_process_times (5,474,735 samples, 0.02%)sched_tick (5,474,735 samples, 0.02%)perf_event_task_tick (5,474,735 samples, 0.02%)perf_adjust_freq_unthr_context (5,474,735 samples, 0.02%)perf_adjust_freq_unthr_events (5,474,735 samples, 0.02%)_raw_spin_lock (54,838,663 samples, 0.18%)_raw_spin_unlock (21,869,392 samples, 0.07%)ext4_has_free_clusters (76,004,081 samples, 0.24%)ext4_claim_free_clusters (337,450,956 samples, 1.08%)percpu_counter_add_batch (53,831,261 samples, 0.17%)_raw_spin_unlock_irqrestore (4,932,212 samples, 0.02%)ext4_get_reserved_space (175,288,416 samples, 0.56%)ext4_has_free_clusters (33,238,679 samples, 0.11%)srso_alias_return_thunk (5,498,486 samples, 0.02%)ext4_da_reserve_space (988,225,690 samples, 3.17%)ext..srso_alias_safe_ret (27,434,049 samples, 0.09%)__es_insert_extent (627,209,563 samples, 2.01%)_..__es_remove_extent (43,752,907 samples, 0.14%)__es_tree_search.isra.0 (81,267,887 samples, 0.26%)ext4_es_can_be_merged.isra.0 (65,269,070 samples, 0.21%)ext4_es_insert_delayed_extent (1,079,706,605 samples, 3.46%)ext..rb_next (16,287,688 samples, 0.05%)srso_alias_return_thunk (32,437,087 samples, 0.10%)ext4_insert_delayed_blocks (2,324,843,715 samples, 7.45%)ext4_inser..srso_alias_safe_ret (5,480,706 samples, 0.02%)srso_alias_return_thunk (5,478,861 samples, 0.02%)srso_alias_safe_ret (50,582,086 samples, 0.16%)ext4_da_map_blocks.constprop.0 (3,395,793,408 samples, 10.89%)ext4_da_map_bloc..up_write (32,953,524 samples, 0.11%)ext4_es_lookup_extent (38,078,831 samples, 0.12%)ext4_insert_delayed_blocks (11,003,008 samples, 0.04%)srso_alias_return_thunk (16,495,788 samples, 0.05%)srso_alias_safe_ret (5,523,220 samples, 0.02%)ext4_da_get_block_prep (4,192,505,662 samples, 13.44%)ext4_da_get_block_pr..up_write (32,438,983 samples, 0.10%)ext4_block_write_begin (5,649,309,249 samples, 18.11%)ext4_block_write_beginext4_da_map_blocks.constprop.0 (5,481,710 samples, 0.02%)ext4_da_get_block_prep (16,516,463 samples, 0.05%)ext4_da_write_begin (9,192,779,791 samples, 29.47%)ext4_da_write_beginsrso_alias_safe_ret (16,058,614 samples, 0.05%)__xa_set_mark (5,478,909 samples, 0.02%)xas_load (5,478,909 samples, 0.02%)__mod_zone_page_state (5,470,391 samples, 0.02%)block_commit_write (169,226,893 samples, 0.54%)mark_buffer_dirty (103,582,309 samples, 0.33%)__folio_mark_dirty (22,004,742 samples, 0.07%)folio_account_dirtied (16,525,833 samples, 0.05%)percpu_counter_add_batch (11,055,442 samples, 0.04%)_raw_spin_lock_irqsave (5,550,749 samples, 0.02%)__raw_spin_lock_irqsave (5,550,749 samples, 0.02%)ext4_da_write_end (223,760,697 samples, 0.72%)ext4_da_do_write_end.isra.0 (223,760,697 samples, 0.72%)block_write_end (185,717,063 samples, 0.60%)mark_buffer_dirty (16,490,170 samples, 0.05%)generic_perform_write (12,332,216,149 samples, 39.54%)generic_perform_writesysvec_apic_timer_interrupt (4,617,381 samples, 0.01%)__libc_write (12,370,211,863 samples, 39.66%)__libc_write__libc_write (12,370,211,863 samples, 39.66%)__libc_writeentry_SYSCALL_64_after_hwframe (12,370,211,863 samples, 39.66%)entry_SYSCALL_64_after_hwframedo_syscall_64 (12,370,211,863 samples, 39.66%)do_syscall_64x64_sys_call (12,359,249,254 samples, 39.63%)x64_sys_call__x64_sys_write (12,359,249,254 samples, 39.63%)__x64_sys_writeksys_write (12,359,249,254 samples, 39.63%)ksys_writevfs_write (12,359,249,254 samples, 39.63%)vfs_writeext4_file_write_iter (12,342,930,547 samples, 39.57%)ext4_file_write_iterext4_buffered_write_iter (12,342,930,547 samples, 39.57%)ext4_buffered_write_itersrso_alias_return_thunk (5,237,076 samples, 0.02%)asm_sysvec_apic_timer_interrupt (5,408,450 samples, 0.02%)sysvec_apic_timer_interrupt (5,408,450 samples, 0.02%)__sysvec_apic_timer_interrupt (5,408,450 samples, 0.02%)hrtimer_interrupt (5,408,450 samples, 0.02%)__hrtimer_run_queues (5,408,450 samples, 0.02%)tick_nohz_handler (5,408,450 samples, 0.02%)update_process_times (5,408,450 samples, 0.02%)sched_tick (5,408,450 samples, 0.02%)sched_balance_trigger (5,408,450 samples, 0.02%)nohz_balancer_kick (5,408,450 samples, 0.02%)_find_next_bit (5,408,450 samples, 0.02%)[unknown] (13,007,655,809 samples, 41.71%)[unknown][eck] (13,007,655,809 samples, 41.71%)[eck]__irqentry_text_end (77,303,234 samples, 0.25%)_raw_spin_lock (4,479,631 samples, 0.01%)native_queued_spin_lock_slowpath (4,479,631 samples, 0.01%)nv_uvm_event_interrupt (4,812,464 samples, 0.02%)uvm_isr_top_half_entry (4,812,464 samples, 0.02%)uvm_isr_top_half (4,812,464 samples, 0.02%)schedule_replayable_faults_handler (4,812,464 samples, 0.02%)up (4,812,464 samples, 0.02%)_raw_spin_lock_irqsave (4,812,464 samples, 0.02%)__raw_spin_lock_irqsave (4,812,464 samples, 0.02%)intrGetPendingNonStall_TU102 (4,312,597 samples, 0.01%)_regRead.constprop.0 (4,312,597 samples, 0.01%)gpuSanityCheckRegisterAccess_IMPL (4,312,597 samples, 0.01%)intrGetLeafStatus_TU102 (42,277,549 samples, 0.14%)_regRead.constprop.0 (42,277,549 samples, 0.14%)osDevReadReg032 (42,277,549 samples, 0.14%)vgpuDevReadReg032 (6,011,253 samples, 0.02%)intrGetPendingStall_GM107 (56,471,784 samples, 0.18%)intrGetPendingStallEngines_TU102 (56,471,784 samples, 0.18%)vectIterNext_IMPL (4,725,128 samples, 0.02%)_intrServiceNonStallLeaf_TU102 (4,736,546 samples, 0.02%)intrServiceNotificationRecords_IMPL (4,736,546 samples, 0.02%)kgraphicsServiceNotificationInterrupt_IMPL (4,736,546 samples, 0.02%)_gpuEngineEventNotificationListNotify (4,736,546 samples, 0.02%)osNotifyEvent (4,736,546 samples, 0.02%)intrServiceNonStall_TU102 (9,877,450 samples, 0.03%)_regRead.constprop.0 (5,140,904 samples, 0.02%)osDevReadReg032 (5,140,904 samples, 0.02%)_regRead.constprop.0 (5,421,540 samples, 0.02%)gpuSanityCheckVirtRegAccess_ac1694 (4,788,552 samples, 0.02%)kdispReadPendingVblank_IMPL (42,576,873 samples, 0.14%)kheadReadPendingVblank_v03_00 (37,155,333 samples, 0.12%)_regRead.constprop.0 (37,155,333 samples, 0.12%)osDevReadReg032 (23,695,997 samples, 0.08%)kdispServiceAwakenIntr_v03_00 (4,916,142 samples, 0.02%)_regRead.constprop.0 (4,916,142 samples, 0.02%)osDevReadReg032 (4,916,142 samples, 0.02%)kheadCheckVblankCallbacksQueued_IMPL (3,349,803 samples, 0.01%)tmrGetCurrentTime_IMPL (3,349,803 samples, 0.01%)tmrGetTimeEx_GH100 (3,349,803 samples, 0.01%)_regRead.constprop.0 (3,349,803 samples, 0.01%)osDevReadReg032 (3,349,803 samples, 0.01%)kheadReadPendingRgLineIntr_v03_00 (15,512,239 samples, 0.05%)_regRead.constprop.0 (15,512,239 samples, 0.05%)osDevReadReg032 (15,512,239 samples, 0.05%)vgpuDevReadReg032 (4,051,545 samples, 0.01%)kheadReadPendingRgSemIntr_v04_01 (4,330,395 samples, 0.01%)_regRead.constprop.0 (4,330,395 samples, 0.01%)osDevReadReg032 (4,330,395 samples, 0.01%)kdispServiceLowLatencyIntrs_KERNEL (77,098,777 samples, 0.25%)regWrite032 (6,413,325 samples, 0.02%)gpuSanityCheckVirtRegAccess_ac1694 (6,413,325 samples, 0.02%)nvidia_isr_msix (167,726,985 samples, 0.54%)nvidia_isr (163,247,354 samples, 0.52%)rm_isr (153,572,898 samples, 0.49%)osIsr (153,572,898 samples, 0.49%)osInterruptPending.constprop.0 (153,572,898 samples, 0.49%)rmDeviceGpuLocksRelease (4,096,008 samples, 0.01%)_rmGpuLocksRelease.isra.0 (4,096,008 samples, 0.01%)gpumgrGetGpu (4,096,008 samples, 0.01%)__common_interrupt (174,514,201 samples, 0.56%)handle_edge_irq (174,514,201 samples, 0.56%)handle_irq_event (172,797,919 samples, 0.55%)__handle_irq_event_percpu (172,797,919 samples, 0.55%)nvme_irq (5,070,934 samples, 0.02%)nvme_pci_complete_batch (5,070,934 samples, 0.02%)blk_mq_end_request_batch (5,070,934 samples, 0.02%)bio_endio (5,070,934 samples, 0.02%)bio_put (5,070,934 samples, 0.02%)bio_free (5,070,934 samples, 0.02%)mempool_free (5,070,934 samples, 0.02%)mempool_free_slab (5,070,934 samples, 0.02%)kmem_cache_free (5,070,934 samples, 0.02%)asm_common_interrupt (179,386,646 samples, 0.58%)common_interrupt (179,386,646 samples, 0.58%)irq_exit_rcu (4,872,445 samples, 0.02%)__irq_exit_rcu (4,872,445 samples, 0.02%)handle_softirqs (4,872,445 samples, 0.02%)tasklet_action (4,872,445 samples, 0.02%)workqueue_softirq_action (4,872,445 samples, 0.02%)bh_worker (4,872,445 samples, 0.02%)process_one_work (4,872,445 samples, 0.02%)ath12k_pci_ce_workqueue (4,872,445 samples, 0.02%)ath12k_ce_per_engine_service (4,872,445 samples, 0.02%)ath12k_ce_recv_process_cb (4,872,445 samples, 0.02%)dma_unmap_page_attrs (4,872,445 samples, 0.02%)dma_unmap_phys (4,872,445 samples, 0.02%)iommu_dma_unmap_phys (4,872,445 samples, 0.02%)__iommu_dma_unmap (4,872,445 samples, 0.02%)iommu_unmap_fast (4,872,445 samples, 0.02%)__iommu_unmap (4,872,445 samples, 0.02%)amdv1_unmap_range (4,872,445 samples, 0.02%)__rcu_read_unlock (36,291,388 samples, 0.12%)do_anonymous_page (5,062,833 samples, 0.02%)__folio_throttle_swaprate (10,202,629 samples, 0.03%)__mem_cgroup_charge (33,545,189 samples, 0.11%)__pte_alloc (5,169,390 samples, 0.02%)pte_alloc_one (5,169,390 samples, 0.02%)alloc_pages_noprof (5,169,390 samples, 0.02%)alloc_pages_mpol (5,169,390 samples, 0.02%)__alloc_frozen_pages_noprof (5,169,390 samples, 0.02%)get_page_from_freelist (5,169,390 samples, 0.02%)rmqueue.isra.0 (5,025,581 samples, 0.02%)__rmqueue_pcplist (5,025,581 samples, 0.02%)rmqueue_bulk (5,025,581 samples, 0.02%)_raw_spin_lock (19,503,976 samples, 0.06%)__rcu_read_lock (14,441,535 samples, 0.05%)blk_cgroup_congested (43,772,257 samples, 0.14%)kthread_blkcg (4,454,355 samples, 0.01%)kthread_blkcg (5,336,649 samples, 0.02%)__folio_throttle_swaprate (55,617,215 samples, 0.18%)srso_alias_safe_ret (6,508,309 samples, 0.02%)__rcu_read_lock (34,066,350 samples, 0.11%)__rcu_read_unlock (38,098,971 samples, 0.12%)memcg1_check_events (5,039,910 samples, 0.02%)__css_rstat_updated (4,778,357 samples, 0.02%)memcg1_commit_charge (89,052,863 samples, 0.29%)count_memcg_events (24,517,751 samples, 0.08%)__css_rstat_updated (3,174,275 samples, 0.01%)charge_memcg (169,269,152 samples, 0.54%)try_charge_memcg (31,327,809 samples, 0.10%)get_mem_cgroup_from_mm (73,487,368 samples, 0.24%)__rcu_read_lock (4,950,166 samples, 0.02%)__mem_cgroup_charge (334,552,183 samples, 1.07%)srso_alias_safe_ret (13,323,073 samples, 0.04%)__rcu_read_unlock (6,302,726 samples, 0.02%)get_mem_cgroup_from_mm (30,096,100 samples, 0.10%)srso_alias_return_thunk (5,366,291 samples, 0.02%)srso_alias_safe_ret (7,580,347 samples, 0.02%)__alloc_frozen_pages_noprof (12,865,714 samples, 0.04%)__next_zones_zonelist (14,810,044 samples, 0.05%)_raw_spin_trylock (17,187,105 samples, 0.06%)post_alloc_hook (247,952,390 samples, 0.79%)kernel_init_pages (234,254,509 samples, 0.75%)__mod_zone_page_state (4,959,953 samples, 0.02%)_raw_spin_lock_irqsave (18,670,013 samples, 0.06%)__raw_spin_lock_irqsave (18,670,013 samples, 0.06%)native_queued_spin_lock_slowpath (18,670,013 samples, 0.06%)__rmqueue_pcplist (216,380,379 samples, 0.69%)rmqueue_bulk (185,157,200 samples, 0.59%)_raw_spin_unlock_irqrestore (5,181,240 samples, 0.02%)asm_common_interrupt (5,181,240 samples, 0.02%)common_interrupt (5,181,240 samples, 0.02%)__common_interrupt (5,181,240 samples, 0.02%)handle_edge_irq (5,181,240 samples, 0.02%)handle_irq_event (5,181,240 samples, 0.02%)__handle_irq_event_percpu (5,181,240 samples, 0.02%)nvidia_isr_msix (5,181,240 samples, 0.02%)nvidia_isr (5,181,240 samples, 0.02%)rm_isr (5,181,240 samples, 0.02%)osIsr (5,181,240 samples, 0.02%)osInterruptPending.constprop.0 (5,181,240 samples, 0.02%)kdispServiceLowLatencyIntrs_KERNEL (5,181,240 samples, 0.02%)kdispReadPendingVblank_IMPL (5,181,240 samples, 0.02%)kheadReadPendingVblank_v03_00 (5,181,240 samples, 0.02%)_regRead.constprop.0 (5,181,240 samples, 0.02%)osDevReadReg032 (5,181,240 samples, 0.02%)rmqueue.isra.0 (218,896,181 samples, 0.70%)get_page_from_freelist (515,735,277 samples, 1.65%)srso_alias_safe_ret (6,636,113 samples, 0.02%)__alloc_frozen_pages_noprof (548,406,739 samples, 1.76%)post_alloc_hook (6,332,896 samples, 0.02%)alloc_pages_mpol (602,556,614 samples, 1.93%)a..srso_alias_safe_ret (5,252,558 samples, 0.02%)asm_common_interrupt (5,252,558 samples, 0.02%)common_interrupt (5,252,558 samples, 0.02%)__common_interrupt (5,252,558 samples, 0.02%)handle_edge_irq (5,252,558 samples, 0.02%)handle_irq_event (5,252,558 samples, 0.02%)__handle_irq_event_percpu (5,252,558 samples, 0.02%)nvidia_isr_msix (5,252,558 samples, 0.02%)nvidia_isr (5,252,558 samples, 0.02%)rm_isr (5,252,558 samples, 0.02%)osIsr (5,252,558 samples, 0.02%)osInterruptPending.constprop.0 (5,252,558 samples, 0.02%)intrGetPendingStall_GM107 (5,252,558 samples, 0.02%)intrGetPendingStallEngines_TU102 (5,252,558 samples, 0.02%)intrGetLeafStatus_TU102 (5,252,558 samples, 0.02%)_regRead.constprop.0 (5,252,558 samples, 0.02%)osDevReadReg032 (5,252,558 samples, 0.02%)alloc_anon_folio (1,128,231,684 samples, 3.62%)allo..vma_alloc_folio_noprof (650,073,905 samples, 2.08%)v..get_vma_policy (5,916,212 samples, 0.02%)__folio_batch_add_and_move (4,107,543 samples, 0.01%)folio_lruvec_lock_irqsave (5,349,545 samples, 0.02%)_raw_spin_lock_irqsave (5,349,545 samples, 0.02%)__raw_spin_lock_irqsave (5,349,545 samples, 0.02%)native_queued_spin_lock_slowpath (5,349,545 samples, 0.02%)folios_put_refs (12,694,824 samples, 0.04%)mod_lruvec_state (10,768,799 samples, 0.03%)mod_memcg_lruvec_state (10,768,799 samples, 0.03%)folio_batch_move_lru (95,887,673 samples, 0.31%)lru_add (71,932,616 samples, 0.23%)lru_gen_add_folio (44,716,830 samples, 0.14%)mod_memcg_lruvec_state (5,417,603 samples, 0.02%)folio_add_lru (147,946,153 samples, 0.47%)__folio_batch_add_and_move (143,277,104 samples, 0.46%)lru_add (7,490,557 samples, 0.02%)folio_add_lru_vma (158,689,809 samples, 0.51%)srso_alias_safe_ret (6,636,113 samples, 0.02%)folio_add_new_anon_rmap (38,420,481 samples, 0.12%)__folio_mod_stat (38,356,821 samples, 0.12%)lruvec_stat_mod_folio (25,555,991 samples, 0.08%)mod_memcg_lruvec_state (5,839,109 samples, 0.02%)pfn_pte (6,799,704 samples, 0.02%)__pte_offset_map (5,635,137 samples, 0.02%)pte_offset_map_lock (27,394,109 samples, 0.09%)__rcu_read_lock (4,966,622 samples, 0.02%)do_anonymous_page (1,489,109,259 samples, 4.77%)do_ano..vma_alloc_folio_noprof (3,359,333 samples, 0.01%)percpu_counter_add_batch (4,918,630 samples, 0.02%)pte_offset_map_rw_nolock (35,938,207 samples, 0.12%)__pte_offset_map (20,115,185 samples, 0.06%)handle_pte_fault (1,574,987,294 samples, 5.05%)handle..srso_alias_safe_ret (14,047,140 samples, 0.05%)pte_offset_map_rw_nolock (5,281,650 samples, 0.02%)__handle_mm_fault (1,637,007,638 samples, 5.25%)__hand..srso_alias_safe_ret (9,992,035 samples, 0.03%)count_memcg_events (12,115,959 samples, 0.04%)mem_cgroup_from_task (5,269,171 samples, 0.02%)handle_mm_fault (1,706,274,382 samples, 5.47%)handle_..srso_alias_safe_ret (5,425,599 samples, 0.02%)lock_mm_and_find_vma (5,300,579 samples, 0.02%)down_read_killable (5,300,579 samples, 0.02%)rwsem_down_read_slowpath (5,300,579 samples, 0.02%)schedule_preempt_disabled (5,300,579 samples, 0.02%)schedule (5,300,579 samples, 0.02%)__schedule (5,300,579 samples, 0.02%)do_user_addr_fault (1,813,751,848 samples, 5.82%)do_user..lock_vma_under_rcu (44,692,869 samples, 0.14%)mas_walk (20,809,448 samples, 0.07%)irqentry_enter (6,692,463 samples, 0.02%)irqentry_exit (11,920,996 samples, 0.04%)arch_exit_to_user_mode_prepare.isra.0 (11,920,996 samples, 0.04%)exc_page_fault (1,887,391,559 samples, 6.05%)exc_page..lock_vma_under_rcu (6,914,388 samples, 0.02%)asm_exc_page_fault (2,074,185,299 samples, 6.65%)asm_exc_p..irqentry_exit (10,697,246 samples, 0.03%)enqueue_hrtimer (6,635,844 samples, 0.02%)timerqueue_add (6,635,844 samples, 0.02%)srso_alias_safe_ret (5,509,555 samples, 0.02%)idle_cpu (5,479,789 samples, 0.02%)perf_adjust_freq_unthr_context (4,984,748 samples, 0.02%)perf_adjust_freq_unthr_events (4,984,748 samples, 0.02%)perf_ibs_stop (4,984,748 samples, 0.02%)native_read_msr (4,984,748 samples, 0.02%)perf_event_task_tick (10,638,484 samples, 0.03%)perf_pmu_nop_void (5,653,736 samples, 0.02%)nohz_balance_exit_idle (5,011,518 samples, 0.02%)idle_cpu (4,174,280 samples, 0.01%)kick_ilb (13,672,885 samples, 0.04%)smp_call_function_single_async (4,514,814 samples, 0.01%)generic_exec_single (4,514,814 samples, 0.01%)__smp_call_single_queue (4,514,814 samples, 0.01%)native_send_call_func_single_ipi (4,514,814 samples, 0.01%)default_send_IPI_single_phys (4,514,814 samples, 0.01%)sched_balance_trigger (24,027,917 samples, 0.08%)nohz_balancer_kick (19,016,399 samples, 0.06%)sched_use_asym_prio.isra.0 (5,343,514 samples, 0.02%)__update_load_avg_se (11,081,264 samples, 0.04%)sched_clock_cpu (5,306,113 samples, 0.02%)sched_clock (5,306,113 samples, 0.02%)sched_clock_noinstr (5,306,113 samples, 0.02%)native_sched_clock (5,306,113 samples, 0.02%)update_cfs_group (21,977,419 samples, 0.07%)reweight_entity (21,977,419 samples, 0.07%)update_cfs_rq_load_avg (3,837,297 samples, 0.01%)__update_load_avg_cfs_rq (3,837,297 samples, 0.01%)dl_server_update (4,792,594 samples, 0.02%)update_curr_dl_se (4,792,594 samples, 0.02%)start_dl_timer (4,792,594 samples, 0.02%)hrtimer_cb_get_time (4,792,594 samples, 0.02%)read_tsc (4,792,594 samples, 0.02%)__hrtimer_run_queues (142,691,884 samples, 0.46%)tick_nohz_handler (121,442,758 samples, 0.39%)update_process_times (115,933,203 samples, 0.37%)sched_tick (105,029,665 samples, 0.34%)task_tick_fair (70,363,264 samples, 0.23%)update_curr (19,468,196 samples, 0.06%)update_deadline (4,722,822 samples, 0.02%)_raw_spin_unlock_irqrestore (4,332,074 samples, 0.01%)read_tsc (4,790,571 samples, 0.02%)hrtimer_interrupt (157,411,724 samples, 0.50%)tick_program_event (5,597,195 samples, 0.02%)srso_alias_safe_ret (5,597,195 samples, 0.02%)__sysvec_apic_timer_interrupt (162,431,537 samples, 0.52%)tick_program_event (5,019,813 samples, 0.02%)rcu_core_si (7,566,765 samples, 0.02%)rcu_core (7,566,765 samples, 0.02%)rcu_do_batch (7,566,765 samples, 0.02%)radix_tree_node_rcu_free (7,566,765 samples, 0.02%)kmem_cache_free (7,566,765 samples, 0.02%)__memcg_slab_free_hook (7,566,765 samples, 0.02%)irq_exit_rcu (12,503,535 samples, 0.04%)__irq_exit_rcu (12,503,535 samples, 0.04%)handle_softirqs (12,503,535 samples, 0.04%)sched_balance_softirq (4,936,770 samples, 0.02%)sched_balance_update_blocked_averages (4,936,770 samples, 0.02%)__sched_balance_update_blocked_averages (4,936,770 samples, 0.02%)update_other_load_avgs (4,906,249 samples, 0.02%)update_dl_rq_load_avg (4,906,249 samples, 0.02%)asm_sysvec_apic_timer_interrupt (202,449,567 samples, 0.65%)sysvec_apic_timer_interrupt (179,175,016 samples, 0.57%)irqentry_exit (4,239,944 samples, 0.01%)schedule (4,233,784 samples, 0.01%)__schedule (4,233,784 samples, 0.01%)pick_next_task (4,204,568 samples, 0.01%)pick_next_task_fair (4,204,568 samples, 0.01%)pick_task_fair (4,204,568 samples, 0.01%)pick_eevdf (4,204,568 samples, 0.01%)asm_sysvec_call_function (12,510,195 samples, 0.04%)entry_SYSCALL_64 (5,237,667 samples, 0.02%)arch_exit_to_user_mode_prepare.isra.0 (5,068,200 samples, 0.02%)exit_to_user_mode_loop (12,127,780 samples, 0.04%)tlb_finish_mmu (5,287,630 samples, 0.02%)__tlb_batch_free_encoded_pages (5,287,630 samples, 0.02%)free_pages_and_swap_cache (5,287,630 samples, 0.02%)folios_put_refs (5,287,630 samples, 0.02%)__page_cache_release.part.0 (5,287,630 samples, 0.02%)lru_gen_del_folio.constprop.0 (5,287,630 samples, 0.02%)__x64_sys_brk (10,502,103 samples, 0.03%)__do_sys_brk (10,502,103 samples, 0.03%)do_vmi_align_munmap (10,502,103 samples, 0.03%)vms_complete_munmap_vmas (10,502,103 samples, 0.03%)unmap_region (10,502,103 samples, 0.03%)unmap_vmas (5,214,473 samples, 0.02%)unmap_single_vma (5,214,473 samples, 0.02%)unmap_page_range (5,214,473 samples, 0.02%)zap_pmd_range.isra.0 (5,214,473 samples, 0.02%)zap_pte_range (5,214,473 samples, 0.02%)zap_present_ptes.constprop.0 (5,214,473 samples, 0.02%)perf_event_sched_in (5,788,601 samples, 0.02%)ctx_sched_in (5,787,253 samples, 0.02%)visit_groups_merge.constprop.0.isra.0 (5,773,899 samples, 0.02%)rb_next (5,702,590 samples, 0.02%)__perf_event_task_sched_in (5,822,066 samples, 0.02%)asm_common_interrupt (4,572,609 samples, 0.01%)common_interrupt (4,572,609 samples, 0.01%)__common_interrupt (4,572,609 samples, 0.01%)handle_edge_irq (4,572,609 samples, 0.01%)handle_irq_event (4,572,609 samples, 0.01%)__handle_irq_event_percpu (4,572,609 samples, 0.01%)nvidia_isr_msix (4,572,609 samples, 0.01%)nvidia_isr (4,572,609 samples, 0.01%)rm_isr (4,572,609 samples, 0.01%)osIsr (4,572,609 samples, 0.01%)osInterruptPending.constprop.0 (4,572,609 samples, 0.01%)rmDeviceGpuLocksRelease (4,572,609 samples, 0.01%)_gpuLocksReleaseHandleDeferredWork (4,572,609 samples, 0.01%)gpumgrGetGpu (4,572,609 samples, 0.01%)tlsEntryGet (4,572,609 samples, 0.01%)_tlsThreadEntryGet (4,572,609 samples, 0.01%)_raw_spin_unlock_irqrestore (4,572,609 samples, 0.01%)finish_task_switch.isra.0 (10,908,146 samples, 0.03%)pick_next_task_fair (14,184,475 samples, 0.05%)sched_balance_newidle (14,184,475 samples, 0.05%)__sched_balance_update_blocked_averages (14,184,475 samples, 0.05%)__update_load_avg_se (4,966,459 samples, 0.02%)pick_next_task (27,639,070 samples, 0.09%)put_prev_entity (13,454,595 samples, 0.04%)__pmu_ctx_sched_out (4,603,774 samples, 0.01%)group_sched_out (4,603,774 samples, 0.01%)event_sched_out (4,603,774 samples, 0.01%)perf_ibs_del (4,603,774 samples, 0.01%)perf_ibs_stop (4,603,774 samples, 0.01%)ctx_sched_out (10,171,764 samples, 0.03%)update_context_time (5,567,990 samples, 0.02%)local_clock (5,567,990 samples, 0.02%)local_clock_noinstr (5,567,990 samples, 0.02%)prepare_task_switch (15,014,101 samples, 0.05%)__perf_event_task_sched_out (10,215,361 samples, 0.03%)perf_event_context_sched_out (10,215,361 samples, 0.03%)psi_group_change (11,775,594 samples, 0.04%)psi_task_switch (16,112,848 samples, 0.05%)sched_clock_cpu (4,337,254 samples, 0.01%)try_to_block_task.constprop.0.isra.0 (5,581,728 samples, 0.02%)dequeue_task (5,581,728 samples, 0.02%)dequeue_task_fair (5,581,728 samples, 0.02%)dequeue_entities (5,581,728 samples, 0.02%)dequeue_entity (5,581,728 samples, 0.02%)futex_do_wait (75,371,550 samples, 0.24%)schedule (75,371,550 samples, 0.24%)__schedule (75,371,550 samples, 0.24%)futex_wait (84,868,062 samples, 0.27%)__futex_wait (80,373,786 samples, 0.26%)futex_wait_setup (4,966,569 samples, 0.02%)futex_hash (4,898,471 samples, 0.02%)__futex_hash (4,870,909 samples, 0.02%)__x64_sys_futex (90,420,143 samples, 0.29%)do_futex (90,420,143 samples, 0.29%)futex_wake (5,524,386 samples, 0.02%)wake_up_q (5,524,386 samples, 0.02%)try_to_wake_up (5,400,671 samples, 0.02%)ttwu_queue_wakelist (5,215,799 samples, 0.02%)sched_clock_cpu (5,215,799 samples, 0.02%)sched_clock (5,215,799 samples, 0.02%)sched_clock_noinstr (5,215,799 samples, 0.02%)native_sched_clock (5,215,799 samples, 0.02%)do_mmap (7,016,027 samples, 0.02%)mmap_region (7,016,027 samples, 0.02%)__mmap_region (7,016,027 samples, 0.02%)__mmap_setup (7,016,027 samples, 0.02%)security_vm_enough_memory_mm (7,016,027 samples, 0.02%)__x64_sys_mmap (7,162,443 samples, 0.02%)ksys_mmap_pgoff (7,162,443 samples, 0.02%)vm_mmap_pgoff (7,162,443 samples, 0.02%)__x64_sys_mprotect (5,105,986 samples, 0.02%)do_mprotect_pkey (5,105,986 samples, 0.02%)mprotect_fixup (4,664,150 samples, 0.01%)perf_event_mmap (4,570,759 samples, 0.01%)perf_event_mmap_event (4,570,759 samples, 0.01%)perf_iterate_sb.constprop.0 (4,570,759 samples, 0.01%)perf_iterate_ctx (4,570,759 samples, 0.01%)vms_complete_munmap_vmas (4,527,397 samples, 0.01%)up_read (4,353,331 samples, 0.01%)rwsem_wake.isra.0 (4,353,331 samples, 0.01%)wake_up_q (4,353,331 samples, 0.01%)try_to_wake_up (4,353,331 samples, 0.01%)ttwu_do_activate (4,353,331 samples, 0.01%)enqueue_task (4,353,331 samples, 0.01%)enqueue_task_fair (4,353,331 samples, 0.01%)enqueue_entity (4,353,331 samples, 0.01%)do_vmi_munmap (4,644,548 samples, 0.01%)do_vmi_align_munmap (4,644,548 samples, 0.01%)__x64_sys_munmap (4,707,518 samples, 0.02%)__vm_munmap (4,707,518 samples, 0.02%)do_syscall_64 (140,513,794 samples, 0.45%)x64_sys_call (118,456,856 samples, 0.38%)entry_SYSCALL_64_after_hwframe (140,516,929 samples, 0.45%)entry_SYSCALL_64_safe_stack (3,582,451 samples, 0.01%)error_return (5,262,293 samples, 0.02%)exc_page_fault (5,248,561 samples, 0.02%)sync_regs (102,817,165 samples, 0.33%)syscall (5,091,411 samples, 0.02%)eck (31,189,077,857 samples, 100.00%)eckall (31,189,230,989 samples, 100%) \ No newline at end of file diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..7699ce8 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,31 @@ +# Documentation + +## User guide / Guide utilisateur + +| Topic / Sujet | English | Français | +| ------------- | ------- | -------- | +| Installation | [en](user-guide/en/installation.md) | [fr](user-guide/fr/installation.md) | +| Command line / Ligne de commande | [en](user-guide/en/cli.md) | [fr](user-guide/fr/cli.md) | +| Terminal UI / Interface | [en](user-guide/en/tui.md) | [fr](user-guide/fr/tui.md) | +| Errors & output / Erreurs | [en](user-guide/en/errors.md) | [fr](user-guide/fr/errors.md) | +| `.encky` format | [en](user-guide/en/format.md) | [fr](user-guide/fr/format.md) | + +> [!NOTE] +> English and French user guides are kept in sync (mirror trees in `user-guide/en` and +> `user-guide/fr`). + +## Developer guide / Guide développeur + +English only (by convention). See [dev/README.md](dev/README.md). + +* [dev/architecture.md](dev/architecture.md) — module design & data flows. +* [dev/modules.md](dev/modules.md) — the `eck/src` map. +* [dev/testing.md](dev/testing.md) — test suite & isolation. +* [dev/fuzzing.md](dev/fuzzing.md) — libFuzzer targets. +* [dev/security.md](dev/security.md) — key handling & audit allowlist. + +## Format + +More on the container: magic `ENK1`, version `3`, postcard metadata, +XChaCha20-Poly1305 per 8 MiB chunk — see [format](user-guide/en/format.md) +(and the French version [format](user-guide/fr/format.md)). diff --git a/doc/dev/README.md b/doc/dev/README.md new file mode 100644 index 0000000..e4b68ed --- /dev/null +++ b/doc/dev/README.md @@ -0,0 +1,65 @@ +# Development guide (English) + +Everything a contributor needs to hack on **Enkryptit!**. + +> [!NOTE] +> This folder is written in English by convention. The user-facing guides are bilingual in +> [`../user-guide`](../README.md). + +## Tooling + +The workspace helperscripts live in `xtask/`: + +| Command | What it does | +| ------- | ------------ | +| `cargo xtask build` / `cargo xtask build --release` | Build `eck` (debug/release). | +| `cargo xtask install` | `cargo install --path ./eck`. | +| `cargo xtask test` | Runs the test binary `--test=eck_tests`. | +| `cargo xtask fuzz` | Lists fuzz targets (`cargo fuzz list`). | +| `cargo xtask fuzz [FLAGS]` | Runs a libFuzzer target; extra flags are passed through (e.g. `-runs=1000`, `-max_total_time=60`). | +| `cargo xtask fmt` | `cargo fmt`. | +| `cargo xtask doc` | Opens rustdoc with private items. | + +## Repository layout + +``` +Cargo.toml # workspace (eck, xtask; eck/fuzz excluded) +eck/ # the crate & binary +eck/src/ # see dev/modules.md +eck/test/ # unit + integration tests + mocks +eck/fuzz/ # cargo-fuzz targets (nightly toolchain) +xtask/ # dev helper binary +doc/ # this documentation +dev/ # benchmarking & audit notes +PROGRESSION.md # day-by-day dev log +CONTRIBUTING.md # contribution rules +``` + +## Quality bar + +* `cargo fmt` clean. +* `cargo clippy -- -D warnings` clean. +* `cargo xtask test` green (unit + integration + TUI flows). +* `cargo doc --no-deps -D warnings` clean. +* `cargo audit` green (see `audit.toml` allowlist). +* The fuzz targets keep running without crashing (see [fuzzing](fuzzing.md)). +* RustDoc comments on public items. + +## CI + +The repo runs GitHub Actions: + +* `ci.yml` — fmt / clippy / tests / rustdoc / audit on `dev` + `main` (push & PR). +* `ci.yml` — bounded fuzz smoke + `#[ignore]`d OS-keyring roundtrip. +* `release.yml` — tagged `v*` release matrix (draft release). +* `dependabot.yml` — weekly dependency updates. + +See the workflow files under `.github/workflows`. + +## Related + +* [Architecture](architecture.md) — module design & data flows. +* [Modules](modules.md) — `eck/src` map. +* [Testing](testing.md) — test suite structure & isolation. +* [Fuzzing](fuzzing.md) — libFuzzer targets. +* [Security](security.md) — key handling, parameters & the audit allowlist. \ No newline at end of file diff --git a/doc/dev/architecture.md b/doc/dev/architecture.md new file mode 100644 index 0000000..5e24743 --- /dev/null +++ b/doc/dev/architecture.md @@ -0,0 +1,71 @@ +# Architecture (English) + +High-level design of the `eck` crate. + +## Pipeline overview + +On an encryption run, the flow is: + +``` +EnkryptitParams + ├─ key_params -> how the key is unlocked (password / file / OS keyring) + ├─ compression -> zstd | lz4 | xz | none | auto + └─ parallelism -> single | multi(auto) | multi(n) | auto + +Path -> treatment::treat_object + ├─ directory? -> encrypt_folder_case + intern archive + └─ file? -> inspect: reads header (magic + version) + ├─ .encky -> decrypt (single stream or folder archive) + └─ other -> encrypt (single file) or inspect-only +``` + +Both encrypt and decrypt cut the stream into 8 MiB chunks (`CHUNK_SIZE`), **compress then +encrypt** every chunk, and dispatch the work to a shared memory-locked worker pool. + +## Concurrency model + +* `EnkryptitPool` (`parallelism/`): + * a synchronized **jobs** channel backed by `sync_channel(size * 2)`; + * a **results** channel; + * each `EnkryptitWorker` loops `recv -> execute -> send` until the channel closes. +* `EnkryptitJob` / `ChunkResult { index, data }` (`chunk_job.rs`) keep chunk ordering intact + despite the unordered worker pool. +* `infer_parallelism` (`context.rs`) chooses `MultiThread` automatically based on file size + thresholds and `available_parallelism()`. + +## Format decision tree + +The plaintext vs `.encky` decision happens in `treatment::treat_object`: +folders go to `encrypt_folder_case`; regular files read the header and branch on the magic +number and `is_folder_archive` flag (see [`format.md`](../user-guide/en/format.md)). + +## Key material + +Keys never stay in plain RAM: + +* `EnkryptitKey { key: LockedKey, key_type }` — `LockedKey` uses `mlock` on the backing + bytes and `zeroize` on drop. +* Resolution happens only at the moment of encrypt/decrypt (`key/resolve.rs`), never + before: encrypting generates a missing key, decrypting **errors** if the key cannot be + found (`KeyNotFoundInFile` / `KeyNotFoundInOs`). + +See [Security](security.md). + +## Error & reporting surface + +* `EnkryptitError` → diagnostics through `miette` (labels, help, codes). +* `EnkryptitOutput` — typed effect returned everywhere; the frontends render it as + `Success` / `Warning` / `Info` / `Error` banners. +* `DOC_URL` constant points at `doc/` so every diagnostic links to the documentation. + +## Frontends + +The core is UI-agnostic: the same `treatment`/`inspector` produce `EnkryptitOutput`, +rendered either by the **CLI** (`frontend/cli.rs`) or the **TUI** +(`frontend/tui.rs`, an iterator `save` loop over a mockable `TuiInput`). + +## Related + +* [Modules](modules.md) — exact file map of `eck/src`. +* [Testing](testing.md) — how the layers are verified. +* [Fuzzing](fuzzing.md) — parsing/roundtrip invariants. \ No newline at end of file diff --git a/doc/dev/fuzzing.md b/doc/dev/fuzzing.md new file mode 100644 index 0000000..56602d5 --- /dev/null +++ b/doc/dev/fuzzing.md @@ -0,0 +1,47 @@ +# Fuzzing (English) + +The parser and roundtrip invariants are fuzzed with **cargo-fuzz + libFuzzer**. + +## Targets + +Defined in `eck/fuzz/` (its own workspace with a `nightly` toolchain): + +| Target | Invariant | +| ------ | --------- | +| `decompress` | Feeding arbitrary bytes to the decompression pipeline never panics or hangs. | +| `compress_roundtrip` | Compress → decompress of arbitrary input yields the original bytes (or fails cleanly). | +| `metadata` | Parsing arbitrary postcard metadata/headers never panics. | + +## Running locally + +```bash +cargo xtask fuzz # list targets +cargo xtask fuzz decompress # run a target (all extra args pass through) +cargo xtask fuzz decompress -runs=1000 -max_total_time=60 +``` + +> [!NOTE] +> `cargo-fuzz` requires the **nightly** Rust toolchain (`eck/fuzz/rust-toolchain.toml`). +> The `fuzz/` workspace is excluded from the main workspace's CI compile jobs. + +## Corpus + +* Interesting inputs are kept in `eck/fuzz/corpus//` (gitignored). +* Reinvesting the corpus on every target run keeps the interesting cases around. + +## In CI + +A **bounded smoke job** runs each target with `-max_total_time=60`; a crash, timeout or +panic fails the job (**blocking**): see [CI](README.md). + +## Adding a target + +1. Create `eck/fuzz/fuzz_targets/.rs` using the standard `fuzz_target!` macro and + one of the crate's entry points. +2. Declare it in `eck/fuzz/Cargo.toml` under `[[bin]]`. +3. Document the invariant above in the doc-comment. + +## Related + +* [Testing](testing.md) — deterministic tests. +* [Modules](modules.md) — the parsing functions being fuzzed. \ No newline at end of file diff --git a/doc/dev/modules.md b/doc/dev/modules.md new file mode 100644 index 0000000..28dd504 --- /dev/null +++ b/doc/dev/modules.md @@ -0,0 +1,47 @@ +# Modules (English) + +Map of `eck/src`, grouped by area. + +| Path | Responsibility | +| ---- | -------------- | +| `main.rs` | Binary entry. `clap` parser (name `eck`, version 0.0.3), subcommands, dispatch to CLI/TUI. | +| `frontend/cli.rs` | CLI rendering: `params_helpers` (show/update), `treatment` (multi-path), `inspection`. | +| `frontend/tui.rs` | Interactive loop `Browse / Parameters / Help / Exit`; `TuiInput` is mockable. | +| `treatment/mod.rs` | `treat_object` dispatcher; `ParsedFile`; folder vs. file branch. | +| `inspect/mod.rs` | `InspectionReport { PlaintextFile, EncryptedFile, EncryptedArchive, Folder }`, comfy-table display, invocation snippet. | +| `types/enkryptit_key.rs` | `EnkryptitKey { key: LockedKey, key_type }`, type classify helpers. | +| `types/key_type.rs` | `KeyType { Password, Pwd256([u8;16]), FromFile, FromOS, None }`. | +| `types/compression_type.rs` | `CompressionType { Auto, Lz4, Zstd, Xz, NoComp }`. | +| `types/parallelism_type.rs` | `ParallelismType { Auto, MultiThread(u8), Single }`. | +| `types/params.rs` | `EnkryptitParams { key_params, compression, parallelism }`, defaults `{PassWord, Auto, Auto}`, config round-trip. | +| `types/metadatas.rs` | `MetaDatas`, `ArchiveHeader` (`magic`, `version`, `is_folder_archive`, `meta_len`), `FileEntry`, postcard. | +| `types/conversions.rs` | To/from string + internal conversions. | +| `parameters/mod.rs` | Loading/saving `config.json`, `ECK_CONFIG_PATH` override, resolution helpers. | +| `context/compression.rs` | `infer_compression` from file-type hint + size thresholds. | +| `context/parallel.rs` | `infer_parallelism` from size + `available_parallelism`. | +| `encryption/mod.rs` | Encryption orchestration. | +| `encryption/encryption_flow.rs` | `encrypt_stream` / `decrypt_stream`, chunking. | +| `encryption/chunk_job.rs` | `EncryptChunkJob` / `DecryptChunkJob`, `ChunkResult { index, data }`. | +| `encryption/encryption_primitives.rs` | XChaCha20-Poly1305; `derive_nonce(master_nonce, step)`. | +| `encryption/file_encryption/` | Single-file `.encky` handling. | +| `encryption/folder_encryption/` | Folder archive (v2 head-patchable format). | +| `key/derivation.rs` | Argon2id KDF (128 MiB, 3 iterations, 1 lane, 32 bytes). | +| `key/resolve.rs` | Key unlock paths: password prompt, `~/private_keys/enkryptit_`, OS keyring (`ENKRYPTIT` service). | +| `parallelism/` | `EnkryptitPool`, `EnkryptitWorker`, `EnkryptitJob`, `EnkryptitExecutable` trait. | +| `compression.rs` | `EnkryptitCompress`/`EnkryptitDecompress` for `zstd`, `lz4_flex`, `xz2`. | +| `diagnostic/mod.rs` | `EnkryptitOutput` kinds, `DOC_URL`, logging (`tracing`, daily rotation). | +| `diagnostic/errors_builder.rs` | `EnkryptitTraitBuilder` mapping errors to diagnostics. | +| `errors.rs` | `EnkryptitError` variants + `code()` table (`crypto::`, `comp::`, `ui::`, `format::`, `io::`, `misc::`). | +| `directory.rs` | `ProjectDirs::from("com", "olruix", "Enkryptit")` for config + logs. | + +## Conventions + +* Errors carry a **code** (`domain::snake_case`), defined in `EnkryptitError::code()`. +* Public types implement `std::fmt::Display` + `TryFrom`/`FromStr` in `conversions.rs`. +* No `unsafe` in the crate; memory locking is delegated to the `mlock`-backing key type. +* `Enum` names are un-prefixed (a `KeyType` without an `Enkryptit` prefix). + +## Related + +* [Architecture](architecture.md) — how these pieces interact. +* Description of each public type lives in the rustdoc generated by `cargo xtask doc`. \ No newline at end of file diff --git a/doc/dev/security.md b/doc/dev/security.md new file mode 100644 index 0000000..d19386e --- /dev/null +++ b/doc/dev/security.md @@ -0,0 +1,68 @@ +# Security (English) + +Threat model, key handling and known caveats. + +## Key handling + +* Keys are stored in **memory-locked** buffers: `LockedKey` keeps the backing bytes + `mlock`ed and `zeroize`s them on drop (page-fault hardening, no copy on disk). +* A key exists only for the duration of an encrypt/decrypt run; it is re-resolved each time, + never cached across CLI invocations. +* `EnkryptitKey` always knows its provenance (`key_type`) and validates lengths during + resolve (`InvalidKeyLength` for bad sizes). + +## Key derivation + +Passwords go through **Argon2id** (RFC 9106-ish, 128 MiB, 3 iterations, 1 lane, 32-byte +output) with a 16-byte random salt per encryption: + +``` +password (+salt) -> argon2id -> 32-byte key +``` + +## Secrets storage + +| Key type | Location | +| -------- | -------- | +| `Password` | prompted, or `-p` from the shell (visible in `ps` if used on the CLI — prefer environment or stdin). | +| `Pwd256` | salted argon2id (above). | +| `File` | `~/private_keys/enkryptit_` (per-file key files). | +| `Os` | OS keyring: service `ENKRYPTIT`, user `enkryptit`, value hex-encoded. | + +> **Note:** the `-p` flag value may be visible in process listings/history. Favor the +> interactive prompt or the OS keyring for sensitive environments. + +## Format / integrity + +* AEAD (XChaCha20-Poly1305) authenticates every chunk; tampering fails decryption with an + auth error rather than silently corrupting data. +* `ENK1END` marker detects truncated single-file streams; folder archives validate their + trailing `FolderMetadata`. +* Magic + version gate every parse; unknown versions error instead of guessing. + +## Known caveats + +* Zip-style folder architectures currently rely on file `relative_path`s; paths with + problematic Unicode are rejected during inspection (`misc::inspection_failed`). +* `panic = "abort"` in release builds jumps straight to abort (no destructors run) — the + keyed buffers are only guaranteed zeroized on the normal drop path in debug/threads. +* The auditing stance below does not silence general advisories, only three transitives. + +## Dependency audit + +`cargo audit` runs in CI. The `audit.toml` at the repo root allowlists exactly three +**unmaintained-transitive** advisories, all from non-crypto indirect dependencies: + +| Advisory | Crate | Reason to allow | +| -------- | ----- | --------------- | +| `RUSTSEC-2023-0089` | `atomic-polyfill` | unmaintained transitive dev/tooling dep; not in the security-critical path. | +| `RUSTSEC-2024-0384` | `instant` | unmaintained transitive; no exploitable path used by the crate. | +| `RUSTSEC-2024-0388` | `derivative` | unmaintained transitive; unsafely used only by an indirect dev dependency. | + +`dev/audit.md` mirrors this allowlist and records the reasoning. Do not extend the allowlist +for direct or security-relevant dependencies. + +## Reporting + +Open an issue / private advisory upstream. Until a fix lands, prefer NOT to weaken the +allowlist above. \ No newline at end of file diff --git a/doc/dev/testing.md b/doc/dev/testing.md new file mode 100644 index 0000000..9732bef --- /dev/null +++ b/doc/dev/testing.md @@ -0,0 +1,48 @@ +# Testing (English) + +How the crate is verified. + +## Layout + +* `eck/test/mod.rs` — declares the single `eck_tests` harness (unit + integration). +* Unit tests live next to the code (`#[cfg(test)]` modules everywhere). +* Integration tests cover end-to-end encrypt/decrypt of files **and** folders. +* `eck/test/` also holds mocks and the messages `eck_tests` asserts on. + +## Running + +```bash +cargo xtask test +``` + +runs `cargo test --test=eck_tests` (all of them). + +## Isolation + +* Tests never touch the real user config: `ECK_CONFIG_PATH` (env var) redirects the + parameters file to a temp dir, and `tempfile` crates throwaway directories. +* The OS-keyring integration test is an `#[ignore]`d test: + `encrypt_decrypt_os_keytype_roundtrip` (`eck/test/integration/encryption_flow.rs:153`). + It is run manually and by CI on macOS only (see [CI](README.md)). + +## What is covered + +1. **Key resolution** — password / key-file / OS-keyring roundtrips. +2. **Chunk flow** — `encrypt_stream`/`decrypt_stream` on overlapping-size inputs. +3. **Folder archives** — full directory roundtrip, preserving files, structure and + permissions. +4. **Compression** — `zstd`, `lz4`, `xz`, `none` over byte ranges. +5. **CLI** — `assert_cmd`-style invocations of the built `eck` binary. +6. **TUI flows** — dialogs simulated through the mockable `TuiInput`. +7. **Errors** — wrong keys, truncated files, nonexistent paths surface the expected codes. + +## New tests + +Contributions should add both a fast unit test in the owning module and an integration test +in `eck/test/integration` when the behavior crosses module boundaries. Fuzz targets live in +[`fuzzing.md`](fuzzing.md). + +## Related + +* [Modules](modules.md) — where the tested code lives. +* [Fuzzing](fuzzing.md) — property-style coverage. \ No newline at end of file diff --git a/doc/user-guide/en/cli.md b/doc/user-guide/en/cli.md new file mode 100644 index 0000000..733fb79 --- /dev/null +++ b/doc/user-guide/en/cli.md @@ -0,0 +1,78 @@ +# Command Line + +`eck` is a scriptable CLI. If no path is given, `eck` opens the [TUI](tui.md). + +## Global options + +| Option | Description | +| ------ | ----------- | +| `eck ...` | Encrypts a plaintext path or decrypts a `.encky` path (file or folder). Multiple paths accepted. | +| `-p, --password ` | Provide the password directly instead of prompting. | +| `eck ui` | Open the TUI. | +| `eck inspect ...` | Inspect paths without encrypting/decrypting. | + +## Commands + +| Command | Meaning | +| ------- | ------- | +| `eck ` | Encrypts a plaintext file / folder, or decrypts an `.encky`. | +| `eck -p ` | Same, with the password given on the command line. | +| `eck ui` | Opens the interactive TUI. | +| `eck parameters` or `eck params` | Shows the current parameters. | +| `eck params -c ` | Changes the compression algorithm. Values: `zstd`, `lz4`, `xz`, `none`, `auto`. | +| `eck params -k ` | Changes the key type (`params -k` alias `--keytype`). Values: `os`, `file`, `pwd`/`password`. | +| `eck params -p ` | Changes the parallelism mode. Values: `single`, `multi`, `multi:` or `auto` | +| `eck inspect ` | Inspects a file or archive without encrypting or decrypting it. | + +`parameters` and `params` are equivalent; both accept `-c`, `-k` (`--keytype`) and `-p`. +The `parameters` variant also exposes the visible alias `kt` for `--keytype`. +Without any flag they print the current parameters instead. + +> **Note:** `eck params -k os` and `eck parameters --keytype os` both set the key type. + +## Behavior + +``` +eck /home/user/secrets/secrets.txt -> secrets.txt.encky +eck /home/user/secrets.txt.encky -> secrets.txt +``` + +* Multiple paths are processed in one call: + + ``` + eck /home/user/secrets/* /home/user/secret.txt /lib/secret.bin + eck inspect myfolder/* + ``` + +* A **folder** is turned into a single **archive** `.encky` (see [format](format.md)). +* Encryption/decryption is chosen automatically: `.encky` files decrypt, everything else encrypts. + +> [!NOTE] +> When you encrypt an object with a **keyfile**, the file is stored in `HOME/private_keys`. + +> [!WARNING] +> Encryption with **keyring** is unstable for now. + +## Examples + +```bash +# Encrypt a file with a password +eck secrets.txt -p 's3cret!' + +# Encrypt a folder (produces secrets.encky) +eck secrets/ + +# Decrypt it back +eck secrets.encky + +# Change compression (persisted in the config) +eck params -c zstd + +# Inspect without decrypting +eck inspect secrets.txt.encky +``` + +## Exit behavior + +Success and failure are reported through **EnkryptitOutput** banners (see [errors](errors.md)). +CLI parameter errors exit with a non-zero status and a warning. \ No newline at end of file diff --git a/doc/user-guide/en/errors.md b/doc/user-guide/en/errors.md new file mode 100644 index 0000000..a91e4e5 --- /dev/null +++ b/doc/user-guide/en/errors.md @@ -0,0 +1,104 @@ +# Errors & output + +Every `eck` run reports through **EnkryptitOutput** banners. Errors are rendered with +`miette` (fancy graphical reports) and are also written to the log file. + +> TODO: verify the exact visual rendering and whether severity shows the ✖ bullet, the +> location line, or an embedded code block depending on kind. + +## Anatomy of an error + +| Field | Meaning | +| ----- | ------- | +| **Message** | Description of what failed. | +| **Code** | Stable machine-readable code, `domain::snake_case` (or `none` if no specific code). | +| **Location** | Where in the code base it happened (when known). | +| **Help** | A remediation hint (when available). | +| **Snippet** | Contextual CLI invocation, e.g. `eck ` for example (when available). | +| **URL** | A link to the documentation: `doc/`. | + +## Error codes + +All codes come from `EnkryptitError::code()` in `eck/src/errors.rs`. Codes are grouped by +domain. + +### `crypto::` + +| Code | Error variant | Message | Help | +| ---- | ------------- | ------- | ---- | +| `crypto::argon2error` | `Argon2Error` | argon2 password hash failed | Key derivation with Argon2 failed. Check your available memory. | +| `crypto::encryption_decryption_failed` | `EncryptionError` | encryption/decryption failed | Check your password/key and make sure the file is not corrupted. | +| `crypto::invalid_key_length` | `InvalidKeyLength` | invalid key length | The key must be exactly 32 bytes long. | +| `crypto::invalid_key_type` | `InvalidKeyType` | Invalid key type. Found X, expected Y | Use a supported key type: Password, Os or File. | +| `crypto::key_derivation_failed` | `KeyDerivationError` | key derivation error | The password or key file seems invalid. | +| `crypto::key_not_found_in_file` | `KeyNotFoundInFile` | The file containing the key was not found | Place your key file at the configured path. | +| `crypto::key_not_found_in_os` | `KeyNotFoundInOs` | The Key couldn't be found in Os' keyring | Store the key in the OS keyring (e.g. keychain / GNOME Keyring) first. | +| `crypto::error_with_os_keyring` | `KeyringError` | keyring error | Make sure your OS keyring is unlocked. | + +### `comp::` + +| Code | Error variant | Message | Help | +| ---- | ------------- | ------- | ---- | +| `comp::lz4_comp_failed` | `Lz4CompressionError` | Lz4 compressionError | The data is corrupted or not Lz4-compressed. | +| `comp::zstd_comp_failed` | `ZstdError` | zstd error | The data is corrupted or not Zstandard-compressed. | + +> **Note:** `Lz4DecompressionError` currently carries no specific code (`none`). + +### `ui::` + +| Code | Error variant | Message | +| ---- | ------------- | ------- | +| `ui::command_not_found` | `CommandNotFound` | Command not found | +| `ui:tui_error` | `TuiError` | Error with the Tui | + +> **Note:** the TUI code uses a single-colon separator `ui:tui_error` (kept as-is in the source). + +### `format::` + +| Code | Error variant | Message | Help | +| ---- | ------------- | ------- | ---- | +| `format::corrupted_file` | `CorruptedFile` | Corrupted File | Wait for `eck recover ` for help. | +| `format::directory_is_folder` | `DirectoryIsFolder` | Directory found is the directory of the folder | You can't treat the current folder itself. | +| `format::metadata_reading_failed` | `FailedToReadMetadata` | Failed to read metadata of file | Check that the file still exists and is readable. | + +### `io::` + +| Code | Error variant | Message | Help | +| ---- | ------------- | ------- | ---- | +| `io::unexpected_eof` | `UnexpectedEof` | unexpected end of file | The file is truncated; it may be corrupted. | +| `io::home_directory_not_found` | `HomeNotFound` | home directory not found | Set the HOME environment variable. | +| `io::misc_io_error` | `IoError` | io error | Check that the path exists and that you have the required permissions. | +| `io::incorrect_path` | `PathIsIncorrect` | Path is incorrect | Check that the path exists. | +| `io::file_not_found` | `FileError`/`SpecificFileError` | error while searching for the file | Check that the file exists and is readable. | +| `io::file_is_a_symlink` | `FileIsASymLink` | File is a SymLink (shortcut) | Use the real path instead of a symbolic link. | +| `io::permissions_reading_failed` | `FilePermissionsParsingError` | Error while parsing file permissions | The permissions of this file could not be parsed. | + +### `misc::` + +| Code | Error variant | Message | Help | +| ---- | ------------- | ------- | ---- | +| `misc::inspection_failed` | `InspectionError` | Inspection failed | Please ensure that the file exists, and that its name does not contain wrong unicode character. | +| `misc::hex_decoding_failed` | `HexError` | hex decoding error | — | +| `misc::config_directory_error` | `ConfigError` | configuration error | Check the config file location and format. | +| `misc::break` | `Break` | operation interrupted | — | + +### Uncoded (`none`) + +The following variants report `none` as their code: + +* `PostcardError` — invalid serialized data ("The file seems corrupted or not in the Enkryptit! format."). +* `StripPrefixError` — path prefix stripping. +* `SendError`, `ReceiveError`, `InvalidWorkerCount` — parallelism plumbing. +* `MemoryLockError` — `mlock` failure while locking keys in RAM. +* `SerdeJsonError` — the parameters/config file is not valid JSON. +* `UnknownAction` — unknown command dispatch. +* `Lz4DecompressionError` — lz4 decompression failure. + +## Logs + +A daily-rotated log file (`log.txt`) is written through `tracing` into the Enkryptit config +directory. Errors are always traced before being displayed. + +## Related + +* [`.encky` format](format.md) — the container these errors parse. \ No newline at end of file diff --git a/doc/user-guide/en/format.md b/doc/user-guide/en/format.md new file mode 100644 index 0000000..766ac79 --- /dev/null +++ b/doc/user-guide/en/format.md @@ -0,0 +1,108 @@ +# The `.encky` format + +An encrypted artifact is written as **`.encky`**. There are **two** container kinds: + +1. a **single-file** encrypted file, +2. a **folder archive** (a whole directory in one `.encky`). + +Everything is serialized with **postcard**; the version is stored in the header +(`VERSION` constant, currently `3` — the 3rd nightly of the first stable line). + +## Common primitives + +* Magic number: `0x45 0x4E 0x4B 0x31` = `ENK1`. +* Chunk size: `CHUNK_SIZE = 8 MiB`. +* Cipher: **XChaCha20-Poly1305** (24-byte nonce, AEAD). +* A 32-byte key (from password / key file / OS keyring). + +## Master nonce & derived nonces + +Each object (or archive entry) has a **master nonce** stored in its metadata. For every +chunk `step`, a derived nonce is built: + +``` +new_nonce[0..16] = master_nonce[0..16] +new_nonce[16..24] = step.to_le_bytes() # 8 bytes, little-endian +``` + +## Encrypted stream (payload) + +For a single logical stream the payload is a sequence of chunks followed by an end marker: + +``` +[ chunk_len:u32 LE ][ compressed_fragment ] +[ chunk_len:u32 LE ][ compressed_fragment ] +... +[ "ENK1END" ] # 7 literal bytes +``` + +* Each chunk is **compressed**, then **encrypted** with the derived nonce of its step. +* The compressed part is placed in a buffer up to `CHUNK_SIZE`, but each written fragment + is preceded by its actual length (`u32`, little-endian). + +## Single-file layout + +``` +offset 0 : header_len:u8 # postcard header length (>= 1 byte) + ArchiveHeader := magic[4] + version:u8 + is_folder_archive:false + meta_len:u32 + MetaDatas := key_type + compression + nonce[24] + then the encrypted payload stream (see above), ended by ENK1END +``` + +`payload_offset` for decryption = `1 + header_len + meta_len`. + +## Folder archive layout (v2+) + +``` +byte 0 : HEADER_REGION_SIZE:u8 (= 64) +bytes 1 .. 65 : ArchiveHeader, zero-padded to 64 bytes + (magic, version, is_folder_archive:true, meta_len) +bytes 65 .. : one encrypted stream per file, sequentially, + each with its own file_nonce (see FileEntry) +end of file : FolderMetadata (postcard) + = key_type + entries[ FileEntry ] +``` + +* `HEADER_REGION_SIZE` keeps the header area fixed so the header can be patched in place + without changing the region size. +* Entries are encrypted one after the other; each `FileEntry.offset` records its absolute + byte offset inside the archive. + +### `FileEntry` (postcard) + +| Field | Type | Meaning | +| ----- | ---- | ------- | +| `relative_path` | `String` | path relative to the archive root | +| `offset` | `u64` | absolute offset of the stream in the archive | +| `permissions` | `Option` | Unix permissions to restore on extract | +| `compression` | `CompressionType` | algorithm used for that entry | +| `file_nonce` | `[u8; 24]` | per-entry master nonce | + +### `MetaDatas` (postcard) + +| Field | Type | Meaning | +| ----- | ---- | ------- | +| `key_type` | `KeyType` | how the key must be resolved | +| `compression` | `CompressionType` | algorithm used | +| `nonce` | `[u8; 24]` | master nonce | + +### `ArchiveHeader` (postcard) + +| Field | Type | Meaning | +| ----- | ---- | ------- | +| `magic` | `[u8; 4]` | `ENK1` | +| `version` | `u8` | the Enkryptit! version that wrote the file | +| `is_folder_archive` | `bool` | file vs. folder container | +| `meta_len` | `u32` | length of the trailing metadata | + +## Truncation & corruption + +* Single-file streams end with `ENK1END`; an unexpected EOF maps to `io::unexpected_eof`. +* Folder archives with `version >= 2` keep their metadata at the end — a missing trailer is + reported as `format::corrupted_file`. + +## Related + +* [Errors](errors.md) — codes for corrupted/truncated artifacts. +* [Installation](installation.md) — how to get `eck`. +* Format history: `PROGRESSION.md` (days 1–17). \ No newline at end of file diff --git a/doc/user-guide/en/installation.md b/doc/user-guide/en/installation.md new file mode 100644 index 0000000..b4fe85b --- /dev/null +++ b/doc/user-guide/en/installation.md @@ -0,0 +1,49 @@ +# Installation + +This page explains how to build and install **Enkryptit!** on your machine. + +> **TODO (fr/en sync):** adapt specific remarks (macOS Gatekeeper, Linux `libsecret`, Windows store) as platform-specific notes get written. + +## Requirements + +* **Rust:** `1.85+` (the declared `rust-version` of the workspace). Edition `2024` is used. +* A native credential store for `Key type = OS Keyring`: + * **Linux:** `libsecret` (GNOME Keyring / KSecretService). + * **macOS:** the system Keychain. + * **Windows:** the Windows Credential Manager. + +## Build + +```bash +git clone https://github.com/VRAM-RAM/Enkryptit +cd Enkryptit/ + +# Debug build +cargo xtask build + +# Release build (recommended) +cargo xtask build --release +``` + +The `eck` binary is produced at `target/release/eck` (or `target/debug/eck`). + +> **Note:** the release profile is aggressive (`lto = "fat"`, `codegen-units = 1`, +> `panic = "abort"`). Compilation can take a while on the first build. + +## Install + +```bash +cargo xtask install +``` + +This runs `cargo install --path ./eck` and puts `eck` on `$PATH`. + +## First run + +```bash +eck # opens the TUI +eck --help # CLI help +eck --version # prints enkryptit version +``` + +See [CLI](cli.md) and [TUI](tui.md) for usage. \ No newline at end of file diff --git a/doc/user-guide/en/tui.md b/doc/user-guide/en/tui.md new file mode 100644 index 0000000..1bc84c7 --- /dev/null +++ b/doc/user-guide/en/tui.md @@ -0,0 +1,66 @@ +# Terminal UI + +The interactive terminal interface lets you browse and treat files and folders without +remembering command-line flags. + +## Launching + +```bash +eck # no argument -> TUI +eck ui # explicit TUI command +``` + +## Main menu + +``` +Enkryptit + Fast & Simple File Encryption Manager v0.0.3 +? What do you want to do? +> Browse + Parameters + Help + Exit +``` + +| Entry | Description | +| ----- | ----------- | +| **Browse** | Pick files, folders, or both, then encrypt / decrypt / inspect them. | +| **Parameters** | Configure compression, key type and parallelism. | +| **Help** | Shows the available commands. | +| **Exit** | Leaves the TUI. | + +## Browse panel + +``` +Browser Panel +? What do you want to Browse? +> Browse Files + Browse Folders + Browse both Files & Folders + Back to main menu +``` + +After picking file(s)/folder(s) with the system picker, each selection runs the standard +treatment (encrypt `.encky` / decrypt plaintext). + +> **Note:** the TUI prompts for the password (and cipher) interactively when needed. + +## Parameters panel + +``` +Parameters Panel +? What do you want to configure? +> Change compression type + Change key type + Change parallelism type + Show current parameters + Back to main menu +``` + +Choices are persisted to the same `config.json` as the CLI (see [Parameters](../cli.md)). + +## Related + +* [CLI](cli.md) — the equivalent command-line interface, including the parameters mapping. + +> **TODO:** add a screenshot of the actual TUI rendering. \ No newline at end of file diff --git a/doc/user-guide/fr/cli.md b/doc/user-guide/fr/cli.md new file mode 100644 index 0000000..4d41340 --- /dev/null +++ b/doc/user-guide/fr/cli.md @@ -0,0 +1,82 @@ +# Ligne de commande + +`eck` est une CLI scriptable. Sans chemin donné, `eck` ouvre la [TUI](tui.md). + +## Options globales + +| Option | Description | +| ------ | ----------- | +| `eck ...` | Chiffre le contenu d'un path (si non chiffré) ou déchiffre son contenu si chiffré : `.encky` (fichier ou dossier). On peut renseigner plusieurs paths. | +| `-p, --password ` | Fournit le mot de passe directement au lieu de le demander. | +| `eck ui` | Ouvre la TUI. | +| `eck inspect ...` | Inspecte des paths sans chiffrer/déchiffrer. | + +## Commandes + +| Commande | Signification | +| -------- | ------------- | +| `eck ` | Chiffre un fichier / dossier en clair, ou déchiffre un `.encky`. | +| `eck -p ` | Identique, avec le mot de passe en ligne de commande. | +| `eck ui` | Ouvre l'interface interactive (TUI). | +| `eck parameters` ou `eck params` | Affiche les paramètres courants. | +| `eck params -c ` | Change l'algorithme de compression. Valeurs : `zstd`, `lz4`, `xz`, `none`, `auto`. | +| `eck params -k ` | Change le type de clé (`params -k`, alias `--keytype`). Valeurs : `os`, `file`, `pwd`/`password`. | +| `eck params -p ` | Change le mode de parallélisme. Valeurs : `single`, `multi`, `multi:`. | +| `eck inspect ` | Inspecte un fichier ou une archive sans le/la chiffrer ni déchiffrer. | + +`parameters` et `params` sont équivalents ; ils acceptent tous deux `-c`, `-k` +(`--keytype`) et `-p`. La variante `parameters` expose aussi l'alias visible `kt` pour +`--keytype`. Sans drapeau, ils affichent les paramètres courants. + +> **Remarque :** `eck params -k os` et `eck parameters --keytype os` changent tous deux le +> type de clé. + +## Comportement + +``` +eck /home/user/secrets/secrets.txt -> secrets.txt.encky +eck /home/user/secrets.txt.encky -> secrets.txt +``` + +* Plusieurs paths sont traités en un seul appel : + + ``` + eck /home/user/secrets/* /home/user/secret.txt /lib/secret.bin + eck inspect mon_dossier/* + ``` + +* Un **dossier** est transformé en une seule **archive** `.encky` + (voir [format](format.md)). +* Le chiffrement/déchiffrement est choisi automatiquement : les fichiers `.encky` sont + déchiffrés, tout le reste est chiffré. + +> [!NOTE] +> Lorsque vous chiffrez un objet avec le mode de clef '**File**, le fichier contenant la clef est stocké dans `/HOME/private_keys` + +> ![WARNING] +> Le stockage des clefs dans le **keyring** OS est instable ! + +## Exemples + +```bash +# Chiffrer un fichier avec un mot de passe +eck secrets.txt -p 's3cret!' + +# Chiffrer un dossier (produit secrets.encky) +eck secrets/ + +# Le déchiffrer +eck secrets.encky + +# Changer la compression (persisté dans la configuration) +eck params -c zstd + +# Inspecter sans déchiffrer +eck inspect secrets.txt.encky +``` + +## Comportement de sortie + +Le succès et les échecs sont rapportés via des bannières +(voir [erreurs](errors.md)). Les erreurs de paramètres CLI sortent avec un statut non nul +et un avertissement. \ No newline at end of file diff --git a/doc/user-guide/fr/errors.md b/doc/user-guide/fr/errors.md new file mode 100644 index 0000000..ad18075 --- /dev/null +++ b/doc/user-guide/fr/errors.md @@ -0,0 +1,105 @@ +# Erreurs et sortie + +Chaque exécution de `eck` rapporte par des bannières **EnkryptitOutput**. Les erreurs sont +rendues avec `miette` (rapports graphiques) et sont aussi écrites dans le fichier de log. + +## Anatomie d'une erreur + +| Champ | Signification | +| ----- | ------------- | +| **Message** | Description lisible de ce qui a échoué. | +| **Code** | Code stable, lisible par machine, `domaine::snake_case` (ou `none` sans code spécifique). | +| **Location** | Où l'erreur est survenue dans le code (si connu). | +| **Help** | Une aide pour résoudre l'erreur (si possible). | +| **Snippet** | L'invocation CLI contextuelle, ex. `eck ` (si disponible). | +| **URL** | Un lien vers la documentation : `doc/`. | + +## Codes d'erreur + +Tous les codes proviennent de `EnkryptitError::code()` dans `eck/src/errors.rs`. Ils sont +regroupés par domaine. + +### `crypto::` + +| Code | Variante | Message | Aide | +| ---- | -------- | ------- | ---- | +| `crypto::argon2error` | `Argon2Error` | argon2 password hash failed | La dérivation de clé Argon2 a échoué. Vérifiez votre mémoire disponible. | +| `crypto::encryption_decryption_failed` | `EncryptionError` | encryption/decryption failed | Vérifiez votre mot de passe/clé et que le fichier n'est pas corrompu. | +| `crypto::invalid_key_length` | `InvalidKeyLength` | invalid key length | La clé doit faire exactement 32 octets. | +| `crypto::invalid_key_type` | `InvalidKeyType` | Invalid key type. Found X, expected Y | Utilisez un type de clé pris en charge : Password, Os ou File. | +| `crypto::key_derivation_failed` | `KeyDerivationError` | key derivation error | Le mot de passe ou le fichier de clé semble invalide. | +| `crypto::key_not_found_in_file` | `KeyNotFoundInFile` | The file containing the key was not found | Placez votre fichier de clé au chemin configuré. | +| `crypto::key_not_found_in_os` | `KeyNotFoundInOs` | The Key couldn't be found in Os' keyring | Stockez d'abord la clé dans le trousseau (ex. Keychain / GNOME Keyring). | +| `crypto::error_with_os_keyring` | `KeyringError` | keyring error | Assurez-vous que votre trousseau système est déverrouillé. | + +### `comp::` + +| Code | Variante | Message | Aide | +| ---- | -------- | ------- | ---- | +| `comp::lz4_comp_failed` | `Lz4CompressionError` | Lz4 compressionError | Les données sont corrompues ou non compressées en Lz4. | +| `comp::zstd_comp_failed` | `ZstdError` | zstd error | Les données sont corrompues ou non compressées en Zstandard. | + +> **Remarque :** `Lz4DecompressionError` ne porte actuellement pas de code spécifique +> (`none`). + +### `ui::` + +| Code | Variante | Message | +| ---- | -------- | ------- | +| `ui::command_not_found` | `CommandNotFound` | Command not found | +| `ui:tui_error` | `TuiError` | Error with the Tui | + +> **Remarque :** le code TUI utilise un séparateur à deux points simple `ui:tui_error` +> (conservé tel quel dans la source). + +### `format::` + +| Code | Variante | Message | Aide | +| ---- | -------- | ------- | ---- | +| `format::corrupted_file` | `CorruptedFile` | Corrupted File | Attendez `eck recover ` pour de l'aide. | +| `format::directory_is_folder` | `DirectoryIsFolder` | Directory found is the directory of the folder | Vous ne pouvez pas traiter le dossier lui-même. | +| `format::metadata_reading_failed` | `FailedToReadMetadata` | Failed to read metadata of file | Vérifiez que le fichier existe et reste lisible. | + +### `io::` + +| Code | Variante | Message | Aide | +| ---- | -------- | ------- | ---- | +| `io::unexpected_eof` | `UnexpectedEof` | unexpected end of file | Le fichier est tronqué ; il est peut-être corrompu. | +| `io::home_directory_not_found` | `HomeNotFound` | home directory not found | Définissez la variable d'environnement HOME. | +| `io::misc_io_error` | `IoError` | io error | Vérifiez que le chemin existe et que vous avez les permissions nécessaires. | +| `io::incorrect_path` | `PathIsIncorrect` | Path is incorrect | Vérifiez que le chemin existe. | +| `io::file_not_found` | `FileError`/`SpecificFileError` | error while searching for the file | Vérifiez que le fichier existe et est lisible. | +| `io::file_is_a_symlink` | `FileIsASymLink` | File is a SymLink (shortcut) | Utilisez le chemin réel au lieu d'un lien symbolique. | +| `io::permissions_reading_failed` | `FilePermissionsParsingError` | Error while parsing file permissions | Les permissions de ce fichier n'ont pas pu être analysées. | + +### `misc::` + +| Code | Variante | Message | Aide | +| ---- | -------- | ------- | ---- | +| `misc::inspection_failed` | `InspectionError` | Inspection failed | Assurez-vous que le fichier existe et que son nom ne contient pas de caractères unicode invalides. | +| `misc::hex_decoding_failed` | `HexError` | hex decoding error | — | +| `misc::config_directory_error` | `ConfigError` | configuration error | Vérifiez l'emplacement et le format du fichier de configuration. | +| `misc::break` | `Break` | operation interrupted | — | + +### Sans code (`none`) + +Ces variantes rapportent `none` comme code : + +* `PostcardError` — données sérialisées invalides (« The file seems corrupted or not in the + Enkryptit! format. »). +* `StripPrefixError` — suppression de préfixe de chemin. +* `SendError`, `ReceiveError`, `InvalidWorkerCount` — mécanique du parallélisme. +* `MemoryLockError` — échec de `mlock` lors du verrouillage des clés en mémoire. +* `SerdeJsonError` — le fichier de paramètres/configuration n'est pas du JSON valide. +* `UnknownAction` — dispatch de commande inconnu. +* `Lz4DecompressionError` — échec de décompression lz4. + +## Journaux + +Un fichier de log à rotation quotidienne (`log.txt`) est écrit via `tracing` dans le +répertoire de configuration d'Enkryptit. Les erreurs sont toujours journalisées avant +d'être affichées. + +## Liens + +* [Format `.encky`](format.md) — le conteneur que ces erreurs analysent. \ No newline at end of file diff --git a/doc/user-guide/fr/format.md b/doc/user-guide/fr/format.md new file mode 100644 index 0000000..568d27e --- /dev/null +++ b/doc/user-guide/fr/format.md @@ -0,0 +1,110 @@ +# Le format `.encky` + +Un conteneur chiffré est écrit sous la forme **`.encky`**. Il existe **deux** types de +conteneurs : + +1. un **fichier** chiffré (objet simple), +2. une **archive de dossier** (un dossier entier dans un seul `.encky`). + +Tout est sérialisé avec **postcard** ; la version est stockée dans l'en-tête +(constante `VERSION`, actuellement `3` — la 3ᵉ nightly de la première stable). + +## Primitives communes + +* Magic number : `0x45 0x4E 0x4B 0x31` = `ENK1`. +* Taille de chunk : `CHUNK_SIZE = 8 MiB`. +* Chiffrement : **XChaCha20-Poly1305** (nonce de 24 octets, AEAD). +* Clé de 32 octets (depuis un mot de passe / un fichier de clé / le trousseau système). + +## Nonce principal et nonces dérivés + +Chaque objet (ou entrée d'archive) possède un **nonce principal** stocké dans ses +métadonnées. Pour chaque bloc `step` (donc à chaque étape), un nonce dérivé est construit : + +``` +new_nonce[0..16] = master_nonce[0..16] +new_nonce[16..24] = step.to_le_bytes() # 8 octets +``` + +## Flux chiffré + +Dans le flux chiffré, le 'payload' est une séquence de blocs (précédés par leur taille) suivie d'un marqueur final, 'ENK1END' : + +``` +[ chunk_len:u32 LE ][ fragment_compressé ] +[ chunk_len:u32 LE ][ fragment_compressé ] +... +[ "ENK1END" ] # 7 octets littéraux +``` + +* Chaque bloc est d'abord **compressé**, puis **chiffré** avec le nonce dérivé de son `step`. +* La partie compressée tient dans un buffer pouvant atteindre `CHUNK_SIZE`, mais chaque + fragment écrit est précédé de sa longueur réelle + +## Disposition d'un fichier simple + +``` +offset 0 : header_len:u8 # longueur de l'en-tête postcard (>= 1 octet) + ArchiveHeader := magic[4] + version:u8 + is_folder_archive:false + meta_len:u32 + MetaDatas := key_type + compression + nonce[24] + puis le flux chiffré (voir ci-dessus), suivi par ENK1END +``` + +`payload_offset` pour le déchiffrement = `1 + header_len + meta_len`. + +## Disposition d'une archive de dossier (v2+) + +``` +octet 0 : HEADER_REGION_SIZE:u8 (= 64) +octets 1 .. 65 : ArchiveHeader, rembourré à zéro sur 64 octets + (magic, version, is_folder_archive:true, meta_len) +octets 65 .. : un flux chiffré par fichier, séquentiellement, + chacun avec son propre file_nonce (voir FileEntry) +fin du fichier : FolderMetadata (postcard) + = key_type + entries[ FileEntry ] +``` + +* `HEADER_REGION_SIZE` garde la zone d'en-tête fixe afin que l'en-tête puisse être remis à + jour en place sans changer la taille de la zone. +* Les entrées sont chiffrées les unes après les autres ; chaque `FileEntry.offset` note son + décalage absolu en octets dans l'archive. + +### `FileEntry` (postcard) + +| Champ | Type | Signification | +| ----- | ---- | ------------- | +| `relative_path` | `String` | chemin relatif à la racine de l'archive | +| `offset` | `u64` | décalage absolu du flux dans l'archive | +| `permissions` | `Option` | permissions Unix à restaurer à l'extraction | +| `compression` | `CompressionType` | algorithme utilisé pour cette entrée | +| `file_nonce` | `[u8; 24]` | nonce principal propre à l'entrée | + +### `MetaDatas` (postcard) + +| Champ | Type | Signification | +| ----- | ---- | ------------- | +| `key_type` | `KeyType` | comment la clé doit être résolue | +| `compression` | `CompressionType` | algorithme de compression utilisé | +| `nonce` | `[u8; 24]` | nonce principal | + +### `ArchiveHeader` (postcard) + +| Champ | Type | Signification | +| ----- | ---- | ------------- | +| `magic` | `[u8; 4]` | `ENK1` | +| `version` | `u8` | la version d'Enkryptit! qui a écrit le fichier | +| `is_folder_archive` | `bool` | conteneur fichier vs. dossier | +| `meta_len` | `u32` | longueur des métadonnées de fin | + +## Troncature & corruption + +* Les flux de fichiers simples se terminent par `ENK1END` ; une fin de fichier inattendue + correspond à `io::unexpected_eof`. +* Les archives de dossiers `version >= 2` conservent leurs métadonnées à la fin — un + en-tête manquant est signalé comme `format::corrupted_file`. + +## Liens + +* [Erreurs](errors.md) — codes pour les artefacts corrompus/tronqués. +* [Installation](installation.md) — comment obtenir `eck`. +* Historique du format : `PROGRESSION.md` (jours 1 à 17). \ No newline at end of file diff --git a/doc/user-guide/fr/installation.md b/doc/user-guide/fr/installation.md new file mode 100644 index 0000000..ca73343 --- /dev/null +++ b/doc/user-guide/fr/installation.md @@ -0,0 +1,47 @@ +# Installation + +Cette page explique comment compiler et installer **Enkryptit!** sur votre machine. + +## Prérequis + +* **Rust :** `1.85+` (le `rust-version` déclaré du workspace). L'édition `2024` est utilisée. +* Un gestionnaire de références natif pour `Type de clé = Trousseau système` : + * **Linux :** `libsecret` (Trousseau GNOME / KSecretService). + * **macOS :** le Trousseau système. + * **Windows :** le Gestionnaire d'informations d'identification Windows. + +## Compilation + +```bash +git clone https://github.com/VRAM-RAM/Enkryptit +cd Enkryptit/ + +# Compilation en debug +cargo xtask build + +# Compilation en release +cargo xtask build --release +``` + +Le binaire `eck` est produit dans `target/release/eck` (ou `target/debug/eck`). + +> **Remarque :** le profil release est agressif (`lto = "fat"`, `codegen-units = 1`, +> `panic = "abort"`). La première compilation peut être *très* longue. + +## Installation + +```bash +cargo xtask install +``` + +Cette commande exécute `cargo install --path ./eck` et place `eck` dans le `$PATH`. + +## Premier lancement + +```bash +eck # ouvre l'interface (TUI) +eck --help # aide de la CLI +eck --version # affiche la version d'Enkryptit +``` + +Voir [CLI](cli.md) et [TUI](tui.md) pour l'utilisation. \ No newline at end of file diff --git a/doc/user-guide/fr/tui.md b/doc/user-guide/fr/tui.md new file mode 100644 index 0000000..79cf568 --- /dev/null +++ b/doc/user-guide/fr/tui.md @@ -0,0 +1,68 @@ +# TUI + +L'interface interactive vous permet de parcourir et traiter des fichiers et dossiers sans +mémoriser les drapeaux de la ligne de commande. + +> **Remarque :** les libellés de l'interface sont en anglais `Browse`, `Parameters`, … +> Ils sont expliqués ici en français. + +## Lancement + +```bash +eck # sans argument -> TUI +eck ui # commande TUI explicite +``` + +## Menu principal + +``` +Enkryptit + Fast & Simple File Encryption Manager v0.0.3 +? What do you want to do? +> Browse + Parameters + Help + Exit +``` + +| Entrée | Description | +| ------ | ----------- | +| **Browse** | Choisir des fichiers, dossiers, ou les deux, puis les chiffrer / déchiffrer / inspecter. | +| **Parameters** | Configurer compression, type de clé et parallélisme. | +| **Help** | Affiche les commandes disponibles. | +| **Exit** | Quitte l'interface. | + +## Panneau de parcours + +``` +Browser Panel +? What do you want to Browse? +> Browse Files + Browse Folders + Browse both Files & Folders + Back to main menu +``` + +Après avoir choisi fichier(s)/dossier(s) via le sélecteur système, chaque sélection lance le +traitement standard (chiffrer le `.encky` / déchiffrer le contenu en clair). + +> **Remarque :** la TUI demande le mot de passe de manière interactive si nécessaire. + +## Panneau de paramètres + +``` +Parameters Panel +? What do you want to configure? +> Change compression type + Change key type + Change parallelism type + Show current parameters + Back to main menu +``` + +Les choix sont persistés dans le même `config.json` que la CLI (voir [Paramètres](../cli.md)). + +## Liens + +* [CLI](cli.md) — l'interface en ligne de commande équivalente, y compris la + correspondance des paramètres. \ No newline at end of file diff --git a/eck/Cargo.toml b/eck/Cargo.toml new file mode 100644 index 0000000..a973faf --- /dev/null +++ b/eck/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "eck" +version = "0.0.4" +description = "A fast, simple file & folder encryption manager, written in Rust" +keywords = ["cli", "encryption", "cryptography"] +edition = "2024" +license = "CECILL-B OR Apache-2.0" +repository = "https://github.com/VRAM-RAM/Enkryptit" +authors = ["Olruix (VRAM-RAM)"] +rust-version = "1.85" + +[dependencies] +argon2 = "0.5.3" +chacha20poly1305 = { version = "0.10.1", features = ["alloc"] } +clap = { version = "4.6.1", features = ["derive"] } +colored = "3.1.1" +comfy-table = "8.0.0" +directories = "6.0.0" +dirs = "6.0.0" +fmodeparser = "0.1.0" +gradient-bar = "0.1.1" +hex = "0.4.3" +indicatif = "0.18.4" +infer = "0.22.0" +inquire = "0.9.4" +keyring = "2.0" +libc = "0.2.186" +lz4_flex = "0.13.1" +miette = { version = "7.6.0", features = ["fancy"] } +oxiarc-core = "0.3.0" +postcard = { version = "1.1.3", features = ["alloc"] } +rand = "0.8" +rfd = "0.17.2" +rpassword = "7.5.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.150" +sysinfo = "0.39.6" +terminal_size = "0.4" +thiserror = "2.0.18" +tracing = "0.1.44" +tracing-appender = "0.2.5" +tracing-subscriber = { version = "0.3.23", features = ["fmt", "env-filter"]} +unicode-segmentation = "1" +unicode-width = "0.2" +walkdir = "2.5.0" +xz2 = "0.1.7" +zeroize = "1.8.2" +zstd = "0.13.3" + +[dev-dependencies] +tempfile = "3" +assert_cmd = "2" +predicates = "3" +ctor = "0.2" +unicode-width = "0.2" + +[[test]] +name = "eck_tests" +path = "test/mod.rs" +harness = true diff --git a/eck/README.md b/eck/README.md new file mode 100644 index 0000000..a084e1e --- /dev/null +++ b/eck/README.md @@ -0,0 +1,124 @@ +# Enkryptit! + +This is the binary directory of **Enkryptit!**, that contains `eck`. + +> This project is currently a work in progress. It is **not audited** for production security. + +> You can find all the informations on [github.com/VRAM-RAM/Enkryptit/](github.com/VRAM-RAM/Enkryptit/). + +## Commands + +In the **WorkSpace**, you have access to the following commands : + + +| Command | Syntax | Description | +| ------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | +| **Build** | `cargo xtask build [--release]` | Builds `eck` binary. | +| **Install** | `cargo xtask install` | Installs `eck` | +| **Test** | `cargo xtask test` | Launches unit and integration tests | +| **Fuzz** | `cargo xtask fuzz [flags]` | Launches libFuzzer fuzz targets (nightly toolchain required) | +| **Format code** | `cargo xtask fmt` | Formats the code | +| **Help** | `cargo xtask help` | Prints the help | + + +## Tests + +Please refer to the [test readme](./test/README.md). + +## Fuzzing + +Fuzzing is done with [**cargo-fuzz**](https://github.com/rust-fuzz/cargo-fuzz) / libFuzzer, in a standalone crate +([`fuzz/`](./fuzz), excluded from the workspace so it never slows down regular builds or `cargo xtask test`). + +> Requires a **nightly** toolchain (pinned in [`fuzz/rust-toolchain.toml`](./fuzz/rust-toolchain.toml)) +> and the `cargo-fuzz` binary installed (`cargo install cargo-fuzz`). + +Fuzz targets live in [`fuzz/fuzz_targets/`](./fuzz/fuzz_targets): + +- `decompress` — crash-safety for arbitrary bytes across all compression codecs +- `compress_roundtrip` — compress then decompress must return the exact input +- `metadata` — postcard deserialization of metadata structures must never panic + +Run one (unlimited until Ctrl-C), or bound it for a quick smoke run: + +```sh +cargo xtask fuzz decompress +cargo xtask fuzz decompress -- -runs=1000 +cargo xtask fuzz # list available targets +``` + +## Code Layout + +```txt +src/ +├── main.rs # entry point, clap CLI definition & dispatch +├── lib.rs # library entry point (used by tests) +├── types/ # enums: CompressionType, KeyType, ParallelismType, KeyParams +│ ├── compression_type.rs +│ ├── key_type.rs +│ ├── parallelism_type.rs +│ └── key_params.rs +├── frontend/ # everything user-facing +│ ├── cli/ # command-line interface +│ │ ├── inspection.rs # eck inspect +│ │ ├── params_helpers.rs +│ │ └── treatment.rs # object treatment (single / multiple paths) +│ ├── tui/ # terminal UI (inquire + rfd) +│ │ ├── action.rs +│ │ ├── browse.rs # file/folder pickers +│ │ ├── help.rs +│ │ ├── input.rs # TuiInput trait (mockable) +│ │ ├── parameters.rs +│ │ └── treatment.rs +│ ├── treat_output.rs # renders success / error / info badges +│ └── mod.rs +├── diagnostic/ # logs, reports, errors & error rendering +│ ├── logging/ # EnkryptitLogger (tracing, daily rotation → log.txt) +│ ├── output/ # EnkryptitOutput, diagnostics & snippets (miette) +│ └── report/ # comfy-table Report rendering +├── context/ # EnkryptitContext (password, config resolution) +│ ├── compression.rs +│ └── parallelism.rs # Auto inference helpers +├── key/ # key resolution, derivation, storage +│ ├── derivation.rs # Argon2id key_from_password / derive_key +│ ├── generation.rs +│ ├── resolve.rs # key from file / OS keyring +│ ├── storage.rs +│ └── mod.rs # EnkryptitKey + LockedKey (mlock / zeroize) +├── encryption/ +│ ├── encryption_primitives.rs # XChaCha20-Poly1305 chunk encrypt/decrypt, nonce derivation +│ ├── encryption_flow.rs +│ ├── chunk_job/ # EncryptChunkJob / DecryptChunkJob / ChunkResult +│ ├── file.rs +│ ├── file_encryption/ # single & multithreaded file encryption +│ │ ├── single.rs +│ │ ├── multithread.rs +│ │ └── mod.rs +│ ├── folder_encryption/ # folder ↔ .encky archive +│ │ ├── collect_entry.rs +│ │ ├── entry/ # FileEntry handling +│ │ ├── intern_archive_encryption/ # per-entry single/multi treatment +│ │ ├── single.rs # entry loop +│ │ └── mod.rs +│ └── mod.rs +├── compression.rs # zstd / lz4 / xz / none (with retry logic) +├── parallelism/ # worker pool for chunk jobs +│ └── pool.rs # EnkryptitPool +├── parameters/ # persisted parameters (config.json) +│ ├── argon2id_parameters.rs # static Argon2id params (128 MiB, 3, 1) +│ └── params.rs # EnkryptitParams load / save +├── metadatas.rs # ArchiveHeader, MetaDatas, FolderMetadata, FileEntry, MAGIC +├── treatment/ # high-level object treatment & inspection +├── conversions.rs +├── directory.rs # ProjectDirs (com.olruix.Enkryptit) +└── errors.rs # EnkryptitError +``` + +## License + +This project is dual-licensed under: + +* **CeCILL-B License** (French law compliant, fully compatible with GNU GPL/Apache) +* **Apache License, Version 2.0** + +Choose the one that best fits your needs. \ No newline at end of file diff --git a/eck/fuzz/Cargo.lock b/eck/fuzz/Cargo.lock new file mode 100644 index 0000000..eb53643 --- /dev/null +++ b/eck/fuzz/Cargo.lock @@ -0,0 +1,3588 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +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 0.61.2", +] + +[[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 0.61.2", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.5.0", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling 3.11.0", + "rustix 1.1.5", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.2", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io 2.6.0", + "async-lock 3.4.2", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.5", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[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 = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "comfy-table" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136c8c4c3823846e8ba6d4bda011b4e5d5827b8bb0ac26c31f9ab31abe9f54f2" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.2", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.5", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[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 = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "eck" +version = "0.0.3" +dependencies = [ + "argon2", + "chacha20poly1305", + "clap", + "colored", + "comfy-table", + "directories", + "dirs", + "fmodeparser", + "gradient-bar", + "hex", + "indicatif", + "infer", + "inquire", + "keyring", + "libc", + "lz4_flex", + "miette", + "oxiarc-core", + "postcard", + "rand", + "rfd", + "rpassword", + "serde", + "serde_json", + "sysinfo", + "thiserror", + "tracing", + "tracing-appender", + "tracing-subscriber", + "walkdir", + "xz2", + "zeroize", + "zstd", +] + +[[package]] +name = "eck-fuzz" +version = "0.0.0" +dependencies = [ + "eck", + "libfuzzer-sys", + "postcard", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.2", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "fmodeparser" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342312c958816266bf5f3638d2727c243cc21e668173745982c96a86e44aa63" +dependencies = [ + "regex", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.5.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gradient-bar" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3608c4e710985c879e80f38f991e95e5c83ff4e003ec1a6f279bc862f9b5dcb" +dependencies = [ + "indicatif", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "inquire" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" +dependencies = [ + "bitflags 2.13.2", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "363387f0019d714aa60cc30ab4fe501a747f4c08fc58f069dd14be971bd495a0" +dependencies = [ + "byteorder", + "lazy_static", + "linux-keyutils", + "secret-service", + "security-framework", + "windows-sys 0.52.0", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + +[[package]] +name = "oxiarc-core" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119a925483e4917b645fd77acef86859164e166faa4cce3353c1d7cecfd3665a" +dependencies = [ + "thiserror", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.5.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.3", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "percent-encoding", + "pollster", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rpassword" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + +[[package]] +name = "rtoolbox" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secret-service" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5204d39df37f06d1944935232fd2dfe05008def7ca599bf28c0800366c8a8f9" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sysinfo" +version = "0.39.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.5.0", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "icu_segmenter", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "twox-hash" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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 = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zvariant" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/eck/fuzz/Cargo.toml b/eck/fuzz/Cargo.toml new file mode 100644 index 0000000..2a92146 --- /dev/null +++ b/eck/fuzz/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "eck-fuzz" +version = "0.0.0" +edition = "2024" +publish = false + +[package.metadata] +cargo-fuzz = true + +[workspace] + +[dependencies] +eck = { path = ".." } +libfuzzer-sys = "0.4" +postcard = "1" + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[[bin]] +name = "decompress" +path = "fuzz_targets/decompress.rs" +test = false +bench = false +doc = false + +[[bin]] +name = "compress_roundtrip" +path = "fuzz_targets/compress_roundtrip.rs" +test = false +bench = false +doc = false + +[[bin]] +name = "metadata" +path = "fuzz_targets/metadata.rs" +test = false +bench = false +doc = false \ No newline at end of file diff --git a/eck/fuzz/fuzz_targets/compress_roundtrip.rs b/eck/fuzz/fuzz_targets/compress_roundtrip.rs new file mode 100644 index 0000000..90ad37c --- /dev/null +++ b/eck/fuzz/fuzz_targets/compress_roundtrip.rs @@ -0,0 +1,34 @@ +#![no_main] + +use eck::compression::{EnkryptitCompress, EnkryptitDecompress}; +use eck::types::CompressionType::{Lz4, NoComp, Xz, Zstd}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if data.len() < 2 { + return; + } + + let compression = match data[0] { + 0..=63 => NoComp, + 64..=127 => Lz4, + 128..=191 => Zstd, + _ => Xz, + }; + + let mut compressed = Vec::new(); + data[1..] + .compress(&mut compressed, compression) + .expect("compressing arbitrary data must never fail"); + + let mut decompressed = Vec::new(); + compressed + .decompress(&mut decompressed, compression) + .expect("decompressing our own output must never fail"); + + assert_eq!( + decompressed, + data[1..], + "compress/decompress roundtrip must preserve the exact input bytes" + ); +}); \ No newline at end of file diff --git a/eck/fuzz/fuzz_targets/decompress.rs b/eck/fuzz/fuzz_targets/decompress.rs new file mode 100644 index 0000000..816888c --- /dev/null +++ b/eck/fuzz/fuzz_targets/decompress.rs @@ -0,0 +1,21 @@ +#![no_main] + +use eck::compression::EnkryptitDecompress; +use eck::types::CompressionType::{Lz4, NoComp, Xz, Zstd}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let Some(&selector) = data.first() else { + return; + }; + + let compression = match selector { + 0..=63 => NoComp, + 64..=127 => Lz4, + 128..=191 => Zstd, + _ => Xz, + }; + + let mut output = Vec::new(); + let _ = data[1..].decompress(&mut output, compression); +}); \ No newline at end of file diff --git a/eck/fuzz/fuzz_targets/metadata.rs b/eck/fuzz/fuzz_targets/metadata.rs new file mode 100644 index 0000000..6286158 --- /dev/null +++ b/eck/fuzz/fuzz_targets/metadata.rs @@ -0,0 +1,9 @@ +#![no_main] + +use eck::metadatas::{FolderMetadata, MetaDatas}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let _ = postcard::from_bytes::(data); + let _ = postcard::from_bytes::(data); +}); \ No newline at end of file diff --git a/eck/fuzz/rust-toolchain.toml b/eck/fuzz/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/eck/fuzz/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/src/compression.rs b/eck/src/compression.rs similarity index 81% rename from src/compression.rs rename to eck/src/compression.rs index 76733e1..9210d67 100644 --- a/src/compression.rs +++ b/eck/src/compression.rs @@ -1,6 +1,6 @@ use crate::errors::EnkryptitError; use crate::types::CHUNK_SIZE; -use crate::types::CompressionType::{self, Lz4, NoComp, Xz, Zstd}; +use crate::types::CompressionType::{self, Auto, Lz4, NoComp, Xz, Zstd}; use lz4_flex::block::{compress_into as lz4compress, decompress_into as lz4decompress}; use std::io::Read; use xz2::read::{XzDecoder, XzEncoder}; @@ -11,9 +11,16 @@ use zstd::bulk::decompress_to_buffer as zstddecompress; /// \ /// Implements the `compress` method : /// -/// ```text +/// ```rust /// fn compress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError>; /// ``` +/// \ +/// When compressing, it **matches** the [`CompressionType`] : +/// - Auto : `unreachable` +/// - Zstd : `compress_with_zstd` +/// - Lz4 : `compress_with_lz4` +/// - Xz : `compress_with_xz` +/// - NoComp : clears the buffer and returns pub trait EnkryptitCompress { fn compress( &self, @@ -26,22 +33,37 @@ pub trait EnkryptitCompress { /// \ /// Implements the `decompress` method : /// -/// ```text +/// ```rust /// fn decompress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError>; /// ``` pub trait EnkryptitDecompress { - fn decompress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError>; + fn decompress( + &self, + output: &mut Vec, + compression: CompressionType, + ) -> Result<(), EnkryptitError>; } impl EnkryptitCompress for Vec { - fn compress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError> { + fn compress( + &self, + output: &mut Vec, + compression: CompressionType, + ) -> Result<(), EnkryptitError> { self.as_slice().compress(output, compression) } } -impl<'a> EnkryptitCompress for &'a [u8] { - fn compress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError> { +impl EnkryptitCompress for [u8] { + fn compress( + &self, + output: &mut Vec, + compression: CompressionType, + ) -> Result<(), EnkryptitError> { match compression { + Auto => unreachable!( + "`Auto` should never be reached here, and always infered before reaching this function. There is an error in the code. If you are reading this as an user, please open an Issue." + ), Zstd => compress_with_zstd(self, output), Lz4 => compress_with_lz4(self, output), Xz => compress_with_xz(self, output), @@ -55,14 +77,25 @@ impl<'a> EnkryptitCompress for &'a [u8] { } impl EnkryptitDecompress for Vec { - fn decompress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError> { + fn decompress( + &self, + output: &mut Vec, + compression: CompressionType, + ) -> Result<(), EnkryptitError> { self.as_slice().decompress(output, compression) } } -impl<'a> EnkryptitDecompress for &'a [u8] { - fn decompress(&self, output: &mut Vec, compression: CompressionType) -> Result<(), EnkryptitError> { +impl EnkryptitDecompress for [u8] { + fn decompress( + &self, + output: &mut Vec, + compression: CompressionType, + ) -> Result<(), EnkryptitError> { match compression { + Auto => unreachable!( + "`Auto` should never be reached here, and always infered before reaching this function. There is an error in the code. If you are reading this as an user, please open an Issue." + ), Zstd => decompress_with_zstd(self, output), Lz4 => decompress_with_lz4(self, output), Xz => decompress_with_xz(self, output), @@ -152,7 +185,7 @@ fn compress_with_zstd(input: &[u8], output: &mut Vec) -> Result<(), Enkrypti match zstdcompress(input, output, 6) { Ok(size) => { - output.truncate(size as usize); + output.truncate(size); return Ok(()); } Err(_) => { @@ -162,7 +195,7 @@ fn compress_with_zstd(input: &[u8], output: &mut Vec) -> Result<(), Enkrypti // Give up after reasonable attempts output.resize(current_size, 0); let size = zstdcompress(input, output, 6)?; - output.truncate(size as usize); + output.truncate(size); return Ok(()); } } @@ -172,7 +205,7 @@ fn compress_with_zstd(input: &[u8], output: &mut Vec) -> Result<(), Enkrypti // Final attempt with very large buffer if all else fails output.resize(CHUNK_SIZE * 8, 0); let size = zstdcompress(input, output, 6)?; - output.truncate(size as usize); + output.truncate(size); Ok(()) } @@ -189,7 +222,7 @@ fn decompress_with_zstd(input: &[u8], output: &mut Vec) -> Result<(), Enkryp match zstddecompress(input, output) { Ok(size) => { - output.truncate(size as usize); + output.truncate(size); return Ok(()); } Err(_) => { @@ -199,7 +232,7 @@ fn decompress_with_zstd(input: &[u8], output: &mut Vec) -> Result<(), Enkryp // Give up after reasonable attempts output.resize(current_size, 0); let size = zstddecompress(input, output)?; - output.truncate(size as usize); + output.truncate(size); return Ok(()); } } @@ -209,7 +242,7 @@ fn decompress_with_zstd(input: &[u8], output: &mut Vec) -> Result<(), Enkryp // Final attempt with very large buffer if all else fails output.resize(CHUNK_SIZE * 8, 0); let size = zstddecompress(input, output)?; - output.truncate(size as usize); + output.truncate(size); Ok(()) } diff --git a/eck/src/context/compression.rs b/eck/src/context/compression.rs new file mode 100644 index 0000000..6f75d55 --- /dev/null +++ b/eck/src/context/compression.rs @@ -0,0 +1,173 @@ +use crate::context::{ + LOW_BOUNDARY, MID_INFERIOR_BOUNDARY, MID_SUPERIOR_BOUNDARY, SUPERIOR_BOUNDARY, +}; +use crate::errors::EnkryptitError; +use crate::types::CompressionType; +use infer::Type; +use infer::get_from_path; +use std::fs::File; + +/// Given the **path** of a file, it infers the [`CompressionType`] to use. +/// Its flow is : +/// - Opens the file, gets its **size** and compute a `CompressionHint` using `CompressionHint::compute()`. +/// - **Matches** successively the *hint* and the *size* of the file and returns the corresponding [`CompressionType`] +pub fn infer_compression(path: &str) -> Result { + let file = File::open(path)?; + let len = file.metadata()?.len(); + let hint = CompressionHint::compute(get_from_path(path)?); + + Ok(match hint { + CompressionHint::AlreadyCompressed => CompressionType::NoComp, + CompressionHint::Compressible => match len { + 0..LOW_BOUNDARY => CompressionType::Lz4, + LOW_BOUNDARY..MID_INFERIOR_BOUNDARY => CompressionType::Zstd, + MID_INFERIOR_BOUNDARY..MID_SUPERIOR_BOUNDARY => CompressionType::Xz, + MID_SUPERIOR_BOUNDARY..SUPERIOR_BOUNDARY => CompressionType::Zstd, + _ => CompressionType::Lz4, + }, + CompressionHint::HighlyCompressible => match len { + 0..LOW_BOUNDARY => CompressionType::Zstd, + LOW_BOUNDARY..SUPERIOR_BOUNDARY => CompressionType::Xz, + _ => CompressionType::Zstd, + }, + + CompressionHint::Unknown => match len { + 0..LOW_BOUNDARY => CompressionType::NoComp, + LOW_BOUNDARY..SUPERIOR_BOUNDARY => CompressionType::Lz4, + _ => CompressionType::NoComp, + }, + }) +} + +/// An **hint** computed in function of the `mime` type of the file. +/// \ +/// Used during [`CompressionType`] inference. +enum CompressionHint { + /// The file is **really** compressible (all files with `text/` mime types) + HighlyCompressible, + + /// The file is compressible + Compressible, + + /// The file is already compressed + AlreadyCompressed, + + /// We don't know : + /// - Or the mime type isn't treated + /// - Or no mime type was found + Unknown, +} + +impl CompressionHint { + /// Computes the [`CompressionHint`] for the compression of a file, basing on its `mime` type. + pub fn compute(kind: Option) -> Self { + match kind { + Some(type_) => { + let mime = type_.mime_type(); + + // Plain text, markup & scripts are highly redundant. + if mime.starts_with("text/") { + return CompressionHint::HighlyCompressible; + } + + match mime { + // Archives & already-compressed containers. + "application/zip" + | "application/gzip" + | "application/zstd" + | "application/x-lz4" + | "application/x-xz" + | "application/x-lzip" + | "application/x-bzip2" + | "application/x-7z-compressed" + | "application/vnd.rar" + | "application/vnd.ms-cab-compressed" + | "application/x-tar" + | "application/x-unix-archive" + | "application/x-cpio" + | "application/x-rpm" + // Documents & e-books (already compressed inside). + | "application/pdf" + | "application/postscript" + | "application/epub+zip" + | "application/x-mobipocket-ebook" + | "application/msword" + | "application/vnd.oasis.opendocument.text" + | "application/vnd.oasis.opendocument.spreadsheet" + | "application/vnd.oasis.opendocument.presentation" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + | "application/vnd.openxmlformats-officedocument.presentationml.presentation" + + // Executables & binaries (largely incompressible, random). + | "application/wasm" + | "application/x-llvm" + | "application/x-nintendo-nes-rom" + | "application/x-mach-binary" + | "application/x-executable" + | "application/vnd.microsoft.portable-executable" + | "application/vnd.android.dex" + | "application/vnd.debian.binary-package" + // Fonts (already optimized / woff is web-compressed). + | "application/font-woff" + | "application/font-sfnt" + // Compressed & lossy images. + | "image/jpeg" + | "image/png" + | "image/gif" + | "image/webp" + | "image/avif" + | "image/heif" + | "image/jp2" + | "image/jxl" + | "image/vnd.djvu" + | "image/vnd.ms-photo" + | "image/openraster" + | "image/vnd.adobe.photoshop" + // Lossy or lossless-compressed audio. + | "audio/aac" + | "audio/amr" + | "audio/m4a" + | "audio/mpeg" + | "audio/ogg" + | "audio/opus" + | "audio/x-flac" + | "audio/x-ape" + // Video codecs always compress their payload. + | "video/mp4" + | "video/webm" + | "video/x-m4v" + | "video/x-matroska" + | "video/quicktime" + | "video/x-msvideo" + | "video/x-ms-wmv" + | "video/x-flv" + | "video/mpeg" + => CompressionHint::AlreadyCompressed, + + // Raw / uncompressed media that compress heavily, plus a few + // structured or mixed binary formats worth attempting. + "application/vnd.sqlite3" + | "application/x-qemu-disk" + | "application/x-ole-storage" + | "application/rtf" + | "application/dicom" + | "image/bmp" + | "image/tiff" + | "image/x-canon-cr2" + | "image/vnd.dwg" + | "audio/x-wav" + | "audio/x-aiff" + | "audio/x-dsf" + | "audio/midi" + => CompressionHint::Compressible, + + // Anything else that `infer` recognized but we don't treat + // specially. + _ => CompressionHint::Unknown, + } + } + None => CompressionHint::Unknown, + } + } +} diff --git a/eck/src/context/mod.rs b/eck/src/context/mod.rs new file mode 100644 index 0000000..ffa4e98 --- /dev/null +++ b/eck/src/context/mod.rs @@ -0,0 +1,108 @@ +pub mod compression; +pub mod parallelism; + +use crate::context::compression::infer_compression; +use crate::context::parallelism::infer_parallelism; +use crate::errors::EnkryptitError; +use crate::types::{CompressionType, ParallelismType}; +use std::fs::File; +use zeroize::Zeroizing; + +pub const LOW_BOUNDARY: u64 = 50 << 20; // 50 MiB +pub const MID_INFERIOR_BOUNDARY: u64 = 250 << 20; // 250 MiB +pub const MID_SUPERIOR_BOUNDARY: u64 = 1 << 30; // 1 GiB +pub const SUPERIOR_BOUNDARY: u64 = 5 << 30; // 5 GiB + +/// The Context, passed trough the program. +/// \ +/// Used for resolving : +/// - The password (during encryption / decryption) +/// - The compression type +/// - The parallelism type +pub struct EnkryptitContext { + /// The context *can* contain a password (if the user provided one with `-p`) + pub password: Option>, + + /// The CompressionType used by the operation, and resolved by the context. + pub compression_type: CompressionType, + + /// The ParallelismType used by the operation, and resolved by the context. + /// This parallelism type is only used for intern entries and files. + /// For folder encryption itself, we infer at the beginning of the treatment, + /// since the parallelism resolution for a folder is based on : + /// - It's size + /// - The number of files it contains + pub parallelism: ParallelismType, +} + +impl EnkryptitContext { + /// Returns an [`EnkryptitContext`] with the given values. + /// \ + /// **password** is an `Option` because the user may not have provided any password. In that case, the context is created with `password : None` and the password is resolved later. + /// \ + /// **compression_type** and **parallelism** can have the value `Auto`. In that case, they are resolved by the context later. + pub fn new( + password: Option, + compression_type: CompressionType, + parallelism: ParallelismType, + ) -> Self { + Self { + password: password.map(Zeroizing::new), + compression_type, + parallelism, + } + } + + /// Resolves the password and returns a `&Zeroizing` (if `Ok<>`) that contains the password. + /// \ + /// Its current flow is : + /// - If password was not resolved (the stored `password` value is `None`) : + /// - If the user is in `Cli` : it prompts the `enter_password` text, read user's input and stores the resolved password in `password`. + /// - If the user is using the `Tui` : it gets the password using [`rpassword::prompt_password`] and stores the resolved password in `password`. + /// - Then, it returns the value that `password` contains (unwrapping it) + /// + /// It is safe because the password is **always** resolved before unwrapping. + pub fn resolve_password(&mut self) -> Result<&Zeroizing, EnkryptitError> { + if self.password.is_none() { + let pwd = rpassword::prompt_password("Enter password: ")?; + + self.password = Some(Zeroizing::new(pwd)); + } + + Ok(self.password.as_ref().unwrap()) + } + + /// Returns the `CompressionType` (if `Ok<>`). + /// \ + /// - If the stored value of `compression_type` is [`CompressionType::Auto`], it infers the compression calling [`infer_compression()`]. + /// - Else, it returns the stored `CompressionType` + pub fn resolve_compression(&self, path: &str) -> Result { + match self.compression_type { + CompressionType::Auto => infer_compression(path), + compression => Ok(compression), + } + } + + /// Returns the `ParallelismType` (if `Ok<>`) with the path of the file as `argument`. + /// \ + /// It opens the file and reads its **size** before calling [`EnkryptitContext::resolve_parallelism_with_size()`]. + pub fn resolve_parallelism(&self, path: &str) -> Result { + let file = File::open(path)?; + let len = file.metadata()?.len(); + self.resolve_parallelism_with_size(len) + } + + /// Returns the `ParallelismType` (if `Ok<>`) with the size of the file as `argument`. + /// \ + /// - If the stored value of `parallelism` is [`ParallelismType::Auto`], it infers the parallelism type calling [`infer_parallelism()`]. + /// - Else, it returns the stored `ParallelismType` + pub fn resolve_parallelism_with_size( + &self, + size: u64, + ) -> Result { + match self.parallelism { + ParallelismType::Auto => infer_parallelism(size), + parallelism => Ok(parallelism), + } + } +} diff --git a/eck/src/context/parallelism.rs b/eck/src/context/parallelism.rs new file mode 100644 index 0000000..91d4e05 --- /dev/null +++ b/eck/src/context/parallelism.rs @@ -0,0 +1,27 @@ +use crate::context::{ + LOW_BOUNDARY, MID_INFERIOR_BOUNDARY, MID_SUPERIOR_BOUNDARY, SUPERIOR_BOUNDARY, +}; +use crate::errors::EnkryptitError; +use crate::types::ParallelismType; + +/// Given the **size** of a buffer or file, it infers the [`ParallelismType`] to use. +/// Its flow is : +/// - Gets an estimate of the default amount of parallelism a program should use. +/// - **Matches** the size of the buffer or file, and return the [`ParallelismType`] that corresponds. +pub fn infer_parallelism(size: u64) -> Result { + let cpus = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1); + + Ok(match size { + 0..LOW_BOUNDARY => ParallelismType::Single, + LOW_BOUNDARY..MID_INFERIOR_BOUNDARY => ParallelismType::MultiThread(8.min(cpus) as u8), + MID_INFERIOR_BOUNDARY..MID_SUPERIOR_BOUNDARY => { + ParallelismType::MultiThread(12.min(cpus) as u8) + } + MID_SUPERIOR_BOUNDARY..SUPERIOR_BOUNDARY => { + ParallelismType::MultiThread(16.min(cpus) as u8) + } + _ => ParallelismType::MultiThread(cpus as u8), + }) +} diff --git a/src/conversions.rs b/eck/src/conversions.rs similarity index 71% rename from src/conversions.rs rename to eck/src/conversions.rs index f005ba5..2c34b5a 100644 --- a/src/conversions.rs +++ b/eck/src/conversions.rs @@ -2,8 +2,7 @@ use crate::errors::EnkryptitError; /// Simple public helper for converting a `key` from a `Vec` to an `[u8; 32]` array safely. pub fn convert_key(vec: Vec) -> Result<[u8; 32], EnkryptitError> { - Ok(vec - .as_slice() + vec.as_slice() .try_into() - .map_err(|_| EnkryptitError::InvalidKeyLength)?) + .map_err(|_| EnkryptitError::InvalidKeyLength) } diff --git a/eck/src/diagnostic/logging/mod.rs b/eck/src/diagnostic/logging/mod.rs new file mode 100644 index 0000000..159fe7d --- /dev/null +++ b/eck/src/diagnostic/logging/mod.rs @@ -0,0 +1,31 @@ +use crate::{directory::project_dir_path, errors::EnkryptitError}; +use tracing_appender::{non_blocking::WorkerGuard, rolling}; +use tracing_subscriber::EnvFilter; + +#[allow(unused)] +/// A structure that stores the [`WorkerGuard`] for logging, and that initializes the `tracing` logging. +pub struct EnkryptitLogger(WorkerGuard); + +impl EnkryptitLogger { + /// Initializes the `tracing` logger. + pub fn init() -> Result { + let file = rolling::daily(project_dir_path()?, "log.txt"); + + let (writer, _guard) = tracing_appender::non_blocking(file); + + tracing_subscriber::fmt() + .with_writer(writer) + .with_ansi(false) + .with_target(false) + .with_thread_ids(true) + .with_thread_names(true) + .with_file(true) + .with_line_number(true) + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + Ok(Self(_guard)) + } +} diff --git a/eck/src/diagnostic/mod.rs b/eck/src/diagnostic/mod.rs new file mode 100644 index 0000000..3917f7b --- /dev/null +++ b/eck/src/diagnostic/mod.rs @@ -0,0 +1,9 @@ +pub mod logging; +pub mod output; +pub mod report; + +pub use output::EnkryptitOutput; + +/// Documentation URL appended to every diagnostic report. +pub const DOC_URL: &str = + "https://github.com/VRAM-RAM/Enkryptit/tree/main/doc/user-guide/en/errors.md"; diff --git a/eck/src/diagnostic/output/diagnostic.rs b/eck/src/diagnostic/output/diagnostic.rs new file mode 100644 index 0000000..ca33e34 --- /dev/null +++ b/eck/src/diagnostic/output/diagnostic.rs @@ -0,0 +1,132 @@ +use crate::diagnostic::DOC_URL; +use crate::diagnostic::output::Snippet; +use crate::errors::EnkryptitError; +use miette::{ + Diagnostic, GraphicalReportHandler, GraphicalTheme, LabeledSpan, NamedSource, Severity, + SourceCode, SourceSpan, ThemeStyles, +}; +use std::fmt; + +/// Diagnostic used to render [`EnkryptitOutput`](crate::diagnostic::EnkryptitOutput) errors through miette. +/// +/// Unlike `MietteDiagnostic`, it implements `source_code()` + `labels()`, so +/// miette can draw the full code frame (`╭─[…]`, `│`, `·`, `─┬─`, `╰──`) +/// around the offending token when a [`Snippet`] is provided. +#[derive(Debug)] +pub struct EnkryptitDiagnostic { + message: String, + code: String, + help: Option, + url: String, + severity: Severity, + source: Option>, + labels: Vec, +} + +impl fmt::Display for EnkryptitDiagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for EnkryptitDiagnostic {} + +impl Diagnostic for EnkryptitDiagnostic { + fn code<'a>(&'a self) -> Option> { + Some(Box::new(&self.code) as Box) + } + + fn severity(&self) -> Option { + Some(self.severity) + } + + fn help<'a>(&'a self) -> Option> { + self.help + .as_ref() + .map(Box::new) + .map(|c| c as Box) + } + + fn url<'a>(&'a self) -> Option> { + Some(Box::new(&self.url) as Box) + } + + fn source_code(&self) -> Option<&dyn SourceCode> { + self.source.as_ref().map(|s| s as &dyn SourceCode) + } + + fn labels(&self) -> Option + '_>> { + Some(Box::new(self.labels.iter().cloned())) + } +} + +/// Builds the graphical theme used for *all* diagnostics: unicode drawings, +/// RGB colors, per-severity glyphs. +pub fn theme() -> GraphicalTheme { + let mut theme = GraphicalTheme::unicode(); + theme.styles = ThemeStyles::rgb(); + theme.styles.error = theme.styles.error.bold(); + theme.styles.warning = theme.styles.warning.bold(); + theme.characters.error = "✖".into(); + theme.characters.warning = "⚠".into(); + theme.characters.advice = "☞".into(); + theme +} + +/// Renders an error report into a plain `String`. +/// +/// Pure function so the layout can be unit-tested without touching stdout. +/// If no [`Snippet`] is provided, the report degrades gracefully to the +/// compact `× message` + `help:` form (no code frame). +pub fn render_error( + msg: &str, + error: &EnkryptitError, + location: &Option, + help: &Option, + snippet: &Option, +) -> String { + let (source, labels) = match snippet { + Some(snippet) => { + // Fall back to the location hint as the curved-label text. + let label = snippet.label.clone().or_else(|| location.clone()); + let source = NamedSource::new(snippet.source_name.clone(), snippet.source.clone()); + let label_span = LabeledSpan::new_with_span(label, SourceSpan::from(snippet.span)); + (Some(source), vec![label_span]) + } + None => (None, Vec::new()), + }; + + // When a snippet is drawn, the location already appears as the curved + // label; only fold it into the help text otherwise. + let help_msg = if snippet.is_some() { + help.as_ref().map(|h| h.to_string()) + } else { + match (location, help) { + (Some(location), Some(help)) => Some(format!("{help} (at {location})")), + (Some(location), None) => Some(format!("Error occurred at {location}")), + (None, Some(help)) => Some(help.to_string()), + (None, None) => None, + } + }; + + let diagnostic = EnkryptitDiagnostic { + message: msg.to_string(), + code: error.code(), + help: help_msg, + url: DOC_URL.to_string(), + severity: Severity::Error, + source, + labels, + }; + + let handler = GraphicalReportHandler::new_themed(theme()) + .with_urls(true) + .with_links(true) + .with_context_lines(1); + + let mut output = String::new(); + handler + .render_report(&mut output, &diagnostic) + .expect("rendering a miette report into String shouldn't fail"); + output +} diff --git a/eck/src/diagnostic/output/display.rs b/eck/src/diagnostic/output/display.rs new file mode 100644 index 0000000..fe2d9cd --- /dev/null +++ b/eck/src/diagnostic/output/display.rs @@ -0,0 +1,234 @@ +use crate::diagnostic::DOC_URL; +use crate::diagnostic::output::Snippet; +use crate::diagnostic::output::diagnostic::{render_error, theme}; +use crate::{ + diagnostic::{EnkryptitOutput, output::kind::EnkryptitOutputKind}, + errors::EnkryptitError, +}; +use colored::Colorize; +use miette::GraphicalReportHandler; +use miette::MietteDiagnostic; +use terminal_size::{Width as TermWidth, terminal_size}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +impl EnkryptitOutput { + /// Matches the [`EnkryptitOutputKind`] that contains the output, and calls the corresponding `display` function. + /// \ + /// - `Info` => `display_info()` + /// - `Phantom` => None + /// - `Error` => `display_error()` + /// - `Success` => `display_success()` + /// - `Warning` => `display_warning()` + pub fn display(self) { + match self.kind { + EnkryptitOutputKind::Info => display_info(&self.msg), + EnkryptitOutputKind::Phantom => (), + EnkryptitOutputKind::Error { + error, + location, + help, + snippet, + } => display_error(&self.msg, &error, &location, &help, &snippet), + EnkryptitOutputKind::Success => display_success(&self.msg), + EnkryptitOutputKind::Warning => display_warning(&self.msg), + }; + } +} + +/// Displays an error, calling [`render_error()`] and logging it using [`tracing::error!`]. +pub fn display_error( + msg: &str, + error: &EnkryptitError, + location: &Option, + help: &Option, + snippet: &Option, +) { + tracing::error!("{}", error); + + println!("{}", render_error(msg, error, location, help, snippet)); +} + +/// Renders a warning into a plain `String`, using [`MietteDiagnostic`] and a +/// graphical handler with **Enkryptit!**'s theme defined in [`theme()`]. +/// +/// Pure function, so the layout can be unit-tested without touching stdout or +/// the log stream; `display_warning()` is the thin side-effecting wrapper. +pub fn render_warning(msg: &str) -> String { + let report = MietteDiagnostic::new(msg) + .with_url(DOC_URL) + .with_severity(miette::Severity::Warning); + + let mut output = String::new(); + + GraphicalReportHandler::new_themed(theme()) + .render_report(&mut output, &report) + .expect("rendering a miette report into String shouldn't fail"); + + output +} + +/// Displays a warning, calling [`render_warning()`], logging it using [`tracing::warn!`] +/// and printing it to stdout. +fn display_warning(msg: &str) { + tracing::warn!("{}", msg); + + println!("{}", render_warning(msg)); +} + +/// The minimal width of the success box. +const SUCCESS_BOX_MIN_WIDTH: usize = 60; + +#[doc(hidden)] +pub const SUCCESS_BOX_PADDING: usize = 2; +#[doc(hidden)] +pub const SUCCESS_BADGE_WIDTH: usize = 3; + +/// Fallback terminal width when the size cannot be resolved (e.g. no tty). +const DEFAULT_TERMINAL_WIDTH: usize = 80; + +/// Displays a success. Its current flow is : +/// - Logs the **success** calling [`tracing::info`] +/// - Prepares the lines of text calling [`wrap_text()`] and using the constants +/// - Creates the horizontal bar of `-` (for both top and bottom) +/// - Prints the top (`╭`+ horizontal bar + `╮`) in green +/// - Prints and format the lines, adding the badge and the sides `|` +/// - Pirints the bottom (`╰` + horizontal bar + `╯`) +/// +/// The box width adapts to the message (never below +/// [`SUCCESS_BOX_MIN_WIDTH`]) and is capped at the terminal width, so paths are +/// usually printed unwrapped on a single aligned line. All lengths are measured +/// in **display columns** (combining marks take 0 columns, wide CJK 2), keeping +/// the borders aligned. +fn display_success(msg: &str) { + tracing::info!("Success : {}", msg); + + let box_width = choose_box_width(msg); + let message_width = success_message_width(box_width); + + let mut lines = wrap_text(msg, message_width); + if lines.is_empty() { + lines.push(String::new()); + } + + let horizontal = "─".repeat(box_width - 2); + + println!(); + println!("{}", format!("╭{horizontal}╮").green()); + for (i, line) in lines.iter().enumerate() { + let badge = if i == 0 { + format!("{} ", "✔".green().bold()) + } else { + " ".repeat(SUCCESS_BADGE_WIDTH) + }; + let text = pad_to(line, message_width); + println!( + "{}{}{}{}{}", + "│".green(), + " ".repeat(SUCCESS_BOX_PADDING), + badge, + text, + "│".green(), + ); + } + println!("{}", format!("╰{horizontal}╯").green()); +} + +/// Number of display columns available for the message text inside a box of +/// `box_width`. The layout is fixed: border + left padding + badge + text + +/// border (the right side has no reserved padding, `pad_to` fills the gap). +#[doc(hidden)] +pub fn success_message_width(box_width: usize) -> usize { + box_width - SUCCESS_BOX_PADDING - SUCCESS_BADGE_WIDTH - 2 +} + +/// Pick a box width that fits the (unwrapped) message, never below the minimum +/// and never wider than the terminal. +fn choose_box_width(msg: &str) -> usize { + let min = SUCCESS_BOX_MIN_WIDTH; + let content = msg.width() + SUCCESS_BOX_PADDING * 2 + SUCCESS_BADGE_WIDTH + 2; + let term = terminal_width().saturating_sub(1); + + content.max(min).min(term.max(min)) +} + +/// Best-effort number of terminal columns, falling back to a default. +fn terminal_width() -> usize { + terminal_size() + .map(|(TermWidth(width), _)| usize::from(width)) + .unwrap_or(DEFAULT_TERMINAL_WIDTH) +} + +/// Displays and logs an information calling [`tracing::info!`] and using a simple [`println!`] with cyan color. +fn display_info(msg: &str) { + tracing::info!("{}", msg); + println!("\n{} {}", "[INFO]".cyan().bold(), msg); +} + +/// Word-wrap `msg` into lines of at most `width` **display columns**. +/// +/// Paragraphs (`\n`) are kept apart and over-long words are hard-split on +/// grapheme boundaries, so combining marks are never separated from the base +/// character they decorate. The terminal's display width is used throughout +/// (0 for combining marks, 2 for wide CJK), keeping wrapped boxes aligned. +#[doc(hidden)] +pub fn wrap_text(msg: &str, width: usize) -> Vec { + let mut wrapped = Vec::new(); + let mut line = String::new(); + + for paragraph in msg.split('\n') { + for word in paragraph.split_whitespace() { + if word.width() > width { + if !line.is_empty() { + wrapped.push(std::mem::take(&mut line)); + } + wrapped.extend(split_word(word, width)); + continue; + } + + if line.is_empty() { + line.push_str(word); + } else if line.width() + 1 + word.width() <= width { + line.push(' '); + line.push_str(word); + } else { + wrapped.push(std::mem::take(&mut line)); + line.push_str(word); + } + } + + if !line.is_empty() { + wrapped.push(std::mem::take(&mut line)); + } + } + + wrapped +} + +/// Split an over-long word into chunks of at most `width` display columns, +/// never cutting inside a grapheme cluster. +fn split_word(word: &str, width: usize) -> Vec { + let mut chunks = Vec::new(); + let mut chunk = String::new(); + + for grapheme in word.graphemes(true) { + let grapheme_width = grapheme.width().max(1); + if !chunk.is_empty() && chunk.width() + grapheme_width > width { + chunks.push(std::mem::take(&mut chunk)); + } + chunk.push_str(grapheme); + } + + if !chunk.is_empty() { + chunks.push(chunk); + } + + chunks +} + +/// Pad `text` on the right with spaces so it is exactly `width` display columns. +#[doc(hidden)] +pub fn pad_to(text: &str, width: usize) -> String { + let extra = width.saturating_sub(text.width()); + format!("{text}{}", " ".repeat(extra)) +} diff --git a/eck/src/diagnostic/output/kind.rs b/eck/src/diagnostic/output/kind.rs new file mode 100644 index 0000000..e221d03 --- /dev/null +++ b/eck/src/diagnostic/output/kind.rs @@ -0,0 +1,28 @@ +use crate::diagnostic::output::Snippet; +use crate::errors::EnkryptitError; + +/// The kind of an [`EnkryptitOutput`](crate::diagnostic::EnkryptitOutput). Defines what type of information the `EnkryptitOutput` contains and so +/// has an impact on the way the `EnkryptitOutput` is displayed. +pub enum EnkryptitOutputKind { + /// An error. Contains : + /// - The error itself (as an [`EnkryptitError`]) + /// - The location of the error (**optional**) + /// - The help message associated (**optional**) + /// - A [`Snippet`] (**optionnal**) + Error { + error: EnkryptitError, + location: Option, + help: Option, + snippet: Option, + }, + + Success, + Warning, + /// Informational output. Currently unused (the TUI call sites were taken + /// out during the WIP rework); kept as public API for future messages. + #[allow(dead_code)] + Info, + + /// The kind of an empty [`EnkryptitOutput`](crate::diagnostic::EnkryptitOutput). May be removed in a later version, but used for now in [`InspectionReport`](crate::treatment::inspect::InspectionReport) + Phantom, +} diff --git a/eck/src/diagnostic/output/mod.rs b/eck/src/diagnostic/output/mod.rs new file mode 100644 index 0000000..94f26b2 --- /dev/null +++ b/eck/src/diagnostic/output/mod.rs @@ -0,0 +1,99 @@ +use crate::diagnostic::output::snippet::Snippet; +use crate::{diagnostic::output::kind::EnkryptitOutputKind, errors::EnkryptitError}; + +pub mod diagnostic; +pub mod display; +pub mod kind; +pub mod snippet; + +/// An Output, defined by its [`EnkryptitOutputKind`] and the message it contains. +/// \ +/// Once created (and maybe passed trough some functions), it is displayed calling [`EnkryptitOutput::display()`] +pub struct EnkryptitOutput { + kind: EnkryptitOutputKind, + msg: String, +} + +impl EnkryptitOutput { + /// Creates an [`EnkryptitOutput`] of the given [`EnkryptitOutputKind`] and with the given message. + pub fn new(kind: EnkryptitOutputKind, msg: impl Into) -> Self { + Self { + kind, + msg: msg.into(), + } + } + + /// Creates an empty [`EnkryptitOutput`] that will not be displayed. + pub fn phantom() -> Self { + Self { + kind: EnkryptitOutputKind::Phantom, + msg: String::new(), + } + } + + /// Creates an [`EnkryptitOutput`] with the given message and of kind [`EnkryptitOutputKind::Success`] + pub fn success(msg: impl Into) -> Self { + Self { + kind: EnkryptitOutputKind::Success, + msg: msg.into(), + } + } + + /// Creates an [`EnkryptitOutput`] with the given message and of kind [`EnkryptitOutputKind::Info`] + /// + /// Currently unused by the binary (see the commented-out call sites in the + /// TUI); kept as public API for future informational output. + #[allow(dead_code)] + pub fn info(msg: impl Into) -> Self { + Self { + kind: EnkryptitOutputKind::Info, + msg: msg.into(), + } + } + + /// Creates an [`EnkryptitOutput`] with the given message and of kind [`EnkryptitOutputKind::Warning`] + pub fn warning(msg: impl Into) -> Self { + Self { + kind: EnkryptitOutputKind::Warning, + msg: msg.into(), + } + } + + /// Creates an [`EnkryptitOutput`] with the given message, error and of kind [`EnkryptitOutputKind::Error`] + pub fn error(msg: impl Into, error: EnkryptitError) -> Self { + Self { + kind: EnkryptitOutputKind::Error { + error, + location: None, + help: None, + snippet: None, + }, + msg: msg.into(), + } + } + + /// Attach a location hint (e.g. the operation/step that failed) to an [`EnkryptitOutput`] of kind [`EnkryptitOutputKind::Error`]. + pub fn with_location(mut self, location: impl Into) -> Self { + if let EnkryptitOutputKind::Error { location: slot, .. } = &mut self.kind { + *slot = Some(location.into()); + } + self + } + + /// Attach a remediation hint for the user to an [`EnkryptitOutput`] of kind [`EnkryptitOutputKind::Error`]. + pub fn with_help(mut self, help: impl Into) -> Self { + if let EnkryptitOutputKind::Error { help: slot, .. } = &mut self.kind { + *slot = Some(help.into()); + } + self + } + + /// Attach a source snippet used to draw a miette code frame pointing at + /// the offending token. Only meaningful for error outputs. + pub fn with_snippet(mut self, snippet: Snippet) -> Self { + if let EnkryptitOutputKind::Error { snippet: slot, .. } = &mut self.kind { + *slot = Some(snippet); + } + self + } +} diff --git a/eck/src/diagnostic/output/snippet.rs b/eck/src/diagnostic/output/snippet.rs new file mode 100644 index 0000000..4a5f3d1 --- /dev/null +++ b/eck/src/diagnostic/output/snippet.rs @@ -0,0 +1,40 @@ +/// Context used to draw a miette source frame around an error. +/// +/// Points at the offending token inside a synthetic *source* line so that +/// miette can render the `╭─[…] │ · ─┬─ ╰──` snippet, e.g.: +/// +/// ```text +/// io::misc_io_error (link) +/// +/// ✖ io error: No such file or directory (os error 2) +/// ╭─[eck:1:5] +/// 1 │ eck this_directory_is_absurd +/// · ────────────┬─────────── +/// · ╰── I/O +/// ╰──── +/// help: Check that the path exists and that you have the required permissions. +/// ``` +pub struct Snippet { + /// Names the *source* shown in the frame header (e.g. `eck inspect`). + pub source_name: String, + /// The surrounding "code" line containing the offending token. + pub source: String, + /// `(byte_offset, byte_len)` of the offending token inside `source`. + pub span: (usize, usize), + /// Optional label rendered on the curved `╰──` pointer. + pub label: Option, +} + +impl Snippet { + /// Convenience builder: highlight the `path` token inside a synthetic + /// command line like `eck inspect `. + pub fn cli_invocation(program: &str, path: &str) -> Self { + let source = format!("{program} {path}"); + Self { + source_name: program.to_string(), + span: (program.len() + 1, path.len()), + source, + label: None, + } + } +} diff --git a/eck/src/diagnostic/report/argument.rs b/eck/src/diagnostic/report/argument.rs new file mode 100644 index 0000000..a4c1582 --- /dev/null +++ b/eck/src/diagnostic/report/argument.rs @@ -0,0 +1,70 @@ +use crate::diagnostic::report::style::EnkryptitStyle; + +/// An argument for a [`Report`](crate::diagnostic::report::Report). Defined by : +/// - Its value +/// - Its [`EnkryptitStyle`] +/// +/// The style has an importance when displaying the [`ReportArgument`] while displaying a [`Report`](crate::diagnostic::report::Report). +pub struct ReportArgument { + pub value: String, + pub style: EnkryptitStyle, +} + +impl ReportArgument { + /// Returns a [`ReportArgument`] of the given [`EnkryptitStyle`] and value. + pub fn new(value: impl Into, style: EnkryptitStyle) -> Self { + Self { + value: value.into(), + style, + } + } + + /// Returns a [`ReportArgument`] of the given value and of style [`EnkryptitStyle::Label`] + pub fn label(value: impl Into) -> Self { + Self { + value: value.into(), + style: EnkryptitStyle::Label, + } + } + + #[allow(dead_code)] + /// Returns a [`ReportArgument`] of the given value and of style [`EnkryptitStyle::Warning`] + pub fn warning(value: impl Into) -> Self { + Self { + value: value.into(), + style: EnkryptitStyle::Warning, + } + } + + /// Returns a [`ReportArgument`] of the given value and of style [`EnkryptitStyle::Value`] + pub fn value(value: impl Into) -> Self { + Self { + value: value.into(), + style: EnkryptitStyle::Value, + } + } + + /// Returns a [`ReportArgument`] of the given value and of style [`EnkryptitStyle::Accent`] + pub fn title(value: impl Into) -> Self { + Self { + value: value.into(), + style: EnkryptitStyle::Accent, + } + } + + /// Returns a [`ReportArgument`] of the given value and of style [`EnkryptitStyle::Value`], formatted with a ` KiB` suffix. + pub fn value_in_kib(value: impl Into) -> Self { + Self { + value: format!("{} KiB", value.into()), + style: EnkryptitStyle::Value, + } + } + + /// Returns a [`ReportArgument`] of the given value and of style [`EnkryptitStyle::Accent`] + pub fn accent(value: impl Into) -> Self { + Self { + value: value.into(), + style: EnkryptitStyle::Accent, + } + } +} diff --git a/eck/src/diagnostic/report/mod.rs b/eck/src/diagnostic/report/mod.rs new file mode 100644 index 0000000..2d5559d --- /dev/null +++ b/eck/src/diagnostic/report/mod.rs @@ -0,0 +1,54 @@ +pub mod argument; +pub mod style; + +use comfy_table::Table; +use comfy_table::{Cell, ContentArrangement, presets::UTF8_FULL_CONDENSED}; + +use crate::diagnostic::report::argument::ReportArgument; + +pub struct Report { + title: ReportArgument, + fields: Vec<(ReportArgument, ReportArgument)>, +} + +impl Report { + pub fn new(title: ReportArgument) -> Self { + Self { + title, + fields: vec![], + } + } + + pub fn field(mut self, name: ReportArgument, value: ReportArgument) -> Self { + self.fields.push((name, value)); + self + } + + pub fn display(&self) { + let mut table = Table::new(); + + table + .load_style(UTF8_FULL_CONDENSED) + .set_content_arrangement(ContentArrangement::Dynamic); + + table.set_header(vec![ + Cell::new(&self.title.value) + .fg(self.title.style.color()) + .add_attribute(self.title.style.attribute()), + Cell::new(""), + ]); + + for (label, value) in &self.fields { + table.add_row(vec![ + Cell::new(&label.value) + .fg(label.style.color()) + .add_attribute(label.style.attribute()), + Cell::new(&value.value) + .fg(value.style.color()) + .add_attribute(value.style.attribute()), + ]); + } + + println!("{table}"); + } +} diff --git a/eck/src/diagnostic/report/style.rs b/eck/src/diagnostic/report/style.rs new file mode 100644 index 0000000..122ba7e --- /dev/null +++ b/eck/src/diagnostic/report/style.rs @@ -0,0 +1,36 @@ +use comfy_table::{Attribute, Color}; + +#[allow(dead_code)] +pub enum EnkryptitStyle { + Label, + Value, + Green, + Orange, + Accent, + Warning, + Error, +} + +impl EnkryptitStyle { + pub fn color(&self) -> Color { + match self { + Self::Accent => Color::Cyan, + Self::Green => Color::Green, + Self::Label => Color::Grey, + Self::Orange => Color::AnsiValue(214), + Self::Value => Color::White, + Self::Warning => Color::Red, + Self::Error => Color::DarkRed, + } + } + + pub fn attribute(&self) -> Attribute { + match self { + Self::Accent => Attribute::Bold, + Self::Label => Attribute::Bold, + Self::Warning => Attribute::Bold, + Self::Error => Attribute::Italic, + _ => Attribute::NoUnderline, + } + } +} diff --git a/eck/src/directory.rs b/eck/src/directory.rs new file mode 100644 index 0000000..d270fa0 --- /dev/null +++ b/eck/src/directory.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +use directories::ProjectDirs; + +use crate::errors::EnkryptitError; + +pub fn project_dir() -> Result { + ProjectDirs::from("com", "olruix", "Enkryptit").ok_or(EnkryptitError::ConfigError) +} + +pub fn project_dir_path() -> Result { + Ok(project_dir()?.config_dir().to_path_buf()) +} diff --git a/eck/src/encryption/chunk_job/decrypt.rs b/eck/src/encryption/chunk_job/decrypt.rs new file mode 100644 index 0000000..0d5275d --- /dev/null +++ b/eck/src/encryption/chunk_job/decrypt.rs @@ -0,0 +1,36 @@ +use crate::compression::EnkryptitDecompress; +use crate::encryption::chunk_job::result::ChunkResult; +use crate::encryption::encryption_primitives::{decrypt_chunk, derive_nonce}; +use crate::parallelism::executable::EnkryptitExecutable; +use crate::types::CHUNK_SIZE; +use crate::types::CompressionType; +use chacha20poly1305::XChaCha20Poly1305; +use std::sync::Arc; + +pub struct DecryptChunkJob { + pub index: u64, + pub data: Vec, + pub master_nonce: Arc<[u8; 24]>, + pub compression: Arc, + pub cipher: Arc, +} + +impl EnkryptitExecutable for DecryptChunkJob { + type Output = ChunkResult; + + fn execute(self) -> Result { + let nonce = derive_nonce(&self.master_nonce, self.index); + + let mut ndata = self.data; + let mut output = vec![0u8; CHUNK_SIZE]; + + decrypt_chunk(&mut ndata, &self.cipher, &nonce, self.index)?; + + ndata.decompress(&mut output, *self.compression)?; + + Ok(ChunkResult { + index: self.index, + data: output, + }) + } +} diff --git a/eck/src/encryption/chunk_job/encrypt.rs b/eck/src/encryption/chunk_job/encrypt.rs new file mode 100644 index 0000000..311db5a --- /dev/null +++ b/eck/src/encryption/chunk_job/encrypt.rs @@ -0,0 +1,35 @@ +use crate::compression::EnkryptitCompress; +use crate::encryption::chunk_job::result::ChunkResult; +use crate::encryption::encryption_primitives::{derive_nonce, encrypt_chunk}; +use crate::parallelism::executable::EnkryptitExecutable; +use crate::types::CHUNK_SIZE; +use crate::types::CompressionType; +use chacha20poly1305::XChaCha20Poly1305; +use std::sync::Arc; + +pub struct EncryptChunkJob { + pub index: u64, + pub data: Vec, + pub master_nonce: Arc<[u8; 24]>, + pub compression: Arc, + pub cipher: Arc, +} + +impl EnkryptitExecutable for EncryptChunkJob { + type Output = ChunkResult; + + fn execute(self) -> Result { + let nonce = derive_nonce(&self.master_nonce, self.index); + + let mut output = vec![0u8; CHUNK_SIZE]; + + self.data.compress(&mut output, *self.compression)?; + + encrypt_chunk(&mut output, &nonce, &self.cipher, self.index)?; + + Ok(ChunkResult { + index: self.index, + data: output, + }) + } +} diff --git a/eck/src/encryption/chunk_job/mod.rs b/eck/src/encryption/chunk_job/mod.rs new file mode 100644 index 0000000..9f15278 --- /dev/null +++ b/eck/src/encryption/chunk_job/mod.rs @@ -0,0 +1,60 @@ +pub mod decrypt; +pub mod encrypt; +pub mod result; + +use crate::encryption::chunk_job::decrypt::DecryptChunkJob; +use crate::encryption::chunk_job::encrypt::EncryptChunkJob; +use crate::errors::EnkryptitError; +use crate::parallelism::EnkryptitJob; +use crate::parallelism::pool::EnkryptitPool; +use crate::types::CompressionType; +use chacha20poly1305::XChaCha20Poly1305; +use std::sync::Arc; + +/// Helper that creates an `EncryptChunkJob` and submits it too the `EnkryptitPool<>`. +pub fn submit_encrypt_chunk( + pool: &EnkryptitPool, + index: u64, + data: Vec, + nonce: Arc<[u8; 24]>, + compression: Arc, + cipher: Arc, +) -> Result<(), EnkryptitError> { + // We create the job + let job = EncryptChunkJob { + index, + data, + master_nonce: nonce, + compression, + cipher, + }; + + // And submit it to the pool + pool.submit(EnkryptitJob { task: job })?; + + Ok(()) +} + +/// Helper that creates a `DecryptChunkJob` and submits it too the `EnkryptitPool<>`. +pub fn submit_decrypt_chunk( + pool: &EnkryptitPool, + index: u64, + data: Vec, + nonce: Arc<[u8; 24]>, + compression: Arc, + cipher: Arc, +) -> Result<(), EnkryptitError> { + // We create the job + let job = DecryptChunkJob { + index, + data, + master_nonce: nonce, + compression, + cipher, + }; + + // We submit the job to the pool + pool.submit(EnkryptitJob { task: job })?; + + Ok(()) +} diff --git a/eck/src/encryption/chunk_job/result.rs b/eck/src/encryption/chunk_job/result.rs new file mode 100644 index 0000000..648a36e --- /dev/null +++ b/eck/src/encryption/chunk_job/result.rs @@ -0,0 +1,4 @@ +pub struct ChunkResult { + pub index: u64, + pub data: Vec, +} diff --git a/src/encryption/encryption_flow.rs b/eck/src/encryption/encryption_flow.rs similarity index 67% rename from src/encryption/encryption_flow.rs rename to eck/src/encryption/encryption_flow.rs index b62d37b..9878074 100644 --- a/src/encryption/encryption_flow.rs +++ b/eck/src/encryption/encryption_flow.rs @@ -5,18 +5,17 @@ use crate::types::CHUNK_SIZE; use crate::types::CompressionType; use chacha20poly1305::KeyInit; use chacha20poly1305::XChaCha20Poly1305; -use indicatif::{ProgressBar, ProgressStyle}; +use gradient_bar::GradientProgressBar; use std::io::{Read, Write}; -use zeroize::Zeroize; /// The heart of **Enkryptit** : it compresses and encrypts a stream of data. pub fn encrypt_stream( writer: &mut W, mut reader: R, master_nonce: [u8; 24], - mut key: [u8; 32], + key: &[u8; 32], compression: CompressionType, - total_size: u64, + progress_bar: Option, ) -> Result { // Steps counter let mut step: u64 = 0; @@ -27,18 +26,11 @@ pub fn encrypt_stream( let mut n = reader.read(&mut buffer)?; - println!(); - // ProgressBar creation. - let pb = ProgressBar::new(total_size); - pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") - .unwrap() - .progress_chars("#>-")); - // Processed counter let mut total_processed: u64 = 0; // Cipher algorithm. - let cipher = XChaCha20Poly1305::new(&key.into()); + let cipher = XChaCha20Poly1305::new(key.into()); // Output (for in-place compression) let mut output = vec![0u8; CHUNK_SIZE]; @@ -47,11 +39,7 @@ pub fn encrypt_stream( let mut next_buffer = vec![0u8; CHUNK_SIZE]; let next_n = reader.read(&mut next_buffer)?; - let mut data = buffer[..n].to_vec(); - if next_n == 0 { - // If this is the end of the file, we add an 'end magic number' - data.append(&mut b"ENK1END".to_vec()); - } + let data = buffer[..n].to_vec(); // We compress, in-place, the data data.compress(&mut output, compression)?; @@ -70,16 +58,23 @@ pub fn encrypt_stream( // We update the processed total total_processed += n as u64; - pb.set_position(total_processed); + + if let Some(pb) = &progress_bar { + pb.update(total_processed); + } buffer = next_buffer; n = next_n; step += 1; } - pb.finish(); - println!(); - key.zeroize(); + writer.write_all(b"ENK1END")?; + bytes_written += 7; + + if let Some(pb) = &progress_bar { + pb.finish(); + } + Ok(bytes_written) } @@ -87,24 +82,17 @@ pub fn encrypt_stream( pub fn decrypt_stream( writer: &mut W, mut reader: R, - total_size: u64, - mut key: [u8; 32], + key: &[u8; 32], compression: CompressionType, master_nonce: [u8; 24], + progress_bar: Option, ) -> Result { let mut step: u64 = 0; let mut bytes_consumed: u64 = 0; - // ProgressBar creation. - println!(); - let pb = ProgressBar::new(total_size); - pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") - .unwrap() - .progress_chars("#>-")); - let mut total_processed: u64 = 0; - let cipher = XChaCha20Poly1305::new(&key.into()); + let cipher = XChaCha20Poly1305::new(key.into()); let mut output = vec![0u8; CHUNK_SIZE]; @@ -124,6 +112,25 @@ pub fn decrypt_stream( bytes_consumed += 4; + // We try to detect a possible ENK1END. + // If we have one, we break. + // Why putting this here ? Because our format is : + // + // [LEN][CHUNK][ENK1END] + // So, it'll read the [LEN], then the [CHUNK], but, when trying to read the next len, it will read `ENK1`... that isn't a length. + // In consequence, we need to catch this case, and treat it as an exception. + if &len_buf == b"ENK1" { + let mut end = [0u8; 3]; + reader.read_exact(&mut end)?; + + if &end != b"END" { + // invalid magic + // EndMagicNumberNotFound / other error + } + + break; + } + let len = u32::from_le_bytes(len_buf) as usize; let mut payload = vec![0u8; len]; @@ -138,29 +145,20 @@ pub fn decrypt_stream( // Then we decompress it in-place payload.decompress(&mut output, compression)?; - // If the outputs ends with `ENK1END`, the process is finished - if output.ends_with(b"ENK1END") { - let new_len = output.len() - 7; - output.truncate(new_len); - writer.write_all(&output)?; - total_processed += len as u64; - pb.set_position(total_processed); - pb.finish(); - println!(); - key.zeroize(); - return Ok(bytes_consumed); - } - total_processed += len as u64; - pb.set_position(total_processed); + + if let Some(pb) = &progress_bar { + pb.update(total_processed); + } writer.write_all(&output)?; step += 1; } // We should add a new kind of error, something like `EndMagicNumberNotFound`, with a warning : file may have been alterated. - pb.finish(); - println!(); - key.zeroize(); + if let Some(pb) = &progress_bar { + pb.finish(); + } + Ok(bytes_consumed) } diff --git a/src/encryption/encryption_primitives.rs b/eck/src/encryption/encryption_primitives.rs similarity index 94% rename from src/encryption/encryption_primitives.rs rename to eck/src/encryption/encryption_primitives.rs index 1ea28a3..1c197a9 100644 --- a/src/encryption/encryption_primitives.rs +++ b/eck/src/encryption/encryption_primitives.rs @@ -17,7 +17,7 @@ pub fn encrypt_chunk( cipher: &XChaCha20Poly1305, step: u64, ) -> Result<(), EnkryptitError> { - let new_nonce = derive_nonce(&nonce, step); + let new_nonce = derive_nonce(nonce, step); cipher.encrypt_in_place(XNonce::from_slice(&new_nonce), b"", data)?; Ok(()) } @@ -44,7 +44,7 @@ pub fn decrypt_chunk( nonce: &[u8; 24], step: u64, ) -> Result<(), EnkryptitError> { - let new_nonce = derive_nonce(&nonce, step); + let new_nonce = derive_nonce(nonce, step); cipher.decrypt_in_place(XNonce::from_slice(&new_nonce), b"", data)?; Ok(()) } diff --git a/eck/src/encryption/file.rs b/eck/src/encryption/file.rs new file mode 100644 index 0000000..4f1195f --- /dev/null +++ b/eck/src/encryption/file.rs @@ -0,0 +1,23 @@ +use std::{fs::File, io::BufReader, path::PathBuf}; + +use crate::{errors::EnkryptitError, types::CHUNK_SIZE}; + +/// A simple structure that contains the `BufReader` of a file, and its len. +pub struct EnkryptitFile { + pub reader: BufReader, + pub len: u64, + pub estimated_steps: u64, +} + +/// An helper that reads a file and returns its `EnkryptitFile` : +/// - Retrieves its length +/// - Computes its `estimated_steps` +pub fn read_file(path: impl Into) -> Result { + let file = File::open(path.into())?; + let len = file.metadata()?.len(); + Ok(EnkryptitFile { + reader: BufReader::new(file), + len, + estimated_steps: len / CHUNK_SIZE as u64, + }) +} diff --git a/eck/src/encryption/file_encryption/mod.rs b/eck/src/encryption/file_encryption/mod.rs new file mode 100644 index 0000000..e9a3068 --- /dev/null +++ b/eck/src/encryption/file_encryption/mod.rs @@ -0,0 +1,83 @@ +pub mod multithread; +pub mod single; + +use crate::context::EnkryptitContext; +use crate::encryption::file_encryption::multithread::{ + decrypt_multithread_file, encrypt_multithread_file, +}; +use crate::encryption::file_encryption::single::{decrypt_file_single, encrypt_file_single}; +use crate::errors::EnkryptitError; +use crate::key::EnkryptitKey; +use crate::metadatas::MetaDatas; +use crate::types::KeyType::{self}; +use crate::types::{Mode, ParallelismType}; + +/// Public function that encrypts a file (it also resolves the key and keytype & compression) +pub fn encrypt_file( + path: &str, + keytype: &KeyType, + context: &mut EnkryptitContext, +) -> Result { + // Creates the enkryptit key (and resolves keytype & key) + let enkryptit_key: EnkryptitKey = + EnkryptitKey::resolve(Mode::Encrypting, keytype, context, path)?; + + // We resolve the compression type + let compression = context.resolve_compression(path)?; + + // We resolve the parallelism type + let parallelism = context.resolve_parallelism(path)?; + + // We match the parallelism type + match parallelism { + ParallelismType::Single => encrypt_file_single(path, compression, enkryptit_key), + ParallelismType::MultiThread(threads) => { + encrypt_multithread_file(path, compression, enkryptit_key, threads) + } + ParallelismType::Auto => unreachable!( + "`Auto` should never be reached here, and always infered before reaching this function. There is an error in the code. If you are reading this as an user, please open an Issue." + ), + } +} + +/// Public function that decrypts a file +pub fn decrypt_file( + path: &str, + meta_bytes: &[u8], + payload_offset: u64, + context: &mut EnkryptitContext, +) -> Result { + // First, we deserialize the metadata + let metadatas: MetaDatas = postcard::from_bytes(meta_bytes)?; + // And extract the compression type... + let compression_type = metadatas.compression; + // ... and the nonce + let master_nonce = metadatas.nonce; + + // We resolve the key + let enkryptit_key = + EnkryptitKey::resolve(Mode::Decrypting, &metadatas.key_type, context, path)?; + + let parallelism = context.resolve_parallelism(path)?; + + match parallelism { + ParallelismType::Single => decrypt_file_single( + path, + payload_offset, + enkryptit_key, + master_nonce, + compression_type, + ), + ParallelismType::MultiThread(threads) => decrypt_multithread_file( + path, + payload_offset, + enkryptit_key, + master_nonce, + compression_type, + threads, + ), + ParallelismType::Auto => unreachable!( + "`Auto` should never be reached here, and always infered before reaching this function. There is an error in the code. If you are reading this as an user, please open an Issue." + ), + } +} diff --git a/eck/src/encryption/file_encryption/multithread.rs b/eck/src/encryption/file_encryption/multithread.rs new file mode 100644 index 0000000..797043b --- /dev/null +++ b/eck/src/encryption/file_encryption/multithread.rs @@ -0,0 +1,298 @@ +use crate::encryption::chunk_job::result::ChunkResult; +use crate::encryption::chunk_job::{ + decrypt::DecryptChunkJob, encrypt::EncryptChunkJob, submit_decrypt_chunk, submit_encrypt_chunk, +}; +use crate::encryption::encryption_primitives::generate_nonce; +use crate::encryption::file::read_file; +use crate::encryption::shared_cipher; +use crate::errors::EnkryptitError; +use crate::key::EnkryptitKey; +use crate::metadatas::{ArchiveHeader, MetaDatas}; +use crate::parallelism::executable::EnkryptitExecutable; +use crate::parallelism::pool::EnkryptitPool; +use crate::types::{CHUNK_SIZE, CompressionType}; +use gradient_bar::GradientProgressBar; +use std::fs::File; +use std::io::Read; +use std::io::{BufWriter, Write}; +use std::io::{Seek, SeekFrom}; +use std::sync::Arc; + +/// Public function that `encrypts` a file, using a pool of workers. `num_threads` determines the number of workers. +/// The function first initialize the pool of workers. Then, when processing, it submits a jobs to the pool of workers. +/// When the number of submitted jobs is equal to the number of threads, we receive every result of *chunk encryption and compression*, +/// sort it in the right order, and write it in the encrypted file. +pub fn encrypt_multithread_file( + path: &str, + compression: CompressionType, + enkryptit_key: EnkryptitKey, + num_threads: u8, +) -> Result { + // First, we initialize the workers pool + let pool = EnkryptitPool::::new(num_threads as usize)?; + + // Then, we open the file and create the reader + let mut file = read_file(path)?; + + // Generates the nonce + let master_nonce = generate_nonce(); + + // We prepare the shared cipher for Multithreading + let cipher = shared_cipher(enkryptit_key.key_as_ref()); + + // And creates the metadata + let metadata = MetaDatas::new(enkryptit_key.key_type(), compression, master_nonce).pack()?; + + let encrypted_path = format!("{}.encky", path); + + // Creates the `cipherfile` placeholder + let cipherfile = std::fs::File::create(&encrypted_path)?; + + let mut writer = BufWriter::new(cipherfile); + + let actual_meta_len = metadata.len() as u32; + + // Creates and pack the header + let header = ArchiveHeader::new(false, actual_meta_len).pack()?; + + // First, we write the header's len + writer.write_all(&[header.len() as u8])?; + + // Then, we write the serialized header + writer.write_all(&header)?; + + // And the metadata + writer.write_all(&metadata)?; + + // We create the Arc<> wrappers around compression type and master nonce + let arc_compression = Arc::new(compression); + let arc_nonce = Arc::new(master_nonce); + + // We read the file in chunks, and submit jobs to the pool + let mut buffer = vec![0u8; CHUNK_SIZE]; + let mut step: u64 = 0; + let mut results = Vec::with_capacity(num_threads as usize); + let mut submitted = 0u8; + + let pb = GradientProgressBar::with_total_steps(file.estimated_steps, "Encrypting..."); + + loop { + let bytes_read = file.reader.read(&mut buffer)?; + + if bytes_read == 0 { + break; + } + + // If we submitted as many jobs as we have workers, we receive and write the results. + if submitted >= num_threads { + receive_results(&mut results, &pool, num_threads)?; + + write_batch(&mut results, &mut writer)?; + + submitted = 0; + } + + // We create the job and submit it + submit_encrypt_chunk( + &pool, + step, + buffer[..bytes_read].to_vec(), + arc_nonce.clone(), + arc_compression.clone(), + cipher.clone(), + )?; + + // We increment + submitted += 1; + step += 1; + pb.update(step); + } + + // At the end of the loop{}, if we have still pending jobs, we receive and treat their output. + if submitted > 0 { + receive_results(&mut results, &pool, submitted)?; + write_batch(&mut results, &mut writer)?; + } + + // We write the ending 'magic' + writer.write_all(b"ENK1END")?; + writer.flush()?; + + pb.finish(); + + Ok(encrypted_path) +} + +/// Private function that decrypts a file using a pool of workers. `num_threads` determines the number of workers. +/// The function first initialize the pool of workers. Then, when processing, it submits a jobs to the pool of workers. +/// When the number of submitted jobs is equal to the number of threads, we receive every result of *chunk encryption and compression*, +/// sort it in the right order, and write it in the encrypted file. +/// If we *read* `ENK1`, we catch it and try to find `END` (that makes the `ENK1END` magic number). If so, we break. +pub fn decrypt_multithread_file( + path: &str, + payload_offset: u64, + enkryptit_key: EnkryptitKey, + master_nonce: [u8; 24], + compression: CompressionType, + num_threads: u8, +) -> Result { + // First, we initialize the workers pool + let pool = EnkryptitPool::::new(num_threads as usize)?; + + // We open the file + let mut file = read_file(path)?; + + let plain_path = path.strip_suffix(".encky").unwrap_or(path); + + // Create a placeholder for the new file + let new_file = std::fs::File::create(plain_path)?; + let mut writer = BufWriter::new(new_file); + + file.reader.seek(SeekFrom::Start(payload_offset))?; + + // We prepare the shared cipher for Multithreading + let cipher = shared_cipher(enkryptit_key.key_as_ref()); + + // We create the Arc<> wrappers around compression type and master nonce + let arc_compression = Arc::new(compression); + let arc_nonce = Arc::new(master_nonce); + + // We read the file in chunks, and submit jobs to the pool + let mut step: u64 = 0; + let mut results = Vec::with_capacity(num_threads as usize); + let mut submitted = 0u8; + + let pb = GradientProgressBar::with_total_bytes(file.estimated_steps, "Decrypting..."); + + loop { + let mut len_buf = [0u8; 4]; + + match file.reader.read_exact(&mut len_buf) { + // If we got no error, we continue + Ok(_) => {} + // If we have an Eof, we exit the loop + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + break; + } + // If we have another error, we convert it into an EnkryptitError and return it + Err(e) => return Err(e.into()), + } + + // We try to detect a possible ENK1END. + // If we have one, we break. + // Why putting this here ? Because our format is : + // + // [LEN][CHUNK][ENK1END] + // So, it'll read the [LEN], then the [CHUNK], but, when trying to read the next len, it will read `ENK1`... that isn't a length. + // In consequence, we need to catch this case, and treat it as an exception. + if &len_buf == b"ENK1" { + let mut end = [0u8; 3]; + file.reader.read_exact(&mut end)?; + + if &end != b"END" { + // invalid magic + // EndMagicNumberNotFound / other error + } + + break; + } + + let len = u32::from_le_bytes(len_buf) as usize; + + let mut payload = vec![0u8; len]; + + file.reader.read_exact(&mut payload)?; + + // If we submitted as jobs as we have workers, we receive and write the results. + if submitted >= num_threads { + receive_results(&mut results, &pool, num_threads)?; + + write_batch_plain(&mut results, &mut writer)?; + + submitted = 0; + } + + submit_decrypt_chunk( + &pool, + step, + payload, + arc_nonce.clone(), + arc_compression.clone(), + cipher.clone(), + )?; + + // We increment + submitted += 1; + step += 1; + pb.update(step); + } + + // At the end of the loop{}, if we have still pending jobs, we receive and treat their output. + if submitted > 0 { + receive_results(&mut results, &pool, submitted)?; + write_batch_plain(&mut results, &mut writer)?; + } + + pb.finish(); + + writer.flush()?; + + Ok(plain_path.to_string()) +} + +/// Helper function to sort and write a batch of chunks, and then clear the `results` vector. Used when encrypting only. +pub fn write_batch( + results: &mut Vec, + writer: &mut BufWriter, +) -> Result { + results.sort_by_key(|r| r.index); + + let mut written = 0u64; + for chunk_result in results.iter() { + let data = &chunk_result.data; + + let len = (data.len() as u32).to_le_bytes(); + writer.write_all(&len)?; + written += 4; // Len of the len buffer + + writer.write_all(data)?; + written += data.len() as u64; + } + + results.clear(); + + Ok(written) +} + +/// Helper function to sort and write a batch of decrypted chunks (the plaintext), +/// without any length prefix, and then clear the `results` vector. Used when decrypting only. +pub fn write_batch_plain( + results: &mut Vec, + writer: &mut BufWriter, +) -> Result { + results.sort_by_key(|r| r.index); + + let mut written = 0u64; + + for chunk_result in results.iter() { + writer.write_all(&chunk_result.data)?; + written += chunk_result.data.len() as u64; + } + + results.clear(); + + Ok(written) +} + +pub fn receive_results( + results: &mut Vec, + pool: &EnkryptitPool, + num_threads: u8, +) -> Result<(), EnkryptitError> { + for _ in 0..num_threads { + let result = pool.recv()??; + results.push(result); + } + + Ok(()) +} diff --git a/eck/src/encryption/file_encryption/single.rs b/eck/src/encryption/file_encryption/single.rs new file mode 100644 index 0000000..25ec34a --- /dev/null +++ b/eck/src/encryption/file_encryption/single.rs @@ -0,0 +1,107 @@ +use crate::encryption::encryption_flow::{decrypt_stream, encrypt_stream}; +use crate::encryption::encryption_primitives::generate_nonce; +use crate::encryption::file::read_file; +use crate::errors::EnkryptitError; +use crate::key::EnkryptitKey; +use crate::metadatas::{ArchiveHeader, MetaDatas}; +use crate::types::CompressionType; +use gradient_bar::GradientProgressBar; +use std::io::{BufWriter, Write}; +use std::io::{Seek, SeekFrom}; +use zeroize::Zeroize; + +/// Public function that encrypts a file (it also resolves the key and keytype) - single thread +pub fn encrypt_file_single( + path: &str, + compression: CompressionType, + enkryptit_key: EnkryptitKey, +) -> Result { + // Opens the file + let file = read_file(path)?; + + // Generates the nonce + let mut master_nonce = generate_nonce(); + + // Builds the metadata, and serialize it + let metadata = MetaDatas::new( + enkryptit_key.key_type_as_ref().clone(), + compression, + master_nonce, + ) + .pack()?; + + let encrypted_path = format!("{}.encky", path); + + // Creates the `cipherfile` placeholder + let cipherfile = std::fs::File::create(&encrypted_path)?; + + let mut writer = BufWriter::new(cipherfile); + + let actual_meta_len = metadata.len() as u32; + + // Creates and pack the header + let header = ArchiveHeader::new(false, actual_meta_len).pack()?; + + // First, we write the header's len + writer.write_all(&[header.len() as u8])?; + + // Then, we write the serialized header + writer.write_all(&header)?; + + // And the metadata + writer.write_all(&metadata)?; + + let progress_bar = GradientProgressBar::with_total_bytes(file.len, "Encrypting..."); + + // We encrypt the stream in place + let _ = encrypt_stream( + &mut writer, + file.reader, + master_nonce, + enkryptit_key.key_as_ref(), + compression, + Some(progress_bar), + )?; + + // And finally, we `zeroize` the master nonce (key is automatically dropped and Zeroized). + master_nonce.zeroize(); + + writer.flush()?; + + Ok(encrypted_path) +} + +/// Public function that decrypts a file (single-thread) +pub fn decrypt_file_single( + path: &str, + payload_offset: u64, + enkryptit_key: EnkryptitKey, + master_nonce: [u8; 24], + compression: CompressionType, +) -> Result { + // We open the file + let mut file = read_file(path)?; + + let plain_path = path.strip_suffix(".encky").unwrap_or(path); + // Create a placeholder for the new file + let new_file = std::fs::File::create(plain_path)?; + let mut writer = BufWriter::new(new_file); + + file.reader.seek(SeekFrom::Start(payload_offset))?; + + let progress_bar = GradientProgressBar::with_total_bytes(file.len, "Decrypting..."); + + // Decrypts the stream in-place + let _ = decrypt_stream( + &mut writer, + file.reader, + enkryptit_key.key_as_ref(), + compression, + master_nonce, + Some(progress_bar), + )?; + + writer.flush()?; + + Ok(plain_path.to_string()) +} diff --git a/eck/src/encryption/folder_encryption/entry/collect_entries_from_folder.rs b/eck/src/encryption/folder_encryption/entry/collect_entries_from_folder.rs new file mode 100644 index 0000000..bbe140e --- /dev/null +++ b/eck/src/encryption/folder_encryption/entry/collect_entries_from_folder.rs @@ -0,0 +1,86 @@ +use crate::encryption::encryption_primitives::generate_nonce; +use crate::encryption::folder_encryption::entry::collect_entry::collect_entry; +use crate::errors::EnkryptitError; +use crate::metadatas::FileEntry; +use crate::{context::EnkryptitContext, diagnostic::EnkryptitOutput}; +use std::path::PathBuf; +use walkdir::WalkDir; + +/// Collect all files from directory tree using walkdir (follow symlinks via follow_links on Unix) +pub fn collect_folder_entries( + folder_path: &str, + context: &EnkryptitContext, +) -> Result, EnkryptitError> { + let mut entries = Vec::new(); + + for entry in WalkDir::new(folder_path).follow_links(true) { + match entry { + Ok(dir_entry) => { + let (relative_path, permissions) = match collect_entry(&dir_entry, folder_path) { + Ok((rp, perm)) => (rp, perm), + Err(e) => { + tracing::warn!( + "An error occurred when collecting an entry. Here's the detailed error : {}", + e + ); + EnkryptitOutput::warning( + "An error occurred when collecting an entry. We skip it.", + ) + .display(); + continue; + } + }; + + let full_path = PathBuf::from(folder_path).join(&relative_path); + let full_path_str = match full_path.to_str() { + Some(path) => path, + None => { + tracing::warn!( + "An error occurred when converting full path (PathBuf) to &str." + ); + EnkryptitOutput::warning( + "An error occurred when treating an entry. We skip it.", + ) + .display(); + continue; + } + }; + + let compression = match context.resolve_compression(full_path_str) { + Ok(compression) => compression, + Err(e) => { + tracing::warn!( + "An error occurred when infering the compression type of an entry. Here's the detailed error : {}", + e + ); + EnkryptitOutput::warning( + "An error occurred when treating an entry. We skip it.", + ) + .display(); + continue; + } + }; + + entries.push(FileEntry { + relative_path, + offset: 0, // Set during encryption + permissions, + compression, // Set during encryption too + file_nonce: generate_nonce(), // Unique per FILE -> one master nonce per file + }); + } + + Err(e) => { + tracing::warn!( + "An error occurred when infering the compression type of an entry. Here's the detailed error : {}", + e + ); + EnkryptitOutput::warning("An error occurred when treating an entry. We skip it.") + .display(); + continue; + } // Skip inaccessible files during collection + } + } + + Ok(entries) +} diff --git a/eck/src/encryption/folder_encryption/entry/collect_entry.rs b/eck/src/encryption/folder_encryption/entry/collect_entry.rs new file mode 100644 index 0000000..c2dc6c8 --- /dev/null +++ b/eck/src/encryption/folder_encryption/entry/collect_entry.rs @@ -0,0 +1,49 @@ +use crate::errors::EnkryptitError; +use std::path::Path; +use walkdir::DirEntry; + +pub fn collect_entry( + dir_entry: &DirEntry, + folder_path: &str, +) -> Result<(String, Option), EnkryptitError> { + if dir_entry.path() == Path::new(folder_path) { + return Err(EnkryptitError::DirectoryIsFolder); + } + + let metadata = match dir_entry.metadata() { + Ok(m) => m, + Err(_) => { + return Err(EnkryptitError::FailedToReadMetadata( + dir_entry.clone().into_path(), + )); + } + }; + + // Only include regular files (skip directories - they'll be created on decrypt) + if !metadata.is_file() && !metadata.file_type().is_symlink() { + return Err(EnkryptitError::FileIsASymLink); + } + + match dir_entry.path().strip_prefix(folder_path) { + Ok(relative_path) => { + let relative_path_str = relative_path.to_string_lossy().to_string(); + + let perms: Option = { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + Some(metadata.permissions().mode()) + } + + #[cfg(not(unix))] + { + None // No permissions on Windows + } + }; + + Ok((relative_path_str, perms)) + } + + Err(e) => Err(EnkryptitError::StripPrefixError(e)), + } +} diff --git a/eck/src/encryption/folder_encryption/entry/mod.rs b/eck/src/encryption/folder_encryption/entry/mod.rs new file mode 100644 index 0000000..84c02e7 --- /dev/null +++ b/eck/src/encryption/folder_encryption/entry/mod.rs @@ -0,0 +1,2 @@ +pub mod collect_entries_from_folder; +pub mod collect_entry; diff --git a/eck/src/encryption/folder_encryption/intern_archive_encryption/mod.rs b/eck/src/encryption/folder_encryption/intern_archive_encryption/mod.rs new file mode 100644 index 0000000..f191f2c --- /dev/null +++ b/eck/src/encryption/folder_encryption/intern_archive_encryption/mod.rs @@ -0,0 +1,209 @@ +use crate::encryption::chunk_job::{decrypt::DecryptChunkJob, encrypt::EncryptChunkJob}; +use crate::encryption::folder_encryption::intern_archive_encryption::{ + multithread::{ + decrypt_multithreading_file_from_archive, encrypt_multithreading_file_into_archive, + }, + single::{decrypt_single_file_from_archive, encrypt_single_file_into_archive}, +}; +use crate::parallelism::pool::EnkryptitPool; +use crate::types::ParallelismType; +use crate::{ + context::EnkryptitContext, errors::EnkryptitError, key::EnkryptitKey, metadatas::FileEntry, +}; +use std::path::Path; +pub mod multithread; +pub mod single; +use crate::diagnostic::EnkryptitOutput; +use std::fs::File; + +pub fn treat_entry_encryption( + pool: &mut Option>, + folder_path: &str, + entry: &mut FileEntry, + current_offset: u64, + context: &EnkryptitContext, + archive_path: &str, + key: &EnkryptitKey, +) -> Result { + entry.offset = current_offset; + + let full_path = Path::new(folder_path).join(&entry.relative_path); + + let compression = + match context.resolve_compression(full_path.to_str().unwrap_or(&entry.relative_path)) { + Ok(compression) => compression, + Err(e) => { + tracing::warn!("Failed to infer compression type for an entry: {}", e); + + EnkryptitOutput::warning( + "Could not determine the optimal compression type. Falling back to no compression." + ) + .display(); + + crate::types::CompressionType::NoComp + } + }; + + let parallelism = + match context.resolve_parallelism(full_path.to_str().unwrap_or(&entry.relative_path)) { + Ok(parallelism) => parallelism, + Err(e) => { + tracing::warn!("Failed to infer parallelism type for an entry: {}", e); + + EnkryptitOutput::warning( + "Could not determine the optimal compression type. Falling back to single.", + ) + .display(); + + ParallelismType::Single + } + }; + + let bytes_written = match parallelism { + ParallelismType::Single => encrypt_single_file_into_archive( + folder_path, + &entry.relative_path, + entry.file_nonce, + compression, + key.key_as_ref(), + archive_path, + )?, + ParallelismType::MultiThread(num_threads) => { + if pool + .as_ref() + .is_none_or(|pool| pool.size() != num_threads as usize) + { + *pool = Some(EnkryptitPool::new(num_threads as usize)?); + } + + let pool = pool.as_ref().unwrap(); + + encrypt_multithreading_file_into_archive( + folder_path, + &entry.relative_path, + entry.file_nonce, + compression, + key.key_as_ref(), + archive_path, + pool, + num_threads, + )? + } + ParallelismType::Auto => unreachable!( + "`Auto` should never be reached here, and always infered before reaching this function. There is an error in the code. If you are reading this as an user, please open an Issue." + ), + }; + + Ok(bytes_written) +} + +/// Everything `treat_entry_decryption` needs to decrypt a single archive entry. +pub struct EntryDecryptionContext<'a> { + pub version: u8, + pub dest_folder: &'a str, + pub entry: &'a FileEntry, + pub context: &'a mut EnkryptitContext, + pub archive_path: &'a str, + pub key: &'a EnkryptitKey, + pub payload_offset: u64, +} + +pub fn treat_entry_decryption( + pool: &mut Option>, + cx: EntryDecryptionContext, +) -> Result<(), EnkryptitError> { + let EntryDecryptionContext { + version, + dest_folder, + entry, + context, + archive_path, + key, + payload_offset, + } = cx; + + let offset = if version >= 2 { + entry.offset + } else { + payload_offset + }; + + let parallelism = match context.resolve_parallelism_with_size(entry.offset) { + Ok(parallelism) => parallelism, + Err(e) => { + tracing::warn!("Failed to infer parallelism type for an entry: {}", e); + + EnkryptitOutput::warning( + "Could not determine the optimal compression type. Falling back to single.", + ) + .display(); + + ParallelismType::Single + } + }; + + let decrypt_result = match parallelism { + ParallelismType::Single => decrypt_single_file_from_archive( + archive_path, + dest_folder, + entry.permissions, + &entry.relative_path, + entry.file_nonce, + entry.compression, + key.key_as_ref(), + offset, + ), + ParallelismType::MultiThread(num_threads) => { + if pool + .as_ref() + .is_none_or(|pool| pool.size() != num_threads as usize) + { + *pool = Some(EnkryptitPool::new(num_threads as usize)?); + } + + let pool = pool.as_ref().unwrap(); + + decrypt_multithreading_file_from_archive( + archive_path, + dest_folder, + entry.permissions, + &entry.relative_path, + entry.file_nonce, + entry.compression, + key.key_as_ref(), + offset, + pool, + num_threads, + ) + } + ParallelismType::Auto => unreachable!( + "`Auto` should never be reached here, and always infered before reaching this function. There is an error in the code. If you are reading this as an user, please open an Issue." + ), + }; + + match decrypt_result { + Ok(bytes_consumed) => { + if version < 2 { + // v1: we don't know the exact offset, but we tried. + // For v1 archives this path is inherently unreliable. + let _ = bytes_consumed; + } + } + Err(e) => { + tracing::warn!("Failed to encrypt an entry: {}", e); + + EnkryptitOutput::warning("Could not encrypt or decrypt an entry. Skipping.").display(); + + // Create 0-byte placeholder file with original filename + let placeholder = Path::new(dest_folder).join(&entry.relative_path); + + if let Some(parent) = placeholder.parent() { + std::fs::create_dir_all(parent)?; + } + + let _ = File::create(placeholder); + } + } + + Ok(()) +} diff --git a/eck/src/encryption/folder_encryption/intern_archive_encryption/multithread.rs b/eck/src/encryption/folder_encryption/intern_archive_encryption/multithread.rs new file mode 100644 index 0000000..aad1525 --- /dev/null +++ b/eck/src/encryption/folder_encryption/intern_archive_encryption/multithread.rs @@ -0,0 +1,229 @@ +use crate::diagnostic::EnkryptitOutput; +use crate::encryption::chunk_job::{ + decrypt::DecryptChunkJob, encrypt::EncryptChunkJob, submit_decrypt_chunk, submit_encrypt_chunk, +}; +use crate::encryption::file::read_file; +use crate::encryption::file_encryption::multithread::receive_results; +use crate::encryption::file_encryption::multithread::write_batch; +use crate::encryption::file_encryption::multithread::write_batch_plain; +use crate::errors::EnkryptitError; +use crate::parallelism::pool::EnkryptitPool; +use crate::types::CHUNK_SIZE; +use crate::types::CompressionType; +use chacha20poly1305::{KeyInit, XChaCha20Poly1305}; +use std::fs::File; +use std::io::Write; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom}; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +#[allow(clippy::too_many_arguments)] +/// Encrypt a single file into the archive stream with unique nonce per file, using multithreading +/// +/// The arguments are intentionally kept separate because each represents +/// an independent part of the archive/encryption operation. +pub fn encrypt_multithreading_file_into_archive( + folder_path: &str, + relative_path: &str, + file_nonce: [u8; 24], + compression: CompressionType, + cipher_key: &[u8; 32], + archive_path: &str, + pool: &EnkryptitPool, + num_threads: u8, +) -> Result { + let full_file_path = Path::new(folder_path).join(relative_path); + + if !PathBuf::from(&full_file_path).exists() { + tracing::warn!("Failed to encrypt an entry : path not found"); + + EnkryptitOutput::warning("Failed to found an entry. Skipping.").display(); + + return Ok(0); + } + + let mut file = read_file(full_file_path)?; + + // We prepare the shared cipher for Multithreading + let cipher = Arc::new(XChaCha20Poly1305::new(cipher_key.into())); + + // Bytes written + let mut bytes_written: u64 = 0; + + let mut archive = BufWriter::new(File::options().append(true).open(archive_path)?); + + let arc_compression = Arc::new(compression); + let arc_nonce = Arc::new(file_nonce); + + // We read the file in chunks, and submit jobs to the pool + let mut buffer = vec![0u8; CHUNK_SIZE]; + let mut step: u64 = 0; + let mut results = Vec::with_capacity(num_threads as usize); + let mut submitted = 0u8; + + loop { + let bytes_read = file.reader.read(&mut buffer)?; + + if bytes_read == 0 { + break; + } + + // If we submitted as many jobs as we have workers, we receive and write the results. + if submitted >= num_threads { + receive_results(&mut results, pool, num_threads)?; + + bytes_written += write_batch(&mut results, &mut archive)?; + + submitted = 0; + } + + submit_encrypt_chunk( + pool, + step, + buffer[..bytes_read].to_vec(), + arc_nonce.clone(), + arc_compression.clone(), + cipher.clone(), + )?; + + submitted += 1; + step += 1; + } + + // At the end of the loop{}, if we have still pending jobs, we receive and treat their output. + if submitted > 0 { + receive_results(&mut results, pool, submitted)?; + bytes_written += write_batch(&mut results, &mut archive)?; + } + + // We write the ending 'magic' + archive.write_all(b"ENK1END")?; + bytes_written += 7; + + Ok(bytes_written) +} + +#[allow(clippy::too_many_arguments)] +/// Decrypt a single file from the archive stream using its unique nonce +/// +/// The arguments are intentionally kept separate because each represents +/// an independent part of the archive/decryption operation. +pub fn decrypt_multithreading_file_from_archive( + archive_path: &str, + folder_path: &str, + permissions: Option, + relative_path: &str, + file_nonce: [u8; 24], + compression: CompressionType, + cipher_key: &[u8; 32], + offset: u64, + pool: &EnkryptitPool, + num_threads: u8, +) -> Result { + let archive = File::open(Path::new(archive_path))?; + let mut reader = BufReader::new(archive); + reader.seek(SeekFrom::Start(offset))?; + let full_file_path = Path::new(folder_path).join(relative_path); + + if let Some(parent) = full_file_path.parent() { + std::fs::create_dir_all(parent)?; + } + + let file = File::create(full_file_path)?; + + if let Some(p) = permissions { + file.set_permissions(std::fs::Permissions::from_mode(p))?; + } + + // We prepare the shared cipher for Multithreading + let cipher = Arc::new(XChaCha20Poly1305::new(cipher_key.into())); + + // We create the Arc<> wrappers around compression type and master nonce + let arc_compression = Arc::new(compression); + let arc_nonce = Arc::new(file_nonce); + + // We read the file in chunks, and submit jobs to the pool + let mut step: u64 = 0; + let mut results = Vec::with_capacity(num_threads as usize); + let mut submitted = 0u8; + + // Bytes written + let mut bytes_written: u64 = 0; + + let mut writer = BufWriter::new(file); + + loop { + let mut len_buf = [0u8; 4]; + + match reader.read_exact(&mut len_buf) { + // If we got no error, we continue + Ok(_) => {} + // If we have an Eof, we exit the loop + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + break; + } + // If we have another error, we convert it into an EnkryptitError and return it + Err(e) => return Err(e.into()), + } + + // We try to detect a possible ENK1END. + // If we have one, we break. + // Why putting this here ? Because our format is : + // + // [LEN][CHUNK][ENK1END] + // So, it'll read the [LEN], then the [CHUNK], but, when trying to read the next len, it will read `ENK1`... that isn't a length. + // In consequence, we need to catch this case, and treat it as an exception. + if &len_buf == b"ENK1" { + let mut end = [0u8; 3]; + reader.read_exact(&mut end)?; + + if &end != b"END" { + // invalid magic + // EndMagicNumberNotFound / other error + // TODO! Add a smooth error system + logging system + } + + break; + } + + let len = u32::from_le_bytes(len_buf) as usize; + + let mut payload = vec![0u8; len]; + + reader.read_exact(&mut payload)?; + + // If we submitted as jobs as we have workers, we receive and write the results. + if submitted >= num_threads { + receive_results(&mut results, pool, num_threads)?; + + bytes_written += write_batch_plain(&mut results, &mut writer)?; + + submitted = 0; + } + + // We create the job + submit_decrypt_chunk( + pool, + step, + payload, + arc_nonce.clone(), + arc_compression.clone(), + cipher.clone(), + )?; + + // We increment + submitted += 1; + step += 1; + } + + // At the end of the loop{}, if we have still pending jobs, we receive and treat their output. + if submitted > 0 { + receive_results(&mut results, pool, submitted)?; + bytes_written += write_batch_plain(&mut results, &mut writer)?; + } + + writer.flush()?; + + Ok(bytes_written) +} diff --git a/src/encryption/folder_encryption/intern_archive_encryption.rs b/eck/src/encryption/folder_encryption/intern_archive_encryption/single.rs similarity index 81% rename from src/encryption/folder_encryption/intern_archive_encryption.rs rename to eck/src/encryption/folder_encryption/intern_archive_encryption/single.rs index 932490b..1fcfca0 100644 --- a/src/encryption/folder_encryption/intern_archive_encryption.rs +++ b/eck/src/encryption/folder_encryption/intern_archive_encryption/single.rs @@ -1,3 +1,4 @@ +use crate::diagnostic::EnkryptitOutput; use crate::encryption::encryption_flow::{decrypt_stream, encrypt_stream}; use crate::errors::EnkryptitError; use crate::types::CompressionType; @@ -12,17 +13,20 @@ pub fn encrypt_single_file_into_archive( relative_path: &str, file_nonce: [u8; 24], compression: CompressionType, - cipher_key: [u8; 32], + cipher_key: &[u8; 32], archive_path: &str, ) -> Result { let full_file_path = Path::new(folder_path).join(relative_path); if !PathBuf::from(&full_file_path).exists() { - return Ok(0); // File no longer exists - skip silently + tracing::warn!("Failed to encrypt an entry : path not found"); + + EnkryptitOutput::warning("Failed to found an entry. Skipping.").display(); + + return Ok(0); } let file = File::open(full_file_path)?; - let total_size: u64 = file.metadata()?.len(); let reader = BufReader::new(file); @@ -36,21 +40,24 @@ pub fn encrypt_single_file_into_archive( file_nonce, cipher_key, compression, - total_size, + None, ) } } +#[allow(clippy::too_many_arguments)] /// Decrypt a single file from the archive stream using its unique nonce +/// +/// The arguments are intentionally kept separate because each represents +/// an independent part of the archive/decryption operation. pub fn decrypt_single_file_from_archive( archive_path: &str, folder_path: &str, permissions: Option, relative_path: &str, file_nonce: [u8; 24], - compressed_size: u64, compression: CompressionType, - cipher_key: [u8; 32], + cipher_key: &[u8; 32], offset: u64, ) -> Result { let archive = File::open(Path::new(archive_path))?; @@ -74,10 +81,10 @@ pub fn decrypt_single_file_from_archive( decrypt_stream( &mut writer, reader, - compressed_size, cipher_key, compression, file_nonce, + None, ) } } diff --git a/eck/src/encryption/folder_encryption/mod.rs b/eck/src/encryption/folder_encryption/mod.rs new file mode 100644 index 0000000..aadafe1 --- /dev/null +++ b/eck/src/encryption/folder_encryption/mod.rs @@ -0,0 +1,147 @@ +pub mod entry; +pub mod intern_archive_encryption; +pub mod single; + +use crate::context::EnkryptitContext; +use crate::encryption::folder_encryption::entry::collect_entries_from_folder::collect_folder_entries; +use crate::encryption::folder_encryption::single::{decrypt_folder_single, encrypt_folder_single}; +use crate::errors::EnkryptitError; +use crate::key::EnkryptitKey; +use crate::metadatas::{ArchiveHeader, FolderMetadata}; +use crate::types::KeyType; +use crate::types::Mode; +use gradient_bar::GradientProgressBar; +use postcard::from_bytes; +use std::fs::File; +use std::io::{BufWriter, Seek, SeekFrom, Write}; + +/// Encrypt a folder into a single .encky archive file (v2 format: metadata at the end) +pub fn encrypt_folder( + folder_path: &str, + context: &mut EnkryptitContext, + keytype: &KeyType, +) -> Result { + // Creates the Enkryptit key (resolves both key and keytype) + let enkryptit_key = EnkryptitKey::resolve(Mode::Encrypting, keytype, context, folder_path)?; + + let pb = GradientProgressBar::new_spinner("Treating folder..."); + + // Step 1: Collect all file entries from directory tree (follow symlinks) + let mut entries = collect_folder_entries(folder_path, context)?; + + if entries.is_empty() { + return Err(EnkryptitError::FileError); + } + + // Step 2: Build FolderMetadata (offsets will be filled after encryption) + let mut folder_meta = FolderMetadata::new(enkryptit_key.key_type_as_ref().clone()); + + for entry in &entries { + folder_meta.entries.push(entry.clone()); + } + + // Step 3: Create .encky archive with a fixed-size header region + // The header is serialized, then zero-padded to HEADER_REGION_SIZE bytes + // so that future in-place updates never change the region size. + const HEADER_REGION_SIZE: usize = 64; + let archive_path = format!("{}.encky", folder_path); + + { + // Creates the archive + let mut archive_file = BufWriter::new(File::create(&archive_path)?); + + // the header placeholder + let placeholder_header = ArchiveHeader::new(true, 0); + + // We serialize it + let mut header_bytes = placeholder_header.pack()?; + // And resize with HEADER_REGION_SIZE + header_bytes.resize(HEADER_REGION_SIZE, 0); + + // We write the region size + archive_file.write_all(&[HEADER_REGION_SIZE as u8])?; + // And the header + archive_file.write_all(&header_bytes)?; + } + + // Compute the beginning offset + let data_start: u64 = 1 + HEADER_REGION_SIZE as u64; + + pb.finish(); + + // Step 4: Encrypt each file, tracking offsets + encrypt_folder_single( + folder_path, + enkryptit_key, + &mut entries, + data_start, + context, + &archive_path, + )?; + + let pb = GradientProgressBar::new_spinner("Finishing folder treatment..."); + + // Step 5: Rebuild metadata with correct offsets and write at end of archive + folder_meta.entries.clear(); + for entry in &entries { + folder_meta.entries.push(entry.clone()); + } + + let serialized_meta = folder_meta.pack()?; + let meta_len = serialized_meta.len() as u32; + + { + let mut archive_file = BufWriter::new(File::options().append(true).open(&archive_path)?); + + archive_file.write_all(&serialized_meta)?; + } + + // Step 6: Seek back and update meta_len in the fixed-size header region + { + let final_header = ArchiveHeader::new(true, meta_len); + let mut final_header_bytes = final_header.pack()?; + final_header_bytes.resize(HEADER_REGION_SIZE, 0); + + let mut archive_file = File::options().read(true).write(true).open(&archive_path)?; + + archive_file.seek(SeekFrom::Start(1))?; + archive_file.write_all(&final_header_bytes)?; + } + + pb.finish(); + + Ok(archive_path) +} + +/// Decrypt a folder archive (.encky file) back to original structure +pub fn decrypt_folder( + archive_path: &str, + meta_bytes: &[u8], + payload_offset: u64, + version: u8, + context: &mut EnkryptitContext, +) -> Result { + // First, we deserialize the metadata + let metadatas: FolderMetadata = from_bytes(meta_bytes)?; + let entries = metadatas.entries; + + // Then, we resolve the key & keytype and create a new EnkryptitKey + let enkryptit_key = + EnkryptitKey::resolve(Mode::Decrypting, &metadatas.key_type, context, archive_path)?; + + // Step 3: Create destination directory structure + let dest_folder = archive_path.strip_suffix(".encky").unwrap_or(archive_path); + std::fs::create_dir_all(dest_folder)?; + // Step 4: Decrypt each file independently - continue on failure! + decrypt_folder_single( + archive_path, + dest_folder, + &entries, + enkryptit_key, + payload_offset, + version, + context, + )?; + + Ok(dest_folder.to_string()) +} diff --git a/eck/src/encryption/folder_encryption/single.rs b/eck/src/encryption/folder_encryption/single.rs new file mode 100644 index 0000000..f0e6e4f --- /dev/null +++ b/eck/src/encryption/folder_encryption/single.rs @@ -0,0 +1,80 @@ +use gradient_bar::GradientProgressBar; + +use crate::{ + context::EnkryptitContext, + encryption::{ + chunk_job::{decrypt::DecryptChunkJob, encrypt::EncryptChunkJob}, + folder_encryption::intern_archive_encryption::{ + EntryDecryptionContext, treat_entry_decryption, treat_entry_encryption, + }, + }, + errors::EnkryptitError, + key::EnkryptitKey, + metadatas::FileEntry, + parallelism::pool::EnkryptitPool, +}; + +pub fn encrypt_folder_single( + folder_path: &str, + key: EnkryptitKey, + entries: &mut Vec, + offset: u64, + context: &mut EnkryptitContext, + archive_path: &str, +) -> Result<(), EnkryptitError> { + let mut current_offset = offset; + let mut pool: Option> = None; + + let pb = GradientProgressBar::with_total_steps(entries.len() as u64, "Encrypting folder..."); + + for entry in entries { + current_offset += treat_entry_encryption( + &mut pool, + folder_path, + entry, + current_offset, + context, + archive_path, + &key, + )?; + pb.inc(1); + } + + pb.finish(); + + Ok(()) +} + +pub fn decrypt_folder_single( + archive_path: &str, + dest_folder: &str, + entries: &Vec, + key: EnkryptitKey, + payload_offset: u64, + version: u8, + context: &mut EnkryptitContext, +) -> Result<(), EnkryptitError> { + let mut pool: Option> = None; + + let pb = GradientProgressBar::with_total_steps(entries.len() as u64, "Decrypting archive..."); + + for entry in entries { + treat_entry_decryption( + &mut pool, + EntryDecryptionContext { + version, + dest_folder, + entry, + context, + archive_path, + key: &key, + payload_offset, + }, + )?; + pb.inc(1); + } + + pb.finish(); + + Ok(()) +} diff --git a/eck/src/encryption/mod.rs b/eck/src/encryption/mod.rs new file mode 100644 index 0000000..55ef258 --- /dev/null +++ b/eck/src/encryption/mod.rs @@ -0,0 +1,14 @@ +pub mod chunk_job; +pub mod encryption_flow; +pub mod encryption_primitives; +pub mod file; +pub mod file_encryption; +pub mod folder_encryption; + +use chacha20poly1305::{KeyInit, XChaCha20Poly1305}; +use std::sync::Arc; + +/// Small helper that returns an `Arc>` where `` is the *key*. +pub fn shared_cipher(key: &[u8; 32]) -> Arc { + Arc::new(XChaCha20Poly1305::new(key.into())) +} diff --git a/eck/src/errors.rs b/eck/src/errors.rs new file mode 100644 index 0000000..47deec9 --- /dev/null +++ b/eck/src/errors.rs @@ -0,0 +1,283 @@ +use std::{ + path::{PathBuf, StripPrefixError}, + sync::mpsc::RecvError, +}; + +use fmodeparser::FullPermissionError; +use thiserror::Error; + +use crate::diagnostic::EnkryptitOutput; +use crate::diagnostic::output::kind::EnkryptitOutputKind; + +#[allow(dead_code)] +#[derive(Debug, Error)] +/// Enum for all the errors of **Enkryptit**. Also implements `From<>` other error types. +/// \ +/// Some `EnkryptitError`s are unused by the binary, but used by the tests. That's why `dead_code` is allowed. +pub enum EnkryptitError { + #[error("operation interrupted")] + Break, + + #[error("Error while parsing file permissions : {0}")] + FilePermissionsParsingError(#[from] FullPermissionError), + + #[error("postcard error: {0}")] + PostcardError(#[from] postcard::Error), + + #[error("Path is incorrect : {0}")] + PathIsIncorrect(String), + + #[error("Directory found is the directory of the folder")] + DirectoryIsFolder, + + #[error("Error while stripping a prefix : {0}")] + StripPrefixError(StripPrefixError), + + #[error("Failed to read metadata of file")] + FailedToReadMetadata(PathBuf), + + #[error("File is a SymLink (shortcut)")] + FileIsASymLink, + + #[error("Send error to mspc channel")] + SendError, + + #[error("Receive error from mspc channel : {0}")] + ReceiveError(#[from] RecvError), + + #[error("Invalid worker count")] + InvalidWorkerCount, + + #[error("encryption/decryption failed : {0}")] + EncryptionError(chacha20poly1305::Error), + + #[error("argon2 password hash failed")] + Argon2Error, + + #[error("key derivation error: {0}")] + KeyDerivationError(String), + + #[error("Invalid key type. Found {0}, expected {1}")] + InvalidKeyType(String, String), + + #[error("keyring error: {0}")] + KeyringError(#[from] keyring::Error), + + #[error("The file containing the key was not found")] + KeyNotFoundInFile, + + #[error("The Key couldn't be found in Os' keyring")] + KeyNotFoundInOs, + + #[error("Inspection failed")] + InspectionError, + + #[error("hex decoding error: {0}")] + HexError(#[from] hex::FromHexError), + + #[error("unexpected end of file")] + UnexpectedEof, + + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Unknown Action : {0}")] + UnknownAction(String), + + #[error("invalid key length")] + InvalidKeyLength, + + #[error("Command not found")] + CommandNotFound, + + #[error("error while searching for the file")] + FileError, + + #[error("error while searching for this specific file")] + SpecificFileError(String), + + #[error("unable to lock memory")] + MemoryLockError, + + #[error("home directory not found")] + HomeNotFound, + + #[error("configuration error")] + ConfigError, + + #[error("Error with the Tui : {0}")] + TuiError(std::io::Error), + + #[error("json error: {0}")] + SerdeJsonError(#[from] serde_json::Error), + + #[error("Lz4 compressionError: {0}")] + Lz4CompressionError(#[from] lz4_flex::block::CompressError), + + #[error("lz4 decompression error: {0}")] + Lz4DecompressionError(#[from] lz4_flex::block::DecompressError), + + #[error("zstd error: {0}")] + ZstdError(#[from] oxiarc_core::error::OxiArcError), + + #[error("Corrupted File")] + CorruptedFile, +} + +impl EnkryptitError { + pub fn code(&self) -> String { + match *self { + // Cryptographic error codes + Self::Argon2Error => "crypto::argon2error".to_string(), + Self::EncryptionError(_) => "crypto::encryption_decryption_failed".to_string(), + Self::InvalidKeyLength => "crypto::invalid_key_length".to_string(), + Self::InvalidKeyType(..) => "crypto::invalid_key_type".to_string(), + Self::KeyDerivationError(_) => "crypto::key_derivation_failed".to_string(), + Self::KeyNotFoundInFile => "crypto::key_not_found_in_file".to_string(), + Self::KeyNotFoundInOs => "crypto::key_not_found_in_os".to_string(), + Self::KeyringError(_) => "crypto::error_with_os_keyring".to_string(), + + // Compression-related error codes + Self::Lz4CompressionError(_) => "comp::lz4_comp_failed".to_string(), + Self::ZstdError(_) => "comp::zstd_comp_failed".to_string(), + + // Ui error codes + Self::CommandNotFound => "ui::command_not_found".to_string(), + Self::TuiError(_) => "ui:tui_error".to_string(), + + // Enkryptit! format error codes + Self::CorruptedFile => "format::corrupted_file".to_string(), + Self::DirectoryIsFolder => "format::directory_is_folder".to_string(), + Self::FailedToReadMetadata(_) => "format::metadata_reading_failed".to_string(), + + // Io error codes + Self::UnexpectedEof => "io::unexpected_eof".to_string(), + Self::HomeNotFound => "io::home_directory_not_found".to_string(), + Self::IoError(_) => "io::misc_io_error".to_string(), + Self::PathIsIncorrect(_) => "io::incorrect_path".to_string(), + Self::SpecificFileError(_) => "io::file_not_found".to_string(), + Self::FileError => "io::file_not_found".to_string(), + Self::FileIsASymLink => "io::file_is_a_symlink".to_string(), + Self::FilePermissionsParsingError(_) => "io::permissions_reading_failed".to_string(), + + // Miscellaneous errors + Self::InspectionError => "misc::inspection_failed".to_string(), + Self::HexError(_) => "misc::hex_decoding_failed".to_string(), + Self::ConfigError => "misc::config_directory_error".to_string(), + Self::Break => "misc::break".to_string(), + _ => "none".to_string(), + } + } + + /// A remediation hint, shown alongside the error output. + pub fn help(&self) -> Option { + match self { + Self::InspectionError => Some("Please ensure that the file exists, and that its name does not contain wrong unicode character.".to_string()), + Self::Argon2Error => Some("Key derivation with Argon2 failed. Check your available memory.".to_string()), + Self::EncryptionError(_) => Some("Check your password/key and make sure the file is not corrupted.".to_string()), + Self::InvalidKeyLength => Some("The key must be exactly 32 bytes long.".to_string()), + Self::InvalidKeyType(_, _) => Some("Use a supported key type: Password, Os or File.".to_string()), + Self::KeyDerivationError(_) => Some("The password or key file seems invalid.".to_string()), + Self::KeyNotFoundInFile => Some("Place your key file at the configured path.".to_string()), + Self::KeyNotFoundInOs => Some("Store the key in the OS keyring (e.g. keychain / GNOME Keyring) first.".to_string()), + Self::KeyringError(_) => Some("Make sure your OS keyring is unlocked.".to_string()), + Self::CorruptedFile => Some("Wait for `eck recover ` for help.".to_string()), + Self::DirectoryIsFolder => Some("You can't treat the current folder itself.".to_string()), + Self::FailedToReadMetadata(_) => Some("Check that the file still exists and is readable.".to_string()), + Self::FileIsASymLink => Some("Use the real path instead of a symbolic link.".to_string()), + Self::PathIsIncorrect(_) => Some("Check that the path exists.".to_string()), + Self::FileError => Some("Check that the file exists and is readable.".to_string()), + Self::UnexpectedEof => Some("The file is truncated; it may be corrupted.".to_string()), + Self::IoError(_) => Some("Check that the path exists and that you have the required permissions.".to_string()), + Self::HomeNotFound => Some("Set the HOME environment variable.".to_string()), + Self::ConfigError => Some("Check the config file location and format.".to_string()), + Self::SerdeJsonError(_) => Some("The parameter/config file is not valid JSON.".to_string()), + Self::Lz4CompressionError(_) | Self::Lz4DecompressionError(_) => Some("The data is corrupted or not Lz4-compressed.".to_string()), + Self::ZstdError(_) => Some("The data is corrupted or not Zstandard-compressed.".to_string()), + Self::InvalidWorkerCount => Some("Choose a worker count greater than 0.".to_string()), + Self::UnknownAction(_) => Some("Use a valid command.".to_string()), + Self::FilePermissionsParsingError(_) => Some("The permissions of this file could not be parsed.".to_string()), + Self::PostcardError(_) => Some("The file seems corrupted or not in the Enkryptit! format.".to_string()), + _ => None, + } + } + + /// A short hint about where in the code the error happened. + pub fn location(&self) -> Option { + match self { + Self::Argon2Error + | Self::KeyDerivationError(_) + | Self::HexError(_) + | Self::MemoryLockError => Some("key derivation".to_string()), + Self::EncryptionError(_) => Some("encryption/decryption".to_string()), + Self::InvalidKeyType(_, _) | Self::InvalidKeyLength => Some("key setup".to_string()), + Self::KeyringError(_) => Some("OS keyring".to_string()), + Self::KeyNotFoundInFile => Some("key file lookup".to_string()), + Self::KeyNotFoundInOs => Some("OS keyring lookup".to_string()), + Self::CorruptedFile => Some("archive parsing".to_string()), + Self::DirectoryIsFolder | Self::StripPrefixError(_) => { + Some("folder treatment".to_string()) + } + Self::FailedToReadMetadata(_) => Some("metadata reading".to_string()), + Self::FileIsASymLink => Some("path check".to_string()), + Self::IoError(_) => Some("I/O".to_string()), + Self::Lz4CompressionError(_) => Some("compression".to_string()), + Self::Lz4DecompressionError(_) => Some("decompression".to_string()), + Self::ZstdError(_) => Some("decompression".to_string()), + Self::PostcardError(_) => Some("read_file()".to_string()), + Self::UnexpectedEof => Some("reading archive".to_string()), + Self::SerdeJsonError(_) => Some("configuration parsing".to_string()), + Self::ConfigError => Some("configuration".to_string()), + Self::HomeNotFound => Some("project directory".to_string()), + Self::FilePermissionsParsingError(_) => Some("fmodeparser".to_string()), + Self::SendError | Self::ReceiveError(_) => Some("parallelism".to_string()), + Self::InvalidWorkerCount => Some("parallelism setup".to_string()), + Self::TuiError(_) => Some("TUI".to_string()), + Self::UnknownAction(_) => Some("command dispatch".to_string()), + Self::PathIsIncorrect(_) | Self::SpecificFileError(_) | Self::FileError => { + Some("file lookup".to_string()) + } + _ => None, + } + } + + pub fn into_output(self) -> EnkryptitOutput { + let msg = self.to_string(); + EnkryptitOutput::new( + EnkryptitOutputKind::Error { + location: self.location(), + help: self.help(), + snippet: None, + error: self, + }, + msg, + ) + } +} + +impl From for EnkryptitOutput { + fn from(error: EnkryptitError) -> EnkryptitOutput { + let msg = error.to_string(); + EnkryptitOutput::new( + EnkryptitOutputKind::Error { + location: error.location(), + help: error.help(), + snippet: None, + error, + }, + msg, + ) + } +} + +impl From for EnkryptitError { + fn from(e: chacha20poly1305::Error) -> Self { + EnkryptitError::EncryptionError(e) + } +} + +impl From for EnkryptitError { + fn from(_: argon2::Error) -> Self { + EnkryptitError::Argon2Error + } +} diff --git a/eck/src/frontend/cli/inspection.rs b/eck/src/frontend/cli/inspection.rs new file mode 100644 index 0000000..6d8e466 --- /dev/null +++ b/eck/src/frontend/cli/inspection.rs @@ -0,0 +1,14 @@ +use crate::diagnostic::output::snippet::Snippet; +use crate::treatment::inspect::inspect_object; + +/// Helper for inspecting one or many paths. +/// \ +/// It just iterates over all the paths and inspects each one calling `inspect_object`. +pub fn inspect_one_or_more_objects(paths: &Vec) { + // We iterate over the paths + for path in paths { + inspect_object(path) + .with_snippet(Snippet::cli_invocation("eck inspect", path)) + .display(); + } +} diff --git a/src/cli.rs b/eck/src/frontend/cli/mod.rs similarity index 54% rename from src/cli.rs rename to eck/src/frontend/cli/mod.rs index 71a3b5a..ffbe19f 100644 --- a/src/cli.rs +++ b/eck/src/frontend/cli/mod.rs @@ -1,14 +1,7 @@ -use crate::errors::EnkryptitError; - -/// Enum for Outputs, also used in Modern Ui -pub enum Output { - Help, - Error { error: EnkryptitError }, - Success { message: String }, - CorruptedFile, - SomethingWentWrong, - ParamsChanged, -} +use crate::types::{CompressionType, KeyParams, ParallelismType}; +pub mod inspection; +pub mod params_helpers; +pub mod treatment; #[macro_export] /// Macro that prints the help @@ -26,21 +19,6 @@ macro_rules! print_params_help { }; } -#[macro_export] -/// Macro that prints a success -macro_rules! success { - ($succes: expr) => { - println!("Success : {}", $succes) - }; -} - -#[macro_export] -macro_rules! log_error { - ($msg:expr) => { - eprintln!("[ERROR] {}", $msg) - }; -} - #[macro_export] macro_rules! exit { () => { @@ -62,12 +40,11 @@ macro_rules! params_changed { }; } -#[macro_export] -macro_rules! show_params { - ($kt: expr, $c: expr) => { - println!( - "\n Actual parameters : \n Key Type : {:?} \n Compression : {:?} \n", - $kt, $c - ) - }; +pub fn show_params(kt: &KeyParams, c: &CompressionType, p: &ParallelismType) { + println!(); + println!( + "Actual parameters : \n KeyType : {:?} \n Compression : {:?} \n Parallelism Option : {:?}", + kt, c, p + ); + println!(); } diff --git a/eck/src/frontend/cli/params_helpers.rs b/eck/src/frontend/cli/params_helpers.rs new file mode 100644 index 0000000..1be86e2 --- /dev/null +++ b/eck/src/frontend/cli/params_helpers.rs @@ -0,0 +1,133 @@ +use crate::diagnostic::EnkryptitOutput; +use crate::parameters::params::{load_params, save_params}; +use crate::types::CompressionType; +use crate::types::KeyParams; +use crate::types::ParallelismType; + +/// Helper for showing the parameters. +/// It loads the current parameters before priting the parameters, or printing an error. +pub fn show_params() { + match load_params() { + Ok(params) => { + println!("\n Actual parameters :"); + println!(" Key Type : {:?}", params.key_params); + println!(" Compression : {:?}", params.compression); + println!(" Parallelism : {:?}", params.parallelism); + println!(); + } + Err(e) => { + e.into_output().display(); + std::process::exit(1); + } + } +} + +/// Helper for parsing the parallelism CLI value. +/// Accepts : `single`, `multi` (or `multithread`), `multi:` (or `multi `), `auto`, `automatic` +/// where `n` is a strictly positive number of threads (defaults to 4). +fn parse_parallelism(value: &str) -> Result { + let lower = value.to_lowercase(); + + if lower == "single" || lower == "no" || lower == "none" { + return Ok(ParallelismType::Single); + } + + if lower == "auto" || lower == "automatic" { + return Ok(ParallelismType::Auto); + } + + // Support "multi:" and "multi " forms, and plain "multi". + let (base, count_str) = if let Some(rest) = lower.strip_prefix("multi:") { + ("multi", Some(rest)) + } else if let Some(rest) = lower.strip_prefix("multi ") { + ("multi", Some(rest)) + } else if lower == "multi" || lower == "multithread" { + ("multi", None) + } else { + return Err(format!( + "Unknown parallelism: {value}. Expected `single`, `multi` or `multi:`" + )); + }; + + if base != "multi" { + return Err(format!("Unknown parallelism: {value}")); + } + + let threads = match count_str { + Some(s) => s + .trim() + .parse::() + .map_err(|_| format!("Invalid thread count: `{s}`"))?, + // Plain "multi" without an explicit count -> pick a sensible default + None => 4, + }; + + if threads == 0 { + return Err("The number of threads must be greater than 0".to_string()); + } + + Ok(ParallelismType::MultiThread(threads)) +} + +/// Helper for updating the parameters. +pub fn update_params( + compression: Option, + key_type: Option, + parallelism: Option, +) { + match load_params() { + Ok(mut params) => { + if let Some(c) = compression { + match c.to_lowercase().as_str() { + "zstd" | "1" => params.compression = CompressionType::Zstd, + "lz4" | "2" => params.compression = CompressionType::Lz4, + "xz" | "3" => params.compression = CompressionType::Xz, + "none" | "no" | "4" => params.compression = CompressionType::NoComp, + "auto" | "a" | "5" => params.compression = CompressionType::Auto, + other => { + EnkryptitOutput::warning(format!("Unknown compression: {}", other)) + .display(); + std::process::exit(1); + } + } + } + + if let Some(kt) = key_type { + match kt.to_lowercase().as_str() { + "password" | "pwd" | "1" => params.key_params = KeyParams::PassWord, + "os" | "2" => params.key_params = KeyParams::Os, + "file" | "3" => params.key_params = KeyParams::File, + other => { + EnkryptitOutput::warning(format!("Unknown Key Type: {}", other)).display(); + std::process::exit(1); + } + } + } + + if let Some(p) = parallelism { + match parse_parallelism(&p) { + Ok(par) => params.parallelism = par, + Err(msg) => { + EnkryptitOutput::warning(msg).display(); + std::process::exit(1); + } + } + } + + match save_params(¶ms) { + Ok(_) => EnkryptitOutput::success("Parameters were changed !").display(), + Err(e) => { + EnkryptitOutput::error("Failed to save parameters.", e) + .with_location("cli::params_helpers::update_params()") + .display(); + + std::process::exit(1); + } + } + } + Err(e) => { + Into::::into(e).display(); + std::process::exit(1); + } + } +} diff --git a/eck/src/frontend/cli/treatment.rs b/eck/src/frontend/cli/treatment.rs new file mode 100644 index 0000000..9f3204b --- /dev/null +++ b/eck/src/frontend/cli/treatment.rs @@ -0,0 +1,41 @@ +use crate::context::EnkryptitContext; +use crate::diagnostic::EnkryptitOutput; +use crate::diagnostic::output::snippet::Snippet; +use crate::errors::EnkryptitError; +use crate::parameters::params::load_params; +use crate::treatment::object_treatment::treat_object; + +/// Helper for treating a path. +/// First, we load the parameters, before converting the path from `&str` to `&Path`. +/// Then, if the path doesn't exist, we return an error. +/// If it exist, we continue, by getting the `key` and the `keytype`. +/// Finally, we delegate the object treatment to `treat_object()`. +pub fn treat_object_with_path( + path_str: &str, + cli_password: Option, +) -> Result { + let parameters = load_params()?; + let mut context = + EnkryptitContext::new(cli_password, parameters.compression, parameters.parallelism); + Ok(treat_object(¶meters, path_str, &mut context) + .with_snippet(Snippet::cli_invocation("eck", path_str))) +} + +/// Function that treat the objects, when the args contain many paths +pub fn treat_objects_with_multiple_paths( + paths: &Vec, + cli_password: Option, +) -> Result<(), EnkryptitError> { + // We load the parameters + let parameters = load_params()?; + // Create the global context + let mut context = + EnkryptitContext::new(cli_password, parameters.compression, parameters.parallelism); + // And iterate to treat every path + for path in paths { + treat_object(¶meters, path, &mut context) + .with_snippet(Snippet::cli_invocation("eck", path)) + .display(); + } + Ok(()) +} diff --git a/eck/src/frontend/mod.rs b/eck/src/frontend/mod.rs new file mode 100644 index 0000000..551a8ac --- /dev/null +++ b/eck/src/frontend/mod.rs @@ -0,0 +1,2 @@ +pub mod cli; +pub mod tui; diff --git a/eck/src/frontend/tui/action.rs b/eck/src/frontend/tui/action.rs new file mode 100644 index 0000000..7c5cb4c --- /dev/null +++ b/eck/src/frontend/tui/action.rs @@ -0,0 +1,48 @@ +use crate::errors::EnkryptitError; +use crate::frontend::tui::input::TuiInput; +use crate::frontend::tui::{browse::launch_browser, help::show_help, parameters::launch_params}; + +/// Abstraction for Tui's actions +/// +/// Used by `launch_ui()` and integration tests. +/// +/// Contains 4 actions : +/// - EncryptObject +/// - LaunchParams +/// - ShowHelp +/// - Browse +/// +/// Only implements the `execute(&self)` method, that `matches` the *action* and calls the corresponding function, and the `from_str(&str)` method, that maps an &str to an EnkryptitTuiAction (and returns `None` if the &str does not correspond to any action.) +/// +/// The `Exit` action is directly handled by the `launch_ui()` function itself. +/// +/// In fact, the content of the enum isn't used here. It is only used in `/tests/`. That's why we need to keep it. +#[allow(dead_code)] +pub enum EnkryptitTuiAction { + LaunchParams, + ShowHelp, + Browse, +} + +impl EnkryptitTuiAction { + #[allow(dead_code)] + pub fn execute(&self, input: &impl TuiInput) -> Result<(), EnkryptitError> { + match self { + Self::LaunchParams => launch_params(input), + Self::ShowHelp => { + show_help(); + Ok(()) + } + Self::Browse => launch_browser(input), + } + } + + pub fn from_string(value: &str) -> Option { + match value { + "Browse" => Some(Self::Browse), + "Parameters" => Some(Self::LaunchParams), + "Help" => Some(Self::ShowHelp), + _ => None, + } + } +} diff --git a/eck/src/frontend/tui/browse.rs b/eck/src/frontend/tui/browse.rs new file mode 100644 index 0000000..adecc77 --- /dev/null +++ b/eck/src/frontend/tui/browse.rs @@ -0,0 +1,85 @@ +use crate::diagnostic::EnkryptitOutput; +use crate::errors::EnkryptitError; +use crate::frontend::tui::input::TuiInput; +use crate::frontend::tui::treatment::launch_treatment; +use colored::*; + +/// Launch the browsing UI +pub fn launch_browser(input: &impl TuiInput) -> Result<(), EnkryptitError> { + println!("\n{}", "Browser Panel".cyan().bold()); + + loop { + let choices = vec![ + "Browse Files", + "Browse Folders", + "Browse both Files & Folders", + "Back to main menu", + ]; + + match input.select("What do you want to Browse?", &choices) { + Ok(choice) if choice == "Browse Files" => browse_files(input, None)?, + Ok(choice) if choice == "Browse Folders" => browse_folders(input, None)?, + Ok(choice) if choice == "Browse both Files & Folders" => { + browse_files_then_folders(input, None)? + } + Ok(choice) if choice == "Back to main menu" => break, + Err(_) => { + // EnkryptitOutput::info("Selection cancelled").display(); replaced by : + tracing::info!("Selection cancelled"); + continue; + } + _ => continue, + } + } + + Ok(()) +} + +/// Helper for browsing file, and calling the object treatment +pub fn browse_files(input: &impl TuiInput, password: Option) -> Result<(), EnkryptitError> { + let objects = input.pick_files("Choose file(s)"); + + if objects.is_empty() { + EnkryptitOutput::warning("You didn't choose any file!").display(); + return Ok(()); + } + + launch_treatment(input, objects, password) +} + +// Private helper for browsing folders and treating those folders +pub fn browse_folders( + input: &impl TuiInput, + password: Option, +) -> Result<(), EnkryptitError> { + let folders = input.pick_folders("Choose folder(s)"); + + if folders.is_empty() { + EnkryptitOutput::warning("You didn't choose any folder!").display(); + return Ok(()); + } + + launch_treatment(input, folders, password) +} + +/// Private helper that merges the chosen files and folders, then treats them. +pub fn browse_files_then_folders( + input: &impl TuiInput, + password: Option, +) -> Result<(), EnkryptitError> { + // First, the user picks the files he wants to treat + let files = input.pick_files("Choose file(s)"); + + // Then, he picks the folders he wants to encrypt + let folders = input.pick_folders("Choose folder(s)"); + + let mut objects = folders; + objects.extend(files); + + if objects.is_empty() { + EnkryptitOutput::warning("You didn't choose any file or folder!").display(); + return Ok(()); + } + + launch_treatment(input, objects, password) +} diff --git a/eck/src/frontend/tui/help.rs b/eck/src/frontend/tui/help.rs new file mode 100644 index 0000000..b54819b --- /dev/null +++ b/eck/src/frontend/tui/help.rs @@ -0,0 +1,12 @@ +use colored::*; + +/// helper for printing the help in TUI mode +pub fn show_help() { + println!("\n{}", "Available Commands".cyan().bold()); + println!( + " Browse -> Browse files, folders or both to encrypt / decrypt or inspect" + ); + println!(" Parameters -> Configure settings"); + println!(" Help -> Show this help"); + println!(" Exit -> Quit application"); +} diff --git a/eck/src/frontend/tui/input.rs b/eck/src/frontend/tui/input.rs new file mode 100644 index 0000000..c6dbd0e --- /dev/null +++ b/eck/src/frontend/tui/input.rs @@ -0,0 +1,87 @@ +use crate::errors::EnkryptitError; +use inquire::ui::RenderConfig; +use inquire::{CustomType, Select, Text}; + +#[allow(dead_code)] +/// Abstraction over the interactive TUI inputs. +/// +/// The real UI is driven by `inquire` prompts and `rfd` native file dialogs, +/// neither of which can run headlessly inside `cargo test`. By routing every +/// interactive read through this trait, the surrounding logic (menus, params +/// persistence, browse object collection, treatment dispatch) can be tested +/// with a `MockTuiInput` while `RealTuiInput` keeps the exact live behaviour. +pub trait TuiInput { + /// Show a single-choice menu and return the selected label. + fn select(&self, message: &str, choices: &[&str]) -> Result; + + /// Prompt for free-form text (unused for now). + fn text(&self, message: &str, help_message: &str) -> Result; + + /// Prompt for a numeric counter (used for the number of threads). + fn custom_counter(&self, message: &str) -> Result; + + /// Open a native dialog to pick one or more files, returning their paths. + fn pick_files(&self, title: &str) -> Vec; + + /// Open a native dialog to pick one or more folders, returning their paths. + fn pick_folders(&self, title: &str) -> Vec; +} + +/// Default implementation that talks to the real terminal (`inquire`) and the +/// OS native file dialogs (`rfd`). Used by the real `launch_ui()` entrypoint. +pub struct RealTuiInput; + +impl TuiInput for RealTuiInput { + fn select(&self, message: &str, choices: &[&str]) -> Result { + match Select::new(message, choices.to_vec()).prompt() { + Ok(value) => Ok(value.to_string()), + Err(_) => Err(EnkryptitError::CommandNotFound), + } + } + + fn text(&self, message: &str, help_message: &str) -> Result { + match Text::new(message).with_help_message(help_message).prompt() { + Ok(value) => Ok(value), + Err(_) => Err(EnkryptitError::CommandNotFound), + } + } + + fn custom_counter(&self, message: &str) -> Result { + let counter: CustomType = CustomType::new(message) + .with_default(4) + .with_error_message("Please type a valid number, greater than 0.") + .with_help_message( + "The number of threads you want to use. Recommanded : 4-8 (must be greater than 0).", + ) + .with_parser(&|u| match u.trim().parse::() { + Ok(val) if val > 0 => Ok(val), + _ => Err(()), + }) + .with_render_config(RenderConfig::default()); + + match counter.prompt() { + Ok(value) => Ok(value), + Err(_) => Err(EnkryptitError::CommandNotFound), + } + } + + fn pick_files(&self, title: &str) -> Vec { + match rfd::FileDialog::new().set_title(title).pick_files() { + Some(paths) => paths + .into_iter() + .filter_map(|p| p.to_str().map(String::from)) + .collect(), + None => Vec::new(), + } + } + + fn pick_folders(&self, title: &str) -> Vec { + match rfd::FileDialog::new().set_title(title).pick_folders() { + Some(paths) => paths + .into_iter() + .filter_map(|p| p.to_str().map(String::from)) + .collect(), + None => Vec::new(), + } + } +} diff --git a/eck/src/frontend/tui/mod.rs b/eck/src/frontend/tui/mod.rs new file mode 100644 index 0000000..557708b --- /dev/null +++ b/eck/src/frontend/tui/mod.rs @@ -0,0 +1,51 @@ +pub mod action; +pub mod browse; +pub mod help; +pub mod input; +pub mod parameters; +pub mod treatment; + +use self::action::EnkryptitTuiAction; +use self::input::TuiInput; +use crate::VERSION; +use colored::*; + +/// Show Params macro helper +#[allow(unused)] // It is not unused, but... Rust Analyser thinks that +macro_rules! show_params { + ($kt:expr, $c:expr) => { + println!("\n{}", "Current Parameters".cyan().bold()); + println!(" Key Type: {}", format!("{:?}", $kt).yellow()); + println!(" Compression: {}", format!("{:?}", $c).yellow()); + }; +} + +/// Public function that launches the TUI +pub fn launch_ui(input: &impl TuiInput) { + println!("\n{}", "Enkryptit".cyan().bold()); + println!(" Fast & Simple File Encryption Manager v0.0.{}", VERSION); + + loop { + let choices = vec!["Browse", "Parameters", "Help", "Exit"]; + + match input.select("What do you want to do?", &choices) { + Ok(choice) if choice == "Exit" => { + println!("\n{}", "Goodbye!".green()); + break; + } + + Ok(value) => { + if let Some(action) = EnkryptitTuiAction::from_string(&value) + && let Err(e) = action.execute(input) + { + e.into_output().display(); + } + } + + Err(_) => { + // EnkryptitOutput::info("Selection cancelled").display(); replaced by : + tracing::info!("Selection cancelled"); + } + } + } +} diff --git a/eck/src/frontend/tui/parameters.rs b/eck/src/frontend/tui/parameters.rs new file mode 100644 index 0000000..ec7ccbf --- /dev/null +++ b/eck/src/frontend/tui/parameters.rs @@ -0,0 +1,137 @@ +use crate::diagnostic::EnkryptitOutput; +use crate::errors::EnkryptitError; +use crate::frontend::cli::show_params; +use crate::frontend::tui::input::TuiInput; +use crate::parameters::params::{EnkryptitParams, load_params, save_params}; +use crate::types::CompressionType::{self, Lz4, NoComp, Xz}; +use crate::types::KeyParams::{File, Os, PassWord}; +use crate::types::ParallelismType; +use colored::*; + +/// Launch the params UI +pub fn launch_params(input: &impl TuiInput) -> Result<(), EnkryptitError> { + println!("\n{}", "Parameters Panel".cyan().bold()); + + loop { + let choices = vec![ + "Change compression type", + "Change key type", + "Change parallelism type", + "Show current parameters", + "Back to main menu", + ]; + + match input.select("What do you want to configure?", &choices) { + Ok(choice) if choice == "Change compression type" => change_compression(input)?, + Ok(choice) if choice == "Change key type" => change_key_type(input)?, + Ok(choice) if choice == "Change parallelism type" => change_parallelism(input)?, + Ok(choice) if choice == "Show current parameters" => show_current_params()?, + Ok(choice) if choice == "Back to main menu" => break, + Err(_) => { + // EnkryptitOutput::info("Selection cancelled").display(); replaced by : + tracing::info!("Selection cancelled"); + } + _ => continue, + } + } + + EnkryptitOutput::success("Parameters updated !").display(); + + Ok(()) +} + +/// Private helper for changing compression +fn change_compression(input: &impl TuiInput) -> Result<(), EnkryptitError> { + let old_params = load_params()?; + + let choices = vec![ + "Auto (automatically choosed by Enkryptit!)", + "Zstd (balanced)", + "Lz4 (fastest)", + "Xz (best compression)", + "No compression", + ]; + + let choice = input.select("Select compression type:", &choices)?; + + let compression = match choice.as_str() { + "Auto (automatically choosed by Enkryptit!)" => CompressionType::Auto, + "Zstd (balanced)" => CompressionType::Zstd, + "Lz4 (fastest)" => Lz4, + "Xz (best compression)" => Xz, + "No compression" => NoComp, + _ => CompressionType::Zstd, + }; + + let params = EnkryptitParams::new(old_params.key_params, compression, old_params.parallelism); + save_params(¶ms)?; + + EnkryptitOutput::success(format!("Compression changed to {:?}", compression)).display(); + + Ok(()) +} + +/// Private helper for changing key type +fn change_key_type(input: &impl TuiInput) -> Result<(), EnkryptitError> { + let old_params = load_params()?; + + let choices = vec!["Password (Argon2id)", "OS Keyring", "Key File"]; + + let choice = input.select("Select key type:", &choices)?; + + let kt = match choice.as_str() { + "Password (Argon2id)" => PassWord, + "OS Keyring" => Os, + "Key File" => File, + _ => PassWord, + }; + + let params = EnkryptitParams::new(kt.clone(), old_params.compression, old_params.parallelism); + save_params(¶ms)?; + + EnkryptitOutput::success(format!("Key type changed to {:?}", kt)).display(); + + Ok(()) +} + +/// Private helper for changing parallelism type +fn change_parallelism(input: &impl TuiInput) -> Result<(), EnkryptitError> { + let old_params = load_params()?; + + let choices = vec![ + "Auto (automatically choosed by Enkryptit!)", + "Single", + "MultiThread", + ]; + + let choice = input.select("Select parallelism type:", &choices)?; + + let pt = match choice.as_str() { + "Auto (automatically choosed by Enkryptit!)" => ParallelismType::Auto, + "Single" => ParallelismType::Single, + "MultiThread" => ParallelismType::MultiThread(choose_threads(input)?), + _ => ParallelismType::Single, + }; + + let params = EnkryptitParams::new(old_params.key_params, old_params.compression, pt); + save_params(¶ms)?; + + EnkryptitOutput::success(format!("Parallelism type changed to {:?}", pt)).display(); + + Ok(()) +} + +/// Private helper for choosing number of threads +fn choose_threads(input: &impl TuiInput) -> Result { + let choosed = + input.custom_counter("Enter the number of threads you want (recommended : 4-16) :")?; + + Ok(choosed) +} + +/// Private helper for printing current parameters +fn show_current_params() -> Result<(), EnkryptitError> { + let params = load_params()?; + show_params(¶ms.key_params, ¶ms.compression, ¶ms.parallelism); + Ok(()) +} diff --git a/eck/src/frontend/tui/treatment.rs b/eck/src/frontend/tui/treatment.rs new file mode 100644 index 0000000..c61ab3a --- /dev/null +++ b/eck/src/frontend/tui/treatment.rs @@ -0,0 +1,64 @@ +use crate::context::EnkryptitContext; +use crate::errors::EnkryptitError; +use crate::frontend::tui::input::TuiInput; +use crate::parameters::params::load_params; +use crate::treatment::inspect::inspect_object; +use crate::treatment::object_treatment::treat_object; +use colored::Colorize; + +/// Launch the treatment UI +pub fn launch_treatment( + input: &impl TuiInput, + objects: Vec, + password: Option, +) -> Result<(), EnkryptitError> { + println!("\n{}", "Browser Panel".cyan().bold()); + + loop { + let choices = vec!["Encrypt/Decrypt", "Inspect", "Go Back"]; + + match input.select("What do you want to do?", &choices) { + Ok(choice) if choice == "Encrypt/Decrypt" => { + treat_objects_encryption(&objects, &password)? + } + Ok(choice) if choice == "Inspect" => treat_objects_inspection(&objects), + Ok(choice) if choice == "Go Back" => break, + Err(_) => { + // EnkryptitOutput::info("Selection cancelled").display(); replaced by : + tracing::info!("Selection cancelled"); + } + _ => continue, + } + } + + Ok(()) +} + +/// Treatment loop over a list of chosen object paths. +/// \ +/// It loads the parameters calling [`load_params`], creates the [`EnkryptitContext`], and then iterate over all the `objects`, +/// calling [`treat_object`], and displaying the [`EnkryptitOutput`](crate::diagnostic::EnkryptitOutput)s. +pub fn treat_objects_encryption( + objects: &Vec, + password: &Option, +) -> Result<(), EnkryptitError> { + let parameters = load_params()?; + + let mut context = EnkryptitContext::new( + password.clone(), + parameters.compression, + parameters.parallelism, + ); + + for path_str in objects { + treat_object(¶meters, path_str, &mut context).display(); + } + Ok(()) +} + +/// Iterates over the objects, inspect each of one calling [`inspect_object`] and displays the [`EnkryptitOutput`](crate::diagnostic::EnkryptitOutput)s. +pub fn treat_objects_inspection(objects: &Vec) { + for path_str in objects { + inspect_object(path_str).display(); + } +} diff --git a/eck/src/key/derivation.rs b/eck/src/key/derivation.rs new file mode 100644 index 0000000..66b3361 --- /dev/null +++ b/eck/src/key/derivation.rs @@ -0,0 +1,33 @@ +use crate::{errors::EnkryptitError, parameters::argon2id_parameters::argon2id_parameters}; +use rand::{RngCore, rngs::OsRng}; + +use argon2::{Algorithm, Argon2, Version}; + +/// Hashes the given `password` with argon2id and defined parameters, and returns `(hash, salt)` (`hash` will be used as the key, and `salt` will be inserted in metadata). +pub fn key_from_password(password: &str) -> Result<([u8; 32], [u8; 16]), EnkryptitError> { + let mut salt = [0u8; 16]; + OsRng.fill_bytes(&mut salt); + + let params = argon2id_parameters()?; + + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let mut key = [0u8; 32]; + + argon2.hash_password_into(password.as_bytes(), &salt, &mut key)?; + + Ok((key, salt)) +} + +// Computes the argon2id hash of [`salt & password`] and returns the hash as the key. +pub fn derive_key(password: &str, salt: [u8; 16]) -> Result<[u8; 32], EnkryptitError> { + let params = argon2id_parameters()?; + + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let mut key = [0u8; 32]; + + argon2.hash_password_into(password.as_bytes(), &salt, &mut key)?; + + Ok(key) +} diff --git a/eck/src/key/generation.rs b/eck/src/key/generation.rs new file mode 100644 index 0000000..bf37850 --- /dev/null +++ b/eck/src/key/generation.rs @@ -0,0 +1,27 @@ +use crate::errors::EnkryptitError; +use crate::key::storage::{save_key_in_file, save_key_in_os}; +use rand::{RngCore, rngs::OsRng}; + +/// Public helper for generating a key and writing the key_file, given the name of the file to encrypt. +/// \ +/// This function generates the key, but delegates the saving & writing to `save_key_in_file()`. +pub fn generate_key_and_write_file(filename: &str) -> Result<[u8; 32], EnkryptitError> { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + + save_key_in_file(filename, key)?; + + Ok(key) +} + +/// Public helper for generating the key and storing it in the OS's keyring. +/// \ +/// This function generates the key, but delegates the saving to `save_key_in_os` +pub fn generate_key_and_store_in_os(filename: &str) -> Result<[u8; 32], EnkryptitError> { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + + save_key_in_os(filename, key)?; + + Ok(key) +} diff --git a/eck/src/key/mod.rs b/eck/src/key/mod.rs new file mode 100644 index 0000000..db25feb --- /dev/null +++ b/eck/src/key/mod.rs @@ -0,0 +1,123 @@ +use crate::errors::EnkryptitError; +use crate::key::derivation::{derive_key, key_from_password}; +use crate::key::resolve::{resolve_key_from_file, resolve_key_from_os}; +use crate::{context::EnkryptitContext, types::KeyType}; +pub mod derivation; +pub mod generation; +pub mod resolve; +pub mod storage; +use crate::types::Mode; +use libc::{mlock, munlock}; +use zeroize::Zeroize; + +#[derive(Debug)] +pub struct EnkryptitKey { + key: LockedKey, + key_type: KeyType, +} + +impl EnkryptitKey { + /// Function that, given the encryption mode (Encrypting / Decrypting), the original KeyType, + /// the context and the path of the file / folder to encrypt / decrypt, resolves the pair (key, keytype) + /// and returns an `EnkryptitKey`. + pub fn resolve( + mode: Mode, + keytype: &KeyType, + context: &mut EnkryptitContext, + path: &str, + ) -> Result { + let (key, key_type) = match keytype { + KeyType::Password => { + // This option is only available when encrypting (because `Password`` only exists before keygen, so at encryption), + // so we return return an error if the mode is wrong. + if mode != Mode::Encrypting { + return Err(EnkryptitError::InvalidKeyType( + "Password".to_string(), + "Pwd256".to_string(), + )); + } + let password = context.resolve_password()?; + let (key, salt) = key_from_password(password)?; + (LockedKey::new(key)?, KeyType::Pwd256(salt)) + } + + KeyType::FromFile => { + let key = resolve_key_from_file(mode, path)?; + (LockedKey::new(key)?, KeyType::FromFile) + } + + KeyType::FromOS => { + let key = resolve_key_from_os(mode, path)?; + (LockedKey::new(key)?, KeyType::FromOS) + } + + KeyType::Pwd256(salt) => { + // This option is only available when decrypting (because `Pwd256` only exists in metadata, and contains the salt), + // so we return KeyType::None. (and return an error if the mode is wrong) + if mode != Mode::Decrypting { + return Err(EnkryptitError::InvalidKeyType( + "Pwd256".to_string(), + "Password".to_string(), + )); + } + + let password = context.resolve_password()?; + let key = derive_key(password, *salt)?; + (LockedKey::new(key)?, KeyType::None) + } + + KeyType::None => { + return Err(EnkryptitError::InvalidKeyType( + "None".to_string(), + "Password, FromFile or FromOS".to_string(), + )); + } + }; + + Ok(Self { key, key_type }) + } + + pub fn key_as_ref(&self) -> &[u8; 32] { + self.key.as_ref() + } + + pub fn key_type(self) -> KeyType { + self.key_type + } + + pub fn key_type_as_ref(&self) -> &KeyType { + &self.key_type + } +} + +#[derive(Debug)] +struct LockedKey([u8; 32]); + +impl LockedKey { + /// Creates a new `LockedKey` from an existing key. + fn new(key: [u8; 32]) -> Result { + let locked = Self(key); + + unsafe { + if mlock(locked.0.as_ptr() as *const _, locked.0.len()) != 0 { + return Err(EnkryptitError::MemoryLockError); + } + } + + Ok(locked) + } + + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl Drop for LockedKey { + fn drop(&mut self) { + unsafe { + munlock(self.0.as_ptr() as *const _, self.0.len()); + } + + self.0.zeroize(); + } +} diff --git a/eck/src/key/resolve.rs b/eck/src/key/resolve.rs new file mode 100644 index 0000000..0b4a20b --- /dev/null +++ b/eck/src/key/resolve.rs @@ -0,0 +1,23 @@ +use crate::key::generation::{generate_key_and_store_in_os, generate_key_and_write_file}; +use crate::key::storage::{load_key_from_file, load_key_from_os}; +use crate::{errors::EnkryptitError, types::Mode}; + +pub fn resolve_key_from_file(mode: Mode, path: &str) -> Result<[u8; 32], EnkryptitError> { + match load_key_from_file(path) { + Ok(key) => Ok(key), + Err(_) => match mode { + Mode::Encrypting => generate_key_and_write_file(path), + Mode::Decrypting => Err(EnkryptitError::KeyNotFoundInFile), + }, + } +} + +pub fn resolve_key_from_os(mode: Mode, path: &str) -> Result<[u8; 32], EnkryptitError> { + match load_key_from_os(path) { + Ok(key) => Ok(key), + Err(_) => match mode { + Mode::Encrypting => generate_key_and_store_in_os(path), + Mode::Decrypting => Err(EnkryptitError::KeyNotFoundInOs), + }, + } +} diff --git a/eck/src/key/storage.rs b/eck/src/key/storage.rs new file mode 100644 index 0000000..a0c7ab6 --- /dev/null +++ b/eck/src/key/storage.rs @@ -0,0 +1,78 @@ +use keyring::Entry; +use std::path::PathBuf; + +/// The service name for OS's keyring key storage +const SERVICE: &str = "ENKRYPTIT"; + +use crate::{conversions::convert_key, errors::EnkryptitError}; + +/// Private helper that creates and returns the `PathBuf` of the key a file was encrypted with. +fn key_path(filename: &str) -> Result { + let mut path = dirs::home_dir().ok_or(EnkryptitError::HomeNotFound)?; + + path.push("private_keys"); + std::fs::create_dir_all(&path)?; + + let file_path = PathBuf::from(filename); + + let stem = if file_path.extension().and_then(|e| e.to_str()) == Some("encky") { + file_path.file_stem() + } else { + file_path.file_name() + } + .ok_or(EnkryptitError::FileError)? + .to_string_lossy() + .into_owned(); + + path.push(format!("enkryptit_{}", stem)); + Ok(path) +} + +/// Public helper for loading the key from the OS's keyring. +pub fn load_key_from_os(filename: &str) -> Result<[u8; 32], EnkryptitError> { + let user = format!("enkryptit{}", filename); + + let entry = Entry::new(SERVICE, &user)?; + + let hex_key = entry.get_password()?; + + let bytes = hex::decode(hex_key)?; + + if bytes.len() != 32 { + return Err(EnkryptitError::InvalidKeyLength); + } + + let mut key = [0u8; 32]; + key.copy_from_slice(&bytes); + + Ok(key) +} + +/// Public helper for saving key in Os's keyring +pub fn save_key_in_os(filename: &str, key: [u8; 32]) -> Result<(), EnkryptitError> { + let user = format!("enkryptit{}", filename); + + let entry = Entry::new("ENKRYPTIT", &user)?; + + entry.set_password(&hex::encode(key))?; + + Ok(()) +} + +/// Public helper for saving key in key file. +pub fn save_key_in_file(filename: &str, key: [u8; 32]) -> Result<(), EnkryptitError> { + let path = key_path(filename)?; + + std::fs::write(path, hex::encode(key))?; + + Ok(()) +} + +/// Public helper for loading key from key file +pub fn load_key_from_file(filename: &str) -> Result<[u8; 32], EnkryptitError> { + let path = key_path(filename)?; + + let key = std::fs::read(path)?; + + convert_key(hex::decode(key)?) +} diff --git a/eck/src/lib.rs b/eck/src/lib.rs new file mode 100644 index 0000000..6c1d73c --- /dev/null +++ b/eck/src/lib.rs @@ -0,0 +1,111 @@ +//! # Enkryptit! +//! \ +//! **Enkryptit!** is a Rust-written cli-tool / interactive manager for file and folder encryption. +//! +//! > This project is currently a work in progress. It is **not audited** for production security. +//! +//! ## Code Layout +//! ```txt +//! src/ +//! ├── main.rs # entry point, clap CLI definition & dispatch +//! ├── lib.rs # library entry point (used by tests) +//! ├── types/ # enums: CompressionType, KeyType, ParallelismType, KeyParams +//! │ ├── compression_type.rs +//! │ ├── key_type.rs +//! │ ├── parallelism_type.rs +//! │ └── key_params.rs +//! ├── frontend/ # everything user-facing +//! │ ├── cli/ # command-line interface +//! │ │ ├── inspection.rs # eck inspect +//! │ │ ├── params_helpers.rs +//! │ │ └── treatment.rs # object treatment (single / multiple paths) +//! │ ├── tui/ # terminal UI (inquire + rfd) +//! │ │ ├── action.rs +//! │ │ ├── browse.rs # file/folder pickers +//! │ │ ├── help.rs +//! │ │ ├── input.rs # TuiInput trait (mockable) +//! │ │ ├── parameters.rs +//! │ │ └── treatment.rs +//! │ ├── treat_output.rs # renders success / error / info badges +//! │ └── mod.rs +//! ├── diagnostic/ # logs, reports, errors & error rendering +//! │ ├── logging/ # EnkryptitLogger (tracing, daily rotation → log.txt) +//! │ ├── output/ # EnkryptitOutput, diagnostics & snippets (miette) +//! │ └── report/ # comfy-table Report rendering +//! ├── context/ # EnkryptitContext (password, config resolution) +//! │ ├── compression.rs +//! │ └── parallelism.rs # Auto inference helpers +//! ├── key/ # key resolution, derivation, storage +//! │ ├── derivation.rs # Argon2id key_from_password / derive_key +//! │ ├── generation.rs +//! │ ├── resolve.rs # key from file / OS keyring +//! │ ├── storage.rs +//! │ └── mod.rs # EnkryptitKey + LockedKey (mlock / zeroize) +//! ├── encryption/ +//! │ ├── encryption_primitives.rs # XChaCha20-Poly1305 chunk encrypt/decrypt, nonce derivation +//! │ ├── encryption_flow.rs +//! │ ├── chunk_job/ # EncryptChunkJob / DecryptChunkJob / ChunkResult +//! │ ├── file.rs +//! │ ├── file_encryption/ # single & multithreaded file encryption +//! │ │ ├── single.rs +//! │ │ ├── multithread.rs +//! │ │ └── mod.rs +//! │ ├── folder_encryption/ # folder ↔ .encky archive +//! │ │ ├── collect_entry.rs +//! │ │ ├── entry/ # FileEntry handling +//! │ │ ├── intern_archive_encryption/ # per-entry single/multi treatment +//! │ │ ├── single.rs # entry loop +//! │ │ └── mod.rs +//! │ └── mod.rs +//! ├── compression.rs # zstd / lz4 / xz / none (with retry logic) +//! ├── parallelism/ # worker pool for chunk jobs +//! │ └── pool.rs # EnkryptitPool +//! ├── parameters/ # persisted parameters (config.json) +//! │ ├── argon2id_parameters.rs # static Argon2id params (128 MiB, 3, 1) +//! │ └── params.rs # EnkryptitParams load / save +//! ├── metadatas.rs # ArchiveHeader, MetaDatas, FolderMetadata, FileEntry, MAGIC +//! ├── treatment/ # high-level object treatment & inspection +//! ├── conversions.rs +//! ├── directory.rs # ProjectDirs (com.olruix.Enkryptit) +//! └── errors.rs # EnkryptitError +//! ``` +//! +//! ## License +//! +//! This project is dual-licensed under: +//! +//! * **CeCILL-B License** (French law compliant, fully compatible with GNU GPL/Apache) +//! * **Apache License, Version 2.0** +//! +//! Choose the one that best fits your needs. +//! +//! ## Contact +//! +//!* **Developer:** Olruix ([VRAM-RAM](https://github.com/VRAM-RAM)) + +pub mod compression; +pub mod context; +pub mod conversions; +pub mod diagnostic; +pub mod directory; +pub mod encryption; +pub mod errors; +pub mod frontend; +pub mod key; +pub mod metadatas; +pub mod parallelism; +pub mod parameters; +pub mod treatment; +pub mod types; + +use crate::types::Version; + +/// The version of `Enkryptit!` +/// Enkryptit versions are separated between : +/// - Nightly versions +/// - Stable versions +/// +/// This constant is only for `Nightly` version. +/// +/// So, for example, if you download **Enkryptit!** v0.1.4, it is the 4th nightly version of 1st stable version. +pub const VERSION: Version = 4; diff --git a/eck/src/main.rs b/eck/src/main.rs new file mode 100644 index 0000000..e878c9c --- /dev/null +++ b/eck/src/main.rs @@ -0,0 +1,171 @@ +use crate::{ + diagnostic::{EnkryptitOutput, logging::EnkryptitLogger, output::snippet::Snippet}, + frontend::{ + cli::{ + inspection::inspect_one_or_more_objects, + params_helpers::{show_params, update_params}, + treatment::treat_objects_with_multiple_paths, + }, + tui::{input::RealTuiInput, launch_ui}, + }, + types::Version, +}; +use clap::{Parser, Subcommand}; +mod compression; +mod context; +mod conversions; +mod diagnostic; +mod directory; +mod encryption; +mod errors; +mod frontend; +mod key; +mod metadatas; +mod parallelism; +mod parameters; +mod treatment; +mod types; + +use crate::frontend::cli::treatment::treat_object_with_path; + +/// The version of `Enkryptit!` +/// Enkryptit versions are separated between : +/// - Nightly versions +/// - Stable versions +/// +/// This constant is only for `Nightly` version. +/// +/// So, for example, if you download **Enkryptit!** v0.1.4, it is the 4th nightly version of 1st stable version. +pub const VERSION: Version = 4; + +#[derive(Parser)] +#[command(name = "eck")] +#[command(author = "Olruix")] +#[command(version = "0.0.3")] +#[command(about = "Fast & Secure File Encryption Manager")] +/// The `Cli` structure for **Enkryptit!** cli-tool +/// \ +/// Contains : +/// \ +/// - The command +/// - The path (for encrypting/decrypting a file or folder) +/// - The password (for encrypting/decrypting a file or folder w/ password) +struct Cli { + #[command(subcommand)] + command: Option, + + /// Chemin(s) vers le fichier ou dossier + #[arg(value_name = "PATH")] + path: Vec, + + /// Mot de passe (optionnel) + #[arg(short = 'p', long = "password")] + password: Option, +} + +#[derive(Subcommand)] +/// All the commands available : +/// Ui --> open the UI +/// Inspect --> Inspect one / many files +/// Params / Parameters +/// |-> no arg : show current parameters +/// |-> compression + : change compression algorithm +/// |-> key type + : change key type +enum Commands { + Ui, + Inspect { + #[arg(value_name = "PATHS")] + paths: Vec, + }, + Params { + /// Change compression algorithm + #[arg(short = 'c', long = "compression", value_name = "ALGO")] + compression: Option, + + /// Change key type + #[arg(short = 'k', long = "keytype", value_name = "TYPE")] + key_type: Option, + + /// Change parallelism type + #[arg(short = 'p', long = "parallelism", value_name = "PARA")] + parallelism: Option, + }, + Parameters { + #[arg(short = 'c', long = "compression", value_name = "ALGO")] + compression: Option, + + #[arg(long = "keytype", visible_alias = "kt", value_name = "TYPE")] + key_type: Option, + + /// Change parallelism type + #[arg(short = 'p', long = "parallelism", value_name = "PARA")] + parallelism: Option, + }, +} + +fn main() { + let cli: Cli = Cli::parse(); + + if let Err(e) = EnkryptitLogger::init() { + EnkryptitOutput::error("Error while initializing the logger.", e) + .with_location("main.rs::main()") + .display(); + } + + match cli.command { + Some(Commands::Ui) => { + // If the command is `Ui`, we launch the ui. + launch_ui(&RealTuiInput); + } + Some(Commands::Params { + compression, + key_type, + parallelism, + }) => { + if compression.is_none() && key_type.is_none() && parallelism.is_none() { + // If we don't have any arg, we show the current parameters + show_params() + } else { + // Else, we update the parameters + update_params(compression, key_type, parallelism); + } + } + Some(Commands::Inspect { paths }) => { + inspect_one_or_more_objects(&paths); + } + Some(Commands::Parameters { + compression, + key_type, + parallelism, + }) => { + // Same we `Parameters` + if compression.is_none() && key_type.is_none() && parallelism.is_none() { + show_params() + } else { + update_params(compression, key_type, parallelism); + } + } + None => { + let path: Vec = cli.path; + // If we have a path, we treat it using `treat_object_with_path()` + match path.len() { + 0 => launch_ui(&RealTuiInput), + 1 => match treat_object_with_path(&path[0], cli.password) { + Ok(output) => output.display(), + Err(e) => { + e.into_output() + .with_snippet(Snippet::cli_invocation("eck", &path[0])) + .display(); + } + }, + _ => match treat_objects_with_multiple_paths(&path, cli.password) { + Ok(()) => (), + Err(e) => e + .into_output() + .with_snippet(Snippet::cli_invocation("eck", &path[0])) + .display(), + }, + } + } + } +} diff --git a/src/metadatas.rs b/eck/src/metadatas.rs similarity index 94% rename from src/metadatas.rs rename to eck/src/metadatas.rs index 10df862..4fdc9ad 100644 --- a/src/metadatas.rs +++ b/eck/src/metadatas.rs @@ -56,7 +56,7 @@ impl ArchiveHeader { magic: MAGIC, version: VERSION, is_folder_archive, - meta_len: meta_len, // Will be set after serialization + meta_len, // Will be set after serialization } } @@ -75,6 +75,7 @@ pub struct FileEntry { pub relative_path: String, pub offset: u64, pub permissions: Option, + pub compression: CompressionType, pub file_nonce: [u8; 24], } @@ -95,15 +96,13 @@ impl Eq for FileEntry {} /// - The entries that the folder contains #[derive(Serialize, Deserialize)] pub struct FolderMetadata { - pub compression: CompressionType, pub key_type: KeyType, pub entries: Vec, } impl FolderMetadata { - pub fn new(compression: CompressionType, key_type: KeyType) -> Self { + pub fn new(key_type: KeyType) -> Self { Self { - compression, key_type, entries: Vec::new(), } diff --git a/eck/src/parallelism/executable.rs b/eck/src/parallelism/executable.rs new file mode 100644 index 0000000..5035cb5 --- /dev/null +++ b/eck/src/parallelism/executable.rs @@ -0,0 +1,7 @@ +use crate::errors::EnkryptitError; + +pub trait EnkryptitExecutable { + type Output: Send + 'static; + + fn execute(self) -> Result; +} diff --git a/eck/src/parallelism/mod.rs b/eck/src/parallelism/mod.rs new file mode 100644 index 0000000..9a0eef6 --- /dev/null +++ b/eck/src/parallelism/mod.rs @@ -0,0 +1,23 @@ +use crate::errors::EnkryptitError; + +use crate::parallelism::executable::EnkryptitExecutable; + +pub mod executable; +pub mod pool; +pub mod worker; + +pub struct EnkryptitJob { + pub task: T, +} + +impl EnkryptitJob { + #[allow(dead_code)] + /// Returns an [`EnkryptitJob`]. Unused in the binary, but used in tests. + pub fn new(task: T) -> Self { + Self { task } + } + + pub fn execute(self) -> Result { + self.task.execute() + } +} diff --git a/eck/src/parallelism/pool.rs b/eck/src/parallelism/pool.rs new file mode 100644 index 0000000..d0384e0 --- /dev/null +++ b/eck/src/parallelism/pool.rs @@ -0,0 +1,55 @@ +use std::sync::{Arc, Mutex, mpsc}; + +use crate::errors::EnkryptitError; +use crate::parallelism::EnkryptitExecutable; +use crate::parallelism::EnkryptitJob; +use crate::parallelism::worker::EnkryptitWorker; + +pub struct EnkryptitPool { + _workers: Vec>, + sender: mpsc::SyncSender>, + receiver: mpsc::Receiver>, +} + +impl EnkryptitPool { + pub fn new(size: usize) -> Result { + if size == 0 { + return Err(EnkryptitError::InvalidWorkerCount); + } + // Jobs : bounded for limiting RAM + let (sender_job, receiver_job) = mpsc::sync_channel(size * 2); + + let (sender_result, receiver_result) = mpsc::channel(); + + let receiver_job = Arc::new(Mutex::new(receiver_job)); + + let mut workers: Vec> = Vec::with_capacity(size); + + for id in 0..size { + let receiver = Arc::clone(&receiver_job); + let sender = sender_result.clone(); + + workers.push(EnkryptitWorker::new(id, receiver, sender)); + } + + drop(sender_result); + + Ok(Self { + _workers: workers, + sender: sender_job, + receiver: receiver_result, + }) + } + + pub fn submit(&self, job: EnkryptitJob) -> Result<(), EnkryptitError> { + self.sender.send(job).map_err(|_| EnkryptitError::SendError) + } + + pub fn recv(&self) -> Result, EnkryptitError> { + self.receiver.recv().map_err(EnkryptitError::ReceiveError) + } + + pub fn size(&self) -> usize { + self._workers.len() + } +} diff --git a/eck/src/parallelism/worker.rs b/eck/src/parallelism/worker.rs new file mode 100644 index 0000000..02bcc70 --- /dev/null +++ b/eck/src/parallelism/worker.rs @@ -0,0 +1,48 @@ +use std::{ + marker::PhantomData, + sync::{ + Arc, Mutex, + mpsc::{Receiver, Sender}, + }, + thread::{self, JoinHandle}, +}; + +use crate::{ + errors::EnkryptitError, + parallelism::{EnkryptitJob, executable::EnkryptitExecutable}, +}; + +pub struct EnkryptitWorker { + _id: usize, + _thread: JoinHandle<()>, + _phantom: PhantomData, +} + +impl EnkryptitWorker { + pub fn new( + id: usize, + receiver: Arc>>>, + sender: Sender>, + ) -> Self { + let thread = thread::spawn(move || { + loop { + let job = match receiver.lock().unwrap().recv() { + Ok(job) => job, + Err(_) => break, + }; + + let result = job.execute(); + + if sender.send(result).is_err() { + break; + } + } + }); + + Self { + _id: id, + _thread: thread, + _phantom: PhantomData, + } + } +} diff --git a/src/parameters/argon2id_parameters.rs b/eck/src/parameters/argon2id_parameters.rs similarity index 100% rename from src/parameters/argon2id_parameters.rs rename to eck/src/parameters/argon2id_parameters.rs diff --git a/src/parameters/mod.rs b/eck/src/parameters/mod.rs similarity index 100% rename from src/parameters/mod.rs rename to eck/src/parameters/mod.rs diff --git a/src/parameters/params.rs b/eck/src/parameters/params.rs similarity index 77% rename from src/parameters/params.rs rename to eck/src/parameters/params.rs index e4fe77d..c2ac3b2 100644 --- a/src/parameters/params.rs +++ b/eck/src/parameters/params.rs @@ -1,9 +1,9 @@ +use crate::directory::project_dir_path; use crate::errors::EnkryptitError; use crate::types::{ - CompressionType::{self, Zstd}, - KeyParams, + CompressionType::{self}, + KeyParams, ParallelismType, }; -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -14,13 +14,19 @@ use std::path::PathBuf; pub struct EnkryptitParams { pub key_params: KeyParams, pub compression: CompressionType, + pub parallelism: ParallelismType, } impl EnkryptitParams { - pub fn new(key_params: KeyParams, compression: CompressionType) -> Self { + pub fn new( + key_params: KeyParams, + compression: CompressionType, + parallelism: ParallelismType, + ) -> Self { Self { key_params, compression, + parallelism, } } } @@ -29,7 +35,8 @@ impl Default for EnkryptitParams { fn default() -> Self { Self { key_params: KeyParams::PassWord, - compression: Zstd, + compression: CompressionType::Auto, + parallelism: ParallelismType::Auto, } } } @@ -42,14 +49,11 @@ fn config_path() -> Result { } // Default: use system config directory - let dirs = - ProjectDirs::from("com", "olruix", "Enkryptit").ok_or(EnkryptitError::ConfigError)?; - - let mut path = dirs.config_dir().to_path_buf(); + let mut path = project_dir_path()?; std::fs::create_dir_all(&path)?; - path.push("Enkryptit_configs.json"); + path.push("config.json"); Ok(path) } @@ -85,8 +89,8 @@ pub fn load_params() -> Result { match serde_json::from_str(&content) { Ok(params) => Ok(params), Err(e) => { - eprintln!( - "[WARN] Config file {} is corrupted ({}), falling back to default parameters.", + tracing::warn!( + "Config file {} is corrupted ({}), falling back to default parameters.", path.display(), e ); diff --git a/eck/src/treatment/file_case.rs b/eck/src/treatment/file_case.rs new file mode 100644 index 0000000..3ba9da1 --- /dev/null +++ b/eck/src/treatment/file_case.rs @@ -0,0 +1,29 @@ +use crate::context::EnkryptitContext; +use crate::diagnostic::EnkryptitOutput; +use crate::encryption::file_encryption::{decrypt_file, encrypt_file}; +use crate::types::KeyType::{self}; + +/// Public helper for encrypting a file (Converts Ok<>/EnkryptitError to Output) +pub fn encrypt_file_case( + path: &str, + context: &mut EnkryptitContext, + key_type: &KeyType, +) -> EnkryptitOutput { + match encrypt_file(path, key_type, context) { + Ok(path) => EnkryptitOutput::success(format!("File was encrypted at {}", path)), + Err(e) => e.into(), + } +} + +/// Public helper for decrypting a file (Converts Ok<>/EnkryptitError to Output) +pub fn decrypt_file_case( + path: &str, + meta: Vec, + context: &mut EnkryptitContext, + payload_offset: u64, +) -> EnkryptitOutput { + match decrypt_file(path, &meta, payload_offset, context) { + Ok(path) => EnkryptitOutput::success(format!("File was decrypted at {}", path)), + Err(e) => e.into(), + } +} diff --git a/eck/src/treatment/folder_case.rs b/eck/src/treatment/folder_case.rs new file mode 100644 index 0000000..b42af77 --- /dev/null +++ b/eck/src/treatment/folder_case.rs @@ -0,0 +1,37 @@ +use crate::context::EnkryptitContext; +use crate::diagnostic::EnkryptitOutput; +use crate::encryption::folder_encryption::{decrypt_folder, encrypt_folder}; +use crate::types::KeyType::{self}; + +/// Public helper for encrypting a folder. (Converts Ok<>/EnkryptitError to Output). +pub fn encrypt_folder_case( + path: &str, + context: &mut EnkryptitContext, + key_type: &KeyType, +) -> EnkryptitOutput { + // We resolve the path by suppressing the suffix '/', because if we don't do that, the ` encrypt_folder()` + // function would write 'path/to/my/folder/.encky' instead of 'path/to/my/folder.encky' + let path = match path.strip_suffix("/") { + Some(p) => p, + None => path, + }; + + match encrypt_folder(path, context, key_type) { + Ok(path) => EnkryptitOutput::success(format!("Folder was encrypted at {}", path)), + Err(e) => e.into(), + } +} + +/// Public helper for decrypting a folder. (Converts Ok<>/EnkryptitError to Output) +pub fn decrypt_folder_case( + path: &str, + context: &mut EnkryptitContext, + metadatas_bytes: Vec, + payload_offset: u64, + version: u8, +) -> EnkryptitOutput { + match decrypt_folder(path, &metadatas_bytes, payload_offset, version, context) { + Ok(path) => EnkryptitOutput::success(format!("Folder was decrypted at {}", path)), + Err(e) => e.into(), + } +} diff --git a/eck/src/treatment/inspect/inspect_encrypted_archive.rs b/eck/src/treatment/inspect/inspect_encrypted_archive.rs new file mode 100644 index 0000000..e81c40c --- /dev/null +++ b/eck/src/treatment/inspect/inspect_encrypted_archive.rs @@ -0,0 +1,47 @@ +use postcard::from_bytes; +use std::{fs::metadata, path::Path}; + +use crate::{metadatas::FolderMetadata, treatment::inspect::InspectionReport}; + +pub fn inspect_encrypted_archive(path: &str, meta: &[u8], version: u8) -> InspectionReport { + let pathstd = Path::new(path); + + let metadata = match metadata(path) { + Ok(m) => Some(m), + Err(e) => { + tracing::warn!("{}", e); + None + } + }; + + let folder_meta: Option = match from_bytes(meta) { + Ok(fm) => Some(fm), + Err(e) => { + tracing::warn!("{}", e); + None + } + }; + + let name = pathstd.file_name().map(|p| p.to_string_lossy().to_string()); + + let directory = pathstd.parent().map(|p| p.to_string_lossy().to_string()); + + let size = metadata.map(|m| m.len()); + + let mut entries_number = None; + let mut keytype = None; + + if let Some(meta) = folder_meta { + entries_number = Some(meta.entries.len() as u64); + keytype = Some(meta.key_type) + } + + InspectionReport::EncryptedArchive { + name, + directory, + size, + version, + entries_number, + keytype, + } +} diff --git a/eck/src/treatment/inspect/inspect_encrypted_file.rs b/eck/src/treatment/inspect/inspect_encrypted_file.rs new file mode 100644 index 0000000..0f06ecf --- /dev/null +++ b/eck/src/treatment/inspect/inspect_encrypted_file.rs @@ -0,0 +1,51 @@ +use std::{fs::metadata, path::Path}; + +use crate::context::parallelism::infer_parallelism; +use postcard::from_bytes; + +use crate::{metadatas::MetaDatas, treatment::inspect::InspectionReport}; + +pub fn inspect_encrypted_file(path: &str, meta: &[u8], version: u8) -> InspectionReport { + let pathstd = Path::new(path); + + let mut size = None; + let mut parallelism = None; + let mut compression = None; + let mut keytype = None; + let mut nonce = None; + + match from_bytes::(meta) { + Ok(m) => { + compression = Some(m.compression); + keytype = Some(m.key_type); + nonce = Some(m.nonce); + } + Err(e) => tracing::warn!("{}", e), + }; + + match metadata(path) { + Ok(metadata) => { + size = Some(metadata.len()); + match infer_parallelism(metadata.len()) { + Ok(p) => parallelism = Some(p), + Err(e) => tracing::warn!("{}", e), + } + } + Err(e) => tracing::warn!("{}", e), + }; + + let name = pathstd.file_name().map(|p| p.to_string_lossy().to_string()); + + let directory = pathstd.parent().map(|p| p.to_string_lossy().to_string()); + + InspectionReport::EncryptedFile { + name, + directory, + size, + version, + compression_type: compression, + predicted_parallelism_type: parallelism, + keytype, + nonce, + } +} diff --git a/eck/src/treatment/inspect/inspect_plain_file.rs b/eck/src/treatment/inspect/inspect_plain_file.rs new file mode 100644 index 0000000..2d58b5a --- /dev/null +++ b/eck/src/treatment/inspect/inspect_plain_file.rs @@ -0,0 +1,80 @@ +use infer::get_from_path; +use std::{fs::metadata, path::Path}; + +use crate::{ + context::{compression::infer_compression, parallelism::infer_parallelism}, + treatment::inspect::InspectionReport, +}; + +pub fn inspect_plain_file(path: &str) -> InspectionReport { + let pathstd = Path::new(path); + + let mut size = None; + let mut permissions = None; + let mut extension = None; + let mut mime_extension = None; + let mut parallelism = None; + + match get_from_path(path) { + Ok(type_) => match type_ { + Some(t) => { + extension = Some(t.extension().to_string()); + mime_extension = Some(t.mime_type().to_string()); + } + None => { + if let Some(ext) = pathstd.extension() { + extension = Some(ext.to_string_lossy().to_string()) + } + } + }, + + Err(e) => tracing::warn!("{}", e), + } + + match metadata(path) { + Ok(m) => { + size = Some(m.len()); + permissions = { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + Some(m.permissions().mode()) + } + + #[cfg(not(unix))] + { + None // No permissions on Windows + } + }; + + match infer_parallelism(m.len()) { + Ok(p) => parallelism = Some(p), + Err(e) => tracing::warn!("{}", e), + } + } + Err(e) => tracing::warn!("{}", e), + } + + let compression = match infer_compression(path) { + Ok(c) => Some(c), + Err(e) => { + tracing::warn!("{}", e); + None + } + }; + + let name = pathstd.file_name().map(|p| p.to_string_lossy().to_string()); + + let directory = pathstd.parent().map(|p| p.to_string_lossy().to_string()); + + InspectionReport::PlaintextFile { + name, + directory, + size, + permissions, + extension, + mime_extension, + predicted_compression_type: compression, + predicted_parallelism_type: parallelism, + } +} diff --git a/eck/src/treatment/inspect/inspect_plain_folder.rs b/eck/src/treatment/inspect/inspect_plain_folder.rs new file mode 100644 index 0000000..4277099 --- /dev/null +++ b/eck/src/treatment/inspect/inspect_plain_folder.rs @@ -0,0 +1,41 @@ +use std::{fs::metadata, path::Path}; + +use crate::treatment::inspect::InspectionReport; + +pub fn inspect_plain_folder(path: &str) -> InspectionReport { + let pathstd = Path::new(path); + + let mut size = None; + let mut permissions = None; + + match metadata(path) { + Ok(m) => { + size = Some(m.len()); + permissions = { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + Some(m.permissions().mode()) + } + + #[cfg(not(unix))] + { + None // No permissions on Windows + } + }; + } + + Err(e) => tracing::warn!("{}", e), + } + + let name = pathstd.file_name().map(|p| p.to_string_lossy().to_string()); + + let directory = pathstd.parent().map(|p| p.to_string_lossy().to_string()); + + InspectionReport::Folder { + name, + directory, + size, + permissions, + } +} diff --git a/eck/src/treatment/inspect/mod.rs b/eck/src/treatment/inspect/mod.rs new file mode 100644 index 0000000..9a7302c --- /dev/null +++ b/eck/src/treatment/inspect/mod.rs @@ -0,0 +1,53 @@ +mod inspect_encrypted_archive; +mod inspect_encrypted_file; +mod inspect_plain_file; +mod inspect_plain_folder; +mod report; +use crate::diagnostic::EnkryptitOutput; +use crate::treatment::inspect::inspect_encrypted_archive::inspect_encrypted_archive; +use crate::treatment::inspect::inspect_encrypted_file::inspect_encrypted_file; +use crate::treatment::inspect::inspect_plain_file::inspect_plain_file; +use crate::treatment::inspect::inspect_plain_folder::inspect_plain_folder; +use crate::treatment::object_treatment::ParsedFile; +use crate::treatment::object_treatment::read_file; +pub use report::InspectionReport; +use std::path::Path; + +pub fn inspect_object(path: &str) -> EnkryptitOutput { + if Path::new(path).is_dir() { + match inspect_plain_folder(path).display() { + Ok(()) => (), + Err(e) => return e.into(), + } + } + + match read_file(path) { + Ok(ParsedFile::Enkryptit { + meta, + version, + is_folder_archive, + .. + }) => { + if is_folder_archive { + match inspect_encrypted_archive(path, &meta, version).display() { + Ok(()) => (), + Err(e) => return e.into(), + } + } else { + match inspect_encrypted_file(path, &meta, version).display() { + Ok(()) => (), + Err(e) => return e.into(), + } + } + } + + Ok(ParsedFile::Plain) => match inspect_plain_file(path).display() { + Ok(()) => (), + Err(e) => return e.into(), + }, + + Err(e) => return *e, + } + + EnkryptitOutput::phantom() +} diff --git a/eck/src/treatment/inspect/report/display.rs b/eck/src/treatment/inspect/report/display.rs new file mode 100644 index 0000000..23ee9c5 --- /dev/null +++ b/eck/src/treatment/inspect/report/display.rs @@ -0,0 +1,348 @@ +use crate::{ + diagnostic::{ + EnkryptitOutput, + report::{Report, argument::ReportArgument, style::EnkryptitStyle}, + }, + errors::EnkryptitError, + treatment::inspect::InspectionReport, + types::{CompressionType, KeyType, ParallelismType}, +}; +use fmodeparser::FullPermission; +use hex::ToHex; + +const NOT_FOUND: &str = "Not Found"; + +impl InspectionReport { + pub fn display(self) -> Result<(), EnkryptitError> { + match self { + Self::PlaintextFile { + name, + directory, + size, + permissions, + extension, + mime_extension, + predicted_compression_type, + predicted_parallelism_type, + } => display_plaintext_file( + name, + directory, + size, + permissions, + extension, + mime_extension, + predicted_compression_type, + predicted_parallelism_type, + ), + Self::EncryptedFile { + name, + directory, + size, + version, + compression_type, + predicted_parallelism_type, + keytype, + nonce, + } => display_encrypted_file( + name, + directory, + size, + version, + compression_type, + predicted_parallelism_type, + keytype, + nonce, + ), + Self::EncryptedArchive { + name, + directory, + size, + version, + entries_number, + keytype, + } => display_encrypted_archive(name, directory, size, version, entries_number, keytype), + Self::Folder { + name, + directory, + size, + permissions, + } => display_plaintext_folder(name, directory, size, permissions), + } + Ok(()) + } +} + +#[allow(clippy::too_many_arguments)] +fn display_plaintext_file( + name: Option, + directory: Option, + size: Option, + permissions: Option, + extension: Option, + mime_extension: Option, + ct: Option, + pt: Option, +) { + // If everything is `None`, then we return that an error occured. + if name.is_none() + && directory.is_none() + && size.is_none() + && permissions.is_none() + && extension.is_none() + && mime_extension.is_none() + && ct.is_none() + && pt.is_none() + { + error_while_inspecting(); + return; + } + + let mut report = Report::new(ReportArgument::title("Inspection Report")) + .field( + ReportArgument::label("Type"), + ReportArgument::new( + "Plaintext File (Not encrypted with Enkryptit!)", + EnkryptitStyle::Orange, + ), + ) + .field( + ReportArgument::label("Name"), + ReportArgument::value(format_option(name)), + ) + .field( + ReportArgument::label("Directory"), + ReportArgument::value(format_option(directory)), + ) + .field( + ReportArgument::label("Size"), + ReportArgument::value_in_kib(format_option_to_kb(size)), + ); + + if let Some(perm) = permissions { + match FullPermission::new(perm) { + Ok(fp) => { + report = report.field( + ReportArgument::label("Permissions"), + ReportArgument::value(fp.to_string()), + ); + } + Err(e) => { + let err: EnkryptitError = e.into(); + err.into_output().display(); + } + }; + } + + report + .field( + ReportArgument::label("Shown extension"), + ReportArgument::value(format_option(extension)), + ) + .field( + ReportArgument::label("Real extension"), + ReportArgument::accent(format_option(mime_extension)), + ) + .field( + ReportArgument::label("Recommanded compression type (for encryption)"), + ReportArgument::accent(format_option(ct)), + ) + .field( + ReportArgument::label("Recommanded parallelism type (for encryption)"), + ReportArgument::accent(format_option(pt)), + ) + .display(); +} + +#[allow(clippy::too_many_arguments)] +fn display_encrypted_file( + name: Option, + directory: Option, + size: Option, + version: u8, + ct: Option, + pt: Option, + keytype: Option, + nonce: Option<[u8; 24]>, +) { + // If everything is `None`, then we return that an error occured. + if name.is_none() + && directory.is_none() + && size.is_none() + && keytype.is_none() + && nonce.is_none() + && ct.is_none() + && pt.is_none() + { + error_while_inspecting(); + return; + } + + Report::new(ReportArgument::title("Inspection Report")) + .field( + ReportArgument::label("Type"), + ReportArgument::value("File encrypted with Enkryptit!"), + ) + .field( + ReportArgument::label("Version"), + ReportArgument::accent(version.to_string()), + ) + .field( + ReportArgument::label("Name"), + ReportArgument::value(format_option(name)), + ) + .field( + ReportArgument::label("Directory"), + ReportArgument::value(format_option(directory)), + ) + .field( + ReportArgument::label("Size"), + ReportArgument::value_in_kib(format_option_to_kb(size)), + ) + .field( + ReportArgument::label("Compression type"), + ReportArgument::accent(format_option(ct)), + ) + .field( + ReportArgument::label("Recommended parallelism type (for decrypting)"), + ReportArgument::accent(format_option(pt)), + ) + .field( + ReportArgument::label("Key Type"), + ReportArgument::accent(format_option(keytype)), + ) + .field( + ReportArgument::label("Nonce"), + ReportArgument::accent(format_nonce(nonce)), + ) + .display(); +} + +fn display_encrypted_archive( + name: Option, + directory: Option, + size: Option, + version: u8, + entries_number: Option, + keytype: Option, +) { + // If everything is `None`, then we return that an error occured. + if name.is_none() + && directory.is_none() + && size.is_none() + && keytype.is_none() + && entries_number.is_none() + { + error_while_inspecting(); + return; + } + + Report::new(ReportArgument::title("Inspection Report")) + .field( + ReportArgument::label("Type"), + ReportArgument::value("Folder (Archive) encrypted with Enkryptit!"), + ) + .field( + ReportArgument::label("Version"), + ReportArgument::accent(version.to_string()), + ) + .field( + ReportArgument::label("Name"), + ReportArgument::value(format_option(name)), + ) + .field( + ReportArgument::label("Directory"), + ReportArgument::value(format_option(directory)), + ) + .field( + ReportArgument::label("Size"), + ReportArgument::value_in_kib(format_option_to_kb(size)), + ) + .field( + ReportArgument::label("Entries number"), + ReportArgument::accent(format_option(entries_number)), + ) + .field( + ReportArgument::label("Key Type"), + ReportArgument::accent(format_option(keytype)), + ) + .display(); +} + +fn display_plaintext_folder( + name: Option, + directory: Option, + size: Option, + permissions: Option, +) { + // If everything is `None`, then we return that an error occured. + if name.is_none() && directory.is_none() && size.is_none() && permissions.is_none() { + error_while_inspecting(); + return; + } + + let mut report = Report::new(ReportArgument::title("Inspection Report")) + .field( + ReportArgument::label("Type"), + ReportArgument::value("Plaintext Folder (Not encrypted with Enkryptit!)"), + ) + .field( + ReportArgument::label("Name"), + ReportArgument::value(format_option(name)), + ) + .field( + ReportArgument::label("Directory"), + ReportArgument::value(format_option(directory)), + ) + .field( + ReportArgument::label("Size"), + ReportArgument::value_in_kib(format_option_to_kb(size)), + ); + + if let Some(perm) = permissions { + match FullPermission::new(perm) { + Ok(fp) => { + report = report.field( + ReportArgument::label("Permissions"), + ReportArgument::value(fp.to_string()), + ); + } + Err(e) => { + let err: EnkryptitError = e.into(); + err.into_output().display(); + } + }; + } + + report.display(); +} + +fn to_kb(size: u64) -> u64 { + size / 1024 +} + +fn error_while_inspecting() { + EnkryptitOutput::error("A severe error occured while inspecting the file.", EnkryptitError::InspectionError) + .with_help("Please ensure that the file exists, and that its name does not contain wrong unicode character.") + .with_location("inspect/mod.rs") + .display(); +} + +fn format_option(value: Option) -> String { + value + .map(|v| v.to_string()) + .unwrap_or_else(|| NOT_FOUND.to_string()) +} + +fn format_option_to_kb(value: Option) -> String { + match value { + Some(value) => to_kb(value).to_string(), + None => NOT_FOUND.to_string(), + } +} + +fn format_nonce(value: Option<[u8; 24]>) -> String { + match value { + Some(value) => value.encode_hex::(), + None => NOT_FOUND.to_string(), + } +} diff --git a/eck/src/treatment/inspect/report/mod.rs b/eck/src/treatment/inspect/report/mod.rs new file mode 100644 index 0000000..3a6fc3f --- /dev/null +++ b/eck/src/treatment/inspect/report/mod.rs @@ -0,0 +1,42 @@ +use crate::types::{CompressionType, KeyType, ParallelismType, Version}; +mod display; + +pub enum InspectionReport { + PlaintextFile { + name: Option, + directory: Option, + size: Option, + permissions: Option, + extension: Option, + mime_extension: Option, + predicted_compression_type: Option, + predicted_parallelism_type: Option, + }, + + Folder { + name: Option, + directory: Option, + size: Option, + permissions: Option, + }, + + EncryptedFile { + name: Option, + directory: Option, + size: Option, + version: Version, + compression_type: Option, + predicted_parallelism_type: Option, + keytype: Option, + nonce: Option<[u8; 24]>, + }, + + EncryptedArchive { + name: Option, + directory: Option, + size: Option, + version: Version, + entries_number: Option, + keytype: Option, + }, +} diff --git a/src/treatment/mod.rs b/eck/src/treatment/mod.rs similarity index 53% rename from src/treatment/mod.rs rename to eck/src/treatment/mod.rs index d7af54c..88a5129 100644 --- a/src/treatment/mod.rs +++ b/eck/src/treatment/mod.rs @@ -1,4 +1,4 @@ mod file_case; -/// Module for treating an object (file or folder) mod folder_case; +pub mod inspect; pub mod object_treatment; diff --git a/src/treatment/object_treatment.rs b/eck/src/treatment/object_treatment.rs similarity index 55% rename from src/treatment/object_treatment.rs rename to eck/src/treatment/object_treatment.rs index 6481c90..6b2bf11 100644 --- a/src/treatment/object_treatment.rs +++ b/eck/src/treatment/object_treatment.rs @@ -1,20 +1,19 @@ -use std::fs::File; -use std::path::Path; - -use crate::cli::Output; +use crate::context::EnkryptitContext; +use crate::diagnostic::EnkryptitOutput; use crate::errors::EnkryptitError; use crate::metadatas::{ArchiveHeader, MAGIC}; use crate::parameters::params::EnkryptitParams; use crate::treatment::file_case::{decrypt_file_case, encrypt_file_case}; use crate::treatment::folder_case::{decrypt_folder_case, encrypt_folder_case}; -use crate::types::KeyType::{self}; use postcard::from_bytes; +use std::fs::File; use std::io::{BufReader, Read, Seek, SeekFrom}; +use std::path::Path; #[allow(dead_code)] /// ParsedFile enum. The result of a file parsing. If the file is encrypted with **Enkryptit!**, we return : /// - Enkryptit { metadata, version, payload_offset, a boolean that indicates if it is a file or a folder } -/// \ +/// /// Else, we return : /// - Plain pub enum ParsedFile { @@ -33,32 +32,32 @@ pub enum ParsedFile { /// Else, we parse the file, and `match` the `ParsedFile` result. pub fn treat_object( parameters: &EnkryptitParams, - path: String, - key: [u8; 32], - key_type: KeyType, -) -> Result { - if Path::new(&path).is_dir() { - return encrypt_folder_case(&path, parameters, key, key_type); + path: &str, + context: &mut EnkryptitContext, +) -> EnkryptitOutput { + let keytype = parameters.key_params.to_type(); + + if Path::new(path).is_dir() { + return encrypt_folder_case(path, context, &keytype); } - match read_file(&path) { + match read_file(path) { Ok(ParsedFile::Enkryptit { meta, payload_offset, is_folder_archive, version, - .. }) => { if is_folder_archive { - decrypt_folder_case(&path, key, meta, payload_offset, key_type, version) + decrypt_folder_case(path, context, meta, payload_offset, version) } else { - decrypt_file_case(&path, meta, key, key_type, payload_offset) + decrypt_file_case(path, meta, context, payload_offset) } } - Ok(ParsedFile::Plain) => encrypt_file_case(parameters, &path, key, key_type), + Ok(ParsedFile::Plain) => encrypt_file_case(path, context, &keytype), - Err(_) => Ok(Output::CorruptedFile), + Err(e) => *e, } } @@ -71,9 +70,23 @@ pub fn treat_object( /// - Compare the Magic number /// - Reads the metadata (two different ways : at the beginning of the file if the version is 1, at the end if the version is 2) /// - Returns the ParsedFile result -fn read_file(path: &str) -> Result { - let file = File::open(path)?; - let file_len = file.metadata()?.len(); +pub fn read_file(path: &str) -> Result> { + let file = match File::open(path) { + Ok(f) => f, + Err(e) => { + let err: EnkryptitError = e.into(); + return Err(Box::new(err.into())); + } + }; + + let file_len = match file.metadata() { + Ok(m) => m.len(), + Err(e) => { + let err: EnkryptitError = e.into(); + return Err(Box::new(err.into())); + } + }; + let mut reader = BufReader::new(file); // Try to read the header length @@ -108,17 +121,49 @@ fn read_file(path: &str) -> Result { if archive_header.is_folder_archive && archive_header.version >= 2 { // v2 folder archive: metadata is at the end of the file if file_len < meta_len as u64 { - return Err(EnkryptitError::CorruptedFile); + return Err(Box::new( + EnkryptitOutput::error( + "Error while reading & parsing the file.", + EnkryptitError::CorruptedFile, + ) + .with_help( + "File may be corrupted. For more informations, please refeer to the doc. To try to fix it, wait for `eck recover `.", + ) + .with_location("object_treatment.rs::read_file()"), + )); } let meta_start = file_len - meta_len as u64; - reader.seek(SeekFrom::Start(meta_start))?; + + if let Err(e) = reader.seek(SeekFrom::Start(meta_start)) { + let err: EnkryptitError = e.into(); + return Err(Box::new(err.into())); + } + if reader.read_exact(&mut meta).is_err() { - return Err(EnkryptitError::CorruptedFile); + return Err(Box::new( + EnkryptitOutput::error( + "Error while reading & parsing the file.", + EnkryptitError::CorruptedFile, + ) + .with_help( + "File may be corrupted. For more informations, please refeer to the doc. To try to fix it, wait for `eck recover `.", + ) + .with_location("object_treatment.rs::read_file()"), + )); } } else { // v1 or single-file: metadata is right after header if reader.read_exact(&mut meta).is_err() { - return Err(EnkryptitError::CorruptedFile); + return Err(Box::new( + EnkryptitOutput::error( + "Error while reading & parsing the file.", + EnkryptitError::CorruptedFile, + ) + .with_help( + "File may be corrupted. For more informations, please refeer to the doc. To try to fix it, wait for `eck recover `.", + ) + .with_location("object_treatment.rs::read_file()"), + )); } } diff --git a/eck/src/types/compression_type.rs b/eck/src/types/compression_type.rs new file mode 100644 index 0000000..f9664a8 --- /dev/null +++ b/eck/src/types/compression_type.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +/// CompressionType enum. Contains : +/// - Lz4 +/// - Zstd +/// - Xz +/// - NoComp +/// - Auto (infered by Enkryptit itself !) +pub enum CompressionType { + Lz4, + Zstd, + Xz, + NoComp, + Auto, +} + +impl CompressionType { + /// Description of the compression type. Single source of + /// truth used by both `Display` and the `String` conversion. + pub fn description(&self) -> String { + match self { + Self::Auto => "Automatic".to_string(), + Self::Lz4 => "Lz4 (fastest)".to_string(), + Self::Xz => "Xz (slowest but most efficient)".to_string(), + Self::NoComp => "No compression".to_string(), + Self::Zstd => "Zstd (Balanced)".to_string(), + } + } +} + +impl From for String { + fn from(value: CompressionType) -> Self { + value.description() + } +} + +impl Display for CompressionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.description()) + } +} diff --git a/eck/src/types/key_params.rs b/eck/src/types/key_params.rs new file mode 100644 index 0000000..51749a7 --- /dev/null +++ b/eck/src/types/key_params.rs @@ -0,0 +1,27 @@ +use crate::types::key_type::KeyType; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// Key Parameters, used **before** encryption / decryption, that describes user's preference to *Enkryptit!*'s backend, and stored in +/// the parameters. +/// +/// Contains : +/// - PassWord +/// - File +/// - Os +pub enum KeyParams { + PassWord, + File, + Os, +} + +impl KeyParams { + /// Converts a [`KeyParams`] into a [`KeyType`] + pub fn to_type(&self) -> KeyType { + match self { + Self::File => KeyType::FromFile, + Self::Os => KeyType::FromOS, + Self::PassWord => KeyType::Password, + } + } +} diff --git a/eck/src/types/key_type.rs b/eck/src/types/key_type.rs new file mode 100644 index 0000000..9487c72 --- /dev/null +++ b/eck/src/types/key_type.rs @@ -0,0 +1,49 @@ +use hex::ToHex; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// KeyType enum. Contains : +/// - Password +/// - Pwd256(salt) +/// - FromFile +/// - FromOS +/// - None +pub enum KeyType { + Password, + Pwd256([u8; 16]), + FromFile, + FromOS, + None, +} + +impl KeyType { + /// Description of the key type. Single source of truth used + /// by both `Display` and the `String` conversion. + pub fn description(&self) -> String { + match self { + Self::FromFile => "from file".to_string(), + Self::FromOS => "from os keyring".to_string(), + Self::None => { + "no keytype used (should not happen if the file is encrypted)".to_string() + } + Self::Password => "password".to_string(), + Self::Pwd256(salt) => format!( + "hashed password, with the following salt : {}", + salt.encode_hex::() + ), + } + } +} + +impl From for String { + fn from(value: KeyType) -> Self { + value.description() + } +} + +impl Display for KeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.description()) + } +} diff --git a/eck/src/types/mod.rs b/eck/src/types/mod.rs new file mode 100644 index 0000000..1012783 --- /dev/null +++ b/eck/src/types/mod.rs @@ -0,0 +1,23 @@ +mod compression_type; +mod key_params; +mod key_type; +mod parallelism_type; + +pub use compression_type::CompressionType; +pub use key_params::KeyParams; +/// Public exports +pub use key_type::KeyType; +pub use parallelism_type::ParallelismType; + +/// Version type +pub type Version = u8; + +/// Chunk size of a bloc to compress & encrypt +pub const CHUNK_SIZE: usize = 8 * 1024 * 1024; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Encryption mode, used when resolving password in [`EnkryptitContext`](crate::context::EnkryptitContext) +pub enum Mode { + Encrypting, + Decrypting, +} diff --git a/eck/src/types/parallelism_type.rs b/eck/src/types/parallelism_type.rs new file mode 100644 index 0000000..8d8f17a --- /dev/null +++ b/eck/src/types/parallelism_type.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ParallelismType { + Auto, + MultiThread(u8), + Single, +} + +impl ParallelismType { + /// Description of the parallelism type. Single source of + /// truth used by both `Display` and the `String` conversion. + pub fn description(&self) -> String { + match self { + Self::Auto => "Automatic".to_string(), + Self::MultiThread(n) => format!("MultiThreading with {} threads", n), + Self::Single => "SingleThread".to_string(), + } + } +} + +impl From for String { + fn from(value: ParallelismType) -> Self { + value.description() + } +} + +impl Display for ParallelismType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.description()) + } +} diff --git a/test/README.md b/eck/test/README.md similarity index 97% rename from test/README.md rename to eck/test/README.md index a3814e1..654e22a 100644 --- a/test/README.md +++ b/eck/test/README.md @@ -65,4 +65,4 @@ Always hold the guard for the whole test, and always run the binary via ## Known Issues -None — the suite is expected to be fully green (89 passed, 1 ignored). +None — the suite is expected to be fully green. diff --git a/eck/test/integration/cli_tests.rs b/eck/test/integration/cli_tests.rs new file mode 100644 index 0000000..9581824 --- /dev/null +++ b/eck/test/integration/cli_tests.rs @@ -0,0 +1,714 @@ +//! CLI Command Parsing Tests +//! +//! Test command-line interface behavior: arguments, subcommands, error handling + +use crate::mocks::helpers::{TestConfigGuard, encrypted_path_for}; +use assert_cmd::assert::OutputAssertExt; +use assert_cmd::cargo::CommandCargoExt; +use std::fs; +use std::process::{Command, Stdio}; +use tempfile::{NamedTempFile, TempDir}; + +/// Build a `eck` command pointing at the freshly built binary (never PATH). +fn eck_cmd(args: &[&str]) -> Command { + let mut cmd = Command::cargo_bin("eck").unwrap(); + cmd.args(args); + cmd +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cli_no_args_launches_tui() { + // Test that running 'eck' without args launches TUI (will timeout) + let mut child = Command::cargo_bin("eck") + .unwrap() + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start eck"); + + // Kill the process after a short delay (TUI waits for input) + std::thread::sleep(std::time::Duration::from_millis(100)); + + let _ = child.kill(); + let _ = child.wait(); + } + + #[test] + fn cli_ui_subcommand_launches_tui() { + // Test that 'eck ui' explicitly launches TUI + let mut child = Command::cargo_bin("eck") + .unwrap() + .arg("ui") + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start eck ui"); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + let _ = child.kill(); + let _ = child.wait(); + } + + #[test] + /// Test that tests if the `eck params` command actually prints the parameters. + fn cli_params_command_shows_config() { + let _guard = TestConfigGuard::new("PassWord", "Zstd"); + + let output = eck_cmd(&["params"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Actual parameters"), "stdout: {stdout}"); + assert!(stdout.contains("Key Type : PassWord"), "stdout: {stdout}"); + assert!(stdout.contains("Compression : Auto"), "stdout: {stdout}"); + assert!(stdout.contains("Parallelism : Auto"), "stdout: {stdout}"); + } + + // -- Compression type settings tests + + #[test] + /// Tests changing the compression type to `zstd` + fn cli_params_set_compression_zstd() { + let guard = TestConfigGuard::new("PassWord", "NoComp"); + + let output = eck_cmd(&["params", "--compression", "zstd"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + // The change must be persisted to the isolated config file + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Zstd\""), "config: {saved}"); + + // And a subsequent read reflects it + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compression : Zstd"), "stdout: {stdout}"); + } + + #[test] + /// Tests changing the compression type to `lz4` + fn cli_params_set_compression_lz4() { + let guard = TestConfigGuard::new("PassWord", "NoComp"); + + let output = eck_cmd(&["params", "--compression", "lz4"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + // The change must be persisted to the isolated config file + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Lz4\""), "config: {saved}"); + + // And a subsequent read reflects it + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compression : Lz4"), "stdout: {stdout}"); + } + + #[test] + /// Tests changing the compression type to `xz` + fn cli_params_set_compression_xz() { + let guard = TestConfigGuard::new("PassWord", "NoComp"); + + let output = eck_cmd(&["params", "--compression", "xz"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed"), + "stdout: {stdout}" + ); + + // The change must be persisted to the isolated config file + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Xz\""), "config: {saved}"); + + // And a subsequent read reflects it + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compression : Xz"), "stdout: {stdout}"); + } + + #[test] + /// Tests setting the compression type back to `auto` (DAY-10 inference). + fn cli_params_set_compression_auto() { + let guard = TestConfigGuard::new("PassWord", "NoComp"); + + let output = eck_cmd(&["params", "--compression", "auto"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Auto\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compression : Auto"), "stdout: {stdout}"); + } + + // --- KeyType settings tests + + #[test] + /// Tests changing the keytype to `os` + fn cli_params_set_keytype_os() { + let guard = TestConfigGuard::new("PassWord", "Zstd"); + + let output = eck_cmd(&["params", "--keytype", "os"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Os\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Key Type : Os"), "stdout: {stdout}"); + } + + #[test] + /// Tests changing the keytype to `file` + fn cli_params_set_keytype_file() { + let guard = TestConfigGuard::new("PassWord", "Zstd"); + + let output = eck_cmd(&["params", "--keytype", "file"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"File\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Key Type : File"), "stdout: {stdout}"); + } + + #[test] + /// Tests changing the keytype to `os` + fn cli_params_set_keytype_pwd() { + let guard = TestConfigGuard::new("PassWord", "Zstd"); + + let output = eck_cmd(&["params", "--keytype", "pwd"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"PassWord\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Key Type : PassWord"), "stdout: {stdout}"); + } + + #[test] + /// Tests changing the keytype to `password` + fn cli_params_set_keytype_password() { + let guard = TestConfigGuard::new("PassWord", "Zstd"); + + let output = eck_cmd(&["params", "--keytype", "password"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"PassWord\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Key Type : PassWord"), "stdout: {stdout}"); + } + + // --- Encryption / decryption tests + + #[test] + /// Tests encrypting & decrypting a `file` with a password (-p flag). + fn cli_encrypt_file_with_password_flag() { + let _guard = TestConfigGuard::new("PassWord", "NoComp"); + let temp_file = NamedTempFile::new().unwrap(); + + fs::write(temp_file.path(), b"Test content for CLI encryption.").unwrap(); + + let encrypted_path = encrypted_path_for(temp_file.path()); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(temp_file.path()) + .assert() + .success(); + assert!( + encrypted_path.exists(), + "archive should exist after encrypt" + ); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(&encrypted_path) + .assert() + .success(); + + let restored = fs::read_to_string(temp_file.path()).unwrap(); + assert_eq!(restored, "Test content for CLI encryption."); + } + + #[test] + /// Tests encrypting & decrypting a folder + fn cli_folder_encrypt_decrypt_roundtrip() { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("cli_folder"); + fs::create_dir_all(folder.join("nested")).unwrap(); + fs::write(folder.join("a.txt"), b"alpha content").unwrap(); + fs::write(folder.join("nested/b.txt"), b"beta content").unwrap(); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("fold3r") + .arg(&folder) + .assert() + .success(); + + let archive_path = encrypted_path_for(&folder); + assert!(archive_path.exists(), "folder archive should exist"); + + fs::remove_dir_all(&folder).unwrap(); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("fold3r") + .arg(&archive_path) + .assert() + .success(); + + assert_eq!( + fs::read_to_string(folder.join("a.txt")).unwrap(), + "alpha content" + ); + assert_eq!( + fs::read_to_string(folder.join("nested/b.txt")).unwrap(), + "beta content" + ); + } + + #[test] + /// Tests encrypting & decrypting a folder and a file with the same command + fn cli_folders_and_files_encrypt_decrypt_roundtrip() { + let tmp = TempDir::new().unwrap(); + let temp_file = NamedTempFile::new().unwrap(); + + let folder = tmp.path().join("cli_folder"); + fs::write(temp_file.path(), b"Test content for CLI encryption.").unwrap(); + + fs::create_dir_all(folder.join("nested")).unwrap(); + fs::write(folder.join("a.txt"), b"alpha content").unwrap(); + fs::write(folder.join("nested/b.txt"), b"beta content").unwrap(); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(temp_file.path()) + .arg(&folder) + .assert() + .success(); + + let archive_path = encrypted_path_for(&folder); + let file_path = encrypted_path_for(temp_file.path()); + + assert!(archive_path.exists(), "folder archive should exist"); + assert!(file_path.exists(), "file should exist"); + + fs::remove_dir_all(&folder).unwrap(); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(archive_path) + .arg(file_path) + .assert() + .success(); + + assert_eq!( + fs::read_to_string(folder.join("a.txt")).unwrap(), + "alpha content" + ); + assert_eq!( + fs::read_to_string(folder.join("nested/b.txt")).unwrap(), + "beta content" + ); + + let restored = fs::read_to_string(temp_file.path()).unwrap(); + assert_eq!(restored, "Test content for CLI encryption."); + } + + // --- Parameters config tests + + #[test] + /// Tests that a corrupted config file (`config.json`) automatically fallsback to default config. + fn corrupt_config_falls_back_to_defaults() { + let _guard = TestConfigGuard::with_raw_content("{ definitely :: not json ]"); + + let output = eck_cmd(&["params"]).output().unwrap(); + + assert!( + output.status.success(), + "corrupt config must degrade to defaults, stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Key Type : PassWord"), "stdout: {stdout}"); + assert!(stdout.contains("Compression : Auto"), "stdout: {stdout}"); + assert!(stdout.contains("Parallelism : Auto"), "stdout: {stdout}"); + } + + #[test] + /// Tests that repeatingly changing parameters doesn't corrupt the file. + fn repeated_param_updates_keep_config_valid_json() { + let guard = TestConfigGuard::new("PassWord", "NoComp"); + + for algo in ["lz4", "xz", "zstd"] { + let output = eck_cmd(&["params", "--compression", algo]) + .output() + .unwrap(); + assert!(output.status.success()); + } + + let saved = fs::read_to_string(guard.path()).unwrap(); + let parsed: serde_json::Value = + serde_json::from_str(&saved).expect("config must remain parseable after updates"); + assert_eq!(parsed["compression"], "Zstd"); + assert_eq!(parsed["key_params"], "PassWord"); + } + + // --- Cli (more tests) + + #[test] + fn cli_unknown_argument_fails() { + let output = eck_cmd(&["--this-does-not-exist"]).output().unwrap(); + + assert!(!output.status.success()); + } + + // --- Parallelism settings tests + + #[test] + fn cli_params_set_parallelism_single() { + use serde_json::json; + let guard = TestConfigGuard::with_parallelism("PassWord", "NoComp", json!("Single")); + + let output = eck_cmd(&["params", "--parallelism", "single"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Single\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Parallelism : Single"), "stdout: {stdout}"); + } + + #[test] + fn cli_params_set_parallelism_multi() { + use serde_json::json; + let guard = TestConfigGuard::with_parallelism("PassWord", "NoComp", json!("Single")); + + let output = eck_cmd(&["params", "--parallelism", "multi:6"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"MultiThread\": 6"), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("MultiThread(6)"), "stdout: {stdout}"); + } + + #[test] + fn cli_params_set_parallelism_zero_threads_fails() { + use serde_json::json; + let guard = TestConfigGuard::with_parallelism("PassWord", "NoComp", json!("Single")); + + let output = eck_cmd(&["params", "--parallelism", "multi:0"]) + .output() + .unwrap(); + + assert!(!output.status.success()); + // Config must remain untouched on failure + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Single\""), "config: {saved}"); + } + + #[test] + /// Tests switching parallelism back to `auto` (DAY-10 inference). + fn cli_params_set_parallelism_auto() { + use serde_json::json; + let guard = TestConfigGuard::with_parallelism("PassWord", "NoComp", json!("Single")); + + let output = eck_cmd(&["params", "--parallelism", "auto"]) + .output() + .expect("Failed to execute eck params"); + + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Parameters were changed !"), + "stdout: {stdout}" + ); + + let saved = fs::read_to_string(guard.path()).unwrap(); + assert!(saved.contains("\"Auto\""), "config: {saved}"); + + let output = eck_cmd(&["params"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Parallelism : Auto"), "stdout: {stdout}"); + } + + #[test] + fn cli_multithread_encrypt_decrypt_roundtrip() { + use serde_json::json; + + let _guard = + TestConfigGuard::with_parallelism("PassWord", "NoComp", json!({ "MultiThread": 4 })); + let temp_file = NamedTempFile::new().unwrap(); + + fs::write(temp_file.path(), b"multithread cli roundtrip content").unwrap(); + + let encrypted_path = encrypted_path_for(temp_file.path()); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(temp_file.path()) + .assert() + .success(); + assert!( + encrypted_path.exists(), + "archive should exist after encrypt" + ); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(&encrypted_path) + .assert() + .success(); + + let restored = fs::read_to_string(temp_file.path()).unwrap(); + assert_eq!(restored, "multithread cli roundtrip content"); + } + + #[test] + fn cli_multithread_encrypt_decrypt_large_file_roundtrip() { + use serde_json::json; + + let _guard = + TestConfigGuard::with_parallelism("PassWord", "Zstd", json!({ "MultiThread": 8 })); + let temp_file = NamedTempFile::new().unwrap(); + + // Large enough to span several chunks across the worker pool. + let content = vec![b'q'; 3 * 1024 * 1024]; + fs::write(temp_file.path(), &content).unwrap(); + + let encrypted_path = encrypted_path_for(temp_file.path()); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(temp_file.path()) + .assert() + .success(); + assert!(encrypted_path.exists()); + + Command::cargo_bin("eck") + .unwrap() + .arg("-p") + .arg("test123") + .arg(&encrypted_path) + .assert() + .success(); + + let restored = fs::read(temp_file.path()).unwrap(); + assert_eq!(restored, content); + } + + // --- miette source-frame display tests --- + + #[test] + fn cli_inspect_missing_path_draws_source_frame() { + let missing = "/definitely/missing/path.encky"; + let output = eck_cmd(&["inspect", missing]) + .output() + .expect("Failed to execute eck inspect"); + + assert!(output.status.success(), "inspect must exit cleanly"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains(missing), + "path must appear in source line: {stdout}" + ); + assert!( + stdout.contains("╰──"), + "curved pointer must appear: {stdout}" + ); + assert!( + stdout.contains("help:"), + "help footer must appear: {stdout}" + ); + } + + #[test] + fn cli_default_command_missing_path_draws_source_frame() { + let _guard = TestConfigGuard::new("PassWord", "Zstd"); + let missing = "/definitely/missing/path.encky"; + let output = eck_cmd(&[missing]) + .output() + .expect("Failed to execute eck "); + + assert!(output.status.success(), "missing path must not hard-fail"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains(missing), + "path must appear in source line: {stdout}" + ); + assert!( + stdout.contains("╰──"), + "curved pointer must appear: {stdout}" + ); + assert!( + stdout.contains("help:"), + "help footer must appear: {stdout}" + ); + } +} diff --git a/eck/test/integration/compression_tests.rs b/eck/test/integration/compression_tests.rs new file mode 100644 index 0000000..d79ead9 --- /dev/null +++ b/eck/test/integration/compression_tests.rs @@ -0,0 +1,176 @@ +//! Context / compression-inference tests +//! +//! Exercises `EnkryptitContext::resolve_compression` with `CompressionType::Auto`, +//! which drives the mime + size based inference in `context::compression`. +//! +//! Files are detected by their magic bytes (via the `infer` crate), and the +//! chosen `CompressionType` also depends on the file size, so we use *sparse* +//! files (`set_len`) to cheaply simulate multi-GiB files without writing them +//! to disk. + +use eck::context::EnkryptitContext; +use eck::types::CompressionType; + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + const PNG: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0DIHDR"; + const ZIP: &[u8] = b"PK\x03\x04"; + /// RIFF....WAVE (12 bytes) — a raw/uncompressed PCM container. + const WAV: &[u8] = b"RIFF\x00\x00\x00\x00WAVE"; + const XML: &[u8] = b""; + /// Bytes that match no `infer` signature -> unknown type. + const UNKNOWN: &[u8] = b"\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9"; + + /// Resolve compression for a real file through the public context API. + fn auto(dir: &TempDir, name: &str, bytes: &[u8]) -> CompressionType { + let path = dir.path().join(name); + std::fs::write(&path, bytes).unwrap(); + let ctx = EnkryptitContext::new( + None, + CompressionType::Auto, + eck::types::ParallelismType::Auto, + ); + ctx.resolve_compression(path.to_str().unwrap()) + .expect("auto inference must succeed") + } + + /// Build a *sparse* file: `head` is the real (detectable) leading bytes, + /// then the file is logically extended to `len` bytes. Cheap even for huge + /// sizes, because the trailing region is never written to disk. + fn auto_sparse(dir: &TempDir, name: &str, head: &[u8], len: u64) -> CompressionType { + let path = dir.path().join(name); + let mut f = std::fs::File::create(&path).unwrap(); + f.write_all(head).unwrap(); + f.set_len(len).unwrap(); + let ctx = EnkryptitContext::new( + None, + CompressionType::Auto, + eck::types::ParallelismType::Auto, + ); + ctx.resolve_compression(path.to_str().unwrap()) + .expect("auto inference must succeed") + } + + /// Mime classification (small files, below LOW_BOUNDARY) + + #[test] + fn png_is_already_compressed() { + let dir = TempDir::new().unwrap(); + assert_eq!(auto(&dir, "img.png", PNG), CompressionType::NoComp); + } + + #[test] + fn zip_archive_is_already_compressed() { + let dir = TempDir::new().unwrap(); + assert_eq!(auto(&dir, "bundle.zip", ZIP), CompressionType::NoComp); + } + + #[test] + fn raw_wav_is_compressible_small() { + let dir = TempDir::new().unwrap(); + // Compressible + small (< 50 MiB) -> fast path. + assert_eq!(auto(&dir, "sound.wav", WAV), CompressionType::Lz4); + } + + #[test] + fn text_xml_is_highly_compressible_small() { + let dir = TempDir::new().unwrap(); + // HighlyCompressible + small (< 50 MiB) -> Zstd. + assert_eq!(auto(&dir, "doc.xml", XML), CompressionType::Zstd); + } + + #[test] + fn unknown_file_small_is_nocomp() { + let dir = TempDir::new().unwrap(); + // Unknown + small (< 50 MiB) -> NoComp. + assert_eq!(auto(&dir, "blob.bin", UNKNOWN), CompressionType::NoComp); + } + + // --- Size-based ramp for the Compressible hint (raw WAV) --- + const LOW: u64 = 50 << 20; // 50 MiB + const MID_INF: u64 = 250 << 20; // 250 MiB + const MID_SUP: u64 = 1 << 30; // 1 GiB + const SUP: u64 = 5 << 30; // 5 GiB + + #[test] + fn compressible_just_below_low_boundary_uses_lz4() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "w1.wav", WAV, LOW - 1), + CompressionType::Lz4 + ); + } + + #[test] + fn compressible_above_low_boundary_uses_zstd() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "w2.wav", WAV, LOW + 1), + CompressionType::Zstd + ); + } + + #[test] + fn compressible_above_mid_inferior_uses_xz() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "w3.wav", WAV, MID_INF + 1), + CompressionType::Xz + ); + } + + #[test] + fn compressible_above_mid_superior_uses_zstd() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "w4.wav", WAV, MID_SUP + 1), + CompressionType::Zstd + ); + } + + #[test] + fn compressible_above_superior_uses_lz4() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "w5.wav", WAV, SUP + 1), + CompressionType::Lz4 + ); + } + + /// HighlyCompressible size ramp (< 50 MiB Zstd, then Xz, then Zstd) + + #[test] + fn highly_compressible_mid_range_uses_xz() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "d.xml", XML, LOW + 1), + CompressionType::Xz + ); + } + + #[test] + fn highly_compressible_very_large_uses_zstd() { + let dir = TempDir::new().unwrap(); + assert_eq!( + auto_sparse(&dir, "d2.xml", XML, SUP + 1), + CompressionType::Zstd + ); + } + + /// Non-Auto resolution bypasses inference entirely + + #[test] + fn explicit_compression_short_circuits_inference() { + let ctx = + EnkryptitContext::new(None, CompressionType::Xz, eck::types::ParallelismType::Auto); + // Path never touched: non-Auto must not hit the filesystem. + let resolved = ctx + .resolve_compression("/nonexistent/does/not/exist") + .unwrap(); + assert_eq!(resolved, CompressionType::Xz); + } +} diff --git a/eck/test/integration/encrypt_chunk_job.rs b/eck/test/integration/encrypt_chunk_job.rs new file mode 100644 index 0000000..7fe73fc --- /dev/null +++ b/eck/test/integration/encrypt_chunk_job.rs @@ -0,0 +1,252 @@ +//! Multithreaded File Encryption Integration Tests +//! +//! Exercises `encrypt_file` / `decrypt_file` with `ParallelismType::MultiThread` +//! for a variety of compression algorithms, thread counts, and file sizes, and +//! verifies interoperability between the single-threaded and multithreaded code +//! paths (a file encrypted on one path decrypts on the other). + +use std::fs; +use std::io::{BufReader, Read}; + +use eck::context::EnkryptitContext; +use eck::encryption::file_encryption::{decrypt_file, encrypt_file}; +use eck::metadatas::ArchiveHeader; +use eck::parameters::params::EnkryptitParams; +use eck::types::{CompressionType, KeyParams, KeyType, ParallelismType}; +use postcard::from_bytes; +use tempfile::NamedTempFile; + +const KEY_FROM_FILE: KeyType = KeyType::FromFile; + +fn _params_multi(compression: CompressionType, threads: u8) -> EnkryptitParams { + EnkryptitParams { + compression, + key_params: KeyParams::File, + parallelism: ParallelismType::MultiThread(threads), + } +} + +/// Reads the header + metadata of a single-file `.encky` archive and returns +/// the deserialized metadata plus the byte offset at which the encrypted payload +/// starts (`1 + header_len + meta_len`). +fn read_file_archive_meta(archive_path: &str) -> (Vec, u64) { + let file = fs::File::open(archive_path).unwrap(); + let mut reader = BufReader::new(file); + + let mut len_buf = [0u8; 1]; + reader.read_exact(&mut len_buf).unwrap(); + let header_len = len_buf[0] as usize; + + let mut header_bytes = vec![0u8; header_len]; + reader.read_exact(&mut header_bytes).unwrap(); + let header: ArchiveHeader = from_bytes(&header_bytes).unwrap(); + + let meta_len = header.meta_len as usize; + let mut meta = vec![0u8; meta_len]; + reader.read_exact(&mut meta).unwrap(); + + let payload_offset = (1 + header_len as u64 + meta_len as u64) as u64; + (meta, payload_offset) +} + +/// Round-trips `content` through `encrypt_file`/`decrypt_file` using the +/// parallelism selected during both phases. Asserts the restored bytes equal +/// the originals. +fn assert_roundtrip( + content: &[u8], + enc_parallelism: ParallelismType, + dec_parallelism: ParallelismType, + compression: CompressionType, +) { + let tmp_file = NamedTempFile::new().unwrap(); + fs::write(tmp_file.path(), content).unwrap(); + + let enc_ctx = &mut EnkryptitContext::new(None, compression, enc_parallelism); + + let archive_path = encrypt_file(tmp_file.path().to_str().unwrap(), &KEY_FROM_FILE, enc_ctx) + .expect("multithread encryption must succeed"); + + let (meta, payload_offset) = read_file_archive_meta(&archive_path); + + let dec_ctx = &mut EnkryptitContext::new(None, compression, dec_parallelism); + decrypt_file(&archive_path, &meta, payload_offset, dec_ctx).expect("decryption must succeed"); + + let restored = fs::read(tmp_file.path()).unwrap(); + assert_eq!(restored, content, "roundtrip must restore original bytes"); +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---- Same-mode roundtrips (encrypt & decrypt with the same parallelism) ---- + + #[test] + fn multithread_no_comp_roundtrip() { + assert_roundtrip( + b"multithread nocomp content", + ParallelismType::MultiThread(4), + ParallelismType::MultiThread(4), + CompressionType::NoComp, + ); + } + + #[test] + fn multithread_zstd_roundtrip() { + assert_roundtrip( + b"multithread zstd content", + ParallelismType::MultiThread(4), + ParallelismType::MultiThread(4), + CompressionType::Zstd, + ); + } + + #[test] + fn multithread_lz4_roundtrip() { + assert_roundtrip( + b"multithread lz4 content", + ParallelismType::MultiThread(4), + ParallelismType::MultiThread(4), + CompressionType::Lz4, + ); + } + + #[test] + fn multithread_xz_roundtrip() { + assert_roundtrip( + b"multithread xz content", + ParallelismType::MultiThread(4), + ParallelismType::MultiThread(4), + CompressionType::Xz, + ); + } + + // ---- Single worker (degenerate pool) must still work ---- + + #[test] + fn multithread_with_single_worker_roundtrip() { + assert_roundtrip( + b"single worker multithread path", + ParallelismType::MultiThread(1), + ParallelismType::MultiThread(1), + CompressionType::Zstd, + ); + } + + // ---- Worker-count interoperability (multithread -> multithread) ---- + + #[test] + fn encrypt_multithread_decrypt_with_different_thread_counts() { + // Different thread counts across the two phases should interoperate. + assert_roundtrip( + b"different thread counts", + ParallelismType::MultiThread(2), + ParallelismType::MultiThread(8), + CompressionType::Lz4, + ); + } + + // ---- Cross-mode interoperability (single <-> multithread) ---- + // Since both the single-threaded and multithreaded paths write the same + // on-disk format (each chunk length-prefixed, `ENK1END` written at the end), + // files must be freely interchangeable between the two. + + #[test] + fn encrypt_multithread_decrypt_single() { + for compression in [ + CompressionType::NoComp, + CompressionType::Zstd, + CompressionType::Lz4, + CompressionType::Xz, + ] { + assert_roundtrip( + b"encrypt multithread, decrypt single", + ParallelismType::MultiThread(4), + ParallelismType::Single, + compression, + ); + } + } + + #[test] + fn encrypt_single_decrypt_multithread() { + for compression in [ + CompressionType::NoComp, + CompressionType::Zstd, + CompressionType::Lz4, + CompressionType::Xz, + ] { + assert_roundtrip( + b"encrypt single, decrypt multithread", + ParallelismType::Single, + ParallelismType::MultiThread(4), + compression, + ); + } + } + + #[test] + fn encrypt_single_decrypt_multithread_large_file() { + // A large (multi-chunk) file encrypted single-threaded must decrypt + // correctly through the multithreaded worker pool. + let large = vec![0xFAu8; 3 * eck::types::CHUNK_SIZE]; + assert_roundtrip( + &large, + ParallelismType::Single, + ParallelismType::MultiThread(4), + CompressionType::NoComp, + ); + } + + // ---- Large multi-chunk files ---- + + #[test] + fn multithread_large_file_roundtrip() { + // Large enough to span several CHUNK_SIZE blocks so multiple workers + // process distinct chunks. + let large = vec![0xCDu8; 3 * eck::types::CHUNK_SIZE]; + assert_roundtrip( + &large, + ParallelismType::MultiThread(4), + ParallelismType::MultiThread(4), + CompressionType::NoComp, + ); + } + + #[test] + fn multithread_file_just_over_chunk_size_roundtrip() { + let content = vec![0xEEu8; eck::types::CHUNK_SIZE + 1]; + assert_roundtrip( + &content, + ParallelismType::MultiThread(4), + ParallelismType::MultiThread(4), + CompressionType::Zstd, + ); + } + + #[test] + fn multithread_many_small_chunks_roundtrip() { + // Highly compressible content that nonetheless produces many chunks. + let pattern: Vec = (0..1024u32).map(|i| (i % 251) as u8).collect(); + let content = pattern.repeat(eck::types::CHUNK_SIZE / 1024 * 3); + assert_roundtrip( + &content, + ParallelismType::MultiThread(8), + ParallelismType::MultiThread(8), + CompressionType::Xz, + ); + } + + // ---- Error handling ---- + + #[test] + fn encrypt_multithread_nonexistent_file_errors() { + let ctx = &mut EnkryptitContext::new( + None, + CompressionType::NoComp, + ParallelismType::MultiThread(4), + ); + let result = encrypt_file("/this/path/does/not/exist.txt", &KEY_FROM_FILE, ctx); + assert!(result.is_err()); + } +} diff --git a/test/integration/encryption_flow.rs b/eck/test/integration/encryption_flow.rs similarity index 91% rename from test/integration/encryption_flow.rs rename to eck/test/integration/encryption_flow.rs index 6f35588..e4a159d 100644 --- a/test/integration/encryption_flow.rs +++ b/eck/test/integration/encryption_flow.rs @@ -29,18 +29,11 @@ fn run_cli_decrypt( path: &std::path::Path, password: Option<&str>, ) -> Result> { - let mut cmd = Command::cargo_bin("eck")?; - - if let Some(pwd) = password { - cmd.arg("-p").arg(pwd); - } - cmd.arg(path); - - Ok(cmd.assert()) + run_cli_encrypt(path, password) } -fn stderr_of(assertion: &Assert) -> String { - String::from_utf8_lossy(&assertion.get_output().stderr).into_owned() +fn stdout_of(assertion: &Assert) -> String { + String::from_utf8_lossy(&assertion.get_output().stdout).into_owned() } #[cfg(test)] @@ -210,9 +203,12 @@ mod tests { let wrong = run_cli_decrypt(encrypted_path.as_ref(), Some("wrong-pass")).unwrap(); - // Failure is reported on stderr (exit code stays 0 by design) - let stderr = stderr_of(&wrong); - assert!(stderr.contains("[ERROR]"), "stderr: {stderr}"); + // Failure is reported as a miette diagnostic on stdout (exit code stays 0 by design) + let stdout = stdout_of(&wrong); + assert!( + stdout.contains("crypto::encryption_decryption_failed"), + "stdout: {stdout}" + ); // The archive must be left untouched and no plaintext restored assert!(encrypted_path.exists()); @@ -237,17 +233,18 @@ mod tests { let encrypted_path = encrypted_path_for(original.path()); let mut archive = fs::read(&encrypted_path).unwrap(); - // Flip a byte near the end of the payload (inside the final AEAD chunk) - let last = archive.len() - 2; + // Flip a byte just before the trailing ENK1END magic so the tamper hits a real + // AEAD ciphertext/tag byte instead of the unprotected magic. + let last = archive.len() - 8; archive[last] ^= 0xFF; let tampered_archive = archive.clone(); fs::write(&encrypted_path, &tampered_archive).unwrap(); let result = run_cli_decrypt(encrypted_path.as_ref(), Some("tamper-test")).unwrap(); - let stderr = stderr_of(&result); + let stdout = stdout_of(&result); assert!( - !stderr.is_empty(), + stdout.contains("crypto::encryption_decryption_failed"), "tampering must produce an error message" ); diff --git a/eck/test/integration/folder_encryption.rs b/eck/test/integration/folder_encryption.rs new file mode 100644 index 0000000..881750f --- /dev/null +++ b/eck/test/integration/folder_encryption.rs @@ -0,0 +1,360 @@ +#[cfg(test)] +mod tests { + use eck::context::EnkryptitContext; + use eck::encryption::folder_encryption::entry::collect_entries_from_folder::collect_folder_entries; + use eck::encryption::folder_encryption::{decrypt_folder, encrypt_folder}; + use eck::metadatas::{ArchiveHeader, FolderMetadata}; + use eck::parameters::params::EnkryptitParams; + use eck::types::{CompressionType, KeyParams, KeyType}; + use postcard::from_bytes; + use std::fs; + use std::io::{BufReader, Read, Seek, SeekFrom}; + use tempfile::TempDir; + + fn test_params_single(compression: CompressionType) -> EnkryptitParams { + EnkryptitParams { + compression, + key_params: KeyParams::File, + parallelism: eck::types::ParallelismType::Single, + } + } + + fn test_params_multi(compression: CompressionType) -> EnkryptitParams { + EnkryptitParams { + compression, + key_params: KeyParams::File, + parallelism: eck::types::ParallelismType::MultiThread(8), + } + } + + fn read_archive_meta(archive_path: &str) -> (u8, Vec) { + let file = std::fs::File::open(archive_path).unwrap(); + let mut reader = BufReader::new(file); + let mut len_buf = [0u8; 1]; + reader.read_exact(&mut len_buf).unwrap(); + let header_len = len_buf[0] as usize; + let mut header_bytes = vec![0u8; header_len]; + reader.read_exact(&mut header_bytes).unwrap(); + let header: ArchiveHeader = from_bytes(&header_bytes).unwrap(); + let meta_len = header.meta_len as usize; + + let file_len = std::fs::metadata(archive_path).unwrap().len(); + let meta_start = file_len - meta_len as u64; + reader.seek(SeekFrom::Start(meta_start)).unwrap(); + let mut meta = vec![0u8; meta_len]; + reader.read_exact(&mut meta).unwrap(); + (header.version, meta) + } + + #[test] + fn encrypt_decrypt_single_file_folder_nocomp_singlethread() { + encrypt_decrypt_single_file_folder_nocomp(test_params_single(CompressionType::NoComp)); + } + + #[test] + fn encrypt_decrypt_single_file_folder_nocomp_multithread() { + encrypt_decrypt_single_file_folder_nocomp(test_params_multi(CompressionType::NoComp)); + } + + fn encrypt_decrypt_single_file_folder_nocomp(params: EnkryptitParams) { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("testfolder"); + fs::create_dir(&folder).unwrap(); + fs::write(folder.join("hello.txt"), b"Hello from single file!").unwrap(); + + let mut context = EnkryptitContext::new(None, params.compression, params.parallelism); + let archive_path = + encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile).unwrap(); + assert!(std::path::Path::new(&archive_path).exists()); + + let (version, meta_bytes) = read_archive_meta(&archive_path); + assert_eq!(version, 3); + let folder_meta: FolderMetadata = from_bytes(&meta_bytes).unwrap(); + assert_eq!(folder_meta.entries.len(), 1); + assert_eq!(folder_meta.entries[0].relative_path, "hello.txt"); + assert!(folder_meta.entries[0].offset > 0); + + fs::remove_dir_all(&folder).unwrap(); + let dest = decrypt_folder(&archive_path, &meta_bytes, 0, version, &mut context).unwrap(); + + let decrypted = fs::read_to_string(std::path::Path::new(&dest).join("hello.txt")).unwrap(); + assert_eq!(decrypted, "Hello from single file!"); + } + + #[test] + fn encrypt_decrypt_multiple_file_folder_nocomp_singlethread() { + encrypt_decrypt_multi_file_folder_nocomp(test_params_single(CompressionType::NoComp)); + } + + #[test] + fn encrypt_decrypt_multiple_file_folder_nocomp_multithread() { + encrypt_decrypt_multi_file_folder_nocomp(test_params_multi(CompressionType::NoComp)); + } + + fn encrypt_decrypt_multi_file_folder_nocomp(params: EnkryptitParams) { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("multifolder"); + fs::create_dir_all(folder.join("subdir")).unwrap(); + fs::write(folder.join("file1.txt"), b"Content of file 1").unwrap(); + fs::write(folder.join("subdir/file2.txt"), b"Content of file 2").unwrap(); + fs::write(folder.join("subdir/file3.txt"), b"Third file content here").unwrap(); + let mut context = EnkryptitContext::new(None, params.compression, params.parallelism); + + let archive_path = + encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile).unwrap(); + + let (version, meta_bytes) = read_archive_meta(&archive_path); + let folder_meta: FolderMetadata = from_bytes(&meta_bytes).unwrap(); + assert_eq!(folder_meta.entries.len(), 3); + + let mut offsets: Vec = folder_meta.entries.iter().map(|e| e.offset).collect(); + let sorted_offsets = offsets.clone(); + offsets.sort(); + assert_eq!(offsets, sorted_offsets); + + fs::remove_dir_all(&folder).unwrap(); + let dest = decrypt_folder(&archive_path, &meta_bytes, 0, version, &mut context).unwrap(); + + let base = std::path::Path::new(&dest); + assert_eq!( + fs::read_to_string(base.join("file1.txt")).unwrap(), + "Content of file 1" + ); + assert_eq!( + fs::read_to_string(base.join("subdir/file2.txt")).unwrap(), + "Content of file 2" + ); + assert_eq!( + fs::read_to_string(base.join("subdir/file3.txt")).unwrap(), + "Third file content here" + ); + } + + #[test] + fn encrypt_decrypt_zstd_roundtrip_singlethread() { + encrypt_decrypt_zstd_roundtrip(test_params_single(CompressionType::Zstd)); + } + + #[test] + fn encrypt_decrypt_zstd_roundtrip_multithread() { + encrypt_decrypt_zstd_roundtrip(test_params_multi(CompressionType::Zstd)); + } + + fn encrypt_decrypt_zstd_roundtrip(params: EnkryptitParams) { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("zstdfolder"); + fs::create_dir(&folder).unwrap(); + fs::write(folder.join("a.txt"), b"Zstd compressed content").unwrap(); + fs::write(folder.join("b.bin"), vec![0xAB; 1024 * 100]).unwrap(); + let mut context = EnkryptitContext::new(None, params.compression, params.parallelism); + let archive_path = + encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile).unwrap(); + + let (version, meta_bytes) = read_archive_meta(&archive_path); + + fs::remove_dir_all(&folder).unwrap(); + let dest = decrypt_folder(&archive_path, &meta_bytes, 0, version, &mut context).unwrap(); + + let base = std::path::Path::new(&dest); + assert_eq!( + fs::read_to_string(base.join("a.txt")).unwrap(), + "Zstd compressed content" + ); + assert_eq!( + fs::read(base.join("b.bin")).unwrap(), + vec![0xAB; 1024 * 100] + ); + } + + #[test] + fn encrypt_decrypt_lz4_roundtrip_singlethread() { + encrypt_decrypt_lz4_roundtrip(test_params_single(CompressionType::Lz4)); + } + + #[test] + fn encrypt_decrypt_lz4_roundtrip_multithread() { + encrypt_decrypt_lz4_roundtrip(test_params_multi(CompressionType::Lz4)); + } + + fn encrypt_decrypt_lz4_roundtrip(params: EnkryptitParams) { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("lz4folder"); + fs::create_dir(&folder).unwrap(); + fs::write(folder.join("data.bin"), vec![0x42; 50_000]).unwrap(); + let mut context = EnkryptitContext::new(None, params.compression, params.parallelism); + let archive_path = + encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile).unwrap(); + + let (version, meta_bytes) = read_archive_meta(&archive_path); + + fs::remove_dir_all(&folder).unwrap(); + let dest = decrypt_folder(&archive_path, &meta_bytes, 0, version, &mut context).unwrap(); + + assert_eq!( + fs::read(std::path::Path::new(&dest).join("data.bin")).unwrap(), + vec![0x42; 50_000] + ); + } + + #[test] + fn encrypt_decrypt_xz_roundtrip_singlethread() { + encrypt_decrypt_xz_roundtrip(test_params_single(CompressionType::Xz)); + } + + #[test] + fn encrypt_decrypt_xz_roundtrip_multithread() { + encrypt_decrypt_xz_roundtrip(test_params_multi(CompressionType::Xz)); + } + + fn encrypt_decrypt_xz_roundtrip(params: EnkryptitParams) { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("xzfolder"); + fs::create_dir(&folder).unwrap(); + fs::write(folder.join("readme.txt"), b"XZ compression test content").unwrap(); + let mut context = EnkryptitContext::new(None, params.compression, params.parallelism); + + let archive_path = + encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile).unwrap(); + + let (version, meta_bytes) = read_archive_meta(&archive_path); + + fs::remove_dir_all(&folder).unwrap(); + let dest = decrypt_folder(&archive_path, &meta_bytes, 0, version, &mut context).unwrap(); + + assert_eq!( + fs::read_to_string(std::path::Path::new(&dest).join("readme.txt")).unwrap(), + "XZ compression test content" + ); + } + + #[test] + fn empty_folder_returns_error() { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("empty"); + fs::create_dir(&folder).unwrap(); + let mut context = EnkryptitContext::new( + None, + CompressionType::NoComp, + eck::types::ParallelismType::Auto, + ); + + let result = encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile); + assert!(result.is_err()); + } + + #[test] + fn collect_entries_finds_nested_files() { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("walk"); + fs::create_dir_all(folder.join("a/b/c")).unwrap(); + fs::write(folder.join("root.txt"), b"r").unwrap(); + fs::write(folder.join("a/mid.txt"), b"m").unwrap(); + fs::write(folder.join("a/b/c/deep.txt"), b"d").unwrap(); + let context = &EnkryptitContext { + password: None, + compression_type: CompressionType::Auto, + parallelism: eck::types::ParallelismType::Auto, + }; + + let entries = collect_folder_entries(folder.to_str().unwrap(), context).unwrap(); + assert_eq!(entries.len(), 3); + + let mut paths: Vec<&str> = entries.iter().map(|e| e.relative_path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["a/b/c/deep.txt", "a/mid.txt", "root.txt"]); + } + + // --- Day-10/11 regression: Auto + Auto end-to-end roundtrip --- + + const PNG_BYTES: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0DIHDR"; + const WAV_BYTES: &[u8] = b"RIFF\x00\x00\x00\x00WAVE"; + const XML_BYTES: &[u8] = b""; + + #[test] + fn encrypt_decrypt_auto_folder_mixed_content_roundtrip() { + let tmp = TempDir::new().unwrap(); + let folder = tmp.path().join("autofolder"); + fs::create_dir(&folder).unwrap(); + fs::write(folder.join("doc.xml"), XML_BYTES).unwrap(); + fs::write(folder.join("sound.wav"), WAV_BYTES).unwrap(); + fs::write(folder.join("img.png"), PNG_BYTES).unwrap(); + + // >= 50 MiB -> Auto parallelism resolves to MultiThread on encryption. + // The ENK1END (+7) bytes of this entry feed the offsets of the following + // ones, so a single off-by-7 would break the whole archive. + let large_size: u64 = 55 * 1024 * 1024; + let large_path = folder.join("large_sparse.bin"); + // All-zero sparse file with no detectable magic: inference = Unknown, + // 55 MiB -> Lz4 compression + MultiThread parallelism. + fs::File::create(&large_path) + .unwrap() + .set_len(large_size) + .unwrap(); + + let mut context = EnkryptitContext::new( + None, + CompressionType::Auto, + eck::types::ParallelismType::Auto, + ); + + let archive_path = + encrypt_folder(folder.to_str().unwrap(), &mut context, &KeyType::FromFile).unwrap(); + + let (version, meta_bytes) = read_archive_meta(&archive_path); + let folder_meta: FolderMetadata = from_bytes(&meta_bytes).unwrap(); + assert_eq!(folder_meta.entries.len(), 4); + + let mut paths: Vec<&str> = folder_meta + .entries + .iter() + .map(|e| e.relative_path.as_str()) + .collect(); + paths.sort_unstable(); + assert_eq!( + paths, + vec!["doc.xml", "img.png", "large_sparse.bin", "sound.wav"] + ); + + // Every entry must have a pinned (non-Auto) compression, identical to + // the context inference. This guards the historical bug where a raw + // `Auto` ended up stored in the metadata and panicked in `decompress`. + let mut min_offset = u64::MAX; + for entry in &folder_meta.entries { + assert_ne!( + entry.compression, + CompressionType::Auto, + "Auto must be resolved before storage: {} (regression guard)", + entry.relative_path + ); + let full = folder.join(&entry.relative_path); + let expected = context.resolve_compression(full.to_str().unwrap()).unwrap(); + assert_eq!( + entry.compression, expected, + "stored compression must match inference for {}", + entry.relative_path + ); + assert_eq!(entry.file_nonce.len(), 24); + min_offset = min_offset.min(entry.offset); + } + // Data starts right after the fixed-size header region (1 + 64). + assert_eq!( + min_offset, 65, + "first data entry must start after the header" + ); + + fs::remove_dir_all(&folder).unwrap(); + let dest = decrypt_folder(&archive_path, &meta_bytes, 0, version, &mut context).unwrap(); + let base = std::path::Path::new(&dest); + + assert_eq!(fs::read(base.join("doc.xml")).unwrap(), XML_BYTES); + assert_eq!(fs::read(base.join("sound.wav")).unwrap(), WAV_BYTES); + assert_eq!(fs::read(base.join("img.png")).unwrap(), PNG_BYTES); + + let restored_large = fs::read(base.join("large_sparse.bin")).unwrap(); + assert_eq!(restored_large.len(), large_size as usize); + assert!( + restored_large.iter().all(|&b| b == 0), + "sparse file must be restored byte-exact" + ); + } +} diff --git a/test/integration/mod.rs b/eck/test/integration/mod.rs similarity index 69% rename from test/integration/mod.rs rename to eck/test/integration/mod.rs index d6e3a17..85fd2d4 100644 --- a/test/integration/mod.rs +++ b/eck/test/integration/mod.rs @@ -3,5 +3,9 @@ //! Test complete encryption/decryption workflows including CLI interaction pub mod cli_tests; +pub mod compression_tests; +pub mod encrypt_chunk_job; pub mod encryption_flow; pub mod folder_encryption; +pub mod tui_flow; +pub mod tui_tests; diff --git a/eck/test/integration/tui_flow.rs b/eck/test/integration/tui_flow.rs new file mode 100644 index 0000000..e9dfa56 --- /dev/null +++ b/eck/test/integration/tui_flow.rs @@ -0,0 +1,389 @@ +//! TUI / Browsing flow tests +//! +//! Exercises the interactive TUI logic (params menu, browse selection, object +//! treatment, main-menu dispatch) headlessly via `MockTuiInput`, including real +//! filesystem encrypt/decrypt roundtrips through the Tui treatment path. + +use std::fs; + +use eck::frontend::tui::browse::{ + browse_files, browse_files_then_folders, browse_folders, launch_browser, +}; +use eck::frontend::tui::launch_ui; +use eck::frontend::tui::parameters::launch_params; + +use crate::mocks::helpers::{TestConfigGuard, encrypted_path_for}; +use crate::mocks::tui_input::MockTuiInput; + +fn read_config(path: &std::path::Path) -> serde_json::Value { + let raw = fs::read_to_string(path).expect("read config"); + serde_json::from_str(&raw).expect("parse config") +} + +fn guard_password() -> TestConfigGuard { + TestConfigGuard::with_parallelism("PassWord", "Zstd", serde_json::json!("Single")) +} + +#[cfg(test)] +mod tests { + use super::*; + use eck::frontend::tui::treatment::treat_objects_encryption; + use tempfile::{NamedTempFile, tempdir}; + + /// Params menu logic + + #[test] + fn params_menu_changes_compression_and_preserves_parallelism() { + let guard = guard_password(); + + let input = MockTuiInput::new() + .with_select("Change compression type") + .with_select("Lz4 (fastest)") + .with_select("Back to main menu"); + + launch_params(&input).unwrap(); + + let cfg = read_config(guard.path()); + assert_eq!(cfg["compression"], "Lz4"); + assert_eq!(cfg["key_params"], "PassWord"); + assert_eq!(cfg["parallelism"], "Single"); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn params_menu_changes_key_type() { + let guard = guard_password(); + + let input = MockTuiInput::new() + .with_select("Change key type") + .with_select("OS Keyring") + .with_select("Back to main menu"); + + launch_params(&input).unwrap(); + + let cfg = read_config(guard.path()); + assert_eq!(cfg["key_params"], "Os"); + assert_eq!(cfg["compression"], "Zstd"); + assert_eq!(cfg["parallelism"], "Single"); + } + + #[test] + fn params_menu_sets_multithread_parallelism_with_count() { + let guard = guard_password(); + + let input = MockTuiInput::new() + .with_select("Change parallelism type") + .with_select("MultiThread") + .with_counter(6) + .with_select("Back to main menu"); + + launch_params(&input).unwrap(); + + let cfg = read_config(guard.path()); + assert_eq!(cfg["parallelism"], serde_json::json!({"MultiThread": 6})); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn params_menu_sets_single_parallelism_from_multithread() { + let guard = TestConfigGuard::with_parallelism( + "PassWord", + "Zstd", + serde_json::json!({"MultiThread": 8}), + ); + + let input = MockTuiInput::new() + .with_select("Change parallelism type") + .with_select("Single") + .with_select("Back to main menu"); + + launch_params(&input).unwrap(); + + let cfg = read_config(guard.path()); + assert_eq!(cfg["parallelism"], "Single"); + } + + /// Browse selection logic + + #[test] + fn browse_files_encrypts_the_chosen_file() { + let _guard = guard_password(); + let original = NamedTempFile::new().unwrap(); + fs::write(&original, b"browse me").unwrap(); + + let input = MockTuiInput::new() + .with_files(vec![original.path().to_str().unwrap()]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + + browse_files(&input, Some("pwd".into())).unwrap(); + + assert!( + encrypted_path_for(original.path()).exists(), + "chosen file should have been encrypted" + ); + assert_eq!(input.pending_files(), 0); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn browse_folders_encrypts_the_chosen_folder() { + let _guard = guard_password(); + let dir = tempdir().unwrap(); + fs::write(dir.path().join("inside.txt"), b"nested").unwrap(); + + let input = MockTuiInput::new() + .with_folders(vec![dir.path().to_str().unwrap()]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + + browse_folders(&input, Some("pwd".into())).unwrap(); + + // Folder archive is written next to the folder with `.encky` suffix. + let archive = format!("{}.encky", dir.path().display()); + assert!(std::path::Path::new(&archive).exists()); + assert_eq!(input.pending_folders(), 0); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn browse_both_merges_files_and_folders() { + let _guard = guard_password(); + let file = NamedTempFile::new().unwrap(); + fs::write(&file, b"file side").unwrap(); + let dir = tempdir().unwrap(); + fs::write(dir.path().join("inside.txt"), b"dir side").unwrap(); + + let input = MockTuiInput::new() + .with_files(vec![file.path().to_str().unwrap()]) + .with_folders(vec![dir.path().to_str().unwrap()]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + + browse_files_then_folders(&input, Some("pwd".into())).unwrap(); + + assert!(encrypted_path_for(file.path()).exists()); + let archive = format!("{}.encky", dir.path().display()); + assert!(std::path::Path::new(&archive).exists()); + assert_eq!(input.pending_files(), 0, "files pick should be consumed"); + assert_eq!( + input.pending_folders(), + 0, + "folders pick should be consumed" + ); + assert_eq!( + input.pending_selects(), + 0, + "treatment menu should be drained" + ); + } + + #[test] + fn browse_both_works_with_only_files() { + let _guard = guard_password(); + let file = NamedTempFile::new().unwrap(); + fs::write(&file, b"only file").unwrap(); + + let input = MockTuiInput::new() + .with_files(vec![file.path().to_str().unwrap()]) + .with_folders(vec![]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + + browse_files_then_folders(&input, Some("pwd".into())).unwrap(); + + assert!(encrypted_path_for(file.path()).exists()); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn launch_browser_exits_on_back_to_main_menu() { + let input = MockTuiInput::new().with_select("Back to main menu"); + + launch_browser(&input).unwrap(); + assert_eq!(input.pending_selects(), 0); + } + + /// TUI treatment path + roundtrips + + #[test] + fn browse_then_inspect_prints_report() { + let _guard = guard_password(); + let original = NamedTempFile::new().unwrap(); + fs::write(&original, b"inspect me").unwrap(); + + let input = MockTuiInput::new() + .with_files(vec![original.path().to_str().unwrap()]) + .with_select("Inspect") + .with_select("Go Back"); + + browse_files(&input, None).unwrap(); + + assert_eq!(input.pending_files(), 0, "file pick should be consumed"); + assert_eq!( + input.pending_selects(), + 0, + "treatment menu should be drained" + ); + assert!( + !encrypted_path_for(original.path()).exists(), + "inspection must not encrypt the object" + ); + } + + #[test] + fn browse_then_inspect_encrypted_archive() { + let _guard = guard_password(); + let original = NamedTempFile::new().unwrap(); + fs::write(&original, b"inspect the archive").unwrap(); + + let encrypt_input = MockTuiInput::new() + .with_files(vec![original.path().to_str().unwrap()]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + browse_files(&encrypt_input, Some("pwd".into())).unwrap(); + let encrypted = encrypted_path_for(original.path()); + assert!(encrypted.exists(), "setup: file should be encrypted first"); + + let inspect_input = MockTuiInput::new() + .with_files(vec![encrypted.to_str().unwrap()]) + .with_select("Inspect") + .with_select("Go Back"); + browse_files(&inspect_input, None).unwrap(); + + assert_eq!(inspect_input.pending_files(), 0); + assert_eq!(inspect_input.pending_selects(), 0); + } + + #[test] + fn launch_treatment_go_back_does_not_encrypt() { + let _guard = guard_password(); + let original = NamedTempFile::new().unwrap(); + fs::write(&original, b"no treatment").unwrap(); + + let input = MockTuiInput::new() + .with_files(vec![original.path().to_str().unwrap()]) + .with_select("Go Back"); + + browse_files(&input, Some("pwd".into())).unwrap(); + + assert!(!encrypted_path_for(original.path()).exists()); + assert_eq!(input.pending_files(), 0); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn tui_treatment_encrypts_then_decrypts_real_file() { + let _guard = guard_password(); + let original = NamedTempFile::new().unwrap(); + let content = b"round trip through the Tui treatment path"; + fs::write(&original, content).unwrap(); + + let plain = original.path().to_str().unwrap().to_string(); + let encrypted = encrypted_path_for(original.path()) + .to_str() + .unwrap() + .to_string(); + + // Encrypt: browse chooses the file, then the treatment menu runs it. + let enc = MockTuiInput::new() + .with_files(vec![plain.as_str()]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + browse_files(&enc, Some("pwd".into())).unwrap(); + assert!(std::path::Path::new(&encrypted).exists()); + assert_eq!(enc.pending_files(), 0); + assert_eq!(enc.pending_selects(), 0, "treatment menu should be drained"); + + // Decrypt back into the plain path (strips the .encky suffix) + let dec = MockTuiInput::new() + .with_files(vec![encrypted.as_str()]) + .with_select("Encrypt/Decrypt") + .with_select("Go Back"); + browse_files(&dec, Some("pwd".into())).unwrap(); + assert_eq!(dec.pending_selects(), 0); + + let restored = fs::read(original.path()).unwrap(); + assert_eq!(restored, content); + } + + #[test] + fn tui_treatment_roundtrip_across_compressions() { + for comp in ["Zstd", "Lz4", "Xz", "NoComp"] { + let guard = + TestConfigGuard::with_parallelism("PassWord", comp, serde_json::json!("Single")); + + let dir = tempdir().unwrap(); + let plain = dir.path().join("data.bin"); + fs::write(&plain, b"compression aware content").unwrap(); + let encrypted_path = format!("{}.encky", plain.display()); + let encrypted = std::path::PathBuf::from(&encrypted_path); + + let plain_s = plain.to_str().unwrap().to_string(); + let enc_s = encrypted.to_str().unwrap().to_string(); + let password = Some("pwd".into()); + + treat_objects_encryption(&vec![plain_s], &password).unwrap(); + assert!(encrypted.exists(), "encrypt with {comp}"); + + treat_objects_encryption(&vec![enc_s], &password).unwrap(); + assert_eq!(fs::read(&plain).unwrap(), b"compression aware content"); + drop(guard); + } + } + + /// Main menu dispatch loop + + #[test] + fn launch_ui_help_then_exit() { + let input = MockTuiInput::new().with_select("Help").with_select("Exit"); + + launch_ui(&input); + assert_eq!( + input.pending_selects(), + 0, + "menu should fully drain before exit" + ); + } + + #[test] + fn launch_ui_browse_then_exit() { + let input = MockTuiInput::new() + .with_select("Browse") + .with_select("Back to main menu") + .with_select("Exit"); + + launch_ui(&input); + assert_eq!(input.pending_selects(), 0); + } + + #[test] + fn launch_ui_browse_then_inspect() { + let _guard = guard_password(); + let original = NamedTempFile::new().unwrap(); + fs::write(&original, b"full ui flow").unwrap(); + let path = original.path().to_str().unwrap().to_string(); + + // Browse -> Browse Files -> pick -> Inspect exercises the whole menu + // stack through `launch_ui`. Encryption is not reachable here because + // `launch_browser` passes `password: None` down to the treatment menu + // (encryption with a Password key would open a headless `/dev/tty` + // prompt); it is covered directly in `tui_treatment_encrypts_then_decrypts_real_file`. + let input = MockTuiInput::new() + .with_select("Browse") + .with_select("Browse Files") + .with_files(vec![path.as_str()]) + .with_select("Inspect") + .with_select("Go Back") + .with_select("Back to main menu") + .with_select("Exit"); + + launch_ui(&input); + + assert!( + !encrypted_path_for(original.path()).exists(), + "inspection must not encrypt the object" + ); + assert_eq!(input.pending_selects(), 0, "whole menu stack should drain"); + } +} diff --git a/eck/test/integration/tui_tests.rs b/eck/test/integration/tui_tests.rs new file mode 100644 index 0000000..0ba1127 --- /dev/null +++ b/eck/test/integration/tui_tests.rs @@ -0,0 +1,97 @@ +//! Tui Tests +//! +//! Doesn't directly test the TUI itself, but tests the highest level possible +//! (`EnkryptitTuiAction`) using a `MockTuiInput` so no real terminal is needed. +//! Object treatment (encrypt/decrypt/inspect) is not reachable from the action +//! enum anymore: it happens inside `launch_treatment` (see `tui_flow.rs`). + +#[cfg(test)] +mod tests { + use crate::mocks::tui_input::MockTuiInput; + use eck::frontend::tui::action::EnkryptitTuiAction; + + #[test] + fn test_from_str_conversion_tui_action() { + let values = ["Parameters", "Help", "Browse"]; + + for value in values { + if EnkryptitTuiAction::from_string(value).is_none() { + panic!( + "All EnkryptitActions should be converted correctly. This one fails : {}", + value + ) + } + } + } + + #[test] + fn from_str_maps_all_actions() { + let cases = [ + ("Parameters", "LaunchParams"), + ("Help", "ShowHelp"), + ("Browse", "Browse"), + ]; + + for (input, expected) in cases { + let action = EnkryptitTuiAction::from_string(input); + + assert!(action.is_some(), "Expected action for {input}"); + + match (expected, action.unwrap()) { + ("LaunchParams", EnkryptitTuiAction::LaunchParams) + | ("ShowHelp", EnkryptitTuiAction::ShowHelp) + | ("Browse", EnkryptitTuiAction::Browse) => {} + _ => panic!("Wrong action for {input}"), + } + } + } + + #[test] + fn from_str_rejects_unknown_action() { + assert!(EnkryptitTuiAction::from_string("NotAnAction").is_none()); + assert!(EnkryptitTuiAction::from_string("").is_none()); + assert!(EnkryptitTuiAction::from_string("Exit").is_none()); + // The old direct-encryption action was removed in the DAY-15 frontend. + assert!(EnkryptitTuiAction::from_string("Encrypt/Decrypt file/folder").is_none()); + assert!(EnkryptitTuiAction::from_string("EncryptObject").is_none()); + } + + #[test] + fn show_help_action_succeeds() { + let action = EnkryptitTuiAction::ShowHelp; + let input = MockTuiInput::new(); + + assert!(action.execute(&input).is_ok()); + // ShowHelp is purely a print; it must not consume any interactive input. + assert_eq!(input.pending_selects(), 0); + assert_eq!(input.pending_texts(), 0); + } + + #[test] + fn launch_params_action_routes_to_params_menu() { + // LaunchParams enters its own loop, so queue a "Back to main menu" to exit. + let action = EnkryptitTuiAction::LaunchParams; + let input = MockTuiInput::new().with_select("Back to main menu"); + + assert!(action.execute(&input).is_ok()); + assert_eq!( + input.pending_selects(), + 0, + "params menu should consume the menu choice" + ); + } + + #[test] + fn browse_action_routes_to_browser_menu() { + // Browse enters its own loop, so queue a "Back to main menu" to exit. + let action = EnkryptitTuiAction::Browse; + let input = MockTuiInput::new().with_select("Back to main menu"); + + assert!(action.execute(&input).is_ok()); + assert_eq!( + input.pending_selects(), + 0, + "browser should consume the menu choice" + ); + } +} diff --git a/test/mocks/helpers.rs b/eck/test/mocks/helpers.rs similarity index 83% rename from test/mocks/helpers.rs rename to eck/test/mocks/helpers.rs index f5bb6d0..13fc2d9 100644 --- a/test/mocks/helpers.rs +++ b/eck/test/mocks/helpers.rs @@ -27,6 +27,23 @@ impl TestConfigGuard { Self::with_raw_content(&pretty) } + /// Creates a config guard explicitly setting the parallelism, using the + /// same externally-tagged representation as `serde` derives for the + /// `ParallelismType` enum (e.g. `"MultiThread": 4` or `"Single"`). + pub fn with_parallelism( + key_params: &str, + compression: &str, + parallelism: serde_json::Value, + ) -> Self { + let json = serde_json::json!({ + "key_params": key_params, + "compression": compression, + "parallelism": parallelism, + }); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + Self::with_raw_content(&pretty) + } + pub fn with_raw_content(content: &str) -> Self { let lock = CONFIG_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); @@ -65,9 +82,9 @@ pub fn encrypted_path_for(original: &Path) -> PathBuf { pub fn generate_random_content(size: usize) -> Vec { let mut content = vec![0u8; size]; - for i in 0..size { - // Simple deterministic pseudo-random generation for reproducibility - content[i] = ((i * 2654435781) % 256) as u8; + // Simple deterministic pseudo-random generation for reproducibility + for (i, byte) in content.iter_mut().enumerate() { + *byte = ((i * 2654435781) % 256) as u8; } content diff --git a/test/mocks/mod.rs b/eck/test/mocks/mod.rs similarity index 88% rename from test/mocks/mod.rs rename to eck/test/mocks/mod.rs index 04db127..a525134 100644 --- a/test/mocks/mod.rs +++ b/eck/test/mocks/mod.rs @@ -3,3 +3,4 @@ //! Helper modules for creating mock data, fakes, and reusable test fixtures (runtime-only) pub mod helpers; +pub mod tui_input; diff --git a/eck/test/mocks/tui_input.rs b/eck/test/mocks/tui_input.rs new file mode 100644 index 0000000..bb6b3cd --- /dev/null +++ b/eck/test/mocks/tui_input.rs @@ -0,0 +1,125 @@ +//! Mock implementation of `TuiInput` for testing interactive TUI logic headlessly. +//! +//! Instead of prompting on a real terminal / opening native dialogs, the mock +//! pops pre-queued responses from FIFO buffers. Each interactive read consumes +//! one queued value, which lets tests script a full menu / action sequence. + +use std::cell::RefCell; +use std::collections::VecDeque; + +use eck::errors::EnkryptitError; +use eck::frontend::tui::input::TuiInput; + +pub struct MockTuiInput { + selects: RefCell>, + texts: RefCell>, + counters: RefCell>, + file_picks: RefCell>>, + folder_picks: RefCell>>, +} + +impl Default for MockTuiInput { + fn default() -> Self { + Self::new() + } +} + +impl MockTuiInput { + pub fn new() -> Self { + Self { + selects: RefCell::new(VecDeque::new()), + texts: RefCell::new(VecDeque::new()), + counters: RefCell::new(VecDeque::new()), + file_picks: RefCell::new(VecDeque::new()), + folder_picks: RefCell::new(VecDeque::new()), + } + } + + /// Append a `select` menu response (the label that will be "chosen"). + pub fn with_select(mut self, choice: &str) -> Self { + self.selects.get_mut().push_back(choice.to_string()); + self + } + + /// Append a `text` prompt response. + pub fn with_text(mut self, value: &str) -> Self { + self.texts.get_mut().push_back(value.to_string()); + self + } + + /// Append a numeric (thread count) response. + pub fn with_counter(mut self, value: u8) -> Self { + self.counters.get_mut().push_back(value); + self + } + + /// Append a `pick_files` native-dialog result. + pub fn with_files(mut self, paths: Vec<&str>) -> Self { + self.file_picks + .get_mut() + .push_back(paths.into_iter().map(String::from).collect()); + self + } + + /// Append a `pick_folders` native-dialog result. + pub fn with_folders(mut self, paths: Vec<&str>) -> Self { + self.folder_picks + .get_mut() + .push_back(paths.into_iter().map(String::from).collect()); + self + } + + /// Number of `select` responses still queued (for dispatch assertions). + pub fn pending_selects(&self) -> usize { + self.selects.borrow().len() + } + + /// Number of `text` responses still queued. + pub fn pending_texts(&self) -> usize { + self.texts.borrow().len() + } + + /// Number of `pick_files` results still queued. + pub fn pending_files(&self) -> usize { + self.file_picks.borrow().len() + } + + /// Number of `pick_folders` results still queued. + pub fn pending_folders(&self) -> usize { + self.folder_picks.borrow().len() + } +} + +impl TuiInput for MockTuiInput { + fn select(&self, _message: &str, _choices: &[&str]) -> Result { + self.selects + .borrow_mut() + .pop_front() + .ok_or(EnkryptitError::CommandNotFound) + } + + fn text(&self, _message: &str, _help_message: &str) -> Result { + self.texts + .borrow_mut() + .pop_front() + .ok_or(EnkryptitError::CommandNotFound) + } + + fn custom_counter(&self, _message: &str) -> Result { + self.counters + .borrow_mut() + .pop_front() + .ok_or(EnkryptitError::CommandNotFound) + } + + fn pick_files(&self, _title: &str) -> Vec { + self.file_picks.borrow_mut().pop_front().unwrap_or_default() + } + + fn pick_folders(&self, _title: &str) -> Vec { + self.folder_picks + .borrow_mut() + .pop_front() + .unwrap_or_default() + } +} diff --git a/eck/test/mod.rs b/eck/test/mod.rs new file mode 100644 index 0000000..219f991 --- /dev/null +++ b/eck/test/mod.rs @@ -0,0 +1,8 @@ +//! Enkryptit Test Suite +//! +//! Comprehensive testing for file/folder encryption using XChaCha20-Poly1305 + Argon2id +//! Tests organized by category with explicit parallel execution support + +pub mod integration; +pub mod mocks; +pub mod unit; diff --git a/test/unit/compression.rs b/eck/test/unit/compression.rs similarity index 98% rename from test/unit/compression.rs rename to eck/test/unit/compression.rs index 0b98e63..f2708e3 100644 --- a/test/unit/compression.rs +++ b/eck/test/unit/compression.rs @@ -9,8 +9,8 @@ use eck::types::CompressionType; fn generate_random_content(size: usize) -> Vec { let mut content = vec![0u8; size]; - for i in 0..size { - content[i] = ((i * 2654435781) % 256) as u8; + for (i, byte) in content.iter_mut().enumerate() { + *byte = ((i * 2654435781) % 256) as u8; } content diff --git a/test/unit/encryption_primitives.rs b/eck/test/unit/encryption_primitives.rs similarity index 95% rename from test/unit/encryption_primitives.rs rename to eck/test/unit/encryption_primitives.rs index eaa67f0..ad748b9 100644 --- a/test/unit/encryption_primitives.rs +++ b/eck/test/unit/encryption_primitives.rs @@ -29,10 +29,10 @@ mod tests { let nonce_0a = eck::encryption::encryption_primitives::derive_nonce(&master, 0); let nonce_0b = eck::encryption::encryption_primitives::derive_nonce(&master, 0); - assert_eq!(nonce_0a, nonce_0b); // Same step → same nonce + assert_eq!(nonce_0a, nonce_0b); // Same step -> same nonce let nonce_1 = eck::encryption::encryption_primitives::derive_nonce(&master, 1); - assert_ne!(nonce_0a, nonce_1); // Different step → different nonce + assert_ne!(nonce_0a, nonce_1); // Different step -> different nonce } #[test] @@ -43,7 +43,7 @@ mod tests { let cipher = XChaCha20Poly1305::new(&key.into()); let master_nonce = generate_nonce(); - let mut data = vec![b'T'; 64].to_vec(); + let mut data = vec![b'T'; 64]; let original_len = data.len(); for step in 0..3 { @@ -129,7 +129,7 @@ mod tests { assert_eq!(&nonce_0[..16], &master[..16]); // First part unchanged let mut expected_step = [0u8; 8]; - 0u64.to_le_bytes().copy_from_slice(&mut expected_step); + expected_step.copy_from_slice(&0u64.to_le_bytes()); assert_eq!(&nonce_0[16..], &expected_step); } @@ -168,10 +168,10 @@ mod tests { fn key_derivation_deterministic() { let salt = [16u8; 16]; - let derived_key_1 = eck::keygen::derive_key("test_pwd", salt).unwrap(); - let derived_key_2 = eck::keygen::derive_key("test_pwd", salt).unwrap(); + let derived_key_1 = eck::key::derivation::derive_key("test_pwd", salt).unwrap(); + let derived_key_2 = eck::key::derivation::derive_key("test_pwd", salt).unwrap(); - assert_eq!(derived_key_1, derived_key_2); // Same pwd + same salt → same key + assert_eq!(derived_key_1, derived_key_2); // Same pwd + same salt -> same key } #[test] @@ -179,10 +179,10 @@ mod tests { let salt1 = [0x55u8; 16]; let salt2 = [0x66u8; 16]; // Different! - let key1 = eck::keygen::derive_key("password", salt1).unwrap(); - let key2 = eck::keygen::derive_key("password", salt2).unwrap(); + let key1 = eck::key::derivation::derive_key("password", salt1).unwrap(); + let key2 = eck::key::derivation::derive_key("password", salt2).unwrap(); - assert_ne!(key1, key2); // Same pwd, different salts → different keys + assert_ne!(key1, key2); // Same pwd, different salts -> different keys } #[test] @@ -268,7 +268,7 @@ mod tests { let mut bad_master = master_nonce; bad_master[23] ^= 0xFF; - // Decrypt with corrupted nonce should fail authentication (AEAD behavior: wrong nonce → invalid tag) + // Decrypt with corrupted nonce should fail authentication (AEAD behavior: wrong nonce -> invalid tag) assert!( eck::encryption::encryption_primitives::decrypt_chunk( &mut data, @@ -413,7 +413,7 @@ mod tests { let nonce_1_step0 = eck::encryption::encryption_primitives::derive_nonce(&master1, 0); let nonce_2_step0 = eck::encryption::encryption_primitives::derive_nonce(&master2, 0); - assert_ne!(nonce_1_step0, nonce_2_step0); // Different masters → different nonces + assert_ne!(nonce_1_step0, nonce_2_step0); // Different masters -> different nonces } #[test] @@ -440,7 +440,7 @@ mod tests { fn derive_nonce_with_zero_master() { let master = [0u8; 24]; // All zeros - for step in (0u64..=100u64).into_iter() { + for step in 0u64..=100u64 { let nonce = eck::encryption::encryption_primitives::derive_nonce(&master, step); assert_eq!(&nonce[..16], &master[..16]); // First part unchanged (bytes 0-15) @@ -461,7 +461,7 @@ mod tests { // Encrypt/decrypt with same step multiple times - should work each time for _ in 0..5 { - let mut data = vec![b'R'; 64].to_vec(); + let mut data = vec![b'R'; 64]; eck::encryption::encryption_primitives::encrypt_chunk( &mut data, diff --git a/eck/test/unit/error_display.rs b/eck/test/unit/error_display.rs new file mode 100644 index 0000000..137a920 --- /dev/null +++ b/eck/test/unit/error_display.rs @@ -0,0 +1,130 @@ +//! Regression tests for the miette source-frame error rendering. +//! +//! `render_error` is the pure, testable core of `display_error`. These tests +//! pin down the arrow/line/curve frame drawn when a `Snippet` is attached, and +//! the graceful compact fallback when there is none. + +use eck::diagnostic::output::{ + diagnostic::render_error, display::render_warning, snippet::Snippet, +}; +use eck::errors::EnkryptitError; + +fn io_error() -> EnkryptitError { + EnkryptitError::IoError(std::io::Error::from_raw_os_error(2)) +} + +/// Strip ANSI escape sequences (colors + OSC-8 hyperlinks) so assertions can +/// match on the visible characters regardless of styling. +fn strip_ansi(s: &str) -> String { + let mut out = String::new(); + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + if c == '\x1b' { + match chars.peek() { + // CSI: ESC [ ... m + Some('[') => { + while let Some(&n) = chars.peek() { + chars.next(); + if n == 'm' { + break; + } + } + } + // OSC: ESC ] ... ESC \ + Some(']') => { + while let Some(&n) = chars.peek() { + chars.next(); + if n == '\x1b' { + chars.next(); // backslash + break; + } + } + } + _ => {} + } + } else { + out.push(c); + } + } + out +} + +#[test] +fn render_error_draws_source_frame_with_snippet() { + let path = "/tmp/missing.encky"; + let rendered = render_error( + "io error: No such file or directory (os error 2)", + &io_error(), + &Some("I/O".to_string()), + &Some("Check that the path exists.".to_string()), + &Some(Snippet::cli_invocation("eck inspect", path)), + ); + let plain = strip_ansi(&rendered); + + assert!(plain.contains("╭─[eck inspect"), "frame header:\n{plain}"); + assert!( + plain.contains(&format!("eck inspect {path}")), + "source line:\n{plain}" + ); + assert!(plain.contains("╰──"), "curved pointer:\n{plain}"); + assert!(plain.contains("I/O"), "location as pointer label:\n{plain}"); + assert!( + plain.contains("help: Check that the path exists."), + "help footer:\n{plain}" + ); +} + +#[test] +fn render_error_without_snippet_stays_compact() { + let rendered = render_error( + "io error: gone", + &io_error(), + &Some("I/O".to_string()), + &Some("Check the path.".to_string()), + &None, + ); + let plain = strip_ansi(&rendered); + + assert!(plain.contains("✖ io error: gone"), "message:\n{plain}"); + assert!( + plain.contains("help: Check the path. (at I/O)"), + "help fold:\n{plain}" + ); + assert!(!plain.contains("╭─["), "no frame without snippet:\n{plain}"); + assert!( + !plain.contains("╰──"), + "no pointer without snippet:\n{plain}" + ); +} + +#[test] +fn render_error_without_help_or_location_is_still_viable() { + let rendered = render_error("boom", &io_error(), &None, &None, &None); + assert!(strip_ansi(&rendered).contains("✖ boom")); +} + +#[test] +fn render_warning_draws_the_warning_glyph() { + let rendered = strip_ansi(&render_warning("low entropy keyfile")); + + assert!( + rendered.contains("⚠ low entropy keyfile"), + "warning frame:\n{rendered}" + ); + assert!(!rendered.contains("✖"), "not an error frame:\n{rendered}"); +} + +#[test] +fn render_warning_keeps_the_message_word_for_word() { + let msg = "passphrase is shorter than 32 bytes"; + let rendered = strip_ansi(&render_warning(msg)); + + assert!(rendered.contains(msg), "message preserved:\n{rendered}"); +} + +#[test] +fn render_warning_with_empty_message_does_not_panic() { + let rendered = strip_ansi(&render_warning("")); + + assert!(!rendered.is_empty(), "miette still draws the ⚠ bare frame"); +} diff --git a/eck/test/unit/file.rs b/eck/test/unit/file.rs new file mode 100644 index 0000000..3af69a6 --- /dev/null +++ b/eck/test/unit/file.rs @@ -0,0 +1,63 @@ +//! `read_file` helper tests (DAY-12) +//! +//! Exercises `encryption::file::read_file` / `EnkryptitFile` : length probing +//! and the `estimated_steps` computation used by the multithreaded +//! folder/file intern archive encryption. + +use eck::encryption::file::read_file; +use eck::types::CHUNK_SIZE; +use std::fs; +use tempfile::NamedTempFile; + +const MIB: usize = 1024 * 1024; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_file_reports_len() { + let tmp = NamedTempFile::new().unwrap(); + fs::write(tmp.path(), vec![0xAB; 10 * MIB]).unwrap(); + + let file = read_file(tmp.path()).unwrap(); + assert_eq!(file.len, 10 * MIB as u64); + } + + #[test] + fn read_file_estimates_steps_by_chunk_size() { + let tmp = NamedTempFile::new().unwrap(); + // Two and a half chunks worth of content -> integer division. + fs::write(tmp.path(), vec![0u8; 2 * MIB]).unwrap(); + + let file = read_file(tmp.path()).unwrap(); + assert_eq!(file.estimated_steps, (2 * MIB as u64) / CHUNK_SIZE as u64); + } + + #[test] + fn read_file_exact_chunk_multiple_is_one_step() { + let tmp = NamedTempFile::new().unwrap(); + fs::File::create(tmp.path()) + .unwrap() + .set_len(CHUNK_SIZE as u64) + .unwrap(); + + let file = read_file(tmp.path()).unwrap(); + assert_eq!(file.len, CHUNK_SIZE as u64); + assert_eq!(file.estimated_steps, 1); + } + + #[test] + fn read_file_zero_length() { + let tmp = NamedTempFile::new().unwrap(); + + let file = read_file(tmp.path()).unwrap(); + assert_eq!(file.len, 0); + assert_eq!(file.estimated_steps, 0); + } + + #[test] + fn read_file_missing_path_errors() { + assert!(read_file("/definitely/not/a/real/path.encky").is_err()); + } +} diff --git a/eck/test/unit/folder_entries.rs b/eck/test/unit/folder_entries.rs new file mode 100644 index 0000000..4ac565e --- /dev/null +++ b/eck/test/unit/folder_entries.rs @@ -0,0 +1,174 @@ +//! Folder entry collection tests (DAY-11) +//! +//! Exercises `collect_entry` (including its error variants) and +//! `collect_folder_entries`, with a focus on the per-entry `CompressionType` +//! resolution that replaced the old folder-level compression field. + +use eck::context::EnkryptitContext; +use eck::encryption::folder_encryption::entry::collect_entries_from_folder::collect_folder_entries; +use eck::encryption::folder_encryption::entry::collect_entry::collect_entry; +use eck::errors::EnkryptitError; +use eck::types::{CompressionType, ParallelismType}; +use std::fs; +use tempfile::TempDir; +use walkdir::WalkDir; + +const PNG: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0DIHDR"; +const WAV: &[u8] = b"RIFF\x00\x00\x00\x00WAVE"; +const XML: &[u8] = b""; + +#[cfg(test)] +mod tests { + use super::*; + + fn root_dir() -> (TempDir, String) { + let tmp = TempDir::new().unwrap(); + let root = tmp.path().join("root"); + fs::create_dir(&root).unwrap(); + let path = root.to_str().unwrap().to_string(); + (tmp, path) + } + + // --- collect_entry --- + + #[test] + fn collect_entry_returns_relative_path_and_permissions() { + let (_tmp, root) = root_dir(); + fs::write(format!("{}/alpha.txt", root), b"x").unwrap(); + + for entry in WalkDir::new(&root) { + let entry = entry.unwrap(); + if entry.path().to_str().unwrap() == root { + continue; + } + let (rel, perms) = collect_entry(&entry, &root).unwrap(); + assert_eq!(rel, "alpha.txt"); + #[cfg(unix)] + assert!(perms.is_some(), "unix mode must be captured"); + return; + } + panic!("file entry never visited"); + } + + #[test] + fn collect_entry_root_dir_is_directory_is_folder_error() { + let (_tmp, root) = root_dir(); + let root_entry = WalkDir::new(&root).into_iter().next().unwrap().unwrap(); + match collect_entry(&root_entry, &root) { + Err(EnkryptitError::DirectoryIsFolder) => {} + other => panic!("expected DirectoryIsFolder, got {other:?}"), + } + } + + #[test] + fn collect_entry_rejects_directory_entries() { + let (_tmp, root) = root_dir(); + fs::create_dir(format!("{}/subdir", root)).unwrap(); + + for entry in WalkDir::new(&root) { + let entry = entry.unwrap(); + if entry.file_type().is_dir() && entry.path().to_str().unwrap() != root { + match collect_entry(&entry, &root) { + Err(EnkryptitError::FileIsASymLink) => {} + other => panic!("expected FileIsASymLink for a directory, got {other:?}"), + } + return; + } + } + panic!("directory entry never visited"); + } + + #[cfg(unix)] + #[test] + fn collect_entry_accepts_symlink_to_file() { + use std::os::unix::fs::symlink; + + let (_tmp, root) = root_dir(); + let target = format!("{root}.target"); + fs::write(&target, b"payload").unwrap(); + symlink(&target, format!("{root}/link.bin")).unwrap(); + + for entry in WalkDir::new(&root).follow_links(true) { + let entry = entry.unwrap(); + if entry.path().to_str().unwrap() == root { + continue; + } + let (rel, _perms) = collect_entry(&entry, &root).unwrap(); + assert_eq!(rel, "link.bin"); + return; + } + panic!("symlink entry never visited"); + } + + // --- collect_folder_entries --- + + #[test] + fn collect_folder_entries_resolves_concrete_compression_per_entry() { + let (_tmp, root) = root_dir(); + fs::write(format!("{root}/doc.xml"), XML).unwrap(); + fs::write(format!("{root}/sound.wav"), WAV).unwrap(); + fs::write(format!("{root}/img.png"), PNG).unwrap(); + + let context = EnkryptitContext::new(None, CompressionType::Auto, ParallelismType::Auto); + + let entries = collect_folder_entries(&root, &context).unwrap(); + assert_eq!(entries.len(), 3); + + // Each entry must carry a concrete (non-Auto) compression, matching what + // the context would infer for that exact file (this is the behaviour that + // replaces the removed folder-level compression field). + let mut compressions = Vec::new(); + for entry in &entries { + assert_ne!( + entry.compression, + CompressionType::Auto, + "Auto must never leak into an entry: {}", + entry.relative_path + ); + let full = std::path::Path::new(&root).join(&entry.relative_path); + let expected = context.resolve_compression(full.to_str().unwrap()).unwrap(); + assert_eq!( + entry.compression, expected, + "entry: {}", + entry.relative_path + ); + + assert_eq!(entry.offset, 0, "offset is set during encryption"); + assert_eq!(entry.file_nonce.len(), 24, "one master nonce per entry"); + #[cfg(unix)] + assert!(entry.permissions.is_some()); + compressions.push(entry.compression); + } + + // XML -> Zstd, WAV -> Lz4, PNG -> NoComp under the documented inference. + let mut set: Vec = compressions; + set.sort_by_key(|c| format!("{c:?}")); + assert_eq!( + set, + vec![ + CompressionType::Lz4, + CompressionType::NoComp, + CompressionType::Zstd + ] + ); + } + + #[test] + fn collect_folder_entries_empty_folder_is_empty() { + let (_tmp, root) = root_dir(); + let context = EnkryptitContext::new(None, CompressionType::Zstd, ParallelismType::Single); + assert!(collect_folder_entries(&root, &context).unwrap().is_empty()); + } + + #[test] + fn collect_folder_entries_keeps_nested_paths_relative() { + let (_tmp, root) = root_dir(); + fs::create_dir_all(format!("{}/a/b", root)).unwrap(); + fs::write(format!("{root}/a/b/c.txt"), b"x").unwrap(); + + let context = EnkryptitContext::new(None, CompressionType::Xz, ParallelismType::Single); + let entries = collect_folder_entries(&root, &context).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].relative_path, "a/b/c.txt"); + } +} diff --git a/test/unit/metadatas.rs b/eck/test/unit/metadatas.rs similarity index 64% rename from test/unit/metadatas.rs rename to eck/test/unit/metadatas.rs index f6036f7..b31493b 100644 --- a/test/unit/metadatas.rs +++ b/eck/test/unit/metadatas.rs @@ -3,7 +3,7 @@ //! Test Postcard-based metadata storage and retrieval (encryption parameters, compression type) use eck::VERSION; -use eck::metadatas::{ArchiveHeader, MAGIC, MetaDatas}; +use eck::metadatas::{ArchiveHeader, FileEntry, FolderMetadata, MAGIC, MetaDatas}; use eck::types::{CompressionType, KeyType}; use postcard; use rand::{RngCore, rngs::OsRng}; @@ -20,7 +20,7 @@ mod tests { #[test] fn serialize_deserialize_zstd_header() { - let key_type = KeyType::Password("test_password".to_string()); + let key_type = KeyType::Password; let compression = CompressionType::Zstd; let nonce = generate_nonce(); @@ -38,7 +38,7 @@ mod tests { #[test] fn serialize_deserialize_lz4_header() { - let key_type = KeyType::Password("lz4_test".to_string()); + let key_type = KeyType::Password; let compression = CompressionType::Lz4; let nonce = generate_nonce(); @@ -60,7 +60,7 @@ mod tests { ] .iter() { - let key_type = KeyType::Password("comp_test".to_string()); + let key_type = KeyType::Password; let nonce = generate_nonce(); let meta = MetaDatas::new(key_type.clone(), *comp, nonce); @@ -74,7 +74,7 @@ mod tests { #[test] fn serialize_deserialize_all_key_types() { for key_type in [ - KeyType::Password("pwd".to_string()), + KeyType::Password, KeyType::Pwd256([0x1Fu8; 16]), KeyType::FromFile, KeyType::FromOS, @@ -95,7 +95,7 @@ mod tests { #[test] fn metadata_contains_compression_type_zstd() { let compression = CompressionType::Zstd; - let key_type = KeyType::Password("zstd".to_string()); + let key_type = KeyType::Password; let nonce = generate_nonce(); let meta = MetaDatas::new(key_type, compression, nonce); @@ -109,7 +109,7 @@ mod tests { #[test] fn metadata_contains_compression_type_lz4() { let compression = CompressionType::Lz4; - let key_type = KeyType::Password("lz4".to_string()); + let key_type = KeyType::Password; let nonce = generate_nonce(); let meta = MetaDatas::new(key_type, compression, nonce); @@ -123,7 +123,7 @@ mod tests { #[test] fn metadata_contains_compression_type_xz() { let compression = CompressionType::Xz; - let key_type = KeyType::Password("xz".to_string()); + let key_type = KeyType::Password; let nonce = generate_nonce(); let meta = MetaDatas::new(key_type, compression, nonce); @@ -137,7 +137,7 @@ mod tests { #[test] fn metadata_contains_compression_type_no_comp() { let compression = CompressionType::NoComp; - let key_type = KeyType::Password("no_comp".to_string()); + let key_type = KeyType::Password; let nonce = generate_nonce(); let meta = MetaDatas::new(key_type, compression, nonce); @@ -157,7 +157,7 @@ mod tests { #[test] fn serialize_deserialize_empty_password_keytype() { - let key_type = KeyType::Password("".to_string()); + let key_type = KeyType::Password; let compression = CompressionType::Zstd; let nonce = generate_nonce(); @@ -174,7 +174,7 @@ mod tests { assert!(packed_header.len() < 50); - let key_type = KeyType::Password("test".to_string()); + let key_type = KeyType::Password; let meta = MetaDatas::new(key_type, CompressionType::Zstd, [0u8; 24]); let packed_meta = meta.pack().unwrap(); @@ -204,7 +204,7 @@ mod tests { #[test] fn header_meta_len_preserved() { for meta_len in [0u32, 1, 100, 1000, u32::MAX].iter() { - let mut header = ArchiveHeader::new(false, *meta_len); + let header = ArchiveHeader::new(false, *meta_len); let packed = header.pack().unwrap(); let unpacked: ArchiveHeader = postcard::from_bytes(&packed).unwrap(); @@ -222,7 +222,7 @@ mod tests { let mut nonce = [0u8; 24]; nonce.copy_from_slice(&nonce_pattern); - let key_type = KeyType::Password("test".to_string()); + let key_type = KeyType::Password; let meta = MetaDatas::new(key_type, CompressionType::Zstd, nonce); let packed = meta.pack().unwrap(); @@ -234,8 +234,7 @@ mod tests { #[test] fn metadata_serialization_with_special_chars_password() { - let special_pwd = "p@$$w0rd!#%^&*()_+-=[]{}|;':,./<>?"; - let key_type = KeyType::Password(special_pwd.to_string()); + let key_type = KeyType::Password; let compression = CompressionType::Zstd; let nonce = generate_nonce(); @@ -249,8 +248,7 @@ mod tests { #[test] fn metadata_serialization_with_unicode_password() { - let unicode_pwd = "パスワード🔐émoji"; - let key_type = KeyType::Password(unicode_pwd.to_string()); + let key_type = KeyType::Password; let compression = CompressionType::Zstd; let nonce = generate_nonce(); @@ -263,26 +261,105 @@ mod tests { } #[test] - fn metadata_serialization_with_very_long_password() { - let long_pwd = "a".repeat(1024 * 1024); - let key_type = KeyType::Password(long_pwd); - let compression = CompressionType::Zstd; - let nonce = generate_nonce(); + fn metadata_serialization_with_version_field() { + let header = ArchiveHeader::new(false, 0); - let meta = MetaDatas::new(key_type, compression, nonce); - let packed = meta.pack().unwrap(); + assert_eq!(header.version, VERSION); + } - assert!(packed.len() > 1_000_000); + // --- FolderMetadata / FileEntry (DAY-11: per-entry compression) --- - let unpacked: MetaDatas = postcard::from_bytes(&packed).unwrap(); + fn sample_file_entry() -> FileEntry { + FileEntry { + relative_path: "sub/dir/a.txt".to_string(), + offset: 42, + permissions: Some(0o644), + compression: CompressionType::Lz4, + file_nonce: [0xAA; 24], + } + } - assert_eq!(meta.key_type, unpacked.key_type); + #[test] + fn file_entry_roundtrip_preserves_compression() { + let entry = sample_file_entry(); + let packed = postcard::to_allocvec(&entry).unwrap(); + + let unpacked: FileEntry = postcard::from_bytes(&packed).unwrap(); + + // Compared field-by-field: `FileEntry::eq` intentionally ignores + // `compression`, so we assert the new field explicitly. + assert_eq!(unpacked.relative_path, "sub/dir/a.txt"); + assert_eq!(unpacked.offset, 42); + assert_eq!(unpacked.permissions, Some(0o644)); + assert_eq!(unpacked.compression, CompressionType::Lz4); + assert_eq!(unpacked.file_nonce, [0xAA; 24]); } #[test] - fn metadata_serialization_with_version_field() { - let header = ArchiveHeader::new(false, 0); + fn file_entry_roundtrip_keeps_each_compression_type() { + for comp in [ + CompressionType::Zstd, + CompressionType::Lz4, + CompressionType::Xz, + CompressionType::NoComp, + CompressionType::Auto, + ] { + let entry = FileEntry { + relative_path: "a.bin".to_string(), + offset: 1, + permissions: None, + compression: comp, + file_nonce: [0u8; 24], + }; + let packed = postcard::to_allocvec(&entry).unwrap(); + let unpacked: FileEntry = postcard::from_bytes(&packed).unwrap(); + assert_eq!(unpacked.compression, comp); + } + } - assert_eq!(header.version, VERSION); + #[test] + fn file_entry_permissions_none_roundtrip() { + let entry = FileEntry { + permissions: None, + ..sample_file_entry() + }; + let packed = postcard::to_allocvec(&entry).unwrap(); + let unpacked: FileEntry = postcard::from_bytes(&packed).unwrap(); + assert_eq!(unpacked.permissions, None); + } + + #[test] + fn folder_metadata_roundtrip_with_multiple_entries() { + let mut meta = FolderMetadata::new(KeyType::FromFile); + meta.entries.push(sample_file_entry()); + meta.entries.push(FileEntry { + relative_path: "root.bin".to_string(), + offset: 1234, + permissions: None, + compression: CompressionType::NoComp, + file_nonce: [0xBB; 24], + }); + + let packed = meta.pack().unwrap(); + let unpacked: FolderMetadata = postcard::from_bytes(&packed).unwrap(); + + assert_eq!(unpacked.key_type, KeyType::FromFile); + assert_eq!(unpacked.entries.len(), 2); + assert_eq!(unpacked.entries[0].relative_path, "sub/dir/a.txt"); + assert_eq!(unpacked.entries[0].compression, CompressionType::Lz4); + assert_eq!(unpacked.entries[1].relative_path, "root.bin"); + assert_eq!(unpacked.entries[1].offset, 1234); + assert_eq!(unpacked.entries[1].compression, CompressionType::NoComp); + assert_eq!(unpacked.entries[1].file_nonce, [0xBB; 24]); + } + + #[test] + fn folder_metadata_empty_entries_roundtrip() { + let meta = FolderMetadata::new(KeyType::Password); + let packed = meta.pack().unwrap(); + let unpacked: FolderMetadata = postcard::from_bytes(&packed).unwrap(); + + assert_eq!(unpacked.key_type, KeyType::Password); + assert!(unpacked.entries.is_empty()); } } diff --git a/test/unit/mod.rs b/eck/test/unit/mod.rs similarity index 64% rename from test/unit/mod.rs rename to eck/test/unit/mod.rs index 0739aab..361b141 100644 --- a/test/unit/mod.rs +++ b/eck/test/unit/mod.rs @@ -4,4 +4,10 @@ pub mod compression; pub mod encryption_primitives; +pub mod error_display; +pub mod file; +pub mod folder_entries; pub mod metadatas; +pub mod parallelism; +pub mod text; +pub mod types; diff --git a/eck/test/unit/parallelism.rs b/eck/test/unit/parallelism.rs new file mode 100644 index 0000000..8a2661a --- /dev/null +++ b/eck/test/unit/parallelism.rs @@ -0,0 +1,405 @@ +//! Parallelism Unit Tests +//! +//! Test the generic job/worker/pool abstractions (EnkryptitPool, EnkryptitJob) and the +//! `EncryptChunkJob` / `DecryptChunkJob` executables in isolation. + +use std::sync::Arc; + +use chacha20poly1305::{KeyInit, XChaCha20Poly1305}; +use eck::context::{ + EnkryptitContext, LOW_BOUNDARY, MID_INFERIOR_BOUNDARY, MID_SUPERIOR_BOUNDARY, SUPERIOR_BOUNDARY, +}; +use eck::encryption::chunk_job::{ + decrypt::DecryptChunkJob, encrypt::EncryptChunkJob, result::ChunkResult, submit_decrypt_chunk, + submit_encrypt_chunk, +}; +use eck::errors::EnkryptitError; +use eck::parallelism::EnkryptitJob; +use eck::parallelism::executable::EnkryptitExecutable; +use eck::parallelism::pool::EnkryptitPool; +use eck::types::{CHUNK_SIZE, CompressionType, ParallelismType}; +use tempfile::TempDir; + +/// A trivial executable that returns its index, so we can check that results +/// are correctly routed back from the workers. +struct EchoJob { + value: u64, +} + +impl EnkryptitExecutable for EchoJob { + type Output = u64; + + fn execute(self) -> Result { + Ok(self.value) + } +} + +/// An executable that always fails, to check error propagation through the pool. +struct FailingJob; + +impl EnkryptitExecutable for FailingJob { + type Output = (); + + fn execute(self) -> Result { + Err(EnkryptitError::Break) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn enkryptit_job_executes_its_task() { + let job = EnkryptitJob::new(EchoJob { value: 42 }); + assert_eq!(job.execute().unwrap(), 42); + } + + #[test] + fn pool_rejects_zero_workers() { + use eck::errors::EnkryptitError; + match EnkryptitPool::::new(0) { + Err(EnkryptitError::InvalidWorkerCount) => {} + _ => panic!("expected InvalidWorkerCount from a zero-sized pool"), + } + } + + #[test] + fn pool_returns_all_outputs_with_four_workers() { + let pool = EnkryptitPool::::new(4).unwrap(); + for i in 0..20u64 { + pool.submit(EnkryptitJob::new(EchoJob { value: i })) + .unwrap(); + } + + let mut received = Vec::new(); + for _ in 0..20 { + received.push(pool.recv().unwrap().unwrap()); + } + received.sort_unstable(); + assert_eq!(received, (0..20).collect::>()); + } + + #[test] + fn pool_returns_all_outputs_with_single_worker() { + let pool = EnkryptitPool::::new(1).unwrap(); + for i in 0..5u64 { + pool.submit(EnkryptitJob::new(EchoJob { value: i })) + .unwrap(); + } + + let mut received = Vec::new(); + for _ in 0..5 { + received.push(pool.recv().unwrap().unwrap()); + } + received.sort_unstable(); + assert_eq!(received, (0..5).collect::>()); + } + + #[test] + fn pool_more_jobs_than_workers() { + // More jobs than workers: the bounded channel buffers, workers drain, + // and every result must still be received. + let pool = EnkryptitPool::::new(2).unwrap(); + for i in 0..50u64 { + pool.submit(EnkryptitJob::new(EchoJob { value: i })) + .unwrap(); + } + + let mut received = Vec::new(); + for _ in 0..50 { + received.push(pool.recv().unwrap().unwrap()); + } + received.sort_unstable(); + assert_eq!(received, (0..50).collect::>()); + } + + #[test] + fn pool_propagates_execution_errors() { + let pool = EnkryptitPool::::new(2).unwrap(); + for _ in 0..3 { + pool.submit(EnkryptitJob::new(FailingJob)).unwrap(); + } + + for _ in 0..3 { + let inner = pool.recv().unwrap(); + assert!(inner.is_err(), "executable error must be surfaced"); + } + } + + #[test] + fn encrypt_chunk_job_roundtrip() { + let key = [0x11u8; 32]; + let cipher = Arc::new(XChaCha20Poly1305::new(&key.into())); + let master_nonce = Arc::new([0x22u8; 24]); + let compression = Arc::new(CompressionType::NoComp); + + let data = b"parallel chunk roundtrip".to_vec(); + + let encrypt_job = EncryptChunkJob { + index: 0, + data, + master_nonce: master_nonce.clone(), + compression: compression.clone(), + cipher: cipher.clone(), + }; + let ChunkResult { + index, + data: encrypted, + } = encrypt_job.execute().unwrap(); + assert_eq!(index, 0); + + let decrypt_job = DecryptChunkJob { + index: 0, + data: encrypted, + master_nonce, + compression, + cipher, + }; + let ChunkResult { data: restored, .. } = decrypt_job.execute().unwrap(); + assert_eq!(restored, b"parallel chunk roundtrip"); + } + + #[test] + fn encrypt_chunk_job_keeps_index() { + let key = [0x33u8; 32]; + let cipher = Arc::new(XChaCha20Poly1305::new(&key.into())); + let master_nonce = Arc::new([0x44u8; 24]); + let compression = Arc::new(CompressionType::Lz4); + + for (idx, payload) in [b"a".to_vec(), b"bb".to_vec(), b"ccc".to_vec()] + .into_iter() + .enumerate() + { + let job = EncryptChunkJob { + index: idx as u64, + data: payload, + master_nonce: master_nonce.clone(), + compression: compression.clone(), + cipher: cipher.clone(), + }; + let result = job.execute().unwrap(); + assert_eq!(result.index, idx as u64); + } + } + + #[test] + fn chunk_output_length_is_bounded() { + // Encryption/compression writes into a pre-allocated CHUNK_SIZE buffer, + // so the produced chunk must never exceed CHUNK_SIZE. + let key = [0x55u8; 32]; + let cipher = Arc::new(XChaCha20Poly1305::new(&key.into())); + let master_nonce = Arc::new([0x66u8; 24]); + let compression = Arc::new(CompressionType::NoComp); + + let data = vec![0xABu8; 1024]; + let job = EncryptChunkJob { + index: 0, + data, + master_nonce, + compression, + cipher, + }; + let result = job.execute().unwrap(); + assert!(result.data.len() <= CHUNK_SIZE); + assert!(!result.data.is_empty()); + } + + // Kept reference-free: just demonstrates the EnkryptitExecutable trait bound + // used by the pool is object-safe and usable from the crate root. + #[test] + fn executable_trait_is_usable() { + let job = EnkryptitJob::new(EchoJob { value: 7 }); + let output: u64 = job.task.execute().unwrap(); + assert_eq!(output, 7); + } + + // --- Auto parallelism inference (DAY-10) --- + + fn cpus() -> u8 { + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) as u8 + } + + fn auto_context() -> EnkryptitContext { + EnkryptitContext::new(None, CompressionType::NoComp, ParallelismType::Auto) + } + + #[test] + fn auto_parallelism_tiny_files_are_single() { + let ctx = auto_context(); + assert_eq!( + ctx.resolve_parallelism_with_size(0).unwrap(), + ParallelismType::Single + ); + assert_eq!( + ctx.resolve_parallelism_with_size(1).unwrap(), + ParallelismType::Single + ); + assert_eq!( + ctx.resolve_parallelism_with_size(LOW_BOUNDARY - 1).unwrap(), + ParallelismType::Single + ); + } + + #[test] + fn auto_parallelism_low_zone_is_multithread_8() { + let ctx = auto_context(); + assert_eq!( + ctx.resolve_parallelism_with_size(LOW_BOUNDARY).unwrap(), + ParallelismType::MultiThread(8.min(cpus())) + ); + assert_eq!( + ctx.resolve_parallelism_with_size(MID_INFERIOR_BOUNDARY - 1) + .unwrap(), + ParallelismType::MultiThread(8.min(cpus())) + ); + } + + #[test] + fn auto_parallelism_mid_inferior_zone_is_multithread_12() { + let ctx = auto_context(); + assert_eq!( + ctx.resolve_parallelism_with_size(MID_INFERIOR_BOUNDARY) + .unwrap(), + ParallelismType::MultiThread(12.min(cpus())) + ); + assert_eq!( + ctx.resolve_parallelism_with_size(MID_SUPERIOR_BOUNDARY - 1) + .unwrap(), + ParallelismType::MultiThread(12.min(cpus())) + ); + } + + #[test] + fn auto_parallelism_mid_superior_zone_is_multithread_16() { + let ctx = auto_context(); + assert_eq!( + ctx.resolve_parallelism_with_size(MID_SUPERIOR_BOUNDARY) + .unwrap(), + ParallelismType::MultiThread(16.min(cpus())) + ); + assert_eq!( + ctx.resolve_parallelism_with_size(SUPERIOR_BOUNDARY - 1) + .unwrap(), + ParallelismType::MultiThread(16.min(cpus())) + ); + } + + #[test] + fn auto_parallelism_superior_zone_uses_every_core() { + let ctx = auto_context(); + assert_eq!( + ctx.resolve_parallelism_with_size(SUPERIOR_BOUNDARY) + .unwrap(), + ParallelismType::MultiThread(cpus()) + ); + assert_eq!( + ctx.resolve_parallelism_with_size(SUPERIOR_BOUNDARY * 2) + .unwrap(), + ParallelismType::MultiThread(cpus()) + ); + } + + #[test] + fn explicit_parallelism_bypasses_size_inference() { + let ctx = EnkryptitContext::new( + None, + CompressionType::NoComp, + ParallelismType::MultiThread(3), + ); + // Even with a 0-byte "file", the explicit type is returned untouched. + assert_eq!( + ctx.resolve_parallelism_with_size(0).unwrap(), + ParallelismType::MultiThread(3) + ); + + let ctx = EnkryptitContext::new(None, CompressionType::NoComp, ParallelismType::Single); + assert_eq!( + ctx.resolve_parallelism_with_size(SUPERIOR_BOUNDARY * 999) + .unwrap(), + ParallelismType::Single + ); + } + + #[test] + fn resolve_parallelism_uses_path_file_size() { + let dir = TempDir::new().unwrap(); + let small = dir.path().join("small.bin"); + std::fs::File::create(&small).unwrap().set_len(16).unwrap(); + let ctx = auto_context(); + assert_eq!( + ctx.resolve_parallelism(small.to_str().unwrap()).unwrap(), + ParallelismType::Single + ); + + let big = dir.path().join("big.bin"); + std::fs::File::create(&big) + .unwrap() + .set_len(LOW_BOUNDARY + 1) + .unwrap(); + assert_eq!( + ctx.resolve_parallelism(big.to_str().unwrap()).unwrap(), + ParallelismType::MultiThread(8.min(cpus())) + ); + } + + // --- chunk_job submit helpers (DAY-12) --- + + #[test] + fn chunk_job_submit_helpers_roundtrip_through_pool() { + let encrypt_pool = EnkryptitPool::::new(2).unwrap(); + let key = [0x99u8; 32]; + let cipher = Arc::new(XChaCha20Poly1305::new(&key.into())); + let master_nonce = Arc::new([0x44u8; 24]); + let compression = Arc::new(CompressionType::NoComp); + + let payloads = [ + b"alpha".to_vec(), + b"beta-beta".to_vec(), + b"gamma chunk payload".to_vec(), + ]; + + for (i, data) in payloads.iter().cloned().enumerate() { + submit_encrypt_chunk( + &encrypt_pool, + i as u64, + data, + master_nonce.clone(), + compression.clone(), + cipher.clone(), + ) + .unwrap(); + } + + let mut encrypted = Vec::new(); + for _ in 0..payloads.len() { + encrypted.push(encrypt_pool.recv().unwrap().unwrap()); + } + encrypted.sort_by_key(|r| r.index); + + let decrypt_pool = EnkryptitPool::::new(2).unwrap(); + for result in encrypted { + submit_decrypt_chunk( + &decrypt_pool, + result.index, + result.data, + master_nonce.clone(), + compression.clone(), + cipher.clone(), + ) + .unwrap(); + } + + let mut restored = Vec::new(); + for _ in 0..payloads.len() { + restored.push(decrypt_pool.recv().unwrap().unwrap()); + } + restored.sort_by_key(|r| r.index); + + let restored: Vec<&[u8]> = restored.iter().map(|r| r.data.as_slice()).collect(); + let expected: Vec<&[u8]> = payloads.iter().map(|p| p.as_slice()).collect(); + assert_eq!(restored, expected); + } +} diff --git a/eck/test/unit/text.rs b/eck/test/unit/text.rs new file mode 100644 index 0000000..25bbbf0 --- /dev/null +++ b/eck/test/unit/text.rs @@ -0,0 +1,135 @@ +//! Tests for the display-width-aware helpers that render the success box +//! (`wrap_text`, `pad_to`, `success_message_width`). +//! +//! Terminals measure text in *display columns*: combining marks such as the +//! decomposed accents macOS puts in filenames ("é" = e + U+0301) take 0 columns +//! and wide CJK takes 2. These tests pin the invariants that keep the box +//! borders aligned no matter the message contents. + +use eck::diagnostic::output::display::{ + SUCCESS_BADGE_WIDTH, SUCCESS_BOX_PADDING, pad_to, success_message_width, wrap_text, +}; +use unicode_width::UnicodeWidthStr; + +/// Realistic macOS path with *decomposed* accents ("é" = e + U+0301, +/// "à" = a + U+0300), the exact regression that desynced the box borders. +const ACCENTED: &str = "File was encrypted at /Users/axel/captures/Capture d'e\u{301}cran 2026-09-08 a\u{300} 07.07.22.png.encky"; + +#[test] +fn wrap_keeps_every_line_within_width() { + let lines = wrap_text(ACCENTED, 53); + assert!(!lines.is_empty()); + for line in &lines { + assert!( + line.width() <= 53, + "line got display width {}: {line:?}", + line.width() + ); + } + assert!( + lines.iter().any(|l| l.contains('\u{301}')), + "test must exercise the combining-mark path" + ); +} + +/// The whole success frame — top/bottom bars and every content line — must be +/// exactly `box_width` display columns, so the right border lines up with the +/// corners. `success_message_width` is shared with `display_success`, so a +/// regression in the layout math (like padding the left side twice) shows up +/// here as a line that is too short. +#[test] +fn success_frame_share_one_width() { + const BOX_WIDTH: usize = 100; + let message_width = success_message_width(BOX_WIDTH); + let horizontal = "─".repeat(BOX_WIDTH - 2); + + let top = format!("╭{horizontal}╮"); + let bottom = format!("╰{horizontal}╯"); + assert_eq!(top.width(), BOX_WIDTH); + assert_eq!(bottom.width(), BOX_WIDTH); + + for line in wrap_text(ACCENTED, message_width) { + let rendered = format!( + "{}{}{}{}{}", + "│", + " ".repeat(SUCCESS_BOX_PADDING), + " ".repeat(SUCCESS_BADGE_WIDTH), + pad_to(&line, message_width), + "│", + ); + assert_eq!(rendered.width(), BOX_WIDTH, "misaligned line: {line:?}"); + } +} + +#[test] +fn hard_split_never_tears_combining_marks() { + let word = "e\u{301}".repeat(8); + let lines = wrap_text(&word, 3); + + assert_eq!(lines.len(), 3); // 3 + 3 + 2 graphemes of width 1 + for chunk in &lines { + assert!( + !chunk.starts_with('\u{301}'), + "leading combining mark: {chunk:?}" + ); + assert_eq!( + chunk.chars().count() % 2, + 0, + "base+mark torn apart: {chunk:?}" + ); + assert!(chunk.width() <= 3); + } + + let joined: String = lines.concat(); + assert_eq!(joined, word); + assert_eq!(joined.width(), word.width()); +} + +#[test] +fn wrap_respects_paragraphs_and_word_boundaries() { + assert_eq!( + wrap_text("one two three\nfour", 7), + ["one two", "three", "four"] + ); +} + +#[test] +fn wrap_flushes_current_line_before_hard_splitting() { + assert_eq!(wrap_text("pre abcdefghij", 5), ["pre", "abcde", "fghij"]); +} + +#[test] +fn wrap_hard_splits_overlong_words() { + let lines = wrap_text("abcdefghijklmnop", 5); + assert_eq!(lines, ["abcde", "fghij", "klmno", "p"]); + for line in &lines { + assert!(line.width() <= 5); + } +} + +#[test] +fn wrap_handles_empty_or_blank_input() { + assert!(wrap_text("", 10).is_empty()); + assert!(wrap_text(" ", 10).is_empty()); + assert!(wrap_text("\n\n", 10).is_empty()); +} + +#[test] +fn pad_to_uses_display_columns() { + let accented = pad_to("e\u{301}", 5); + assert_eq!(accented.chars().count(), 6, "2 chars + 4 spaces"); + assert_eq!(accented.width(), 5); + + let wide = pad_to("日", 4); + assert_eq!(wide.chars().count(), 3, "1 wide char + 2 spaces"); + assert_eq!(wide.width(), 4); + + assert_eq!(pad_to("abc", 5), "abc "); +} + +#[test] +fn message_width_geometry_is_consistent() { + // border + left padding + badge + text + border == total box width + let text = success_message_width(60); + assert_eq!(1 + SUCCESS_BOX_PADDING + SUCCESS_BADGE_WIDTH + text + 1, 60); +} diff --git a/eck/test/unit/types.rs b/eck/test/unit/types.rs new file mode 100644 index 0000000..fe33b53 --- /dev/null +++ b/eck/test/unit/types.rs @@ -0,0 +1,84 @@ +//! Regression tests for the `Display`/`String` conversions of the enum types. +//! +//! These used to recurse infinitely: `Display::fmt` called `self.to_string()` +//! which dispatched straight back to `Display::fmt` (via the blanket +//! `ToString` impl). A stack overflow is fatal to the process, so any of these +//! tests running to completion proves the recursion is gone. + +use eck::types::{CompressionType, KeyType, ParallelismType}; + +#[test] +fn key_type_display_and_string_conversion_match() { + assert_eq!(KeyType::Password.to_string(), "password"); + assert_eq!(String::from(KeyType::Password), "password"); + assert_eq!(KeyType::FromFile.to_string(), "from file"); + assert_eq!(KeyType::FromOS.to_string(), "from os keyring"); + assert_eq!( + KeyType::None.to_string(), + "no keytype used (should not happen if the file is encrypted)" + ); +} + +#[test] +fn key_type_pwd256_displays_salt() { + let salt = [0xDEu8; 16]; + let rendered = KeyType::Pwd256(salt).to_string(); + assert!(rendered.starts_with("hashed password, with the following salt : ")); + assert!( + rendered.ends_with("dededededededededededededededede"), + "got: {rendered}" + ); +} + +#[test] +fn compression_type_display_and_string_conversion_match() { + assert_eq!(CompressionType::Auto.to_string(), "Automatic"); + assert_eq!(CompressionType::Lz4.to_string(), "Lz4 (fastest)"); + assert_eq!( + CompressionType::Xz.to_string(), + "Xz (slowest but most efficient)" + ); + assert_eq!(CompressionType::NoComp.to_string(), "No compression"); + assert_eq!(CompressionType::Zstd.to_string(), "Zstd (Balanced)"); + assert_eq!(String::from(CompressionType::Zstd), "Zstd (Balanced)"); +} + +#[test] +fn parallelism_type_display_and_string_conversion_match() { + assert_eq!(ParallelismType::Auto.to_string(), "Automatic"); + assert_eq!(ParallelismType::Single.to_string(), "SingleThread"); + assert_eq!( + ParallelismType::MultiThread(8).to_string(), + "MultiThreading with 8 threads" + ); + let into: String = ParallelismType::MultiThread(4).into(); + assert_eq!(into, "MultiThreading with 4 threads"); +} + +#[test] +fn description_methods_agree_with_display() { + for k in [ + KeyType::Password, + KeyType::FromFile, + KeyType::FromOS, + KeyType::None, + ] { + assert_eq!(k.to_string(), k.description()); + } + for c in [ + CompressionType::Auto, + CompressionType::Lz4, + CompressionType::Xz, + CompressionType::NoComp, + CompressionType::Zstd, + ] { + assert_eq!(c.to_string(), c.description()); + } + for p in [ + ParallelismType::Auto, + ParallelismType::Single, + ParallelismType::MultiThread(16), + ] { + assert_eq!(p.to_string(), p.description()); + } +} diff --git a/src/encryption/file_encryption/mod.rs b/src/encryption/file_encryption/mod.rs deleted file mode 100644 index e1ed6fd..0000000 --- a/src/encryption/file_encryption/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::encryption::encryption_flow::{decrypt_stream, encrypt_stream}; -use crate::encryption::encryption_primitives::generate_nonce; -use crate::errors::EnkryptitError; -use crate::keygen::{derive_key, key_from_password}; -use crate::metadatas::{ArchiveHeader, MetaDatas}; -use crate::parameters::params::EnkryptitParams; -use crate::types::KeyType::{self, Pwd256}; -use std::fs::File; -use std::io::BufReader; -use std::io::{BufWriter, Write}; -use std::io::{Seek, SeekFrom}; -use zeroize::Zeroize; - -/// Public function that encrypts a file (it also resolves the key and keytype) -pub fn encrypt_file( - path: &str, - key: [u8; 32], - parameters: &EnkryptitParams, - key_type: KeyType, -) -> Result { - // Resolves both key and keytype - let (mut key, new_key_type) = match key_type { - KeyType::Password(pwd) => { - let (key, salt) = key_from_password(&pwd)?; - (key, Pwd256(salt)) - } - _ => (key, key_type.clone()), - }; - - // Opens the file - let file = File::open(path)?; - let total_size: u64 = file.metadata()?.len(); - - let reader = BufReader::new(file); - - // Generates the nonce - let mut master_nonce = generate_nonce(); - - // Builds the metadata, and serialize it - let metadata = MetaDatas::new(new_key_type, parameters.compression, master_nonce).pack()?; - - let encrypted_path = format!("{}.encky", path); - - // Creates the `cipherfile` placeholder - let cipherfile = std::fs::File::create(&encrypted_path)?; - - let mut writer = BufWriter::new(cipherfile); - - let actual_meta_len = metadata.len() as u32; - - // Creates and pack the header - let header = ArchiveHeader::new(false, actual_meta_len).pack()?; - - // First, we write the header's len - writer.write_all(&[header.len() as u8])?; - - // Then, we write the serialized header - writer.write_all(&header)?; - - // And the metadata - writer.write_all(&metadata)?; - - // We encrypt the stream in place - let _ = encrypt_stream( - &mut writer, - reader, - master_nonce, - key, - parameters.compression, - total_size, - )?; - - // And finally, we `zeroize` both the master nonce and the key. - master_nonce.zeroize(); - key.zeroize(); - - writer.flush()?; - - Ok(encrypted_path) -} - -/// Public function that decrypts a file -pub fn decrypt_file( - path: &str, - meta_bytes: &[u8], - key: [u8; 32], - payload_offset: u64, - key_type: KeyType, -) -> Result { - // First, we deserialize the metadata - let metadatas: MetaDatas = postcard::from_bytes(meta_bytes)?; - // And extract the compression type... - let compression_type = metadatas.compression; - // ... and the nonce - let master_nonce = metadatas.nonce; - - // We resolve the key - let mut key = if let KeyType::Password(pwd) = &key_type { - let salt = match &metadatas.key_type { - KeyType::Pwd256(s) => *s, - _ => return Err(EnkryptitError::CorruptedFile), - }; - - derive_key(&pwd, salt)? - } else { - key - }; - - // We open the file - let file = File::open(path)?; - let total_size: u64 = file.metadata()?.len(); - let mut reader = BufReader::new(file); - - let plain_path = path.strip_suffix(".encky").unwrap_or(path); - // Create a placeholder for the new file - let new_file = std::fs::File::create(plain_path)?; - let mut writer = BufWriter::new(new_file); - - reader.seek(SeekFrom::Start(payload_offset))?; - - // Decrypts the stream in-place - let _ = decrypt_stream( - &mut writer, - reader, - total_size, - key, - compression_type, - master_nonce, - )?; - - key.zeroize(); - writer.flush()?; - - Ok(plain_path.to_string()) -} diff --git a/src/encryption/folder_encryption/entries.rs b/src/encryption/folder_encryption/entries.rs deleted file mode 100644 index 8a6fc46..0000000 --- a/src/encryption/folder_encryption/entries.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::encryption::encryption_primitives::generate_nonce; -use crate::errors::EnkryptitError; -use crate::metadatas::FileEntry; -use std::path::Path; -use walkdir::WalkDir; - -/// Collect all files from directory tree using walkdir (follow symlinks via follow_links on Unix) -pub fn collect_folder_entries(folder_path: &str) -> Result, EnkryptitError> { - let mut entries = Vec::new(); - - for entry in WalkDir::new(folder_path).follow_links(true) { - match entry { - Ok(dir_entry) => { - // Skip the root directory itself, only process files and subdirs - if dir_entry.path() == Path::new(folder_path) { - continue; - } - - let metadata = match dir_entry.metadata() { - Ok(m) => m, - Err(_) => continue, // Skip files we can't read metadata for - }; - - // Only include regular files (skip directories - they'll be created on decrypt) - if !metadata.is_file() && !metadata.file_type().is_symlink() { - continue; - } - - // Calculate relative path from folder root - match dir_entry.path().strip_prefix(folder_path) { - Ok(relative_path) => { - let relative_path_str = relative_path.to_string_lossy().to_string(); - - let perms: Option = { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - Some(metadata.permissions().mode()) - } - - #[cfg(not(unix))] - { - None - } // No permissions on Windows - }; - - entries.push(FileEntry { - relative_path: relative_path_str, - offset: 0, // Set during encryption - permissions: perms, - file_nonce: generate_nonce(), // Unique per FILE -> one master nonce per file - }); - } - Err(_) => continue, // Skip if can't calculate relative path - } - } - Err(_) => continue, // Skip inaccessible files during collection - } - } - - Ok(entries) -} diff --git a/src/encryption/folder_encryption/mod.rs b/src/encryption/folder_encryption/mod.rs deleted file mode 100644 index ca950f8..0000000 --- a/src/encryption/folder_encryption/mod.rs +++ /dev/null @@ -1,207 +0,0 @@ -pub mod entries; -pub mod intern_archive_encryption; - -use crate::encryption::folder_encryption::KeyType::Pwd256; -use crate::encryption::folder_encryption::entries::collect_folder_entries; -use crate::encryption::folder_encryption::intern_archive_encryption::{ - decrypt_single_file_from_archive, encrypt_single_file_into_archive, -}; -use crate::errors::EnkryptitError; -use crate::keygen::derive_key; -use crate::keygen::key_from_password; -use crate::metadatas::{ArchiveHeader, FolderMetadata}; -use crate::parameters::params::EnkryptitParams; -use crate::types::KeyType; -use postcard::from_bytes; -use std::fs::File; -use std::io::{BufWriter, Seek, SeekFrom, Write}; -use std::path::Path; -use zeroize::Zeroize; - -/// Encrypt a folder into a single .encky archive file (v2 format: metadata at the end) -pub fn encrypt_folder( - folder_path: &str, - parameters: &EnkryptitParams, - key: [u8; 32], - key_type: crate::types::KeyType, -) -> Result { - // Resolves the key and the keytype - let (mut key, new_key_type) = match key_type { - KeyType::Password(pwd) => { - let (key, salt) = key_from_password(&pwd)?; - (key, Pwd256(salt)) - } - _ => (key, key_type.clone()), - }; - - // Step 1: Collect all file entries from directory tree (follow symlinks) - let mut entries = collect_folder_entries(folder_path)?; - - if entries.is_empty() { - return Err(EnkryptitError::FileError); - } - - // Step 2: Build FolderMetadata (offsets will be filled after encryption) - let mut folder_meta = FolderMetadata::new(parameters.compression, new_key_type); - - for entry in &entries { - folder_meta.entries.push(entry.clone()); - } - - // Step 3: Create .encky archive with a fixed-size header region - // The header is serialized, then zero-padded to HEADER_REGION_SIZE bytes - // so that future in-place updates never change the region size. - const HEADER_REGION_SIZE: usize = 64; - let archive_path = format!("{}.encky", folder_path); - - { - // Creates the archive - let mut archive_file = BufWriter::new(File::create(&archive_path)?); - - // the header placeholder - let placeholder_header = ArchiveHeader::new(true, 0); - // We serialize it - let mut header_bytes = placeholder_header.pack()?; - // And resize with HEADER_REGION_SIZE - header_bytes.resize(HEADER_REGION_SIZE, 0); - - // We write the region size - archive_file.write_all(&[HEADER_REGION_SIZE as u8])?; - // And the header - archive_file.write_all(&header_bytes)?; - } - - // Compute the beginning offset - let data_start: u64 = 1 + HEADER_REGION_SIZE as u64; - - // Step 4: Encrypt each file, tracking offsets - let mut current_offset = data_start; - - // We iterate on each entry - for entry in &mut entries { - entry.offset = current_offset; - - // For more informations, please refeer to `encrypt_single_file_into_archive()` - let bytes_written = encrypt_single_file_into_archive( - folder_path, - &entry.relative_path, - entry.file_nonce, - parameters.compression, - key, - &archive_path, - )?; - - // We update the offset - current_offset += bytes_written; - } - - // Step 5: Rebuild metadata with correct offsets and write at end of archive - folder_meta.entries.clear(); - for entry in &entries { - folder_meta.entries.push(entry.clone()); - } - - let serialized_meta = folder_meta.pack()?; - let meta_len = serialized_meta.len() as u32; - - { - let mut archive_file = BufWriter::new(File::options().append(true).open(&archive_path)?); - - archive_file.write_all(&serialized_meta)?; - } - - // Step 6: Seek back and update meta_len in the fixed-size header region - { - let final_header = ArchiveHeader::new(true, meta_len); - let mut final_header_bytes = final_header.pack()?; - final_header_bytes.resize(HEADER_REGION_SIZE, 0); - - let mut archive_file = File::options().read(true).write(true).open(&archive_path)?; - - archive_file.seek(SeekFrom::Start(1))?; - archive_file.write_all(&final_header_bytes)?; - } - - key.zeroize(); - - Ok(archive_path) -} - -/// Decrypt a folder archive (.encky file) back to original structure -pub fn decrypt_folder( - archive_path: &str, - meta_bytes: &[u8], - key: [u8; 32], - payload_offset: u64, - key_type: KeyType, - version: u8, -) -> Result { - let metadatas: FolderMetadata = from_bytes(meta_bytes)?; - let compression_type = metadatas.compression; - let entries = metadatas.entries; - - // Step 2: Derive base key from stored KeyType (same as encryption) - let key = if let KeyType::Password(pwd) = &key_type { - let salt = match &metadatas.key_type { - KeyType::Pwd256(s) => *s, - _ => return Err(EnkryptitError::CorruptedFile), - }; - - derive_key(pwd, salt)? - } else { - key - }; - - // Step 3: Create destination directory structure - let dest_folder = archive_path.strip_suffix(".encky").unwrap_or(archive_path); - std::fs::create_dir_all(dest_folder)?; - - // Step 4: Decrypt each file independently - continue on failure! - for entry in entries { - let offset = if version >= 2 { - entry.offset - } else { - payload_offset - }; - - let decrypt_result = decrypt_single_file_from_archive( - archive_path, - dest_folder, - entry.permissions, - &entry.relative_path, - entry.file_nonce, - entry.offset, // used for progress bar display - compression_type, - key, - offset, - ); - - match decrypt_result { - Ok(bytes_consumed) => { - if version < 2 { - // v1: we don't know the exact offset, but we tried. - // For v1 archives this path is inherently unreliable. - let _ = bytes_consumed; - } - } - Err(e) => { - eprintln!( - "[WARNING] Failed to decrypt {}: {} - creating placeholder", - entry.relative_path, e - ); - - // Create 0-byte placeholder file with original filename - let placeholder = Path::new(dest_folder).join(&entry.relative_path); - if let Some(parent) = placeholder.parent() { - std::fs::create_dir_all(parent)?; - } - - let _ = File::create(placeholder); - - continue; - } - } - } - - Ok(dest_folder.to_string()) -} diff --git a/src/encryption/mod.rs b/src/encryption/mod.rs deleted file mode 100644 index 154633f..0000000 --- a/src/encryption/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod encryption_flow; -pub mod encryption_primitives; -pub mod file_encryption; -pub mod folder_encryption; diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index d246589..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,84 +0,0 @@ -use thiserror::Error; - -#[allow(dead_code)] -#[derive(Debug, Error)] -/// Enum for all the errors of **Enkryptit**. Also implements `From<>` other error types. -pub enum EnkryptitError { - #[error("operation interrupted")] - Break, - - #[error("postcard error: {0}")] - PostcardError(#[from] postcard::Error), - - #[error("encryption/decryption failed")] - Encryption, - - #[error("argon2 password hash failed")] - Argon2Error, - - #[error("key derivation error: {0}")] - KeyDerivationError(String), - - #[error("unable to determine key type")] - KeyTypeGetError, - - #[error("keyring error: {0}")] - KeyringError(#[from] keyring::Error), - - #[error("hex decoding error: {0}")] - HexError(#[from] hex::FromHexError), - - #[error("unexpected end of file")] - UnexpectedEof, - - #[error("io error: {0}")] - IoError(#[from] std::io::Error), - - #[error("invalid key length")] - InvalidKeyLength, - - #[error("Command not found")] - CommandNotFound, - - #[error("error while searching for the filel")] - FileError, - - #[error("unable to lock memory")] - MemoryLockError, - - #[error("home directory not found")] - HomeNotFound, - - #[error("configuration error")] - ConfigError, - - #[error("json error: {0}")] - SerdeJsonError(#[from] serde_json::Error), - - #[error("Lz4 compressionError: {0}")] - Lz4CompressionError(#[from] lz4_flex::block::CompressError), - - #[error("lz4 decompression error: {0}")] - Lz4DecompressionError(#[from] lz4_flex::block::DecompressError), - - #[error("zstd error: {0}")] - ZstdError(#[from] oxiarc_core::error::OxiArcError), - - #[error("Corrupted File")] - CorruptedFile, - - #[error("Thread panicked")] - ThreadPanicked, -} - -impl From for EnkryptitError { - fn from(_: chacha20poly1305::Error) -> Self { - EnkryptitError::Encryption - } -} - -impl From for EnkryptitError { - fn from(_: argon2::Error) -> Self { - EnkryptitError::Argon2Error - } -} diff --git a/src/keygen.rs b/src/keygen.rs deleted file mode 100644 index b3c8434..0000000 --- a/src/keygen.rs +++ /dev/null @@ -1,181 +0,0 @@ -use keyring::Entry; -use libc::{mlock, munlock}; -use rand::{RngCore, rngs::OsRng}; -use std::path::PathBuf; -use zeroize::Zeroize; - -/// The service name for OS's keyring key storage -const SERVICE: &str = "ENKRYPTIT"; - -use crate::{ - conversions::convert_key, errors::EnkryptitError, - parameters::argon2id_parameters::argon2id_parameters, -}; - -use argon2::{Algorithm, Argon2, Version}; - -/// LockedKey Structure, that implements `unsafe` memorylock and memoryunlock (not used in the current state of Enkryptit, I created it at the beginning, but never used it). -struct LockedKey([u8; 32]); - -impl LockedKey { - /// Creates a new `LockedKey` from an existing key. - fn new(key: [u8; 32]) -> Result { - let locked = Self(key); - - unsafe { - if mlock(locked.0.as_ptr() as *const _, locked.0.len()) != 0 { - return Err(EnkryptitError::MemoryLockError); - } - } - - Ok(locked) - } - - fn as_ref(&self) -> &[u8; 32] { - &self.0 - } -} - -impl Drop for LockedKey { - fn drop(&mut self) { - unsafe { - munlock(self.0.as_ptr() as *const _, self.0.len()); - } - - self.0.zeroize(); - } -} - -/// Private helper that creates and returns the `PathBuf` of the key a file was encrypted with. -fn key_path(filename: &str) -> Result { - let mut path = dirs::home_dir().ok_or(EnkryptitError::HomeNotFound)?; - - path.push("private_keys"); - std::fs::create_dir_all(&path)?; - - let file_path = PathBuf::from(filename); - - let stem = if file_path.extension().and_then(|e| e.to_str()) == Some("encky") { - file_path.file_stem() - } else { - file_path.file_name() - } - .ok_or(EnkryptitError::FileError)? - .to_string_lossy() - .into_owned(); - - path.push(format!("enkryptit_{}", stem)); - Ok(path) -} - -/// Hashes the given `password` with argon2id and defined parameters, and returns `(hash, salt)` (`hash` will be used as the key, and `salt` will be inserted in metadata). -pub fn key_from_password(password: &str) -> Result<([u8; 32], [u8; 16]), EnkryptitError> { - let mut salt = [0u8; 16]; - OsRng.fill_bytes(&mut salt); - - let params = argon2id_parameters()?; - - let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); - - let mut key = [0u8; 32]; - - argon2.hash_password_into(password.as_bytes(), &salt, &mut key)?; - - Ok((key, salt)) -} - -// Computes the argon2id hash of [`salt & password`] and returns the hash as the key. -pub fn derive_key(password: &str, salt: [u8; 16]) -> Result<[u8; 32], EnkryptitError> { - let params = argon2id_parameters()?; - - let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); - - let mut key = [0u8; 32]; - - argon2.hash_password_into(password.as_bytes(), &salt, &mut key)?; - - Ok(key) -} - -/// Public helper for generating a key and writing the key_file, given the name of the file to encrypt. -/// \ -/// This function generates the key, but delegates the saving & writing to `save_key_in_file()`. -pub fn generate_key_and_write_file(filename: &str) -> Result<[u8; 32], EnkryptitError> { - let mut key = [0u8; 32]; - OsRng.fill_bytes(&mut key); - - save_key_in_file(filename, key)?; - - Ok(key) -} - -/// Public helper for generating the key and storing it in the OS's keyring. -/// \ -/// This function generates the key, but delegates the saving to `save_key_in_os` -pub fn generate_key_and_store_in_os(filename: &str) -> Result<[u8; 32], EnkryptitError> { - let mut key = [0u8; 32]; - OsRng.fill_bytes(&mut key); - - save_key_in_os(filename, key)?; - - Ok(key) -} - -/// Public helper for using the master key... but unused in the current state of the program. (This function was extracted from `ZetaNet`) -pub fn with_master_key(f: F, filename: &str) -> Result -where - F: FnOnce(&[u8; 32]) -> R, -{ - let key = LockedKey::new(load_key_from_os(filename)?)?; - - Ok(f(key.as_ref())) -} - -/// Public helper for loading the key from the OS's keyring. -pub fn load_key_from_os(filename: &str) -> Result<[u8; 32], EnkryptitError> { - let user = format!("enkryptit{}", filename); - - let entry = Entry::new(SERVICE, &user)?; - - let hex_key = entry.get_password()?; - - let bytes = hex::decode(hex_key)?; - - if bytes.len() != 32 { - return Err(EnkryptitError::InvalidKeyLength); - } - - let mut key = [0u8; 32]; - key.copy_from_slice(&bytes); - - Ok(key) -} - -/// Public helper for saving key in Os's keyring -pub fn save_key_in_os(filename: &str, key: [u8; 32]) -> Result<(), EnkryptitError> { - let user = format!("enkryptit{}", filename); - - let entry = Entry::new("ENKRYPTIT", &user)?; - - entry.set_password(&hex::encode(key))?; - - Ok(()) -} - -/// Public helper for saving key in key file. -pub fn save_key_in_file(filename: &str, key: [u8; 32]) -> Result<(), EnkryptitError> { - let path = key_path(filename)?; - - std::fs::write(path, hex::encode(key))?; - - Ok(()) -} - -/// Public helper for loading key from key file -pub fn load_key_from_file(filename: &str) -> Result<[u8; 32], EnkryptitError> { - let path = key_path(filename)?; - - let key = std::fs::read(path)?; - - Ok(convert_key(hex::decode(key)?)?) -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 27bd72c..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod cli; -pub mod compression; -pub mod conversions; -pub mod encryption; -pub mod errors; -pub mod keygen; -pub mod metadatas; -pub mod parameters; -pub mod treatment; -pub mod tui; -pub mod types; -use crate::types::Version; -pub const VERSION: Version = 2; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index af445eb..0000000 --- a/src/main.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::{ - cli::Output, - errors::EnkryptitError, - parameters::params::load_params, - tui::launch_ui, - types::CompressionType, - types::{KeyParams, Version}, -}; -use clap::{Parser, Subcommand}; -mod cli; -mod compression; -mod conversions; -mod encryption; -mod errors; -mod keygen; -mod metadatas; -mod parameters; -mod treatment; -mod tui; -mod types; - -/// The version of `Enkryptit!` -pub const VERSION: Version = 2; - -use crate::keygen::{ - generate_key_and_store_in_os, generate_key_and_write_file, load_key_from_file, load_key_from_os, -}; -use crate::parameters::params::save_params; -use crate::treatment::object_treatment::treat_object; -use crate::types::KeyParams::{File, Os, PassWord}; -use crate::types::KeyType; - -#[derive(Parser)] -#[command(name = "eck")] -#[command(author = "Olruix")] -#[command(version = "1.0")] -#[command(about = "Fast & Secure File Encryption Manager")] -/// The `Cli` structure for **Enkryptit!** cli-tool -/// \ -/// Contains : -/// \ -/// - The command -/// - The path (for encrypting/decrypting a file or folder) -/// - The password (for encrypting/decrypting a file or folder w/ password) -struct Cli { - #[command(subcommand)] - command: Option, - - /// Chemin vers le fichier ou dossier - #[arg(value_name = "PATH")] - path: Option, - - /// Mot de passe (optionnel) - #[arg(short = 'p', long = "password")] - password: Option, -} - -#[derive(Subcommand)] -/// All the commands available : -/// Ui --> open the UI -/// Params / Parameters -/// |-> no arg : show current parameters -/// |-> compression + : change compression algorithm -/// |-> key type + : change key type -enum Commands { - Ui, - Params { - /// Change le type de compression - #[arg(short = 'c', long = "compression", value_name = "ALGO")] - compression: Option, - - /// Change le type de clé - #[arg(short = 'k', long = "keytype", value_name = "TYPE")] - key_type: Option, - }, - Parameters { - /// Change le type de compression - #[arg(short = 'c', long = "compression", value_name = "ALGO")] - compression: Option, - - /// Change le type de clé - #[arg(long = "keytype", visible_alias = "kt", value_name = "TYPE")] - key_type: Option, - }, -} - -fn main() { - let cli: Cli = Cli::parse(); - - match cli.command { - Some(Commands::Ui) => { - // If the command is `Ui`, we launch the ui. - launch_ui(); - } - Some(Commands::Params { - compression, - key_type, - }) => { - if compression.is_none() && key_type.is_none() { - // If we don't have any arg, we show the current parameters - show_params() - } else { - // Else, we update the parameters - update_params(compression, key_type); - } - } - Some(Commands::Parameters { - compression, - key_type, - }) => { - // Same we `Parameters` - if compression.is_none() && key_type.is_none() { - show_params() - } else { - update_params(compression, key_type); - } - } - None => { - // If we have a path, we treat it using `treat_object_with_path()` - if let Some(path) = cli.path { - match treat_object_with_path(&path, cli.password) { - Ok(output) => treat_output(output), - Err(e) => eprintln!("[ERROR] {}", e), - } - } else { - // If we don't have any path, we launch the UI - launch_ui(); - } - } - } -} - -/// Helper for treating a path. -/// First, we load the parameters, before converting the path from `&str` to `&Path`. -/// Then, if the path doesn't exist, we return an error. -/// If it exist, we continue, by getting the `key` and the `keytype`. -/// Finally, we delegate the object treatment to `treat_object()`. -fn treat_object_with_path( - path_str: &str, - cli_password: Option, -) -> Result { - let parameters = load_params()?; - let path = std::path::Path::new(path_str); - - if !path.exists() { - return Err(EnkryptitError::FileError); - } - - let (key, keytype) = match parameters.key_params { - PassWord => { - let password = match cli_password { - Some(p) => p, - None => { - enter_password!(); - let mut pwd = String::new(); - std::io::stdin().read_line(&mut pwd)?; - pwd.trim().to_string() - } - }; - ([0u8; 32], KeyType::Password(password)) - } - File => match load_key_from_file(path_str) { - Ok(k) => (k, KeyType::FromFile), - Err(_) => (generate_key_and_write_file(path_str)?, KeyType::FromFile), - }, - Os => match load_key_from_os(path_str) { - Ok(k) => (k, KeyType::FromOS), - Err(_) => (generate_key_and_store_in_os(path_str)?, KeyType::FromOS), - }, - }; - - treat_object(¶meters, path_str.to_string(), key, keytype) -} - -/// Helper for treating an `Output`. Given an `Output`, it prints a text in the terminal -fn treat_output(output: Output) { - match output { - Output::Success { message } => println!("Success : {}", message), - Output::Error { error } => eprintln!("[ERROR] {}", error), - Output::CorruptedFile => eprintln!("[ERROR] File is corrupted, or doesn't exist."), - Output::SomethingWentWrong => eprintln!("[ERROR] Something went wrong"), - Output::ParamsChanged => println!("\n Params were changed ! \n"), - _ => {} - } -} - -/// Helper for showing the parameters. -/// It loads the current parameters before priting the parameters, or printing an error. -fn show_params() { - match load_params() { - Ok(params) => { - println!("\n Actual parameters :"); - println!(" Key Type : {:?}", params.key_params); - println!(" Compression : {:?}", params.compression); - println!(); - } - Err(e) => { - eprintln!("[ERROR] {}", e); - std::process::exit(1); - } - } -} - -/// Helper for updating the parameters. -fn update_params(compression: Option, key_type: Option) { - match load_params() { - Ok(mut params) => { - if let Some(c) = compression { - match c.to_lowercase().as_str() { - "zstd" | "1" => params.compression = CompressionType::Zstd, - "lz4" | "2" => params.compression = CompressionType::Lz4, - "xz" | "3" => params.compression = CompressionType::Xz, - "none" | "no" | "4" => params.compression = CompressionType::NoComp, - other => { - eprintln!("[ERROR] Unknown compression: {}", other); - std::process::exit(1); - } - } - } - - if let Some(kt) = key_type { - match kt.to_lowercase().as_str() { - "password" | "pwd" | "1" => params.key_params = KeyParams::PassWord, - "os" | "2" => params.key_params = KeyParams::Os, - "file" | "3" => params.key_params = KeyParams::File, - other => { - eprintln!("[ERROR] Unknown key type: {}", other); - std::process::exit(1); - } - } - } - - match save_params(¶ms) { - Ok(_) => println!("\n Params were changed ! \n"), - Err(e) => { - eprintln!("[ERROR] Failed to save: {}", e); - std::process::exit(1); - } - } - } - Err(e) => { - eprintln!("[ERROR] {}", e); - std::process::exit(1); - } - } -} diff --git a/src/treatment/file_case.rs b/src/treatment/file_case.rs deleted file mode 100644 index 1335530..0000000 --- a/src/treatment/file_case.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::cli::Output; -use crate::encryption::file_encryption::{decrypt_file, encrypt_file}; -use crate::errors::EnkryptitError; -use crate::parameters::params::EnkryptitParams; -use crate::types::KeyType::{self}; - -/// Public helper for encrypting a file (Converts Ok<>/EnkryptitError to Output) -pub fn encrypt_file_case( - parameters: &EnkryptitParams, - path: &str, - key: [u8; 32], - key_type: KeyType, -) -> Result { - match encrypt_file(path, key, parameters, key_type) { - Ok(path) => Ok(Output::Success { - message: format!("file was encrypted at {} !", &path).to_string(), - }), - Err(e) => return Ok(Output::Error { error: e }), - } -} - -/// Public helper for decrypting a file (Converts Ok<>/EnkryptitError to Output) -pub fn decrypt_file_case( - path: &str, - meta: Vec, - key: [u8; 32], - key_type: KeyType, - payload_offset: u64, -) -> Result { - match decrypt_file(path, &meta, key, payload_offset, key_type) { - Ok(path) => Ok(Output::Success { - message: format!("file was decrypted at {} !", &path).to_string(), - }), - Err(e) => return Ok(Output::Error { error: e }), - } -} diff --git a/src/treatment/folder_case.rs b/src/treatment/folder_case.rs deleted file mode 100644 index 4b74fb3..0000000 --- a/src/treatment/folder_case.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::cli::Output; -use crate::encryption::folder_encryption::{decrypt_folder, encrypt_folder}; -use crate::errors::EnkryptitError; -use crate::parameters::params::EnkryptitParams; -use crate::types::KeyType::{self}; - -/// Public helper for encrypting a folder. (Converts Ok<>/EnkryptitError to Output). -pub fn encrypt_folder_case( - path: &str, - parameters: &EnkryptitParams, - key: [u8; 32], - key_type: KeyType, -) -> Result { - match encrypt_folder(path, parameters, key, key_type) { - Ok(path) => Ok(Output::Success { - message: format!("folder was encrypted at {} !", path), - }), - Err(e) => Ok(Output::Error { error: e }), - } -} - -/// Public helper for decrypting a folder. (Converts Ok<>/EnkryptitError to Output) -pub fn decrypt_folder_case( - path: &str, - key: [u8; 32], - metadatas_bytes: Vec, - payload_offset: u64, - key_type: KeyType, - version: u8, -) -> Result { - match decrypt_folder( - path, - &metadatas_bytes, - key, - payload_offset, - key_type, - version, - ) { - Ok(path) => Ok(Output::Success { - message: format!("folder was decrypted at {} !", path), - }), - Err(e) => Ok(Output::Error { error: e }), - } -} diff --git a/src/tui.rs b/src/tui.rs deleted file mode 100644 index 3af5888..0000000 --- a/src/tui.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::VERSION; -use crate::cli::Output; -use crate::errors::EnkryptitError; -use crate::keygen::{ - generate_key_and_store_in_os, generate_key_and_write_file, load_key_from_file, load_key_from_os, -}; -use crate::parameters::params::{EnkryptitParams, load_params, save_params}; -use crate::treatment::object_treatment::treat_object; -use crate::types::CompressionType::{self, Lz4, NoComp, Xz}; -use crate::types::KeyParams::{File, Os, PassWord}; -use crate::types::KeyType; -use colored::*; -use inquire::{Select, Text}; - -// Macros with colors - -/// Success macro helper -macro_rules! success { - ($message:expr) => { - println!("\n[OK] {}", $message.to_string().green()); - }; -} - -/// Log macro helper -macro_rules! log_error { - ($msg:expr) => { - eprintln!("\n[ERROR] {}", $msg.to_string().red()); - }; -} - -/// Show Params macro helper -macro_rules! show_params { - ($kt:expr, $c:expr) => { - println!("\n{}", "Current Parameters".cyan().bold()); - println!(" Key Type: {}", format!("{:?}", $kt).yellow()); - println!(" Compression: {}", format!("{:?}", $c).yellow()); - }; -} - -// Basic UI - -/// Public function that launches the TUI -pub fn launch_ui() { - println!("\n{}", "Enkryptit".cyan().bold()); - println!(" Fast & Secure File Encryption Manager v{}", VERSION); - - loop { - let choices = vec!["Encrypt/Decrypt file/folder", "Parameters", "Help", "Exit"]; - - let choice = Select::new("What do you want to do?", choices) - .with_starting_cursor(0) - .prompt(); - - match choice { - Ok("Encrypt/Decrypt file/folder") => { - if let Err(e) = handle_encrypt_object() { - log_error!(e); - } - } - Ok("Parameters") => { - if let Err(e) = launch_params() { - log_error!(e); - } - } - Ok("Help") => show_help(), - Ok("Exit") => { - println!("\n{}", "Goodbye!".green()); - break; - } - Err(_) => { - log_error!("Selection cancelled"); - continue; - } - _ => continue, - } - } -} - -/// Private helper for printing the help in TUI mode -fn show_help() { - println!("\n{}", "Available Commands".cyan().bold()); - println!(" Encrypt/Decrypt -> Process a file"); - println!(" Parameters -> Configure settings"); - println!(" Help -> Show this help"); - println!(" Exit -> Quit application"); -} - -/// Private helper for encrypting an object. -fn handle_encrypt_object() -> Result<(), EnkryptitError> { - let path = Text::new("Enter file path:") - .with_help_message("Path to the file/folder to encrypt/decrypt") - .prompt() - .map_err(|_| EnkryptitError::CommandNotFound)?; - - let parameters = load_params()?; - - let (key, keytype) = match parameters.key_params { - PassWord => { - let password = rpassword::prompt_password("Enter password: ").unwrap(); - ([0u8; 32], KeyType::Password(password)) - } - File => match load_key_from_file(&path) { - Ok(k) => (k, KeyType::FromFile), - Err(_) => (generate_key_and_write_file(&path)?, KeyType::FromFile), - }, - Os => match load_key_from_os(&path) { - Ok(k) => (k, KeyType::FromOS), - Err(_) => (generate_key_and_store_in_os(&path)?, KeyType::FromOS), - }, - }; - - match treat_object(¶meters, path, key, keytype)? { - Output::Success { message } => { - success!(message); - Ok(()) - } - Output::Error { error } => Err(error), - Output::CorruptedFile => { - log_error!("File is corrupted or doesn't exist"); - Ok(()) - } - Output::SomethingWentWrong => { - log_error!("Something went wrong"); - Ok(()) - } - _ => Ok(()), - } -} - -/// Parameters UI - -/// Launch the params UI -pub fn launch_params() -> Result<(), EnkryptitError> { - println!("\n{}", "Parameters Panel".cyan().bold()); - - loop { - let choices = vec![ - "Change compression type", - "Change key type", - "Switch Ui", - "Show current parameters", - "Back to main menu", - ]; - - let choice = Select::new("What do you want to configure?", choices).prompt(); - - match choice { - Ok("Change compression type") => change_compression()?, - Ok("Change key type") => change_key_type()?, - Ok("Show current parameters") => show_current_params()?, - Ok("Back to main menu") => break, - Err(_) => { - log_error!("Selection cancelled"); - continue; - } - _ => continue, - } - } - - success!("Parameters updated successfully!"); - Ok(()) -} - -/// Private helper for changing compression -fn change_compression() -> Result<(), EnkryptitError> { - let old_params = load_params()?; - - let choices = vec![ - "Zstd (balanced)", - "Lz4 (fastest)", - "Xz (best compression)", - "No compression", - ]; - - let choice = Select::new("Select compression type:", choices) - .prompt() - .map_err(|_| EnkryptitError::CommandNotFound)?; - - let compression = match choice { - "Zstd (balanced)" => CompressionType::Zstd, - "Lz4 (fastest)" => Lz4, - "Xz (best compression)" => Xz, - "No compression" => NoComp, - _ => CompressionType::Zstd, - }; - - let params = EnkryptitParams::new(old_params.key_params, compression); - save_params(¶ms)?; - - success!(format!("Compression changed to {:?}", compression)); - Ok(()) -} - -/// Private helper for changing key type -fn change_key_type() -> Result<(), EnkryptitError> { - let old_params = load_params()?; - - let choices = vec!["Password (Argon2id)", "OS Keyring", "Key File"]; - - let choice = Select::new("Select key type:", choices) - .prompt() - .map_err(|_| EnkryptitError::CommandNotFound)?; - - let kt = match choice { - "Password (Argon2id)" => PassWord, - "OS Keyring" => Os, - "Key File" => File, - _ => PassWord, - }; - - let params = EnkryptitParams::new(kt.clone(), old_params.compression); - save_params(¶ms)?; - - success!(format!("Key type changed to {:?}", kt)); - Ok(()) -} - -/// Private helper for printing current parameters -fn show_current_params() -> Result<(), EnkryptitError> { - let params = load_params()?; - show_params!(params.key_params, params.compression); - Ok(()) -} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 4ee8529..0000000 --- a/src/types.rs +++ /dev/null @@ -1,46 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Version type -pub type Version = u8; - -/// Chunk size of a bloc to compress & encrypt -pub const CHUNK_SIZE: usize = 8 * 1024 * 1024; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -/// KeyType enum. Contains : -/// - Password(String) -/// - Pwd256(salt) -/// - FromFile -/// - FromOS -/// - None -pub enum KeyType { - Password(String), - Pwd256([u8; 16]), - FromFile, - FromOS, - None, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -/// KeyParams enum. Contains : -/// - PassWord -/// - File -/// - Os -pub enum KeyParams { - PassWord, - File, - Os, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -/// CompressionType enum. Contains : -/// - Lz4 -/// - Zstd -/// - Xz -/// - NoComp -pub enum CompressionType { - Lz4, - Zstd, - Xz, - NoComp, -} diff --git a/test/integration/cli_tests.rs b/test/integration/cli_tests.rs deleted file mode 100644 index 8eadedd..0000000 --- a/test/integration/cli_tests.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! CLI Command Parsing Tests -//! -//! Test command-line interface behavior: arguments, subcommands, error handling - -use crate::mocks::helpers::{TestConfigGuard, encrypted_path_for}; -use assert_cmd::assert::OutputAssertExt; -use assert_cmd::cargo::CommandCargoExt; -use std::fs; -use std::process::{Command, Stdio}; -use tempfile::{NamedTempFile, TempDir}; - -/// Build a `eck` command pointing at the freshly built binary (never PATH). -fn eck_cmd(args: &[&str]) -> Command { - let mut cmd = Command::cargo_bin("eck").unwrap(); - cmd.args(args); - cmd -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn cli_no_args_launches_tui() { - // Test that running 'eck' without args launches TUI (will timeout) - let mut child = Command::cargo_bin("eck") - .unwrap() - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("Failed to start eck"); - - // Kill the process after a short delay (TUI waits for input) - std::thread::sleep(std::time::Duration::from_millis(100)); - - let _ = child.kill(); - } - - #[test] - fn cli_ui_subcommand_launches_tui() { - // Test that 'eck ui' explicitly launches TUI - let mut child = Command::cargo_bin("eck") - .unwrap() - .arg("ui") - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("Failed to start eck ui"); - - std::thread::sleep(std::time::Duration::from_millis(100)); - - let _ = child.kill(); - } - - #[test] - fn cli_params_command_shows_config() { - let _guard = TestConfigGuard::new("PassWord", "Zstd"); - - let output = eck_cmd(&["params"]) - .output() - .expect("Failed to execute eck params"); - - assert!( - output.status.success(), - "stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Actual parameters"), "stdout: {stdout}"); - assert!(stdout.contains("Key Type : PassWord"), "stdout: {stdout}"); - assert!(stdout.contains("Compression : Zstd"), "stdout: {stdout}"); - } - - #[test] - fn cli_params_set_compression_zstd() { - let guard = TestConfigGuard::new("PassWord", "NoComp"); - - let output = eck_cmd(&["params", "--compression", "zstd"]) - .output() - .expect("Failed to execute eck params"); - - assert!( - output.status.success(), - "stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Params were changed"), "stdout: {stdout}"); - - // The change must be persisted to the isolated config file - let saved = fs::read_to_string(guard.path()).unwrap(); - assert!(saved.contains("\"Zstd\""), "config: {saved}"); - - // And a subsequent read reflects it - let output = eck_cmd(&["params"]).output().unwrap(); - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Compression : Zstd"), "stdout: {stdout}"); - } - - #[test] - fn cli_params_set_keytype_os() { - let guard = TestConfigGuard::new("PassWord", "Zstd"); - - let output = eck_cmd(&["params", "--keytype", "os"]) - .output() - .expect("Failed to execute eck params"); - - assert!( - output.status.success(), - "stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Params were changed"), "stdout: {stdout}"); - - let saved = fs::read_to_string(guard.path()).unwrap(); - assert!(saved.contains("\"Os\""), "config: {saved}"); - - let output = eck_cmd(&["params"]).output().unwrap(); - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Key Type : Os"), "stdout: {stdout}"); - } - - #[test] - fn cli_encrypt_file_with_password_flag() { - let _guard = TestConfigGuard::new("PassWord", "NoComp"); - let temp_file = NamedTempFile::new().unwrap(); - - fs::write(temp_file.path(), b"Test content for CLI encryption.").unwrap(); - - let encrypted_path = encrypted_path_for(temp_file.path()); - - Command::cargo_bin("eck") - .unwrap() - .arg("-p") - .arg("test123") - .arg(temp_file.path()) - .assert() - .success(); - assert!( - encrypted_path.exists(), - "archive should exist after encrypt" - ); - - Command::cargo_bin("eck") - .unwrap() - .arg("-p") - .arg("test123") - .arg(&encrypted_path) - .assert() - .success(); - - let restored = fs::read_to_string(temp_file.path()).unwrap(); - assert_eq!(restored, "Test content for CLI encryption."); - } - - #[test] - fn cli_folder_encrypt_decrypt_roundtrip() { - let _guard = TestConfigGuard::new("PassWord", "Zstd"); - - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("cli_folder"); - fs::create_dir_all(folder.join("nested")).unwrap(); - fs::write(folder.join("a.txt"), b"alpha content").unwrap(); - fs::write(folder.join("nested/b.txt"), b"beta content").unwrap(); - - Command::cargo_bin("eck") - .unwrap() - .arg("-p") - .arg("fold3r") - .arg(&folder) - .assert() - .success(); - - let archive_path = encrypted_path_for(&folder); - assert!(archive_path.exists(), "folder archive should exist"); - - fs::remove_dir_all(&folder).unwrap(); - - Command::cargo_bin("eck") - .unwrap() - .arg("-p") - .arg("fold3r") - .arg(&archive_path) - .assert() - .success(); - - assert_eq!( - fs::read_to_string(folder.join("a.txt")).unwrap(), - "alpha content" - ); - assert_eq!( - fs::read_to_string(folder.join("nested/b.txt")).unwrap(), - "beta content" - ); - } - - #[test] - fn corrupt_config_falls_back_to_defaults() { - let _guard = TestConfigGuard::with_raw_content("{ definitely :: not json ]"); - - let output = eck_cmd(&["params"]).output().unwrap(); - - assert!( - output.status.success(), - "corrupt config must degrade to defaults, stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Key Type : PassWord"), "stdout: {stdout}"); - assert!(stdout.contains("Compression : Zstd"), "stdout: {stdout}"); - } - - #[test] - fn repeated_param_updates_keep_config_valid_json() { - let guard = TestConfigGuard::new("PassWord", "NoComp"); - - for algo in ["lz4", "xz", "zstd"] { - let output = eck_cmd(&["params", "--compression", algo]) - .output() - .unwrap(); - assert!(output.status.success()); - } - - let saved = fs::read_to_string(guard.path()).unwrap(); - let parsed: serde_json::Value = - serde_json::from_str(&saved).expect("config must remain parseable after updates"); - assert_eq!(parsed["compression"], "Zstd"); - assert_eq!(parsed["key_params"], "PassWord"); - } -} diff --git a/test/integration/folder_encryption.rs b/test/integration/folder_encryption.rs deleted file mode 100644 index d84873d..0000000 --- a/test/integration/folder_encryption.rs +++ /dev/null @@ -1,248 +0,0 @@ -#[cfg(test)] -mod tests { - use eck::encryption::folder_encryption::entries::collect_folder_entries; - use eck::encryption::folder_encryption::{decrypt_folder, encrypt_folder}; - use eck::metadatas::{ArchiveHeader, FolderMetadata}; - use eck::parameters::params::EnkryptitParams; - use eck::types::{CompressionType, KeyParams, KeyType}; - use postcard::from_bytes; - use std::fs; - use std::io::{BufReader, Read, Seek, SeekFrom}; - use tempfile::TempDir; - - fn test_key() -> [u8; 32] { - [0xAB; 32] - } - - fn test_params(compression: CompressionType) -> EnkryptitParams { - EnkryptitParams { - compression, - key_params: KeyParams::PassWord, - } - } - - fn read_archive_meta(archive_path: &str) -> (u8, Vec) { - let file = std::fs::File::open(archive_path).unwrap(); - let mut reader = BufReader::new(file); - let mut len_buf = [0u8; 1]; - reader.read_exact(&mut len_buf).unwrap(); - let header_len = len_buf[0] as usize; - let mut header_bytes = vec![0u8; header_len]; - reader.read_exact(&mut header_bytes).unwrap(); - let header: ArchiveHeader = from_bytes(&header_bytes).unwrap(); - let meta_len = header.meta_len as usize; - - let file_len = std::fs::metadata(archive_path).unwrap().len(); - let meta_start = file_len - meta_len as u64; - reader.seek(SeekFrom::Start(meta_start)).unwrap(); - let mut meta = vec![0u8; meta_len]; - reader.read_exact(&mut meta).unwrap(); - (header.version, meta) - } - - #[test] - fn encrypt_decrypt_single_file_folder_nocomp() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("testfolder"); - fs::create_dir(&folder).unwrap(); - fs::write(folder.join("hello.txt"), b"Hello from single file!").unwrap(); - - let params = test_params(CompressionType::NoComp); - let archive_path = - encrypt_folder(folder.to_str().unwrap(), ¶ms, test_key(), KeyType::None).unwrap(); - assert!(std::path::Path::new(&archive_path).exists()); - - let (version, meta_bytes) = read_archive_meta(&archive_path); - assert_eq!(version, 2); - let folder_meta: FolderMetadata = from_bytes(&meta_bytes).unwrap(); - assert_eq!(folder_meta.entries.len(), 1); - assert_eq!(folder_meta.entries[0].relative_path, "hello.txt"); - assert!(folder_meta.entries[0].offset > 0); - - fs::remove_dir_all(&folder).unwrap(); - let dest = decrypt_folder( - &archive_path, - &meta_bytes, - test_key(), - 0, - KeyType::None, - version, - ) - .unwrap(); - - let decrypted = fs::read_to_string(std::path::Path::new(&dest).join("hello.txt")).unwrap(); - assert_eq!(decrypted, "Hello from single file!"); - } - - #[test] - fn encrypt_decrypt_multi_file_folder_nocomp() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("multifolder"); - fs::create_dir_all(folder.join("subdir")).unwrap(); - fs::write(folder.join("file1.txt"), b"Content of file 1").unwrap(); - fs::write(folder.join("subdir/file2.txt"), b"Content of file 2").unwrap(); - fs::write(folder.join("subdir/file3.txt"), b"Third file content here").unwrap(); - - let params = test_params(CompressionType::NoComp); - let archive_path = - encrypt_folder(folder.to_str().unwrap(), ¶ms, test_key(), KeyType::None).unwrap(); - - let (version, meta_bytes) = read_archive_meta(&archive_path); - let folder_meta: FolderMetadata = from_bytes(&meta_bytes).unwrap(); - assert_eq!(folder_meta.entries.len(), 3); - - let mut offsets: Vec = folder_meta.entries.iter().map(|e| e.offset).collect(); - let sorted_offsets = offsets.clone(); - offsets.sort(); - assert_eq!(offsets, sorted_offsets); - - fs::remove_dir_all(&folder).unwrap(); - let dest = decrypt_folder( - &archive_path, - &meta_bytes, - test_key(), - 0, - KeyType::None, - version, - ) - .unwrap(); - - let base = std::path::Path::new(&dest); - assert_eq!( - fs::read_to_string(base.join("file1.txt")).unwrap(), - "Content of file 1" - ); - assert_eq!( - fs::read_to_string(base.join("subdir/file2.txt")).unwrap(), - "Content of file 2" - ); - assert_eq!( - fs::read_to_string(base.join("subdir/file3.txt")).unwrap(), - "Third file content here" - ); - } - - #[test] - fn encrypt_decrypt_zstd_roundtrip() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("zstdfolder"); - fs::create_dir(&folder).unwrap(); - fs::write(folder.join("a.txt"), b"Zstd compressed content").unwrap(); - fs::write(folder.join("b.bin"), vec![0xAB; 1024 * 100]).unwrap(); - - let params = test_params(CompressionType::Zstd); - let archive_path = - encrypt_folder(folder.to_str().unwrap(), ¶ms, test_key(), KeyType::None).unwrap(); - - let (version, meta_bytes) = read_archive_meta(&archive_path); - - fs::remove_dir_all(&folder).unwrap(); - let dest = decrypt_folder( - &archive_path, - &meta_bytes, - test_key(), - 0, - KeyType::None, - version, - ) - .unwrap(); - - let base = std::path::Path::new(&dest); - assert_eq!( - fs::read_to_string(base.join("a.txt")).unwrap(), - "Zstd compressed content" - ); - assert_eq!( - fs::read(base.join("b.bin")).unwrap(), - vec![0xAB; 1024 * 100] - ); - } - - #[test] - fn encrypt_decrypt_lz4_roundtrip() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("lz4folder"); - fs::create_dir(&folder).unwrap(); - fs::write(folder.join("data.bin"), vec![0x42; 50_000]).unwrap(); - - let params = test_params(CompressionType::Lz4); - let archive_path = - encrypt_folder(folder.to_str().unwrap(), ¶ms, test_key(), KeyType::None).unwrap(); - - let (version, meta_bytes) = read_archive_meta(&archive_path); - - fs::remove_dir_all(&folder).unwrap(); - let dest = decrypt_folder( - &archive_path, - &meta_bytes, - test_key(), - 0, - KeyType::None, - version, - ) - .unwrap(); - - assert_eq!( - fs::read(std::path::Path::new(&dest).join("data.bin")).unwrap(), - vec![0x42; 50_000] - ); - } - - #[test] - fn encrypt_decrypt_xz_roundtrip() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("xzfolder"); - fs::create_dir(&folder).unwrap(); - fs::write(folder.join("readme.txt"), b"XZ compression test content").unwrap(); - - let params = test_params(CompressionType::Xz); - let archive_path = - encrypt_folder(folder.to_str().unwrap(), ¶ms, test_key(), KeyType::None).unwrap(); - - let (version, meta_bytes) = read_archive_meta(&archive_path); - - fs::remove_dir_all(&folder).unwrap(); - let dest = decrypt_folder( - &archive_path, - &meta_bytes, - test_key(), - 0, - KeyType::None, - version, - ) - .unwrap(); - - assert_eq!( - fs::read_to_string(std::path::Path::new(&dest).join("readme.txt")).unwrap(), - "XZ compression test content" - ); - } - - #[test] - fn empty_folder_returns_error() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("empty"); - fs::create_dir(&folder).unwrap(); - - let params = test_params(CompressionType::NoComp); - let result = encrypt_folder(folder.to_str().unwrap(), ¶ms, test_key(), KeyType::None); - assert!(result.is_err()); - } - - #[test] - fn collect_entries_finds_nested_files() { - let tmp = TempDir::new().unwrap(); - let folder = tmp.path().join("walk"); - fs::create_dir_all(folder.join("a/b/c")).unwrap(); - fs::write(folder.join("root.txt"), b"r").unwrap(); - fs::write(folder.join("a/mid.txt"), b"m").unwrap(); - fs::write(folder.join("a/b/c/deep.txt"), b"d").unwrap(); - - let entries = collect_folder_entries(folder.to_str().unwrap()).unwrap(); - assert_eq!(entries.len(), 3); - - let mut paths: Vec<&str> = entries.iter().map(|e| e.relative_path.as_str()).collect(); - paths.sort(); - assert_eq!(paths, vec!["a/b/c/deep.txt", "a/mid.txt", "root.txt"]); - } -} diff --git a/test/mod.rs b/test/mod.rs deleted file mode 100644 index 7ea6b25..0000000 --- a/test/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Enkryptit Test Suite -//! -//! Comprehensive testing for file/folder encryption using XChaCha20-Poly1305 + Argon2id -//! Tests organized by category with explicit parallel execution support - -pub mod integration; -pub mod mocks; -pub mod unit; - -/// Configure test thread pool size explicitly (default 8 threads) -#[cfg(test)] -fn setup_parallel_execution() { - // Enable parallel test execution - adjust number of threads as needed - unsafe { - std::env::set_var("RUST_TEST_THREADS", "16"); - } -} - -#[cfg(all(test))] -mod runtime_tests { - use super::*; - - #[test] - fn thread_configuration_valid() { - setup_parallel_execution(); - assert!(std::env::var("RUST_TEST_THREADS").is_ok()); - } -} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..b8c431c --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "xtask" +version = "0.0.0" +edition = "2024" +rust-version = "1.85" +publish = false \ No newline at end of file diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..ff41d30 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,96 @@ +use std::process::Command; + +fn main() { + let args: Vec = std::env::args().collect(); + let task = args.get(1).map(|s| s.as_str()).unwrap_or("help"); + let release = args.iter().any(|a| a == "--release"); + + match task { + "build" => { + if release { + run_cargo(&["build", "--release"]); + } else { + run_cargo(&["build"]); + } + } + + "install" => { + run_cargo(&["install", "--path", "./eck"]); + } + + "test" => { + run_cargo(&["test", "--test=eck_tests"]); + } + + "fuzz" => { + let args: Vec = std::env::args().skip(2).collect(); + + if args.is_empty() || args.iter().any(|a| a == "--help" || a == "-h") { + run_cargo_in("eck", &["fuzz", "list"]); + } else { + let mut cmd: Vec<&str> = vec!["fuzz", "run", args[0].as_str()]; + if args.len() > 1 { + cmd.push("--"); + cmd.extend(args[1..].iter().map(String::as_str)); + } + run_cargo_in("eck", &cmd); + } + } + + "fmt" => { + run_cargo(&["fmt"]); + } + + "help" | "--help" | "-h" => { + print_help(); + } + + "doc" | "rustdoc" | "rsdoc" => { + run_cargo(&["doc", "--open", "--document-private-items"]); + } + _ => { + eprintln!("Unknown task: {task}"); + print_help(); + std::process::exit(1); + } + } +} + +fn print_help() { + eprintln!( + "\ +cargo xtask — dev helpers for Enkryptit! + +USAGE: + cargo xtask [--release] + +BUILD COMMANDS: + build Build eck (Enkryptit!'s binary) + install Install eck + +TEST: + test Run workspace tests + fuzz List fuzz targets (libFuzzer, via cargo-fuzz) + fuzz [FLAGS] Run a fuzz target; extra flags go to libFuzzer + (e.g. -runs=1000, -max_total_time=60) + +OTHER: + fmt Format code + " + ); +} + +pub fn run_cargo(args: &[&str]) { + run_cargo_in(".", args); +} + +pub fn run_cargo_in(dir: &str, args: &[&str]) { + let status = Command::new("cargo") + .args(args) + .current_dir(dir) + .status() + .expect("Unable to run cargo"); + if !status.success() { + std::process::exit(status.code().unwrap_or(1)); + } +}