ripasso-0.8.0/.cargo/audit.toml000064400000000000000000000003011046102023000144570ustar 00000000000000[advisories] ### motivations: ### RUSTSEC-2022-0040: We don't use the reexported OwningHandle type in cursive ### see https://github.com/gyscos/cursive/issues/678 ignore = ["RUSTSEC-2022-0040"]ripasso-0.8.0/.cargo_vcs_info.json0000644000000001360000000000100125210ustar { "git": { "sha1": "dce021320540f3730b34f8b1f0344568d171fb0e" }, "path_in_vcs": "" }ripasso-0.8.0/.github/FUNDING.yml000064400000000000000000000000271046102023000144650ustar 00000000000000github: alexanderkjall ripasso-0.8.0/.github/workflows/rust.yml000064400000000000000000000053531046102023000164340ustar 00000000000000on: push: branches: - master - release/* pull_request: name: Continuous integration jobs: check: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Dependencies run: sudo apt update -y && sudo apt install libgpgme11-dev libgpg-error-dev nettle-dev -y - uses: actions-rs/cargo@v1 with: command: check ubuntutest: name: Test Suite on Ubuntu runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Dependencies run: sudo apt update -y && sudo apt install libssl-dev libclang-dev libadwaita-1-dev libgpgme11-dev libgpg-error-dev libgtk-4-dev libxcb-shape0-dev libxcb-xfixes0-dev nettle-dev -y - uses: actions-rs/cargo@v1 with: command: test args: --all macostest: name: Test Suite on MacOS runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Install dependencies run: | brew update || true brew install gpgme nettle || true - uses: actions-rs/cargo@v1 with: command: test args: -p ripasso-cursive fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add rustfmt - name: Dependencies run: sudo apt update -y && sudo apt install libgpgme11-dev libgpg-error-dev nettle-dev -y - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add clippy - name: Dependencies run: sudo apt update -y && sudo apt install libgpgme11-dev libgpg-error-dev nettle-dev -y - uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings audit: name: Audit runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/checkout@v3 - uses: actions-rust-lang/audit@v1 name: Audit Rust Dependencies ripasso-0.8.0/.gitignore000064400000000000000000000001341046102023000132770ustar 00000000000000/target/ *.qmlc **/*~ **/*.bk .idea/ *.iml *.kdev4 #Generated binary translation files *.moripasso-0.8.0/.rustfmt.toml000064400000000000000000000001011046102023000137600ustar 00000000000000imports_granularity = "Crate" group_imports = "StdExternalCrate" ripasso-0.8.0/BUILD_INSTRUCTIONS.md000064400000000000000000000043421046102023000145210ustar 00000000000000# Build instructions ## Build dependencies Ripasso depends on a number of local libraries through it's dependencies: * openssl - for git operations * libgit2 - for git operations * libgpgerror - for the gpgme encryption backend * gpgme - for the gpgme encryption backend * xorg - for the clippboard * nettle-dev - for the sequoia encryption backend They are named different things on different platforms ### Mac OS X ``` $ brew update $ brew install automake cmake gettext gtk+4 gpgme $ git clone https://github.com/cortex/ripasso.git $ cd ripasso $ cargo run ``` ### Ubuntu ``` $ apt install cargo libssl-dev libclang-dev libadwaita-1-dev libgpgme11-dev libgpg-error-dev libgtk-4-dev libxcb-shape0-dev libxcb-xfixes0-dev nettle-dev $ cargo build --all ``` ### Fedora #### All ``` $ dnf install cargo gpgme-devel openssl-devel libxcb libxcb-devel nettle-devel ``` #### GTK ``` $ dnf install rust-gdk-devel ``` ## Building Perform the build with: ``` cargo build --all --frozen --release ``` The argument `--frozen` ensures that the content of the `Cargo.lock` file is respected so that the build is repeatable, this is mostly of interest for package maintainers in distributions. ### Build artifacts The build produces a number of artifacts: * `./target/release/ripasso-cursive` - the main application binary, with the curses TUI * `./target/release/ripasso-gtk` - the GTK application, still in an experimental phase * `./target/man-page/cursive/ripasso-cursive.1` - The manual page for ripasso-cursive * `./target/translations/cursive/de.mo` - german translation * `./target/translations/cursive/fr.mo` - french translation * `./target/translations/cursive/it.mo` - italian translation * `./target/translations/cursive/nb.mo` - norwegian bokmål translation * `./target/translations/cursive/nn.mo` - norwegian nynorsk translation * `./target/translations/cursive/sv.mo` - swedish translation The translation files are in gettext binary format, and should be installed in `/usr/share/locale/{}/LC_MESSAGES/ripasso-cursive.mo` where `{}` should be replaced with the locale. If that location doesn't conform to your distribution's guidelines, then you can supply the environmental variable `TRANSLATION_INPUT_PATH` when building to specify another. ripasso-0.8.0/Cargo.lock0000644000002747340000000000100105150ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anyhow" version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arboard" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", "core-graphics", "image", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", "windows-sys 0.48.0", "x11rb", ] [[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 = "ascii-canvas" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" dependencies = [ "term 0.7.0", ] [[package]] name = "ascii-canvas" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" dependencies = [ "term 1.0.1", ] [[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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "base32" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags 2.9.0", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec 0.6.3", ] [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec 0.8.0", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[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 0.14.7", ] [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ "objc2", ] [[package]] name = "buffered-reader" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db26bf1f092fd5e05b5ab3be2f290915aeb6f3f20c4e9f86ce0f07f336c2412f" dependencies = [ "bzip2", "flate2", "libc", ] [[package]] name = "build-rs" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b00b8763668c99f8d9101b8a0dd82106f58265464531a79b2cef0d9a30c17dd2" [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" dependencies = [ "bzip2-sys", ] [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" dependencies = [ "cc", "pkg-config", ] [[package]] name = "capnp" version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e985a566bdaae9a428a957d12b10c318d41b2afddb54cfbb764878059df636e" dependencies = [ "embedded-io", ] [[package]] name = "capnp-futures" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f3ee810b3890498e51028448ac732cdd5009223897124dd2fac6b085b5d867" dependencies = [ "capnp", "futures", ] [[package]] name = "capnp-rpc" version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe57ab22a5e121e6fddaf36e837514aab9ae888bcff2baa6fda5630820dfc501" dependencies = [ "capnp", "capnp-futures", "futures", ] [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-expr" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", "target-lexicon", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] [[package]] name = "config" version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80" dependencies = [ "pathdiff", "serde", "toml", "winnow", ] [[package]] name = "constant_time_eq" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" [[package]] name = "conv" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" dependencies = [ "custom_derive", ] [[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 = "core-graphics" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types 0.5.0", "libc", ] [[package]] name = "core-graphics-types" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation", "libc", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.7", "typenum", ] [[package]] name = "cstr-argument" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bd9c8e659a473bce955ae5c35b116af38af11a7acb0b480e01f3ed348aeb40" dependencies = [ "cfg-if", "memchr", ] [[package]] name = "ctor" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn", ] [[package]] name = "custom_derive" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" [[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 = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dyn-clone" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embedded-io" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "ena" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[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.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "error-code" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "filetime" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.59.0", ] [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fixedbitset" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared 0.1.1", ] [[package]] name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", "foreign-types-shared 0.3.1", ] [[package]] name = "foreign-types-macros" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", "winapi", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[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 = "generic-array" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" dependencies = [ "typenum", ] [[package]] name = "gethostname" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", "windows-targets 0.48.5", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git2" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9" dependencies = [ "bitflags 2.9.0", "libc", "libgit2-sys", "log", "openssl-probe", "openssl-sys", "url", ] [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gpg-error" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545aae14d0e95734d639c8076304e6e86de765c19c76bead3648583d9caed919" dependencies = [ "libgpg-error-sys", ] [[package]] name = "gpgme" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57539732fbe58eacdb984734b72b470ed0bca3ab7a49813271878567025ac44f" dependencies = [ "bitflags 1.3.2", "cfg-if", "conv", "cstr-argument", "gpg-error", "gpgme-sys", "libc", "memoffset", "once_cell", "smallvec", "static_assertions", ] [[package]] name = "gpgme-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "509223d659c06e4a26229437d6ac917723f02d31917c86c6ecd50e8369741cf7" dependencies = [ "build-rs", "libc", "libgpg-error-sys", "system-deps", "winreg 0.10.1", ] [[package]] name = "h2" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "half" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[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.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "libc", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "image" version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", "num-traits", "png", "tiff", ] [[package]] name = "indexmap" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", "windows-sys 0.59.0", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ "getrandom 0.3.2", "libc", ] [[package]] name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "lalrpop" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas 3.0.0", "bit-set 0.5.3", "ena", "itertools 0.11.0", "lalrpop-util 0.20.2", "petgraph 0.6.5", "regex", "regex-syntax", "string_cache", "term 0.7.0", "tiny-keccak", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7047a26de42016abf8f181b46b398aef0b77ad46711df41847f6ed869a2a1d5b" dependencies = [ "ascii-canvas 4.0.0", "bit-set 0.8.0", "ena", "itertools 0.14.0", "lalrpop-util 0.22.1", "petgraph 0.7.1", "regex", "regex-syntax", "sha3", "string_cache", "term 1.0.1", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop-util" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ "regex-automata", ] [[package]] name = "lalrpop-util" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d05b3fe34b8bd562c338db725dfa9beb9451a48f65f129ccb9538b48d2c93b" dependencies = [ "regex-automata", "rustversion", ] [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libgit2-sys" version = "0.18.1+1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" dependencies = [ "cc", "libc", "libssh2-sys", "libz-sys", "openssl-sys", "pkg-config", ] [[package]] name = "libgpg-error-sys" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500a4cbc0816ed820a5bcf73a19e74dd6df4bedeabc0f64471c61186938b6c82" dependencies = [ "build-rs", "system-deps", "winreg 0.52.0", ] [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", ] [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", "redox_syscall", ] [[package]] name = "libssh2-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", ] [[package]] name = "libz-sys" version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[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.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "litemap" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memsec" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nettle" version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936" dependencies = [ "getrandom 0.2.15", "libc", "nettle-sys", "thiserror 1.0.69", "typenum", ] [[package]] name = "nettle-sys" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a3f5406064d310d59b1a219d3c5c9a49caf4047b6496032e3f930876488c34" dependencies = [ "bindgen", "cc", "libc", "pkg-config", "tempfile", "vcpkg", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", "objc2-encode", ] [[package]] name = "objc2-app-kit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.9.0", "block2", "libc", "objc2", "objc2-core-data", "objc2-core-image", "objc2-foundation", "objc2-quartz-core", ] [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-core-image" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[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.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.9.0", "block2", "libc", "objc2", ] [[package]] name = "objc2-metal" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.0", "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ "bitflags 2.9.0", "cfg-if", "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[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 = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", "indexmap", ] [[package]] name = "petgraph" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset 0.5.7", "indexmap", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "png" version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 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.15", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags 2.9.0", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "windows-registry", ] [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "ripasso" version = "0.8.0" dependencies = [ "anyhow", "arboard", "chrono", "config", "criterion", "flate2", "git2", "glob", "gpgme", "hex", "rand", "reqwest", "sequoia-gpg-agent", "sequoia-openpgp", "tar", "tempfile", "toml", "totp-rs", "whoami", "zeroize", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] name = "rustix" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.9.3", "windows-sys 0.59.0", ] [[package]] name = "rustls" version = "0.23.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[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 = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "sequoia-gpg-agent" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f01803c82bdada34baa0f049e523c77b446ee347035df239a1f890c5d70c48" dependencies = [ "anyhow", "chrono", "futures", "lalrpop 0.22.1", "lalrpop-util 0.22.1", "libc", "sequoia-ipc", "sequoia-openpgp", "stfu8", "tempfile", "thiserror 2.0.12", "tokio", ] [[package]] name = "sequoia-ipc" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92579bbd37f62bbcc41e4dce7771fea395037bebaf9b8e10c20b765be8280ab" dependencies = [ "anyhow", "capnp-rpc", "ctor", "dirs", "fs2", "lalrpop 0.20.2", "lalrpop-util 0.20.2", "libc", "memsec", "rand", "sequoia-openpgp", "socket2", "tempfile", "thiserror 2.0.12", "tokio", "tokio-util", "winapi", ] [[package]] name = "sequoia-openpgp" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "015e5fc3d023418b9db98ca9a7f3e90b305872eeafe5ca45c5c32b5eb335c1e8" dependencies = [ "anyhow", "argon2", "base64", "buffered-reader", "bzip2", "chrono", "dyn-clone", "flate2", "getrandom 0.2.15", "idna", "lalrpop 0.20.2", "lalrpop-util 0.20.2", "libc", "memsec", "nettle", "regex", "regex-syntax", "sha1collisiondetection", "thiserror 2.0.12", "xxhash-rust", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha1collisiondetection" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" dependencies = [ "digest", "generic-array 1.2.0", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha3" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest", "keccak", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socket2" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stfu8" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" [[package]] name = "string_cache" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared", "precomputed-hash", ] [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "system-deps" version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", "heck", "pkg-config", "toml", "version-compare", ] [[package]] name = "tar" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", "xattr", ] [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", "rustix 1.0.5", "windows-sys 0.59.0", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "term" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3bb6001afcea98122260987f8b7b5da969ecad46dbf0b5453702f776b491a41" dependencies = [ "home", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tiff" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", "jpeg-decoder", "weezl", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tokio" version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", "windows-sys 0.52.0", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "totp-rs" version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" dependencies = [ "base32", "constant_time_eq", "hmac", "sha1", "sha2", "url", "urlencoding", ] [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[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 = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "weezl" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "whoami" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", "web-sys", ] [[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[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-core" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings 0.4.0", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", "windows-strings 0.3.1", "windows-targets 0.53.0", ] [[package]] name = "windows-result" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 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-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 0.52.6", "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-targets" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[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_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[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_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[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_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[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_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[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_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[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_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[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_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[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 = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] [[package]] name = "winreg" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.0", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "x11rb" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix 0.38.44", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xattr" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", "rustix 1.0.5", ] [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn", ] ripasso-0.8.0/Cargo.toml0000644000000042260000000000100105230ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" name = "ripasso" version = "0.8.0" authors = [ "Joakim Lundborg ", "Alexander Kjäll ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A password manager that uses the file format of the standard unix password manager 'pass'" readme = "README.md" keywords = [ "password-manager", "pass", ] license = "GPL-3.0-only" repository = "https://github.com/cortex/ripasso/" [lib] name = "ripasso" path = "src/lib.rs" [[bench]] name = "library_benchmark" path = "benches/library_benchmark.rs" harness = false [dependencies.anyhow] version = "1" [dependencies.arboard] version = "3" [dependencies.chrono] version = "0.4" features = ["clock"] default-features = false [dependencies.config] version = "0.15" features = ["toml"] default-features = false [dependencies.git2] version = "0.20" [dependencies.glob] version = "0.3" [dependencies.gpgme] version = "0.11" [dependencies.hex] version = "0.4" [dependencies.rand] version = "0.8" [dependencies.reqwest] version = "0.12" features = ["blocking"] [dependencies.sequoia-gpg-agent] version = "0.6" [dependencies.sequoia-openpgp] version = "2" [dependencies.toml] version = "0.8" [dependencies.totp-rs] version = "5" features = ["otpauth"] [dependencies.whoami] version = "1" [dependencies.zeroize] version = "1" features = [ "zeroize_derive", "alloc", ] [dev-dependencies.criterion] version = "0.5" [dev-dependencies.flate2] version = "1" [dev-dependencies.tar] version = "0.4" [dev-dependencies.tempfile] version = "3" [profile.release] lto = true codegen-units = 1 debug = 0 strip = true ripasso-0.8.0/Cargo.toml.orig000064400000000000000000000022171046102023000142020ustar 00000000000000[package] name = "ripasso" description = "A password manager that uses the file format of the standard unix password manager 'pass'" repository = "https://github.com/cortex/ripasso/" keywords = ["password-manager", "pass"] version = "0.8.0" authors = ["Joakim Lundborg ", "Alexander Kjäll "] license = "GPL-3.0-only" edition = '2024' [dependencies] arboard = "3" glob = "0.3" gpgme = "0.11" chrono = { version = "0.4", default-features = false, features = ["clock"] } git2 = "0.20" rand = "0.8" whoami = "1" toml = "0.8" reqwest = { version = "0.12", features = ["blocking"] } hex = "0.4" totp-rs = { version = "5", features = ["otpauth"] } sequoia-openpgp = "2" anyhow = "1" sequoia-gpg-agent = "0.6" zeroize = { version = "1", features = ["zeroize_derive", "alloc"] } [dependencies.config] version = "0.15" default-features = false features = ["toml"] [dev-dependencies] tempfile = "3" flate2 = "1" tar = "0.4" criterion = "0.5" [workspace] members = [ "gtk", "cursive" ] [[bench]] name = "library_benchmark" harness = false [profile.release] lto = true codegen-units = 1 strip = true debug = false ripasso-0.8.0/LICENCE000064400000000000000000001045151046102023000123040ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ripasso-0.8.0/README.md000064400000000000000000000051721046102023000125750ustar 00000000000000# ripasso [![Build Status](https://github.com/cortex/ripasso/actions/workflows/rust.yml/badge.svg)](https://github.com/cortex/ripasso/actions/workflows/rust.yml) [![Crates Version](https://img.shields.io/crates/v/ripasso.svg)](https://crates.io/crates/ripasso) [![Documentation Status](https://docs.rs/ripasso/badge.svg)](https://docs.rs/ripasso/) [![Packaging Status](https://repology.org/badge/tiny-repos/ripasso-cursive.svg)](https://repology.org/project/ripasso-cursive/versions) A simple password manager written in Rust. The root crate `ripasso` is a library for accessing and decrypting passwords stored in [pass](https://www.passwordstore.org/) format, that means PGP-encrypted files optionally stored in a git repository. Multiple UI's in different stages of development are available in subcrates. To build all UI's: ``` cargo build --all ``` PR's are very welcome! ## History This is a reimplementation of https://github.com/cortex/gopass in Rust. I started it mainly because https://github.com/go-qml/qml is unmaintained. Also, using a safe language for your passwords seems like a good idea. ## UI's ### Cursive - Terminal interface ![Screenshot of ripasso-cursive](doc/ripasso-cursive-0.4.0.png) TUI interface based on [cursive](https://github.com/gyscos/Cursive) Supports password age display and password editing. I use this as my daily password-manager. #### Build ``` cargo build -p ripasso-cursive ``` ### GTK GUI - (WIP) ![Screenshot of ripasso-gtk](doc/ripasso-gtk.png) Not at feature-parity with the cursive code base yet, but basic operations work. #### Build ``` cargo build -p ripasso-gtk ``` ## Install instructions ### Arch TUI version ``` pacman -S ripasso ``` ### Fedora Available in [Copr](https://copr.fedorainfracloud.org/coprs/atim/ripasso/) ``` sudo dnf copr enable atim/ripasso -y ``` TUI version ``` sudo dnf install ripasso ``` GTK version (unstable) ``` sudo dnf install ripasso-gtk ``` ### Nix TUI version ``` nix-env -iA nixpkgs.ripasso-cursive ``` ### Mac OS X The best way to install ripasso on mac right now is the nix package system, first [install that](https://nixos.org/download/) and then ``` nix-env -iA nixpkgs.ripasso-cursive ``` ### Alpine Ripasso-cursive is currently in the testing repository for apk, so the testing repository needs to be added to the apk repositories file. TUI version ``` apk add ripasso-cursive ``` ## Build instructions [See here](https://github.com/cortex/ripasso/blob/master/BUILD_INSTRUCTIONS.md) ## Translations Do you want to have ripasso in your native language? Help out with a translation: [See here](https://github.com/cortex/ripasso/blob/master/TRANSLATIONS.md) ripasso-0.8.0/TRANSLATIONS.md000064400000000000000000000024131046102023000135540ustar 00000000000000## Updating an existing translation If you spot a typo or something else that needs to be improved, these are the steps: ``` git clone git@github.com:cortex/ripasso.git cd ripasso poedit cursive/res/fr.po ``` [poedit](https://poedit.net/) is a stand-alone program to edit translation files. There is other alternatives, but poedit is quite easy to use. After that you just need to submit a [pull request on github](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) with your changes in them. ## Adding a new translation If you want to translate ripasso to your language here is how: 1. Check out the source code `git clone git@github.com:cortex/ripasso.git` 2. Edit `generate_pot.sh` and add a line at the bottom for the new language 3. Execute `./generate_pot.sh` 4. Translate the strings `poedit cursive/res/fr.po` After that you just need to submit a [pull request on github](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) with your changes. Poedit will generate a `.mo` file in addition to the `.po` file, that is only a binary representation of the information in the `.po` file and should not be included in the pull request. ripasso-0.8.0/benches/library_benchmark.rs000064400000000000000000000035201046102023000167440ustar 00000000000000use std::{fs::File, path::PathBuf}; use criterion::{Criterion, criterion_group, criterion_main}; use flate2::read::GzDecoder; use ripasso::{crypto::CryptoImpl, pass}; use tar::Archive; fn unpack_tar_gz(mut base_path: PathBuf, tar_gz_name: &str) -> Result<(), std::io::Error> { let target = format!("{}", base_path.as_path().display()); base_path.push(tar_gz_name); let path = format!("{}", base_path.as_path().display()); let tar_gz = File::open(path)?; let tar = GzDecoder::new(tar_gz); let mut archive = Archive::new(tar); archive.unpack(target)?; Ok(()) } fn cleanup(mut base_path: PathBuf, path_name: &str) -> Result<(), std::io::Error> { base_path.push(path_name); std::fs::remove_dir_all(base_path)?; Ok(()) } fn pop_list(password_dir: PathBuf) -> pass::Result<()> { let store = pass::PasswordStore::new( "", &Some(password_dir), &None, &None, &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords().unwrap(); assert_eq!(results.len(), 4); Ok(()) } fn criterion_benchmark_load_4_passwords(c: &mut Criterion) { let mut base_path: PathBuf = std::env::current_exe().unwrap(); base_path.pop(); base_path.pop(); base_path.pop(); base_path.pop(); base_path.push("testres"); let mut password_dir: PathBuf = base_path.clone(); password_dir.push("populate_password_list_large_repo"); unpack_tar_gz( base_path.clone(), "populate_password_list_large_repo.tar.gz", ) .unwrap(); c.bench_function("populate_password_list 4 passwords", |b| { b.iter(|| pop_list(password_dir.clone())) }); cleanup(base_path, "populate_password_list_large_repo").unwrap(); } criterion_group!(benches, criterion_benchmark_load_4_passwords); criterion_main!(benches); ripasso-0.8.0/doc/ripasso-cursive-0.4.0.png000064400000000000000000000660151046102023000163670ustar 00000000000000PNG  IHDRRki5!zTXtRaw profile type exifxYr$;D^'r835U}Ҕw8Qq'K.J[|89ѧD)q/q vs}(۵}=7?`,a])${Yx)E)9?ع׏?c(||O__Qx?~_5vz~B{n%Bm+|j*3M[9xMZDV}3L1=W#+R ju-|3Tf^+c`W'us!X0[b]82 9{* S/' 冹c![✸N|vAsBbB_BP5Xr WplR*S=Q4bJRiV4W8%Iv"RDJ^REJ)ZL&*ZTj^SUjZkm&4uzgН;W>H#e訣>Sf:l"WYV]m6Ty.[wmN:)GO=WԞ~D-|BQ O |782D09 b !hf\I  r-sݟ@tO~ V%], ->mj[)!=TeOE$8kۗ{䓹HcNP+vv%=U%֖h}mz2[ث PA6T{ͩY5g>#,h|5.Uh{ٔ~97Y\gtfaucC%׃|?ft1)M2ccǜKku8ZqJj7w/;;ԯ |wL%:覔,{jɾls8Ե_bR]ϹB2(+.-kq/aܟad˥]xEr֍*d"eal ,;;萷lB4ZXa.R`l'>6N,?g=%;AG a玵2>F+z (!@nj-j fmַ 7>e//ޑ-<=QcP:ҰCo Ơ?$cv[JD=,Tvܖ>| e;R`(Tq 6qmZfq&)~1'5^P!U bU$eQzKW)c_b<9zYY$4"T 2scC-07͔~zx#-(Y@]L(dx8g|v]p,GeE覘Yq>&XȏZr.^҂k'[ҢZ8kY0jj'Nj]X/!OԯjM %‚(䡛7ނ[Bg~&48 =zmou"ުVВH"C#Em?cPЄ"q(nSC0kN|W/^ǃp$M%>ٳd硱iBNɢbO&˖rg'e7I F>lbY`E PUZCLΞ,eƹFTdE*'P\OԄΕ8uGF΢@#9Xxf XZ0ߨF(]lej2X"Un+FkI_j?qK`uQc {bpЁb&,U叾!䵞nFY4T)% tɴ|Mс3ذ/b7D0R"meyF~Gj(d(VʲFl{iԬZ6#fq[1XjQ h k$GC piv`oYl66>4`%ͣ[ .ftc6.^3`r5`ݹyYA=ewOQ#o]T;ڿ )Lm=>dߓ]6U̐ݷo}a5oi_" pHYs.#.#x?vtIME+0η IDATxgtי0g{ v,`ŢBRT\c˒~Rvwqs쇷y7YI$IcK-Y,Q]DHID! 0#CR?;3w@޹ƛB!x?^z0B!°!B!a=B!BhiXX!ABP*D$Ir{77';}^ !!X-> m۷ntfa(B! Bh(ƵB\ Y,L&$ v= AVaW|>1>cRe#!9BTJQ~?d\.D,^o#!tFv](F&;vO/q{yo?g[T3/GwppӣLmjnܾc@ l gt\|5dַعDܿ;BaXBOL\d8qwܾ_~e^IQTsKSyEۇI$tw4~_"457UUUuH>%{Ds 3 yEeX,]ɞY.MNNŢj.#^t°!*6Ģ@ pF afDt5-w~LO55p /FR^ݞd2 v'N|sqp`Cm|xx/=BLX޻a ҩԪW^}vNZPx>rEݸ޵5Ց>&$ &O L&ď3L"Xq{n܎lxAWwzKKmߺm`0855sݾ's`v{".VF!?&G!D`cX''8NeUΝ?D91@nrLꫯ0̳g:#]wmO_Y**.*+-j˴2Zu|JNz;RcC!<0G;yTyvm+-NXb(_\(BTOyMm5żse sXU|bmܾ.^"C']DB"@l}} c˳ˡ(ϢB0_G"Z5氋ϖ$s.7ɔH$)%3PPd1`B#+@ACCB!O[79:~ztm6-[>+ c)..bOPA(r)%g6;mm}8G.yf =:X!P ZAjǎ~bmJD/qwIoq1sr8wnܢ(jmZR_{޶m[KJ9|tUZmTѣm%33?P FOI&;u7|cmjzrrR$kp8}}SSxBzz ]xYZ⅋7f2+ÉlZ1a_ɾ{v?std'"+K]?W^}L[V-I&7&hr44i+=ӑF_xaM=g(ygr#!z΄/MO# $l2_6-~yyPxc8w~s'JR)zF  W+h 1@! B%FQԌ~1 f d5?7??7d`2LF^#ZA8e!B! B!B+  ЪfCB! BIuX!VcXVXթ_¯) !3!B=0G!BzB!!B! B!°!B!a=B!BzB!!BaXB!°!B!a=B!BzB!0G!BaXB!°!B!a=B!B#B!0G!BaXB!°!B! B!B#B!0G!B6K&5ÍdB>0&d a2L0ӝlՅdiH&Y 0٘ a2L0&d =WWXd B! !JEQac0B<XJ! BU:"#v $ɩ{E°!ZrRR*E~"/1L6P*LGhts! #}'X]%۟rer:.Dd3دUt|onul?:7vrH\6$2d Tɫt4z)BO0bi|>? 6NᇯR`*T*5LLw y2zIE1/y_ aGXƓ]2lc>Ǡ}X App-}>;_NOӚFo*z DXTAAE]3P!+3Ϩ%XAc?QV)kҨ><~>W*)Y f[4MZiY$2nSπmPd^aA^ |yU0 3tL,Q!wyAےiɢ)6r%J2ۙp@9";CS[4H$)W@e9A?08Jb?/ I.3,L]A79Hdt)J` scqJW-r)KWtE_}g}+-|tBh^+-:s΀ko6R/,Cz;&{IouC?Շ#p$1+=a*lZ  zV"W]3eҌ疤sq2t9- c7ɮW>a]補qf\ȟKw*x ӗ= N:s1*ٮ+>鉳,=i4d4| "[d\i{AkLk$ =fW칬jp#/56V+n6• ¿o m[؉ =. ؂vvtk1R*-٘>c;3qKzwl2t Mj*=LjEUzU`8xaҬR.Ӷh"ۿ*~\vX!+zm<OΓi::Y;% AO91>W}@p/4W,>k q8WϙgbBNl&\l8Fg\ӗwO2(Y_7tov#b%)cferZ4 |k{_$m3xuZ![8뙽o`4N!@_sSm>[Lܵ!*$ًzRDH"^!#%#Tw 8@H1ޚ8㜡s~~ۀޥO/zazaqPygY,.Zג0W} Aj? 8Ad;x-F̷u^W*rN8FEQ<6@5:7f~g0QJ<&Cz iTF 76V~|KFc6F]2Lzf/=@G3aJ^(oPs,1kzGH-.ShPzgÎ Cz5ұq[W,.:6+t;GCmyB ɽelWwL^pʚ|"y(r/B XS7=,>  z?<=ByB ]{,)9zazaqd[p\yBMM+geb!$Ԃ ,.H=P8#7x5B͌ˠw͔JS[>%k.[ EJ>Wg@o_$erY m-mB]nנ*jکwՇ'97u1 hb*Jz /ޔP\ Jn%\Aѓ[j5ڥc=vIi+ޘ>#jjکb굮4wz ncx?/J}ڋnąMx|N8'AO(O;:s-LרVeo| KWPp|~^"͞e і1Lt^3xlW^AF@8T\}k}kLj}Ү!53.3mbTc'iPOA(v r=cg/7Kؒ؝&ysAEhwX .VEXY| DNmsP+88O'P~+U_@]¥ҒgEXe)PuE5C-%O!>}=%vƒ/[P,)Wq&\V%Շdb !B$rXO3H*31a= ˀ`h/L_ rꧬ.:N*ö$\ ctwd20W}ց<6q`tШzy/zL2 FƖr 3.}뀀ŧN *d΀뺡k)Ƕ"xfaT8W!K`Atc2{F/VB+)LA(şp:qtҖ1-`zIgz!BryL;yt 'Sfln:&#zvnctXȀl]Fsl BG#TDQNA8RTΓ}/Nr *k_/ge\4!*;i@0,deb4d峓o1 <6#}^m.ڢx^+)-8X\;{w31uPT@Ęf_PYvz&Fks}I'4\GF5>3L:vua#`*ۤw<p&M}cP\$\}]3\~bT̫:EedSvک7g&: ΀nN4<ސܒdnPwz*}xb~ ,gQn#cnm5yLStI+> H4i;P8oΓ -b8{&LΓ*cmp͞ 3F0@\j9]2Lٔhrb)g*$T6MƮMmI++d^zd Y&sAUBfIԴSBsfR|#2A9@(,v\xp6g =6)%/(6OMW[/`pn6ɗjmy4?{ǟ' ¸L:y1ZQ-Dp Y:66#YSHI'?gb!V[jzhb8DWsnO 볫9)r~2CO IDAT(ۨwͰ\s V"O} 1@' >W Чq:^&PzJzGT;jAK0GhIKeS(ǾP4s`m.O(ZB#|`! BRiVӛËB!ZEa=B!CTŒi&&N'BzBhb0 kjkb12I5յ]7'ưB#Bz3|>(y$0L6R6nhz=& !a@-;t`OH"׿rFejk|~0!I~ @(UjrrrRi]m}Fz\`0F7!Zˬ={_*/~z쳱:X,>fyI"B 0cyiumm;;]g[{kcڡ/O...ڻoKf "\F؄Ǔx'6]QQ.|>ؘvnK>76T)t2j.3RY477ܼv&sss5lni1 L!PV*H ?&a߱}H$Rsx\[s@Po2+N(JeR6{%ҲyMEo;\/z+IyrysKj:%x~;}^B-'>bb0kz"bqy%jJPań٫e"W;osy܎Ǜ;}f9zFӜF'H25쥗J%Z380h0) 6od2:}&RTV۞غ\x9%/4@ ]rޚʓ 0R#r@BÉGu@,q/U*^~]j@-B~A@pҕ.%L3?8?P`0ҿ;"OP(Do555‹Ϸnrn}gC6mnI6f6%j=qiV/CElսn}uaJRN^tiQXXRLOM/tÂbr6t8JT)f&OB!~ޒN"!X.GRF^u828*442L x>bd2iP8?/A!jtUs$'7GT,;|P,s932d2:HOΒ+LfSURZT*|>ד \S*EE"TJ$Ç׻|u6UR_ɗZ,]7wd1 fW嫑d##f9'7GW]uvsK6L:7lZ^J-ڞ=/|/<1a}SsAdK8>{#tޗ yddG[wp>~D??鿼_wɓ_Nl]Е~gFeD[{k{{_888Ϣ4|ۑ?Ο$0A!j ‚m4GFFsrstW:5Z>x{Utg/nnisc)YaD}r5Ft}1esL2rF ( !Lz/NO7z8GI*AQEAsby=ޘxll<'7d@/.)޳%2H8o1!W+*+b#ڲ0۷n QS=|л>isBdccc!2R[{wwݴX,B$dvz< yyMM33,} <OwM%RISsSUU[$?x<ۓyA!<3)^#,hd F.uR{#LΘMtAHkƴdjёK/o޲i=cպfzT,{=^OBᖖ~Is׎ƦFu95q Yy|ޡJŽ>=y4N}dǒv]J%bzva!J Vkl+}wET.[n2[fˍ&v511Gyj|ʅ#u^p8}0/NqxucsmV*L7rsrrJJůЅN''&'nwwu/ÇlG=;}? FMD"I&_@Bdsȡ\\5%% º prӴCVX_###099 ~A,5Hvu/))ik)HCG}g'YgѼ^~{rD'QYYr ?>x{nw&^'8KJuVgt?@)Vd0cc'ntA2Sx XVVZPOQٲe*볡FC$ Ͻ}S[oA8lPfsl\۰ O[5oj!߮-*.= SJ ]t`` wR|~uN| 5pܥdP J EjEAAjlaq?hTVUtUwnI)j@co};þwfwR,98rss fZ04K3UNO\tZKVR]Y*^:v%L$Tajyfs[3eXlK{^pΝd>[ZZԸmm۷鞛ݍo&̄~ 0?7 NWDMm}{6>I/SBSiJHd {ybPYZܺukmZgǗXgySy|ϧ#wݒF%j~@w斦_{u}fICy?bpp(_:yl!zK&7?rIic=vI֭G>M,O_Y**.*+-j˴2Zu|t =:$2]P3..?Lꫯ0̳g:#~vڶq)9/R !RCSJQXYU6orՀ껻nfZْF%b|uLeURܱc{3H zmTq\y#B$sޙB۽cK2Z|Ii}C]8Жkc>;w Xh1[[,V*\b6mnMm/^{N'lFw:JL.%R;NNxIMm5żs7j2 QZ-B$rXO3@$"H*tU1aLJ5 xW Z5?nh4e΢1[V㟝8p LZn_T%*]%ܿw?#a}$yf2H'TT?tʌgM~ѭ|73s,p9͠_^129"FBhIY_&'*Z&pmEEg}vsE׆Qy<^ZJݿ?K?~>2:|X_\\b?r@ P(v=s122jX5y֘K$ y UkiNrJJKR:wJPXP2R2tWg_ǰuBBa B)Ӊu:zih$I%kkʪώ}]7Jž}{Tg ¢ҲMm &c)xO$in??/flt, T=)J~4yh4i4?Oq.]<.Xr /<___x_Y6{޶m[KJ9|~pJ-*jijXm۷>hp8q#+"$:LOC,Y\V"K[F#dyJWz"L5D*yE`i/O..)mѕ|$qnsxtĢ+z_8657ݿ?02$4mkFETW*++ ??/f(tv^}pRuuu[@B!_۸N:Fܒdn<6;x}6myd>hr44yп \zLGt2]~p(<66F#ܾvwڡUE`0ӸNiO;wn/++s'~ttټeSAa=.+\-+Ӗ1}ɍ cfp8Ŋa=BeWx9ޢmxh\UExl-L `0^U>ʼnɇ /|}]idΫl-ձI_2J&[AW>xZZָ>=AXBdE +U` 81>ł0G!V@E)f5g0K?shgΝ]EaXB J# `+{ݾ} Xc!_ /-X(ާ>B@+EB!Г{Btꢢ"X !nMNM8N,"!`0ԉb?H1>1e°!ZtUkiE~z~%d*jVc2BOjX}[,߸\.,_BO.W[SI>x:AP(HҺr`nBV[vȟ~t/k?=XrϨX,>fyI"B lu׵mvz|mkؘvnK>76T)t2j.3Rj X$J&O433sBB*J$bt?Ipwl%ǃP(;55;X槩BT&eWru͛l+z{^7N~x\L:/Ji4(UJ5^ޫՖIz\d^ڗNg)$\!߰a}QQa&Wϟp܅l[8{ג E!74x#Y,Xf-^W$X,.İ^V)p8`0tպj:%x,_jwg%7ͽw&s·9&ӑOd0kK/J˵gp``0Rm޲d~uL$Rx78έݗ+[=utr6rK^i47@ @). 5GƇzB(T8x{TRuU +/]xR4 `0 #ӻ8E[Pukjjomy|0٭r8;,8x]7^osK^Yǿ6̯ZMm2[Gh˖]]g!Y ()-Q*>ttIpm\niiX"zfE*X,f骸7lZ^J-ڞ=/|/@˗DS\!l1ckZ#|G~?_N8yk7<-?l~~>qMkEwW7lٲ9VVVTF_?5t/3MBZBr Ƒќr{lV:077/~=_ [=z/G?88Ϣco}X"߾ AbcScdB縂5P(|盇2[8q2> k'YnEQ^W$͕xcⱱܜҒE/e"YL[G' ߿=P(Ҕ_ϣO.MNNŢj.ctVp?iͅ/Wg}iu¡0i<`2nߺ=44BDMCϛƽ{wwݴX,BD,z u< yyMM33h"B(y "\g(SIFY&4\-d-F1*=V* ֶiT#.^޼e޽{u-V <OWgIDATwM%RISsSUU[M5$عkGcS:Q\R 8,Հ<>J{./7M;iU022@/'IJX$\ں汦d=$I~qvَq2w~LO D"ц)Hx]wbuNh4)UJ1>6=vF7$WI XVVZPOQٲeR>B)ʪ(oiiz;4zIs/nox`<%2[n-6i~9Cid)_ 8c:331M^^y5 ll6GO3”R$"'t ޙ.t !RCQ@ZP{26m1xZ###U:]՝wW}8G{ηݾ7{5:F# &srS r`߃l/_%5KW>x@nmԪW_{'GE_0n <O S>mLrcY/yr;w>A8 q )L@{*D^Zp5p8 YϾ6p8ߟYA2 >KL9B) ܕN!Z&"C٫>FnݺqkjK|t?2 n¨-+XZ-?s{W17RTz<>_ 4~S'O0!2Do M.o~ҥѓ8zN<ӭ[G|=I iQqݽ~*odҌA_n)"B(oapW4; +*`McײᡑT!C"y<^CC}wWe2٫d2ϞUݵ{gktC"a}j@J[3UJrǎ1C,rPp蝷%RIsq"pZDbp;[(2vrlIƁ43o rm{箝~Z㊟nf#ն͖p銟B Ƿtnș" z218"( ƌERT 麆X(`+K-뚟{~~Zo4" jj,۽1ei*'dٲj@aޞj-~MSM .{J굄ۢh=)'G GG[e6_Z_䖤43%`0 /k߿w #F,))ג41>Ѿ왎Ux ~"M 嬞3E!PcSFw8oPn@aA[%gB0j|e $q~~^lDW1K>**Z?;9tV(퉤֭ ܈ EeLOHTT\-+y^lNv"h4g'GͳFIp||se`!<S ;d[`ynX馛W̘9#yAۿ{?(ޟ֛[_ĩSoYO_׳ߺB{{6L/(y8۷r+1_yeKqqqr;gYT*VZ{~?:c}|' {9goɧkoogꢢEE3C?pvqnh4äONOB8ߛ^5=0=B_R.Ǎޞxcy{Ö>9=?//%%䉓h$V!+++??'i|#|?X>ħc]u_2x ! 'cDh9~3n{Ymsl"3e-}rzrùJ,[_Ƥz󓳷7tzGA#_v"0ފOLx~C5vfhɷ.ovD!$:;,Yd=]-뮵8 Ͽh#06z@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yd=z@ Yz@d= Yd=z@ Yz@d= Yd=z@ Yz@d= Yd=z@ Yz@d= Yd=z@ Yz@d= Yd=z@ Yz@d= Yd=z@ Yz@d= Yd=z@ Yz6z@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yzd= @Yz@d= Yd=z@ Yz@d= Yd=z@3̇!|tؖ1z"kzVqzd=z@ Yz@d= Y__CYY_6E<ޘH$?;-7ВE"!#ȸ"YssG{xֳ^;,u-_6ſvuusnYQ̂h4ַ-YjjH!H}7k˛#!DH_HOjkk=Xw}}}/ O_vBOOO?z /HDBx`gG4SN0w323RS;:O)עW5{Wny7C&+.s=wedY܆ǎC|MK^|nmnnO>S[[1-_q7N0BzС755{6}I=9ٻvnjk ׮}8-=mīWҋ_xO?45-u{YBXxѐW9y`9}>wZ߼cӻwI'{XEí-_U3:!z껣/o~%V}7wvv>C555.,7n8zh^~ޜ9^xBU.oƚG볲.vȿiӦ {GO_RٓH$n ?Lr豁OζBYK{oߑlBOwϡC{5YBXr{?'bDU`9}1ګMG=!/D[[[}]%Ğ3)U܋6(Ch4џȝD_;{رu&M:=^\ykYY^ !-LMK=!(֎3xN}!OeddcvҤIִ݃kB$9=AG޽{cXyUeUé1v}ȑ,Oe:u|ΝUؒWUVeff;Y?|=ɓB8qbڃ'O5('',=pF>)#8匙3JK.|~H!̛7ζ2y͒3pg+WXw֤:|cǎO/~<5wnf[UM˛vpñA1~qC'9x?n7w9ݻoسZXT3#.IvvVq?ش _9sf]ʝU~E<.tvt8{<ҥ׆pfҤIR'X<'7'iEOYMn' ǥ]U>kU`.RR N8ikpuHIENDB`ripasso-0.8.0/doc/ripasso-cursive.png000064400000000000000000000551451046102023000157340ustar 00000000000000PNG  IHDREݥHsBIT|dtEXtSoftwaregnome-screenshot> IDATxyp\wZղlɒ%%/VC2ICJBe̤r<ܧLu!1a1}xś$[ڷn!q# ΑW|9-,c|S}*g6=i3!"2Ҷ&#"""""b1EM&BDDDDDLQ!"/Xt±NEDDNUe۷Odd$Kn[)SxDD̹\W9կ#""D-Ftt4˖-k"ho :: zyW;ݏyl@eE% {&)9ښSb'Lr h]w߅_mã3g?v MDDƞ&bSF:'?;yb|(FE|||@LTTy9OdD$1&nb|R+xϟ7FHLLO[\R!-=mX[""_~/#""ğ_} ˺RYYcLJm +򳲦~:.qՓ'b\.^}$%%9- X{Nr~3m1g""29""j.]]{bVάZ޵l, Ui -[Qzl~ b>9|?/*b1cn4mZ&֠ ***kEDdtH8s rrfqp%̰Z<9s ,m --jqoǟx?Ƨ'OaGrmKĉ/""㏚qٴ5_L/sgYd1(;K.\kn"""X|)nóf|_̙; nx-ϰ0WVZ _be;ܹ|vރswf&BDdڹ];wLko}{N?_k~[=gAe&Ÿ^8tMבE(Q!"""""t&4DDkLG"DDDDDĔ=u#Av~OD>_;=/y2Z#"""""b1EM&BDDDDDLQ!"""""s":먣Qg3`%G)EŴAiLo*YAb3!nhl7ި42:]x|,~s3gDcW#&/1w=̌Axp85|Z Y9"""""hD,LUhn0iSI@rd^|6 ل0o,A؂lLBBM(7Dx|¯4 WMylO 7WYX,7ݸi"(wV݌磹u'tV:ȌTEDDDDFt&#N˟lHnAcWA叙dO!#f(f+""""5npV/Ӌ"2$ڎ:mQODH.tEDDDDMqUh+<N|x|@ɾʃDۢXh#"""""2Fhm~zn0mv[.RVt, er:Y5Ä1-63"B"FzDDDDDF(olùi83Hx|^&M`ٔۘ91Gju͛8i#5"""""2MFDDp/ެ 2<ϋ4.b%8)NqS8}eF¸&b8[Y}˝>f[_KNcѤS8)NqSW2nX~0`3 4oq3J.ƾV6! %8)NqS{Acȿ~0}o_&DDDDDFH5"""""0D -=?HG"DDDDD5"""""b1EM&:ܺxOXNEDDDDdԌZڱo'""Me.2;XMTzzz?Νqq[e4?#QFHměIHLJdm#&&^_ܱQVVΔ)Yz%v];wX{?'&s`Aڜmdegx^96܍g#G#!!yr|---DEEG()ȋ_"<<;ﺃ?ȟu|B*==~0j0҅||`XEn^ii$$C=`7; 2`}~}t!L=[ EEŜ9}f6"8$ɸnvIoo/uky\ UWrJrrsļsDDDH]뢳 !>0Z|_ge^L~F5uhfJKIE.W]wyʚ (PġS_WOrrx_`ۙ6-_{t: GL7pm#~e~G{;>oV}g@vv{MGG'om;|MTDDDFݗ%Ng111l6Nwo4Z8)?h~F5h]79$''rQ_E+f,\VnFR:::ˡ3)]]0|ڨ IHL{.N?Z[]+===֠.nO d4WI9[PP#11a剈3R=0Y^Ѻ(׌2Zי¸;G;vw8:LsS3qq^9zN"NgSҮ4-1A{6UqqHH+#1}z647hy̜93gatnنE3 {9 EŤz*W^p{=  Bf4Mxu׷dRj*:/%_Lzu):ܠx6ׁpφ:7Dw53ґ1eDDlqݗȈxYD)j"DDDDD5"""""b1EMu8|ﳇ~|s"vqgj&>*ͼsȊ6)RGSӱ` y9\DvlA6ң0+a&!`mKB6؂lDLMztZz8טOKw 8f@J!m_]g=uu9{(v"B"Ȟ0b^7M;+qAVl&7MRnf7P欠՗c4e|v]iA+ɑN%<8|DknS4u5b &-:,Aו|}ύ3=e4?#uQFHMMf=Nn(`c_:dO!#f*JD٢4-РPz<=;+8\} KRp8S-gyhl˟pojz2:]x|,Zs3gDcW#&/1w=̌Axp85|Z YC'F8t0uLfBh,^'%IJ @HȊzz9Q).fUog$Ćưh:ݝn8K[S^w""dt<73 xn9F#Q7 qDt:9]Z qaq% `n7bXYu'tu2o\ҢYN+aqI(}\KeąM &4:`zQK1u%0#FY 7lN'/HZ*~RGhBbCc^lA!sa-:DSw1MHBN,#⣰<7L&,8 yd 2ɞBsw Ί!2ɯܚ)f5wK!8/gg$X -X-NͿm`gVMDۢ||͌39e4?uQFHMmTef~\nIbaW^:]%uB^7."/)([gϙknTiX2tp >:x}^ʝdL7QNDp870zϳ`%Z \@y/d{*^~YX~4?8K%.W7ٍ $G&l aJd;8;=ύ2:(o&3F:r8Kɻ_iilA6OCY)y=e?=!DBĄƐ9ZJZ/,H\Bxe_'}HGk`BXl\/W]u212y?d׃MqE==v$lC> &S6!䈤a(wVƄ pݞAJCgmĆR(Bslvn"B"X=%%r"rNY2.w7C"Y>e)!>״ f|CTw R3ɞb*?u~zh&3FD2wkgO֟ ij͖m}>,WXzM$rwQ(gnON&п(uj4k9 >!̘ ʜ\lȞ}$G&q[6}9 ;v}M3hjnO.fd$y|: 1;)`k0-%||V ><8U)QIHQNk▋^9ul &4(S|pi|χk1:M\vM|Y~FhMfJl C>/!<nEFtrcR퓘w#1a18[SfMMG-.pyQ6;=|>Zz{nũJdHĠz5Lb3)uqE-GrSDp8+ӗuImGѶ~]nV'"$bT. ũ]GUe;jt՞ ,8̊ͤY'G ""8NW'aa`xzp{݄Gqv""2}x><"زg1ޫC]71n@k`k011>.p\ɭ IL",(וb~ erBBI|ԩt8\0C糦n 8dpܺ︰ $'mhm5`·1 .j)&<8g廉KV ]*yWrdR%g>w_xMqkZ,Xp:G,ZޗIL^?Q@rD∯_DD=ύ2:(i,+Fg'Q[ZǮ̈([mm9Y:@8"8+̈́F.9JW7jN7ĈDz<=:H Ow7MY}dfCiij:jN Qjazy8{ۀh^+Gۢ_sUqE**Bz. ΤCƴ̀bCcuWwmfRr}U)~` l &329*▋C"ILRRba{yFOb%UƸt:9prxnt(s{¸i""C"#c5gR|^O/HҢ C2*71O/g =;杋lkY-VVLYJAS!mtdcJdr Ylh,K'/lc>ble IDAT 3%z2s'-BK1 .Ul fBX,˧,RSRV;UI+GShjTi<>/&lmL\}^.^8`@LLe[ՕQo7j4k+ӗs4je(N#,(KR4#t3 a cERN֝pB!dM$woqfsg13[7 |v >6 in\7ơu'ijfUcʐ]{,VP k }_u- #gC &f[_ƢI F1#*Ra="`ׇDMf Nt{y._`d~'ykۜ=svH{0!64,gS 7/ k 7,\x qq8;#G}q`ٸ%ʙIdd$V'N'GF" n_z9xр)iSX|))Rpݔ^*;ikkR~S&rIJOOٹs7n{H0""gf\5:ed|8??1qzlpH3!:ĤDn6+KQa[x7;hBCz,Xx kX(++gʔɬZήq9 m6Xsj^/G,X!2238z(u$$3o\./(%%yqKs]wsMyzm}V\ng떁Vc{EDjf<22DGp1**nאzj\5Z猥qDưzJeMR]]Ν ))xWn} nͷh> XVrreRJ ^*>pM̓:{v.547W^VNFfiW y{ (8_~K?ڱ==Ko')9Moo/O'GXjYYYo~?8/o 3yr1.^DA~!{vR)V%Kn'GioorI)̜5sM~C6onF~~}AfcGž=ypZBl!zq\p>W^~ޞ^n7qxdd eeò-F;V:g,5V/&:&M$nwzW=y$&&PZ35_4l=3v!L=N[ EEŜ9}fD  !cb2n;vxhZn3.}xꕜ/,&&'1o\!̡uىxKDD59Nm}p׷dZf&EE|_ge^a(l;{CsFۨ4=\0re{w{靃!+k7B~L}]=Il|~ngڴL^Ay]{weS?$ult/^;\Ýߺ gqCZ3w p:l~# Jff&MMQv6>7n\;|l}M8M~Nh8~X{557yJFLL 7 ""[ shw{/WGcLJL\|Wԡt1%-JBZZo4~gC|}{QonoqQ1kXͷ% f&44PΛCSShu8p:L~.qqHHH IPpӧgHss̙͟38s,^ܼNMgmpffN?Iԩyuu9ɝETtQQ$%&R{wħ̟?G7>p\,^^~zʿ/0o\ZZZ)--#88 n;H~Ge9< 1,_gso8CcNAzz:sT_y.F7ph3Mhu3de,^|+4P_pqp۵k7,[M~~Y~ӿeφHSS3gNWcGM3g0g8~섿())˗;bP_-oZ2fΚV}=o[\TLzz׬"88^}5.~ѽCXXaaa߰.`@;,/dM3omی?Ȫ5+޻7o~1G;v7fe]njxרz/˯^x;\ڵÉ'pm|3WGFß{n_m/&"?m,^ruwW7||0EE}}3r=n7w8Sy|ᏞmosYC~h෿iAAAܾ6rrg9rԿ} o监1 rr7f[vQ|}l6n} rf'9ɑ6˗2)M2>ڱo'""_]fՉ)դNNsܹ1QF3RoetD2!:ĤDnoo^H͟ /p2eeelyMr%N;伖X/:=FEE X`董֑ϼysp!efMHp^b[Xsj8DYY9SLfvv{`ĹisŚ;Vz9z؈gtEEEG()ȋ_"<<;ﺃ?ȟ (/0&<" lߐ͛[_W^~?r}ww? W2z(o$6*DXVv_xxxh6mzH;oͭazv}f/_f:.ur*Wr5o.{#eR Y: b欙>u{<|#\}wvuq|W,4&ʾ_fgO{= 7t:)((>n=֠ ҧr\FuMVv=N]mݐdt\MOOKz]DDBaaY]ha7Vr$"1)/Yf5]]]+=rr8x?ر㔕LC}=%%ʞysi$uBߩ={7IIc8w6 `؈fٲ}s3~d=zoP\\Ώvϴiwt{OSs3SXpGk+R'b &NL|o?$$S[[Gn^- 66f8tGLCEE%fsx"##}ĔOM睷?/'62-+%W^DDD`=Nyyqq>EyY9/ ===DEE^qhd4?zZFѪ_&JOO#"2_?LZLxta>>X,!EEECZz DFPDGD…"RVV>h`>| 3VG)Q!!!TpXW^pm[5}'2311͎;eyo? űc' ==pY<_O?n|rXd1.>?GəO<Ƨ'Oaۙ7TV劋@BB_i$(8ӳhllFc9sGn^Su6.:'ɘ:?79"*:Djl~F7o.--̂z)o'""_oF#G2w?X3gƎW2z(l2Mhu3de,^|+4P_pp۵k7,[M~~ـkgC ٴ5_3A555sـ^ۗp틉{q'a^d՚sTVVyN?:˖eKaA!;?5gΚR}=r`A"4VYEpp055k\{ᅦ0Xa]W_ȌfP_@Cc#g4?;ioog|2.7uuk p=p7R7j{[;/wܹk裏O{pFhag~  >6jR]߾ITr=\yK3/W_[^.e>b}cWf96:GhDDDD0h _lb/ :cY& ‚BmUT绿~@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*ϛUȍ}މ|;zw"""(""""(""""(""""(""""(""""(""""(r'}O153yL'}sC 7~/ ns:hxsS.!7gGCC~_q٫}=~9_?_6lhɌ3kd{t5.܎>/ ~lƟ^lذ1ݻQ_;2yr'OɛwoJ0+^s_Yg^z9[lM2`u<~a֬YK.M3KϪ$I]]]T>+>1u֏C^{$1bxgmLlriˠA_:;hWriqZ~lbS<۴a[W.5k%KqB}ۿiN:i}d; 64K,=cr5Wgչ ٱ{D#rfq#Ұ!+VOO̪wV5}>3߳bʦL=A,[^x1''>^oaC'<ȣ6테3:tX Æ WX2?erCˇe˜e挙7t5'|3x<ܥǯd=$jN?~g6oޜv99|Sraxyԑv?ǁ]]]]{6lh*[ngTC/ʊW䡇ΤISrԠo\˾jҫW ?_B>If|5[lgN8ϴżAR$ɠArg&dm}}ݻU❈~eEihhVܥs^͏+X}q{7oVӿu떧z&7|CF7lΞ=7'|^n}wM]%^rٴiSM7z+?3zLMI y?[ny?CΊdiͮoٲIm۶eY/՞ I:w>,]vI=riG{ܣԖsP?Je3r1bxzsAd1Ϣ~wyFzw_yy֯_殦]nͯϝ_*;ܥ%Kf%Isgo_ӜNԌ8-#Gs^}J9hhhwNh?OuN>elL~6I2m 9s'ftMēOY`Ԯ߱:!ӷ_TWW39T۫eKnG|mWfygeC,T{^H֮m _޽{ٶu+T:/IrXr{K<;\2Ymk:$!O??j͛#7(OdRI{Zo=v9+W̼yodЃ{x?۳';lt=;vL>}]fMmy|Km~Ɔ=A4}˖-4qr^9[W[yYhQ֭رc֛ rXrҨ6ue+3[oaǎ 68"ΝFWr?,Y4 ,̼yZum{{/y/9ow{޶J/jm۶۷鲵kfrƸ3ct)552uV栮nmR]]#<2-ȰaCSUU=D%oUUU=&C M~}ӣ{$-"b_Jykwp;t7-5k{P2/M$￿ݹݻBӱc6{&6;v4kβeK/z'[ontR뾽GZ\qw3)\FՌjuZ̙37cƎÆ O~|HNmu,]fmYf>BꤓFfTͨL𻬭[N݆NmVXeKe͚ڏ Jݗy9t萜8U3*o ׯ/^sYfM1bx:vꔥK?ЀM6刏uQGΝ;C/L{1u+'xbmlosy$iv՜V.ǧCM\r;YpQN9d޼7c9{֬ӳg-[e˖娣JnZT9mے$]tUwY>@-\ 6K.jnݺ^:jNo|cӥkaTm1/Yw_ۃ7--r^ =!]|a>,]4ݻwϘtɲt={WRS3*_,\ vݻWNuR֭[y*5eҔu/~QJUuUwvDSO>Ç;߽.Sں'Gdݿm:a:W\yyfΘ o!\rQ{9\rŹK୅vٸqCjs/jW|p9eZg}V?ԭ]s6Xԡ][lO<$Yjuz|Kpf_^NUUUFtbN9唼{yW#9_^yez[=Esw +-d98qRv|xOM>}r_~%NsnfcOO);%}wsok JJ nsNϪY=wZU N>sI}}}f0ڑ6]"/\~ey'ڧvTdLmY7/މDR4ܨIENDB`ripasso-0.8.0/doc/ripasso-gtk.png000064400000000000000000002415441046102023000150410ustar 00000000000000PNG  IHDRFiCCPICC profile(}=H@_SKU*vqP"Z"TB&~A$Qp-8XupqU?@\]]%1ݽ;@hTfundBV+B0dfsGwqѧ-DYf6mp'tAG+q.,̨IGb+JFv |f>Iп \\5e ٔ])HS(30x yqd pp){ݝ{IrV!a pHYs.#.#x?vtIME T0tEXtCommentCreated with GIMPW IDATxw`egKd7T Ѓ"%J/T4&DDO VDA=P,H;wBPB%n~ﻷ 1e'3>d瑵kNx 9M>>U RFhZ^_( h!xJJ^)Gt)5n6cǎ9۶my@>.oҳ!^) ///ooo7Pg @9?.5\wtv=i !ȑ#wرcǎQFP oş[zvx_}РA榥m۶_-**rZ^Yfd2!̙3sUj\^EV5LnAώZϯe˖?pBBڵk5k5,,LJ:2,,,ZEV{R5Q4o|ҥ#Fs玴BsJp79shR_~ElٲXM6/bxxo߾elvx7|͛B//uݻuҦ3gΟ?_`07nB.//C.W`/Z_uԡCyTK.WIu˗/;~/^|1cH?o޹իW}] jwB_~p޽qqq`(oe+yDQ,[d2Iu֕NM@-T'ʕ+ bΜ9]v~|ΝOdڵUZw‡Z{X+WhBQ;CCCHSRR1r.] 'N^ԩS.j{٭[p^s͛7L#iժãCCC}}}sssO>o߾;vT̘1qqquh4٧NZ~99(WQL}^zGnڴ~ԩ[n&JJJ:tеk8?3~~~B;vmϞ=k'-\roۙ3gvNlݺXJN>]Zlk֬4MϞ=qi}\\?uVҊO:*SŽZ7߼ۦe˖?;7nhB?~|+W߿)I&9*--ΝO5111R6QNc5cxܹs9vRB_|ѻd[lsҥ3gJ?N2*)++9(**skl,dz׬YST=;<O<Ѷm۶m>qH3Ⱥeyyy8BN:m޼>7n\\\\TTP曻wv^V;u4gΜ7k׮%k] <Vޗ_v$EEEҥKrb;*P;<_RRiZZիW-[.[LS ʕ+~d2Ǐ=_Wbbs;% -iHNN7_ѣ+ bۗ,Ye˖[:R={VieJ‡~طo_A޽{w/s8lұ|eia^^^رc/]T>}BӧGFF !۷k,t[*,,,6-99sVkO<镥K:W^|E\.4iΝ;2%gGpƍk:~tkTF!!!=o':~q ?|on4;t0a/OBBg!v{aa~eee-_^-((4iRyB PJQ[X⥗^~|~ܲ!$$W_uYiyǏѣ?[gGo\t?}Ro߾XfΝzzQ "_uzz;}J֭[SL9sc7|sǏ2LJ:vXNQP8o~9e^R2wܯə 믿|rp{PCAώZ뭷~z;xয়~:z蘘__ߌ}X"++˹n6m?>pH//+W:thZ{2QѣO>>޴`ءjz=]zVT^EBAnP(hTCp_/*aB`UDй@%;Pմ× J*wءRh5U/TCTj_2*a\.T_2*@UQ(jZR)JBܻvj-((X,6WOPt:g*JR|X#p3VC;#i4Fg6kl%V M]cGnjz=@kښY7pBA*>>> VC2& 7P(̽ZHjF5aJp AnT*ip1\Cv;L|S%>I)9s}>?h۷o߾Q}BBB4iB;P;I֭իW;0}@5iѢL&BTf?aaaB|ZQMvy̙k׮}w 6lذ!..&D&7oޜ4i@Ug(Tc,ZvРA=zhРBHII9tК5k,KɈĄܾ};--m׮]n/qF -**yٳg嗣GR> 22ٳFѱIP 6wޑvŋ'N/f~ǜ>S;w LKK[`ArrT@1cƌÇTq#Gl۶zzzrrw}wq2?o:֏?>11S?!숍]n]ppcMVZj#L6-##ñ~ҤIcƌ/waSN5 .;֭+Xݭ[v{III3gtN:"""ϟ߲eKǚvڵk;ws\vΝ;GZӠAk׮&2lذaSNDEEEEEo]t6MP8~!WXqٔ:uwȐ!K/(rʕ={>}֭[%,,o߾}mԨQ||wv}…BÇ_%44m۶rn=AzwߍNII1cF^^cVBBB? .(ʘiӦկ_G.>ŋfO8Űaf̘aZWXk׮7nDDD 0`СC tƍ?\.߻w^۵k/F3a!ƍҨ;ϓO>8{l^^""" m۶m6^|G~GAVB|W˖-s$##ԩST{ɒ%M6MMM1cFnnֱcJIǤIVr޽Ǐ#""zUVQnܸQxgsޝ;w&L0uԭ[EGGOҲ^h4nݺ׏c5JPz~ФI?? d 6t6lXHHHff槟~Zh4-[^vrުjG!I$//oÆ BVZŋ˘tH5ŻiYPEGGsp1nܸ#FoߞS56:N:X͛Bppx@q܍Z?Ѻu댌^x!33ӥ@ A4N>]W5jT|SzhKNoQXXݨQ3gp HCBB2YHl6 H/sJe۝[5w ֭[###|||_~yS'JAw}wi 90] 2dȐ{ l,gŽ/>Sƍ }իWo߾uiӦ1¥[y.]DEEEDDb IDATܸqѣ[l)%鐬Y&88xС}t< sS>Cqqq 6ZIII/^ܾ}#G*v}ڵ۶m>|xttt 222nܸqm۶ONN3f̤IZlx*@vIuVGwN:=;noGu{}"6wh4ş̰cɒ%K,~_hѢ-[lٲ?ϥT#77s%nZtҥKKܴm۶C\|A355o.y6l`0JdXM[nR233w8zhU*UPP4R֪4ZBٜ%M5`˫7~~zM6-77700xQbyHOOZJrС'Otҭ[\FJG4.rll4q*?s O>:tСbIOOtRRRv@jR:kTg^KH%::::::..~rH`PL&__˗/oٲf%%%IӐ6iҤ}z~VJ' dWJSԫW׷fVΝ;G&44#OSeSAAϯ =jٔJe=hlRVv҂lB࿸:O`XDYf^^^.4E۶mÅ*JHn߾}A!Dddȑ#u:B&*ÇwIlP(Jexx?,0Lnr9.//=zT*!'b0f3~d3L!v>GN5bXY悂NhtT@~~hWVJ@-dZkZ|fshT*JTr>iVbx  jZnًf3Lz/t0Ţi΋V%zal|M~~~ Ma4ip\v{l6@W3#1v@m`0fsͬad<^^^^MD'e6 t:F5aFc|zŁfݹsh4jJT*rz^eۭVkAAb1f3L5wK#(vB< a(vB< a(vB< a(vB< a(vB<ʟv'%%%%%uڵd#FXf͡C߿bŊΝ;W[|A'tJ9\|ݻ;tѣcP9jL9aO?ݽ{;wSխ[NNNdPp^:dn9aȑ#+Vضmd2ڵϭOaaaFF3\b\T*J\.^LvjZV|G# ;JeÆ O9?~i.\п~ZG!jFQ* pUG)?n!d2BP(^^^ZVaZ-K" nL&g?uȐZV{/9Dn$9tpdM"J^fl6{.P Vhދ Kyd.#쨊C[h4i,PPU \Vۯ4\21_i80Fԏnl6_Q,pGj`ZJQJrܸq>hÆ JeZZڶm~R^Ҽy &t100055k׮=t萣@֭ׯ_񫯾r,oڴi֬YGWJr?˗/ߢEO>$!!eӨQ͛ݱcҏT*)-7m4))IZ޹sIjOҡR|}}iF΃GJF( IA4ԽfsNAA=wT_ѨQwye˖5M6mڴ)S3f*JZӸqƍ/\`#ŴX,jB"-XV!Dpp͛w޲e˖-[>c ,Xnf9QB~9h4zp\l( FÈ`Bt:l6l6nw{___VjrQMaϗ_~ѣ7olР!CZKƌ׿jKZZZddaF=jԨDɓ'?B#F9ry?7o((hƌB[J+gggǿ+6mHC=˚Ǐ/[l }ڑt8|zYfUFFiӦ><ICSWVt _VϢT*M&OT)h4LVƼ'00PZ8|ʤݻw_`ADDh4&%%]zd2 0@9ri(l TNx-n;t:bl~<#: CYYY;w4iR%߽yRܲe˲eRRRKzzɓK,rX/őtviDRcZV\.w{ޡj CMK:DqEiae|.xիWw4hRLMM9sKw-q###CѠAw~My@9ud2 I*I:b VxFQaGff44ĉccc]ӧK>쳂F`Z]:(3F! wp!DΝ,C-kPnn"""~"@mh44pՅN#tU4Xt\(ߙ vM|˗/߻wK7}哒}ٳgwu˖--:~xff\. ڵkvvև8qbxx~G%''k&M9Rlv.~1ch/"!!رcjUVlٲe|ڸqe˖ 6,_72ٳ{%3g^_t[_2~xW_J+U*UN=DѴiӄZsη~;o޼|077wƌ Eƍ,YVϝ;X_E){tѣG8PT={]WT!0 f;>#BPM! *wUpV^dɒ333WZ%j.Q;V.߸q>s{'M0j([I&O:ƍB͛;VH#/…o֥|5mB\ҥ/ŋ׬Y#hѢEv2eovޫW/Lm۶'[W\Su\ לaJ ; #nCZP(kJ)'On___}v|A-BBB]Vs\ޢtIIIB tz8*b۶mR=zTuefҪ&*uң75귉sp;IH o/f~iL̀믿 !d2Yo]4jHZ:!2$;;ѣBUW7#G/@:ܛwa jȠH73yXXGtt"//nCr;wP.e>>o񆏏۷z-R͛)MHKK[<Bk9r\ɟ|I[h1f̘YfI'N8t \+/nevTg,UqZ`H2o޼ѸtB4nܸĝKCXsӦMBٳ/:tHѸqcs6$$ihn;oҤ"==bTieqZq5Ͷp .X1Gl>}.^X@^cǎڡC9s'O.^bG'=cj54ʕ+W\/}oҥ4cT+Sf{v;wnǎ-[믿^^zw1c֭[TTTm۾**%%^+%~G}sBGzKrTN' !ߟT )oÍxP v{.M6hР?,/(}jRifΜ9~~~.o޼`H2kv͛7T ޽\FFL&R+OǮ@׷ZoΝ;}ѦMegg9sfͧOv)h0p駟ZSN;v  ξvիWR&-ǏO4iРA;w ׯ_߽{͛?\SEX]D1g6`RQ;G!xQ;V;xh x6%Mp .Te2Y-vڦM:uԩSG.ggggdd;vG# K6m>cLZʽ;ٳiׯ><<<<~n;]|y˖- 69sfN <{좢">ډJqƍg}vw-[8jԨ͛7;'.._W_ !u6i$>ZP#$!!!.\}ڥM4y۵k~ 6;v̥t/^~}߾}ܸqもm۶eff>B'xڵk??YfV+ œO>٧OȢǏ.ُ6o|Ȑ!۷ JMM= 7BDGE !Ӯ" QQQB?EEEBHHHUK:u֬YP|j}w[ 6\`Af\v\.1cƁ\≛7of-^`0̞=ekbbɓoK;999.vɓ322WvqٲeaРAFѱ~fjRKL&ooԡCJłl"8qɓ'/oٲet:]2çOR\> 6$$$8~*uBCC{Xjժz^^^%Nnݺ!C;x3\w\W?IؑtŁ"Z"*-jkZwXOZWUpEAP"@$$@|;d\\9bŊk6… ׭[O+** "JX*WXѷo_f9}mJKKG`͖ Q 0W4%K,^joUTTlذÙ5kɓyYfFdV~z޽韉NXXk$+T*bl6Rg#쪑{k<h$I^^^bbkמ?^PPߥK?| _}۷O(߿Ν;rĈ#F@ϟzjJJRtrr5kɲehv477}}};wW_uP(ٓ.:f ˗/X@;!ʕ+WWUVBΜ9s=z,[ҒvU666vvvLCWWwǎIIIaaa<(++322tvvp ;v…?CLLLNNرcG]g}Fwޙ3g.]4xaÆi;|}} !7od"`߾}bÇw~ܹ֭sǍW^^m۶EEE%&&뫍޽{۷o_\\$TڥK''>H[[{ʕ{[b!$::ĉiiifffnnn&M211ٲe˄ ԆP2LArppYx℄zᤥ=}4&&&999//O. :tذa4U߾}Ϝ9RUU8} AY8qZIrrrLLLddG}ʼ5c P(H&ODL &O\Q~۷3fff޻w/""n 9rtB]0!{L>ҒBzMqStRazqqq/?`A1...r|޼yLW???????11Qu[PHsYrett4-|ۋgΜ9gΜK3VxtÇL9:t(!ĉL)ShcԩL^|ۿq<ٶ}ً/ji*L:}dҤIfee]x믿5jԐ!CN:Ō tbNXXiannnRRRtt?lٲ3fh. XaÆBVX:*333999""BGGgaaa` Fzzzlll^^ޤI|}}7nܨ3rʒ%Kȸ|ݻv_^pA5<B]]]:Y`߾}LqZvxUt"66V3 oFu<礤$x!_/:vZޡCBȣGWWWBÇUk\j;%!Nx$--- ;A==0FDD\.Umyw5 oݺEӎІ|ҥKD 7?JJJh[9;;kbƍG:j'L@&A);vE 8YYYUH<,ͱ Fc3gΜ!14pUU4&o|jzi7>޽K14-4\77={vI,k׎|jꢢ"jQ 鋲2v_| !,..V-IT}׫VDӦM6mZIII||˗ϟ?/˙>f:RZ4*QGN9s[!<2UԦ`RKHUkyf!|>=ϟ?cIs@ )ZbffءCcccӧO@ҥKh?---++|Ȑ!j5iX,t -C{چG-AX:uT*.~Q<==jaa5k֬K2 0KԲhff=/+,,|ahcgǪUj4м$2ԴOT&MK5,JK.gR5hРUќv@Ctq\. {)Ξ4imbXuFS\\LLӧӽ{w6#;_ \pA3Q\.? !C 9Ao>bڑ.,,zEooAmڴI.9,Iʴ7 SSjzМ,EEE2ťa,[=HIR7V[jCCCcbbTrJM[մB^{] ;FYlMՊ͞=/%%emMNNVҦnzM6l}ի3224{WtF٢ZTYL7_~%nB\ڵkÇl--#F|JKx䄇E^uFg777 (^Ba߾}KKKֻe>Tj9'!YˋO>|>_T BȞ={Ξ=[&NNNvvv4As4+ :_+x_GoHsǏ?wΝ;[|ꔾj^$i ;Zpv9[S'C&LlAKQ?##pqq9t,X/z: V\)˗/_ɓ###r9[tiS`Ҕ6lȐ!篿bKP/^8{,!dԨQnnn kyf>>@.8p))H/^L~2[`!ĉL 9ظU*&hii͝;f߹s'>>"@s3x`5 OCdee,WX|شi֭[Us;vXd -qtt|3ziaa󝝝BD"y1~BH߾}=I3k9:uTUU; TanݚfwڵpB{{{m_ji۶meee9skPPrţG!C >|xddMQQݻ#Js h߾QΝLRGmcccbb2`}OPlٲ@s# H: fdÆ ˗//--qf```nnԷ_'t҅0K9::=9;uٳ}ӧS^QQQRRZ/,Xn:)SL2y޽{_7[7oLIIqttꫯK:E*\?tqqřϟ˂FEEx-8m4-Z222RTN6jӦM6qΝ˗/ ._?G 4fGDD]F-37w6l011H$˖-+**bު_F<111>>Ɔ&ݠB"n`@˶nݺQFY[[xO>xȑ#RǍ7mڴ~:##ԩSgΜYtK{ڵG3gܻw/Sװ?xuoooGGGXlllի7nDEE\P("""Ν;駟ƞ;wauqH:3g~666֕=JMMb.M+))i}|`hhQ32l֭ÇرL&{?N#SǏǎ;uTgggssŒٳs7n\[\ROOO>{EllCT?44?Qttt||T*v4+ 88ʪx###### ryPPjD"i#VږbgkKy֖J% @_Ԕ cΜ99!$,,+hr7!䫯y& ՟ OƉ'._+;;[uP'I'L@׉r=z(++[d[JtXX,ͦiPU6'FЋ/BM};hkhjҔ:Xmaa; lٲ=zdUVaIXR(AAASNuwwӧϵk.]t=D"ڵk'{e˖-t !111͛2yzz6_mgg7zh'''CCÜSNݽ{bBCC5gΟ?իW_~e~~>!TKK+;;;--fAcб AAA<>|x---9ӧO/c888={FFFiiiO29RLKKKNN*//WFD;V,'} 7n9sxxxTVV'''Tqj=@Z$RK.͚5}5`"HP\td8//zMF9}tfSkkkkkkOOϓ'OٳάIHH8vرcOM_~Jrƍ4Ѯ]{&SmH"$$А)ڵk׮]h"Dv&L`AD"w5]e˘Mh3:;;|Y {tR&MF'O裏 ŦMhav@ oݽW^ڵcXW^kք&MJJR͠|X蘒R]9/ zjnnСCG痑t'N:-]t޼yP`BѣGڅ$999...''P__cǎG6555kݻwMP(:ѣLcccooo???33\Rرcivٳݻw?~|N|-[D"QjjjXXؓ'Olv.]---fΜ9~dRĉjc@5k.&Uw n=@Z,1 Њx<2ڞu vM8aÆW}L8q.\=ʐ{:88L8,kņGQٳՎrڵ{2'Ç93B$u"##U?'ܺuKsqBȸqh Bbcc޽{AssѣGW3hѺСӼ k=Є-@ dT*'xxxq ]]ǏUQQC׿rrrvE3fLG٫W 6.+((HHH Tq<99t0N}`Yhh9##!uV]BRQQAsau/^?[~UUU!!!/_Ӥu˲2Mhj JC4GhQ՘#;;7&%]pKS` @  ]]] !NښWS&q)c..."rСr]\\E"P(x<\QQVB}]zcǎ2lʕv266^z >}Z^z^`@ pXcϞ= j'DF4c R;mܹ4WT*)//j|#>{eq" NI$իWoٲEWW788x޼yu^`@ 0x`BHLL̆ .Z۵ﭨƍW9 :1{씔gϞV[v-ˍb2zٍʥ3CCC&1kl۶f[N,/Z2 IDATZH*qzm Z СXEUll,MZ޽{bffVsꫯJePPPbb" Vrsrr6n3ܥ&Ȩ;:%uO/2lذw|BCC !+W˨zm QZZZKB咒DBȀhVNB3EWWWZQQ̙ΪU͛7Vs.ٷ'IQӧO+ ==ٳg3H|#yyy.\ >G:u_%kΜ9o_֣7WOOOrM7MAYcXބk׮U ʕ+}}}c_ZZkB@ `q>}4<<ޮ];z,H4lذ~yѢE"H$۷BT@LLL'''###33>}_&(//oLS0`\.R]vY__߅ >~8//rƍ .ٳgϞ̉UVVjAhs8___+V02,''\~iie͛׽{wBQ^^C+$%%%$${T*7mdll8ydfIzW^WT(JĄbqZٳg%''/Z?o,fgLgX,fWEfv5WV(..~U*!!)!ЊBh v4;LPh:uԹsgR?zxT'0k_bZ4e˖Bw7y}Z ЌЄvVb͛vz'@VbUVVVVVr貵6_~o*w|t\٭r?XR>ZoסR*9;2L&CS@ӒdmaY`@B;,KPA) k`{VSSs@4U>h^D6]UUUZZ+--b٘ 푖aZhJ:%-9(;? ]"A!8=cX):㠢իWh hW^UTT+ ;jwc|4@IIIyy9aXG+`@PSGBR)P/%%%tIkMh(Z^*b> ѫWh><-e'%p4,KTj`XEoWYY+3Cs4G}`d@3RKXRh*---,,7>Tb͔@6P(JeYYL&:::h( IҪ*h^4xoFl6[T*ʪҲ2'0mJ2LP.W\{-D=쨎 h&`XsP(RT*p8ZZZ\.WKKNxiSL Vj Fy*hTwTZ%TUU1^hS oK׭ͷ =_U'hNVTV_+rv@sVW,Kyzv4έjBWR]h ;R{I|hhxFp6 -gT*1uAMev4ccc4D4hkjnG^^^j;Z^q ?Z߭oj-߫B`* o=!f3qBE &ءP栞*&ޡZharZZZGKKfۊB?JZVB%#8`|>qfם*/////WT*F|5v JZ-p8RP(my-Rf@ eee_f ;;?\>&@tttx<^IIIee%t(6`Qhh6J'322Bjpx<ӦKu}Y}}} ԅ=m3{F:tuu&Pw4 H4.gwxP(4#)F__2i;HI@_ D!uCyQMJr&\.WWWM%+ @3:4nv,233GղU.]|ڵ+))IMM={lllluttfcccddgϞ%%%r\~mll,,,^|󤤤Ǐ7zy<===---sss'NU[-LÚ[nSN޽{HHȥK)//b,KP٭yږbgkKyYE%P+JXMCP FS,/\GVN>{j76mѣ51117oT-1bɓ4ŋoV{8uT''j[TTy_ի---ʋCBBU 4mllLҷo_k;!R;!,hyyyv (lv#H .ve̘1`ĈOVjҤIcǎ%\vyyycƌѣWUUզM ꫯ!߿y'O>C33Yf1C$^zׯ_? +V={vNNN$$$ظ8""l6O>ƍ344\v… ҘodbxҤIl6[T`@ (++#CoFv@*#:A%kʔ)BСCj38LLLBCC Ri@@L&crpplob}ބ fï\vSB###iv>}bccՆB:uew5z׬Y^\\Z>ށ`@3B;ZZZXfff;;;BHIIIM)9SRR***HkУԱ3ߵk׏?r^Ϸ <|:w%%JcNGdj,Z݌-d2B+++2ZEEEbׂatHEX,Ő!C 3&iYZZұ<󖩩iii;6ZdA=N$yȑ# !ϟLySP,ӚUUUΝZB8wUOtuu ʹgzod*Ќ;|N}Դ]vEEEٙg-Ebb3ojjP(rrr.]tI5odT0Z$VmPFv A)*v@`*v@`*v4k/qqEmVhUVhUVhUV&hڷogZXXd'O>}ԩSj aÆ7ã]vYYYW~ !ȑ#:u駟٣ѣ,YˡC9rfff"o믿B\|||ڷoonnÇ?{g'/3fܹsݻաC.ɪ=P(0aB~̔J/߿_+J՚;v?~P(|yjjovm<v ?绺hvn:}}}ZbmmݰweJBC~~~Âu"H! U7ܹsΝ ukNvK###i3gS|rգ{zz: vXc~Wڴvۖ-["`@딛O?ݹsG.w?ݻ{HH̙35o۶,***11\___*6ฺ;vMJJ {AYY I$k׮=@__K. 222j?VwP(ٓ.:f ˗/X@ƍ !7oތJOO妦l6[P05ǎp~!&&&''oرGNMM? M<Ύ9s)S\\\~M>/^4...r|޼yL$??????11\ĉնMNN422裏BCCx$ իWU߻wdϟ?o  B=sNf;wP(Θ1rhZɓ۷Ϝ9sΜ9W"S@!A)@ PQQ(R?cƌdƍtBD---SS{Ej$ɍ7! >wD:+}ѱcG0 4??~zzz<`"\ۘ+`@p}ª'NBlmmY,ڻuYLb80cƌΝ;rErss !&&&9ׄHBk߼SNwj%]spp3ЂBiД_ZJ$M6mڴi%%%/_>\.gj jwޘ+rnnn={ԩX,n׮?)vBP+С!$++BƌS&o+4 -b@y)_>j(OOϾ}ҕbf͚tR1UUUU"wwKBҲˇ RhԄB2Y%)):xʊRPPP\\\/@.?ss!CL2E,o߾}Ĉ4!J=<<:vyf.=}Y5;;{ҤIuV$;EVVVN׮ի- c!˖-}vٳg̟?-\zj+M rvՖ{yyBRSS7D„ '''<<|ҥ##nݺ+6l}ի322 ..d'[ ^~pss 1hРsվ}{4{ԯ_?z#bq:w|sܹpwA__ ( IDATQuĨH$BJBe077W+8pA!׮'{oq'̪ AS+ .pT ZXߝŋ !ӦMfdd\.xK.ƃ 6x`浧'ٲ +V`>;mڴi֭麌;v,Y8::9r$44th̺Ce8tnkk+ ]\\,XqFBș3g뵫SNr>:d33: 6l۶m;wV|3fB[[;;;7DzEm\]]BŀoNsE[ !...>뻓~:!d„ !!!;wfXbxĈ'Ndj߿ҷoߣGzzzU]lX,8p vx#PJ;fdÆ ˗//--.co`` =kܥKZ¬;h1i̙3gDT;v￯222vرxb `շrp}::::::jY(++[hQݓ6슮^zY__>}Ӈ)())׌;̚5K eeeQQQ{N`k֬ݻBׯ_=z֌T*ӦMڴimImmmΝ;/_4hNOOXv V ͇ LLL$ɲeˊ~#F$&&&OLLI7g!///PIhUϟ_QQЩS'cc{;v͛ o}#GHRsss͛7w\rZ굓H$s8pEQQы/Oc|MeemK%WT("'N|r//)ShVj,@~֮]K0aB^^!ѣlɒ%M'_mj,yyy,f4?U#xВԤ)))t+W!X`X˖-ѣL&[j[t@0%u !111͛2yzz6_mgg7zh'''CCÜSNݽ{bBCC5~Ο?իW_~e~~>!TKK+;;;--fVscб AAA<>|x---9ӧOd2Y0zݻ>}p8J2---999**\Q\\zHa޽.],foeeSV˖-"(555,,ɓ'l6K.AAA3g?~zTzĉr}}}1 GEٵk Rx<2{zz1add4qDBȆ ^J WRR2qӧ_p+++CBB0qăXŋ&&&9rDuϳgV;VJJʵkkhh8dȐjÇ93B$7lu-M!ƍ&LC{G]3f̠FCL67@  OPR4>>QU0uuu?~+BHTTTEE]ٵk!d̘1ݻw9rd^ 6l@gԮ !!bccnrr2`sQDsO!GQ] RZZJѵkWSڵkW}#ݻw1yd:IzP-;ZEtDnݺeWjTTT\j]/?l6{֭_|EUUUHH˗/x4q|Lm~Z䂪R+СMq}4jL݀D.\ťn}}[0 j]P^^^°&TI\ؽ{H$":tq\ggg{{{H$ y<ǫ%WTTy"u_^&!رcu(++[r]W^`O֯D!xyyq87VسgBI-͘T*-))jrss;w.*J322rrr˽_?siiitSd[l 7o^aaa}צ  <aÆj+|ᇋ-222rvv}v{+**277qի~Ce&}̞=;%%ٳg֮]r###lvr)<{АF̚5۶m{;Y!!!֭-Jom^h:t@4aQKVxzzqw%+R󃂂Ux{{sܜ7>{L3wi 2N ߿A]$ 6߲PBʕ+2*m^`@sGu޼y:%%%Ьf>j_͙3G-`Q-UVikk7o.)) TF\.WoOnZOV(zzzgfG.\@>|x=];uԯJիל9sXGoZnz`@b !׮]vƕ+W!LǾׄ@$|ixx8۷ݽ]vX"hذaSE,,,:DH$۷o'ߟF3|7NNNFFFfff}Y~=MQ^^ޘxə3g!^^^;w8pyvyj7ٳgOAA /lmmi#xnݺM2.c.tq___>m/^BlllwN$69;5'''BHttt5߹slOOOf BH@@q1R9~xKK5kBRVkll,'|2`\w1+\vٳ .|q^^!ƍ.\ٳgϞ=,--p8+V`e2YNNZe˖͛7{ \GGVHJJJHHxKNTnڴqHjշ^:yd>uVTzÇ~fLJRTTT{;wkiiDXXXUUgLLL233i?رc{d@BAd PQ nZ몊QXWԭmu,.Tdq|CFx=nMN=9>yyy^} clr„ q222Tn֭mڴiܸgϞM3ܺu7x?>tD"~kH߸q#<<ˋ޹sR~<ٳgfٳg@@L&KKKKOOkjT*]x fΜ:Vrss&NYVV:Ep\<!9)==]U@TE * Riggb8ԩS߿?k֬:o,f>rrrX,P"gE͝8i8yJ1#jF. zݝU0:LP7oޢE Ry9tڀ`{YxYXX̛7r̙t-n,F&@ 7ouƍkv%d2![}͛9r_U(u_9,D$e+VH$^62rv3T*ES@͒J aY`QQP(b1jX,V(L/wU^>0.t͖奥hR\f:"؆I&%Q !A!-(7%%%hڅhwRc؍]qPQQQ\\WTT4+ ;eee()))++#L0 D9!D" URRRBw`]=>AH1cQD"zSqq1tCRvRB=HX,Rybq8\^VV&,,,\@;LV\\L7ath߱0":bdRh*--ϗd4@NU_!`4'zlBT*bT*fffh( bD"i\e,0 lRT*rT,x<@- L&H$RTP+hQBf"E B"H$cbbrMLL肗K~Ĺ***d2YEEAwb# W `Twh}a-Br93Oȿ/oov8V3A4{0.~\WV_9;XFZcZT"Q>uY!`[ȅD[h ;T}v;jׯPfp0 VTb Ae6dv4  ;э:|U ok Q.}Mxǽ;* Bl6s1Uvh8 vF:@9G.32`{F/ա/511p8&&&l6z| EEE\.T5VB}Bgp0|>A4,fs7eeeeee3>JڌR!|@i8P( L.Y !O@=:CT B3334 0@ o޼a6)v;r\HE++))d4ҡP(``h6J_geeHpxH@(|f~GÉw `tHBI&`Hr $ށ`qa+t@5.֯7;jRRPr&P\.W(6dx,̬Fe˖~~~&&&Ϟ=;vǏu=`6m4jԨ$55ԩSZ [YY><))رc4iXXXŋǏUx~z꯿:qDAATÚUV[tRt332\fY,B`!Ã">YAIOOWP+J{{{\ \ T1gΜٮ];'OnڴI&L:tŋ1112L`XXظq5˗+VPDFFj}ނ_E\\\ԎEGG'&&Ǐܹ3DRZZJEoԈk F`l6BȲe%SSSӅBaAXXXzzɓ';v !׮];~xNN˰aڵk$W^޽Ny[ \]]Ӷm[GGǨ)S0S$D"oqqŋ=zGGΝ;zSN6EGG޽{fwiĈK,9s'OT6رclR`@ tl<$Q{0K2ҡ:C h(QUǏ޽{ ;;͛7[ZZJ$T߳#Gl۶ Y*88w077_n޽{\쑑{۷455ԩS||BH׬Y[^ŋvZTT4}tڵky<^rr_~Bpaaatϔ˗/߿ӦM5;RZZ*Hjcr@RzB|~<ڞ={֮]"77w݄@zرcl˗/wܩV 6BF:4if8^|If_rE3@yѣGy]v%ڵKm.Fjjjll,!ǧ}]]PXgN8qʕO>E;vc:q6a1wARsrS\ɭ=L&KNNV{ RRR!NJ9{,Tەѿ%ɪU"##׬YY}r8-nJRܹsZϺp!bi;Yٳ,hڴ)! !׮]H$ܹs:*'$$/uvz@I<#ӓRRRRYJ䊊 d ,z[l9dB׫r|3!ѣG !vvvCNeuvz{jϺA[H0]]] A|>NS*bX666ν{ٳϙlsqqs%tT*Sf(v' t!I$={9nffFϷ%u rZ W(O޶mj$ ʹ5W}W6{Z2ށe,F>`9n8KKKTz4 u[XXH155#F yyy*33B$G7޼yTjڵ3`G999w:NI@gpϞ=裏!#<<|}ݱcǫVVVk^Yk2Um:t1**bݿ_s~ѨQ#@12qD???Rzj1w=ztǎ311Ah*tu6uTB˗g^[!^^^: \^ *,,I[n]ك7k֌ TZj׏f<0A355]xq!y󦭭fBLFoߺuzݻ?nkkNu:tH5^@СŋMMMV^mjj,ti \gnnҦMBHBB¾}bJ ֮]+֭[{ƍ __߱cZ[[lٲEZ5A |W(JްEWP(jԩSZlΜ9crgٳfm??&㰵ݳgHJJꫯ!'N۷H$ZLPm޼Yֶ:m6**RWD$TÚoFGG_tƻ\NNbtڎ|Ly,,, 8K&}qqq͛7,((~otھ}ݻ;uԱcG'''Fdee={_ձ>'Mޭ[7B}ҥ~MsqM-Uưf^7j{f0 J^A;^A;^A>qƍ7uٳgύ7M݌;wf͞=ƍ۶mC7z&Њn:%%E,޳hт6_XZZvҥ{M4iԨUiiiaaajj͛7\RPPV24Xvh~;߿Ϗf+$񳵵8qbXXQ=nmmmmmѻwoTzر={ _`!jժ!ѣGDڵ#<|͛7W9::B H5,,l֬Yڵk.]JNN˓H$]t qqq裏z5o޼T{/H$ e2Y^^ZT!gJrϞ=.\xq= vܹs=f8qĉ]ti >l…… sҊ_zu-[ 6,&&F,mݺ{1ĉ7nDn޽{էx>>> ;ޘ1cƎP(bbb=\.?|pbbuWZ5zh;x'/h0iӦ vO2tRݑFjjٳ5j4i$!@CkX߻ =w\6}?S=z}vBȀђu XAAAaaa>>>/^tҙ3gJi&<|:ׅԞ\.7++ҥKΝ݌ O>MII9w_VLa۷o߿ۿmҫW/wwb,kȐ!ݻwoժR}mڶmۮ]'O޺urСQFYZZs߾}Z+v޽W^7vrr255LIIٳgϫ棏>1cBXdӧUj֬ȑ#۷oommŋ#Gܽ{W'&&}2dWyyٳg׭[Ep8!!!M4Q*)))?D"a :tΜ9vӦMoܸAo_zu֬Y_}/4hPPPP&M8Γ'On޼w^TZ@@tꫯz}_~Eŋ?7nܸqm۶ڵK)Sƍb#NNNNNN;vL=,,lƌB)lmmt2`heV{0!+77*^QϷf4mڴiӦ} hʘnݚTM6s=888&&`m߾}CCC'O:&Rl%KZgggggg___fGEE\.g^-//7ڭoiѢE7n\emhoo`Ν;3G7o޼y|njjJxxxxxx9rd͚5oƍ\2889ڌZ,[B.XB(//gjtBLpƍL؎|}}7y䜜| ޴iS-b],--=<J:&"N:˖-/**qP(5jLUݾ}{РAZ_oؼk׮x"{ԨQNNN3f̸sNFF 0w\BHLL9sL&/^>|C;vLh?Gff@@ =ziӦǏs8VZ}˗/1b^tѣGN<9222--mĈ|*-[$''wذaϟ1cF5?gСC[hQRR2jԨ/_҃_~֐֮]ŋZZZ2Duw̙3gΨ3vUVA!zڴi;wn۶|URRR\\܏?(͛7i$n۶m-,,˧O[^Wbt45sLB7l<ԫWTۧ2Uj7ݘbXk_~N={?r\:dչ򡡡\nv*O?#奥i}p]&Mdmm7nܸ\fk;"##7n7vXf .|Wݻ~u>6Сzs044ߟ2}dBD"Y~ !EEEW6Ҝv̘1j޿ŋ߿͛ `g,kGQt… VZZںu늊&O^zUHzz:M^NMLL 0mgӬu Æ #L~ͧGmP(֯_O! :T܋/6iҤ !ѣG?CBi3!rMLLj GsU4mڴ:TϞ=,YfnW󚚛?|Ps"OlllyyP(TzV@  ۷oW[RRRraB_MgT7T߀@*gnnN|ru/էW T{իW ;F~xzz#wܑJ,kǎ&MjѢ]q?#tڕrM=_Ikp|}}I% ;t6ӨL렵teJ߾}Ϣ^wwwz5qww3\RVV]z&5"dXtH?hՙ&DmaU/aݺu[lٹs={4 БyWyy9f@A:Ӑ9VjaPPP@FRm>g2J`ƥrtt iҤͰ0*ŋw믿0a„ JJJ._|ٷ?#V gТE 333\^ѠY qF++;vQT]vUGPBgޕ>S_ kkhhػw2b 6J1Am*&#il>gưlխ4jΝJS xٳg,gϞjNsX\vܹ}AAAAAASL;w.Be<݀xLs٬XbΜ9ٵDGx&(1)))/47I|P)C"qrrLM_uHHHnn͛/^|Y755#޶m[wwwbt gϞ={,!ɩw4u:C Xfffߵno vܾ}{صk׮]LLL:t0bĈ;e"8YYYcǎU]eƁFݽ{s\R@~~իWg͚UK}Y.|GaСCyOԩSÓˆpqڇsAlAȨAW^-[:u?Kߥ^Uuԉ+J+Q{--ZjU WvڱcG&B[t\Մu\ኊiӦuuWC#Y7fDxxͭX|}}9sf6 ޽;sחnDCi7n榤|}ŢE2224窨&0sctAEFFK+T?me]ܷ%={<ƍAޡ.] WբEǏ>}zÆ 7Ӻuk/$"GV3/ LII!DFFꙭPt91F0'fϞMB}k1$Ǐ !_|EDDDƍZh1~xD>~޽{ܹ388ͭ_~sww/((شiSoϞ=&Mwvv655~~~| !$//!#^^ӫZ1BH\\-Z055eXaaaj aINNUʕ+-[ڬY3KKK6mgg7r>}g40hQF@B!€~zTjjjm۶)SY[[{zzN4i…Z_ڵkb1=eɾ666۷vQgQQDeVV֜9s8NXXg}֣GFFF^p!??<55uӦM3gΤ}: !k׮m߾s׭[GXjq(?7d2ܹsVQQQ]tazɶm!;w޿`` }X6moo^#о\6qƙ3g6mڔk'g}{☙͝;̌bѴ&:>^ `@4K*Bh٭\"++k ndaaĔoڴ)z{{#̶t>>>hO!gQw^tt3,YB)++c~/((l넷Z|;̙3ibB*##Cme6=l0oooIII˖-SMUйsgbx֬YLGfxLcsOjŨe˖-^cǎzիBt,͛dj'OFDD4!ӄ:BWX tԔ6Ԝ>}ȑ#j={XZZn۶M"[ΰ3f,]nǏWj]_.^%22RuT*)駟֮]ۥK;Y43bĈݻ{{{K҄k׮n9W^=uThhhN:u DjsC,Ϙ1cǎџ~)>c߾}Jr„ WMMM{Usl6mڼyڶmP($P(n߾\|>`rSS>^ c @mٳ'NOO]d; ʕ+͛WPP%89111!!ݝ&ݠ Y[[hR;_Gn֬'IKK{ӧSMJJ###322Ν;wԩiӦ1BT*}. IDAT׬Y3wwwTvǏݰ}ׯggg߸qEEELɃv姟~KtzU+FM6-44?sAA˗/~7mӦMr<$$2##&;`H$၁ݺusvv755}eFFƑ#GTGTZZȑ#'OܺuFx񂹂]#FL0[no޼㏹s4H󔤤#G~mڴ򲴴MHH k׮ݻwo=ztnn.Ӌ?^gK.7^~=}۷[YY[n„ tB=}G}ԴiSwwwӧO+MOO}v&?~z͜K.š4iR^^?X[[`֏ПjM HA`` FE ,_յhۧ<**J{KɷZXnOBȋghx/xxxuOTUFeɨ48p@. ;V'++3fT@{ B 777l̘1^?~PPPVV8vc]t3FM7r˗/o׮X,3g]mTK׀XrrrX,Oogg֣GEtpp0!ŋȭ[R) 4`\9tP___KKgf犎fX7o\:_gtԩS=\\\LLLD-f.]BCCJUhQF[nR LxyyoٲEkH"::Ғ9Ҳe˖-[̚5+//OGfvرcei޼y&zb s*KKˎ;Ν;IꚝO7to,͌Wz`W/T^\}:t߿k׮͚5kԨEIII^^^NNέ[]H*4))Iuk+W'99Yχ8pয়~*{W^999w~j۶mΝ;}tAgmm=c Byyy߿yfvvv~~H$j֬СCLkM~P(vgϞ;::~j{>f'믿N:ajjںu#GjMt׬Yckksδ46=qDɓ'kl2DrĉD"9 Z 2d72(z`@ǣ)x=z5,͛7ˁz; +W^zLOOJJJƌ'?e2Ytt֭[ƌk.5{lKKğYN\׮]ۺue޽N?8q"s^^ޣGJKKG̪[۷Ou#۷oknnB1bt0aBH||k.''Cj=qҤI4ۨ4i4aذMHP ~ڵ@ dLxH$tGs}ZQz ^`{ ((ἵX=l٢P(tc>ь)H$(**zV@@_|AsJ$첲7?g^'O<M_+hѢ5k˗O>=??_sk;!!!/\Rk>}̚5ݻƍ-ҿ}e}L:599<==,YrۗdxUDD]\ ϟ?dgڵkSRRbFGG/]~f͒H$o=[AAK&Mxyy}XTӤo}Ҷm[GGG=+ʨD>ejjZ&88fggZ皹K&]ҭ[7 }ӗIݼys̈́M.\PY9z ƎN(--uVeeJJJ !ݻwY9 !zPZ<oڴij ̾kSSӣG&$$ĔxzzN8Qs\ͱAHUmhuIBann>uT5R|>_3HNN !k׮]_Ä:L6z⚛7(v5Lv]HW\!D"f`_ZZBH^ӧ{~k׮5ekktEg͚윞{nBH^^޺u!ݺuc vvv|󍯯cN-[FmU)BHPPІ 8995jԨs111ZOٲeׯlvtt~Ajj̹0؎;澡Um/_B[nM$/zFΎ__@fٳg ԵkB"""hC)ʑ#G,^"HLLLZ#|ݻw///oR]vԩЙ3gBnܸq^z3ds m6`T*vrrR+_ZZ:o޼ӧnzРA R(eeefff@RRҝ;wj)իW7iUU[իƍ}D"9~={t_t;(ՋRPPdQQѽ{ڶm۵kWعs\. Dϟ?{:t(..nРAyyy^} clr„ q222Tn֭mڴiܸgϞM3ܺu7x?>tD"~kH߸q#<<ˋ޹sR~<ٳgfٳg@@L&KKKKOOkjT*]x fΜ:Vrss&NYVV:Ep\<!9)==]U@TE * Riggb8ԩS߿?k֬:o,f>rrrX,P"gE͝8i8yJ1#jF. zݝU0:LP7oޢE Ry9tڀ`{YxYXX̛7r̙t-n,F&@ 7ouƍkv%d2![}͛9r_U(u_9,D$e+VH$^62rv3T*ES@͒J aY`QQP(b1jX,V(L/wU^>0.t͖奥hR\f:"؆I&%Q !A!-(7%%%hڅhwRc؍]qPQQQ\\WTT4+ ;eee()))++#L0 D9!D" URRRBw`]=>AH1cQD"zSqq1tCRvRB=HX,Rybq8\^VV&,,,\@;LV\\L7ath߱0":bdRh*--ϗd4@NU_!`4'zlBT*bT*fffh( bD"i\e,0 lRT*rT,x<@- L&H$RTP+hQBf"E B"H$cbbrMLL肗K~Ĺ***d2YEEAwb# W `Twh}a-Br93Oȿ/oov8V3A4{0.~\WV_9;XFZcZT"Q>uY!`[ȅD[h ;T}v;jׯPfp0 VTb Ae6dv4  ;э:|U ok Q.}Mxǽ;* Bl6s1Uvh8 vF:@9G.32`{F/ա/511p8&&&l6z| EEE\.T5VB}Bgp0|>A4,fs7eeeeee3>JڌR!|@i8P( L.Y !O@=:CT B3334 0@ o޼a6)v;r\HE++))d4ҡP(``h6J_geeHpxH@(|f~GÉw `tHBI&`Hr $ށ`qa+t@5.֯7;jRRPr&P\.W(6dx,̬Fe˖~~~&&&Ϟ=;vǏu=`6m4jԨ$55ԩSZ [YY><))رc4iXXXŋǏUx~z꯿:qDAATÚUV[tRt332\fY,B`!Ã">YAIOOWP+J{{{\ \ T1gΜٮ];'OnڴI&L:tŋ1112L`XXظq5˗+VPDFFj}ނ_E\\\ԎEGG'&&Ǐܹ3DRZZJEoԈk F`l6BȲe%SSSӅBaAXXXzzɓ';v !׮];~xNN˰aڵk$W^޽Ny[ \]]Ӷm[GGǨ)S0S$D"oqqŋ=zGGΝ;zSN6EGG޽{fwiĈK,9s'OT6رclR`@ tl<$Q{0K2ҡ:C h(QUǏ޽{ ;;͛7[ZZJ$T߳#Gl۶ Y*88w077_n޽{\쑑{۷455ԩS||BH׬Y[^ŋvZTT4}tڵky<^rr_~Bpaaatϔ˗/߿ӦM5;RZZ*Hjcr@RzB|~<ڞ={֮]"77w݄@zرcl˗/wܩV 6BF:4if8^|If_rE3@yѣGy]v%ڵKm.Fjjjll,!ǧ}]]PXgN8qʕO>E;vc:q6a1wARsrS\ɭ=L&KNNV{ RRR!NJ9{,Tەѿ%ɪU"##׬YY}r8-nJRܹsZϺp!bi;Yٳ,hڴ)! !׮]H$ܹs:*'$$/uvz@I<#wqM^OQ(j] CUźEUuնzcUUW[ԁuA@/$0Ex=\9׹N9C)...%g***ړ5軨9С×_~IuVM/ZXXB^xQ]GBLLLɇRԴ Z~% vc3(//)sښ>P@(2v\.FAT(b,,,|}}ﯥ&,--\ g255-))iԴ/ VsݓLkvAx!CB.\ :::C|PHK񪪪;.Hd&]]]@1-le/*Sfo4]Y; F|;V 矲iTk !G[`BȹsrssթUZZZEEϯ]>xꚫS:Vhh2h$nnnt~-n TD&ehhh̝;ٳb_5!㙚 4oǎ'NE1% T׼cej썆Zu -Yxx8z:UV*@U1"JWZ4;UPPPPPcǎ7ZZZ5.f6WWzLMfv |>ٲe|>???ŊD@VVPFҪ+0hРaÆB<~R۷o4zs!!!bekDaR6;/hZ.O?ԺuK*ͣDrJObnnNS&''+-лwSB]o߾Z\ݻw !:*2t\&ԩSu'o۶-!$==A+Sfo1:tPUUGObbb///:l6ÇW5&<vڵk_x}ooowms IDATww@PTTt۾޹s*Sf蝰OSSsɒ%nnn$22֭[Օ|!rϲX,oooBȽ{޽{'l.],YdɒZoJd΁(&&^};b ]5;/;9--%Kt]*nܸ˪ӵ'aaar_=R**Oҥˏ?p-Z"4еk׮]{&?Wz޽ϟz>ellBy˗/25mvGXМq8Kv֍r;w++,,޽{%ooo___##Cy8 N8|lҭ[Kr8UVq8wKK ^PPp\=== Ν;Bbcc8PӋJׯ_f [vo߾]QQd9w+ĉ۷oMallg---o?on5۷o߾bi i׮!$667rrr۷1.((~۷߿AB !!Mq!- ͖H$-?uTkkk...cǎMHH~:Mu\]]mbm۶{n4G`Ԁ!\N.]!ϟ?}sj +fhhb iii/_yffff^^QNyѵkVTTೠC,ұcG|>.;@]l6ɓ 4+-h~lmm׮]kaauֳgJ$YX\\\r[[ٳg㫯2117oT*'B˗ =OGM-i ; 6nhaaŋoӲ9)))3g?!4ioN0/y&ZBZΝ;kiīl6{ٲeB0!!!444??/H$k׮p!dBtA@Dװ<{v ;>i>>>ݻw/--]pH$RǏG& 9;> vrrOOOz-ѣufjj/_>r'G&N_ggg'&&۷Dz壣 9>f̘ϟǖ رP(MLL̉'T,5wÇO>ɓ'ClݺFRRҝ;w[^^_\Cigg篾M6ZZZɓ'iHt9>˗+o߾b߿ވ?,88o߾ZJKK[dɫWmvȑnnn鉉G}AMoͦ={ɾu#GԩUzzɓ'gΜիWBJKK)SxzzFDD(<oСU}ja GgIҷo>}󱱱ӈjj۶m666O< pihhnZ*|2..n߾}ra)Z2mڴwUnR588pׯ_|yuh@؛BhjfeƜƟ}٠A"""^:w|q% VVVVVVQQQvbWTTTUUihhbaNb8:::00sʕ 0SRR";ݻ? b1bڴi9zhdd$s>x!ݺu+))9rl 4w\GϞ={:::o޼a;wnʔ)|>SN>:Ç|բxxx,\А9booooo"!6mdcc0c HedOڵk׮]-ZSh;T>8SBpѢE={d8:::::~gϞ]fl@ ޴iSvJKKw!sud@@E(ɓIIIfffcƌ1115j4ّ|LL̵kמ?^^^޾}P{1AlyQ>III_rǏb0{Q H$[lyYrrP(>|… g͚%[vޡCmmmD'{|ر~~~K9quuuȅ8uT`` ٳ=~BN>-W5k֔:t(..33 F1{۷_r%33" `ĈǏW6BzE~￧={dddtew1%srrRRRlmm۴ic'O$%%TwjahrC%''bc*7'%%Px<޶mۄB/6mڔѱcӧ[[[/_<88XqWwJ}q&p8[ZYYEEENjD9;;g% v@ l׮]qq~[z0///))ҥKL1CCð0BΝ;۷ L4I+#G TzzzllC_~t4M3o޼=f+W* srr8?*77w̘1`ZZ˗͛7l0__3gܺu SMvhh(!_U= OKK۽{S^_ !Ύrܑ‹/Bڶm[SX,/]Q} [Z_8 v(n:K&}SNB-{.9sbiʕJrPP߿X,յWn[&$$$0Glmm !{-Ds=2 ^]齫Q 255پ}Wr׮]۱c)S>񂃃狉tPŴ...NU٬ۧg.;j(BȶmۘH%H֭[G0 EOOr5%0ƍ"!Ǐ۴i[ u,@6C-e݅khh8;;eI ??70yj*::Z,5#@ ѣT*={lumFj=zX,rppPbzzzZZZԶٳgriDC.}=ݻuڌwQrvddkVVԩSsrr ҦP:7$114 QݝjW;g[[[MMMBHYYٹsj2ښRQQQ%U7]affԺuk###}}}jZ۵kSUU/ 6НYw.J_z?DGG+xRw]zwW^^^SRÇ^]:hW@ df,,,+O)**=( !̂(5; "wj4pر"FRIII .~ 0.Kaٲ;V(ELJR@umϟbRizzzjjjII ߿i.ay\Yx;;v0z'NZJ=t !Nݻ7ǣՠkXSJay6&)>3فf\;d9::J1c>AwI&nz„ >T4˗/{OMm>8= `ann;dWWWvAŢ_h{͛7_r2p8$vNJSR)))vڵkVn݂w>lذ7o8p@ͺݽ{۷fff655uuu}}MϷqƜ9sbEEEf B&k:u$;&p84 СCy<ޥK؝ja\'5Θ1cΝZZjդIlن/_{CxJ !>u&,XbNٳ3gKr˴^ho!*Q'5=zp\T*G](~񼽽 ![l믿a"I/fe ;UTTܼy3,,zNkPRN!X .tHFqwwiTJ4}62e&?@8fkii n:1 EGŵ{xxB @׿B|D}tvڝ8q"::zek| h]Bwibmm竓BIX+Jsuuݽ{ĉg͚z{{bBHn.\pҥŋ (Iu ;˗S&Lѻxb֭ez_E6lXac޾ݻ^z*Vo}P~~>a4jԨ;=_vY,ر2Lh&kk˗xE1NZ_~ٻwl&##?cݺu}=o߾͛79`@s HVZ%J|iȃuyƌ̬CB|}}weiiicc3plڴUz)!o[rҜ"ne:} +++vڍ7nojժdgŅ5,Ϟ=S:`Ŋ˖-o۶@ `&&&#G0`&`kݻ5fGGǔ'O!={޲e >|xWX!{e˖UB[nϞ= **J$q?sڵ˗ر͞={KRRRp8N4U<ӧá 9GU5N޽QHuPJ&L^j!SC>ۜXu޽3''iӦM2.`D/>uꔫP(LMMpСC4Tja!˖-[tiݽ%IUU\սBԩSu/]+).. [`kPPPPPD"D{ݹsWZ0uj';;{̙K.?~㙧˷m70@Sֿ6~ֶ +V.Xy?}ZEڻwɈ#>ɡ իMeeeBBBbb"G}%%%۷o5jTZZZ#P#Go:w rrrbcc@MG111"yxx Ѥx˗[[[-\0++Kp[HŲ##?64ّm(QHR0}P]2*hjlmmϪ*ooFˢ?߿BvIYGBD"Qׅ =z謬 zzzfdd7N0vci};j(m˻tRZZwF t؍%;;blUgs2jL@hAhŽO6ZC[[&{N3=-,,aGb,XХK"@ Bvl=={5GMD"  ?~|^z?\zɓ'Z ]tY3) !W\G޽[^^Qqu6mAfffrr3g=z$gDDڼybڈ3g{nʔ)999MM=zTKK+###))?HOO{9RCC?tPu[;88v())ٳ*.SCCcСR4))LJ*++-FD1B(~W={444Xrӊ[YY|ǏWWL鼌 @!˗/۩6!CB._ܘo&Tuɓ'ի_~;Pv4...իHyy;wsM 2qDf*ӧOoٲÇG1qDl}T*]r%tjj֭r //-[( IDDDɦ^СC|||̙+WQF1w޽+577_` mFoot0Y{grf[[[Ӽ4v/WLXk=@Zj ( IDATtпɶɋ/f͚eeeիWnݺjՊb%&&޸qCŷRhjx٭=_޷o_P35O5dȐ?{qFVVߐ!CRRRo83fдІf"Nٳ7ohӦ?s4&GVVVPP]\ o޼LbYf˗/f%&&FDDBpsD|UC^hZn@auM'|!L:ifڴiR4<<<..冇s82^^^+W|bR 2G[___2 !y#߲;wl޼booxbuf4t(v4utZGIIݻw+S\\GӧIaֳ>rHEEvXX\B);v,66vmڴ -F皚cCPٳDOOoԩ5R\.W1HvvK!ҥK#ߵ3g9rҭ[iћ'Wޢ ФX,///B?t !|>ؗd_޻w/o۶W^Zellgg3gErrݻ !k׮%ݛ)F3Offf=zXlMQVVVxչs!ׯׯyVzzj/ٲeK^^͎cggGA[[cǎƍSgEرn7ߨ.\{-!ֶSN4J(Ф9;;BbbbTOf=<<9 .\:th^OBHPPqaT:rHKK˥KBD"W͛7>}!믿gϞlB۷/]ݵk׮]2,))QP QQQ-bgff˕/))Y`3:u4tСCJ$2Z >> tRUƎ4Qwƍcr_~E$8qbϞ=o:$x{{B U,**z𡫫k^h`bΝUUU|>͛7&&&t~ᘘCY[[feeݻw:t0a=OJJmݺsVVV .;w.zw~VVViii D{k[\.7'''N(//_:gΜ[[[[[[WVVz*999&&i8K.]~ٳsssU^NNNxxxHHH6mdTw[!!$=-G@3%'' HR>ۃ ٟD*X,4N36uԀǏϙ3[NNbٴlŜ^hB@Qq'hfhF~_T &. f֖hr<,9::kN*^x]! 1.(`b>i , ߍ7ZJ;^XJWWwBݻ6lh Юsv дXJMM ٚ?ѣNH$_>9,"eE"bkM M%;KhR@bI$R4ԗRDt}v|dՍ?ZukXe.4`UUU%%%h*6e,iii):KZBB[P@-B>P $ǰ 4*⠢ݻwh wUTT+ ;;0jT3CIn ADw@Xh`OkNh(Z^$a= ݻw4A;iII !MŒJX,FUUUYYYee&Fs\eew&>̜;MAŪ/))A[J `j h'zlD"JKKKy< !"v+X PzfKRTZUUURRRZZ񰰥eDobɑAQLbV"D"HEo[&UQQQYYYQQArW-W PZ:%TUU1^hQ/%wP ;Uw?e(.f+d/T9;l8Sv4`G3Wf>\(( m aG!΀`@=C#@cbTb:v|#[T+%Cqh Ҩn4r#;;I V͸W Ox+D"a o=!f3㐅`'3`lC 4zxl@#١Ơ}DRQQQUUETi᭄``,r\ 4NKblWUUJr3>Z;> R .2-H$*++<$IK`'@vBT*Ax3[;:?555|>R:::ŕ4!HZ`dlOmmmD:@ mmmӢKvY>6u|]{Z;(f tM@}4 tH6#)EjAWW2;ZN&tH$m555-$ށ`@¤W:]H_5ov4-IIuuu555&P-*Y)>3M z9m<<<\\\RSS?U۷4hPΝ[jU\\_ݼySiawww[[[7o?~\,+oݺYaaazzz||'x𰴴p8YYYɓ' oԮ;v8~N:EDD\z^:NYYYUUfXnγX6v6vvTfO!$99cU@6..JB!n DOOBp]tQX9{Mp„ C+W^Rǎxo;99?Y^:66֗lkkdKKKEEEqqq2kv[[qٓHTRRBEԋl{&`l6BȲelmmE"ѥKuuu۷o?|p7xgʽj̘1#F ?'Nζ>|x.]<==VZӧϴi!O>{W  jff>ydfwvv~ݕ+W^x{33={0pѢESN̬DDD߿l6G~={vRRS*Sf cƌbR!<$fv' 3;YF:dux<%jjܸqw[abbyf@ ˙6nf= YyyyyBÙzzzk׮ݻw}AAA{8pp8=zyBcddvLL?\]ti^f̘!pppXfgfΜoʨ캺FE6vod(k׮4ŋ˄4fu~ޞ͛Z !H$R|ٙ W=66ik5xM|jii5{+>ڴiC)...%g***ړ5軨9С×_~IuVM/ZXXB^xQ]GBLLLɇRԴ Z~% vc3(//)sښ>P@(2v\.FAT(b,,,|}}ﯥ&,--\ g255-))iԴ/ VsݓLkvAx!CB.\ :::C|PHK񪪪;.Hd&]]]@1-le/*Sfo4]Y; F|;V 矲iTk !G[`BȹsrssթUZZZEEϯ]>xꚫS:Vhh2h$nnnt~-n TD&ehhh̝;ٳb_5!㙚 4oǎ'NE1% T׼cej썆Zu -Yxx8z:UV*@U1"JWZ4;UPPPPPcǎ7ZZZ5.f6WWzLMfv |>ٲe|>???ŊD@VVPFҪ+0hРaÆB<~R۷o4zs!!!bekDaR6;/hZ.O?ԺuK*ͣDrJObnnNS&''+-лwSB]o߾Z\ݻw !:*2t\&ԩSu'o۶-!$==A+Sfo1:tPUUGObbb///:l6ÇW5&<vڵk_x}ooowww@PTTt۾޹s*Sf蝰OSSsɒ%nnn$22֭[Օ|!rϲX,oooBȽ{޽{'l.],YdɒZoJd΁(&&^};b ]5;/;9--%Kt]*nܸ˪ӵ'aaar_=R**Oҥˏ?p-Z"4еk׮]{&?Wz޽ϟz>ellBy˗/25mvGXМq8Kv֍r;w++,,޽{%ooo___##Cy8 N8|lҭ[Kr8UVq8wKK ^PPp\=== Ν;Bbcc8PӋJׯ_f [vo߾]QQd9w+ĉ۷oMallg---o?o<..U ҧO333cc=x?+ܶmۑ#G'&&=z՝sNNNW^ekG0`_~ /\vZ<AAA>>>[J/_۷oH$b~w۷xE IDAT7n̙3}UV۶myIXXկ9w?}O޷oHX_RRO4 Oڵcۏ;ڰaCaasyٲl1c&N Bo߾ tt0oeeѣG=|||-Z\vqƍchh#`X#F6máG|||=)JeϬ?o<___戩!?-leeb ///lrٲetڵk׮-z)=(Hb1S=XLTVVֽMlllf̘!Pzb8:::00sʕr0`!$&&@޶m >|kii9xd˖-Ϟ=KNN ~~~ÇXpYdK5jɄWҥKG1{۷_r%33" `ĈǏg M6k׮t׮]qqqήO>ׯ_7|CyɹsҌ8 ۶mP(|ŦM444:v8}tkk˗B;vرǿz*88oܸ>))),,yJ:u*00Sx<^~!O'2i$CCܱcЃyyy;8''ɓ''Nիezz:}l񄐝;wFEE1/OMMy3gzG!+++666..nVVV_޽{?X,1c3"''''''..NI!/_zvڢа3' l׮]qq~[z0///))I.ظq7ƏѸvڃ9biik׮`:::ׯo׮]jji kW/^$$$8::;r߾}{=|q A)@ӥ;|pBȶmۘH5=a||<`9r>h۶-spĈ\.7--mrsrrd9ԣx&p­[!5 !ZZZK=t0/uuummPBȯD:>Hi jbz_\\\qܵkv1==}ʔ)yyyuީS!}֖-G9sR ;Z"[[[6MrJýKAA`llc7n(_ 'B}4B///gX;v4iRvhc*rtt$|lbb"s#vddkVVԩSi5^ttX,իsP C*={hh4Xt`lhӦ !4}#G*1ׯ !l6Ғ)U署͛O0a„ ű׮]plzÇh~B5m:6;}_hRwx<^uA͛7+ Qcǎeffֽz޽z92^^^'rOK Vn=jJ(RNnݺ5l0={YXXxzzzzzN^3;WUU! #h#FЩ%&Ii݌V*2I%L?WSSӹsΝ;G 4g̘AW\IHHxl# ;sEJJݻw; Efмxbҥ{ׯ_zzŋ+**:yy7nxĉ\\\_|i ^;;\BHRRҏ?سo6>>^$-[7<\z->0_t!$**ꍜ:,,𷝟`Ԑ!C_DtX,NLL$888 6nnn<@Bv|DzcW'O-[&y<^FFs'/OISL!DDDXsΩT*ur&Mbޔq!Zsĉ;vp\Zp<O{>Ϥ;~WseFpqV>S rcbb鼆G }յC鰟911QVdddӧ3G9yV7nǣkX.-hd_ yHIIr} $==}۶mLj0B7|駟??ڵh2@W)##CRYYYٳ? ptt^xի;~8!$""b߾}SL߿? ٽ{wwʕ)))AAANNNC dX uz#Gtss  !,!{!;á4fH$ҏ+itNrrr"##ie.o/]f05Ѷmjkklvff+|||h^pO[[D[[[6/,,ܸqNMOOˣ!7oZZZOxwǏV߿ٛ7oaÆ3f 0駟~rttd'''''EaSSSrr~D"%Vuo߾}iiiiii% cƌ1<֭[nin._l8zwrr"zcl۶KM޾}d ~&V6Rnv}gϞ={uرcǎuYx۶mkL<)hҤI:Ѕ<==iK.555н81źu,˓; : 9;\\\HX[[ӄNBktN_Ka3bFR֬Y0G |ZwEkя}}e{WpBȥKhɍ7T*uhhh111Çŕ'O,))a*X]vXlYdddcc\[[K3fLHH'O***fO4F||H$JNN{'Lp>|x֭Gl7&&o69NttthhN(--=zRԯfgg0k,@0s̱c:::>yd͕4%KBBBjuJJJiiik轗&$$=v@YpeQQѭ[`(**r…&@ Z~%*0$$$,,l޽튛DEEighhw}EUߺu+77w֬Y .lMsܸq:n4ѯ_ݻw;88Lgee I6443%C:t)S$Iˎ;w.;GnNBaRR mƀɓ'oڴE}{{ѣG'&&ۗxxxbSZ5..ޣ۷n[Zv۷ovvs󫫫|||fϞ=qDV&C^^^ӧO5MMzm=._"L0?qFBTTxs@oB233h:z`jqqqqqq嵵ׯ72Dkt9kk됐*,,ppp7o!nF ++++T:o޼ ?jujjݻ}}}͛o>rJ{{9>>Źʮ^{n{{O-Z$JkD?d?BUgϞM뽺͛BfϞM#L_RRo>Pc/i0`Ӽ{k=0`ܵk/^|ĉrTT*>rVrǏrj OP bL9Tttt>}ݻPG6778)řwQQQFKKK]VWWGzyyt0;[mllbÇ[ԗH$hmmmmm[nnnnQB׉/ MBHnnr|ՙNNNk׮]|Ç7J p8'mDF c B*`GCCw'4WBJ2,,@۬Fu퓒H$k׮ݺuk>}RRR.]Z__o{z;~LBtRZZ nBBC@@+E?\(]k:u*#>>ׯ_oaaqŜj&dzgbccN%|==k{u 6Bwuw*h)s7`___b|"44,))!x _t:]rrrqqMrr~p XyǏ.5 ttIȄ LOo2mڴ wEYz)rz;!nܸZTJa;4+'!YҧOǎknn^dIQk֬:~xQQі-[RE񹅅>::'݆QV۷o||bĈNڴiƍ?rBB[ee !dH4a>u̘17n6JegN"رcĉB_~cǎݲeѷdeeձԏ>h 6|S\twodd+>}JQzfm΄/]ڵk|  e怜;w.::zG!:nΜ9֭#( KKKf|z3g;MMM_R^z+VܻwRPPpɓ'9rȑ̅jLf8p"##}}}?3\RbPآL&KJJZttttttVU*۷oݺMNӥ;9913Mjo]r%..f۶m ĉ_u0 'O&<mlhhsN``---i`b޽&44=~ѣGt{zxx8::J$gϞݼy… C]`=NUUvoժU+WClrƍ{幹 СCo JQPP |}}mlljkkoݺu Ju÷Wju:3B`"4!!5ZfijjjX,s7{ЁN4 : C@EFv&(H2dN׿x0b~쒒!gϞ}yCe؍ЄvT}ٵk@ qFffڵz|; RjC˗w?j|vگ잿s<X C<6mzSV(G62rv3JBS@RTaY`YQjr9\.jL7`p@G>0/th42 '4 2xcT.cZ$ZMD0`bA}AJh څhwjǰ VtAssscc#Z:^a `FNP* JJ%ieZG`Yhm p! hTJw`]='AL1cQBz0Qcc#tCzSvRBz`X:p8FTj;;; 8ZH7atٱ0#m J9R2 md2Y}}ZWv @3e8уfkZN'U*˵EC!D.+ FC;,c7 m-d:Ni4L&˭\.NjZPT*VK~ٮGoz/џAm,IDAT1LbV* Bp,---,,,--邗^M~/Ĺjuss3Awbэ^+0G;>谖~ !B'W¶_lv80A {0?/~1\=WnhGsqQ4`Gc`n#?c $U=^G"IENDB`ripasso-0.8.0/generate_pot.sh000075500000000000000000000015321046102023000143250ustar 00000000000000#!/bin/bash xgettext cursive/src/*rs -kgettext --sort-output -o cursive/res/ripasso-cursive.pot touch cursive/res/sv.po && msgmerge --update cursive/res/sv.po cursive/res/ripasso-cursive.pot touch cursive/res/nb.po && msgmerge --update cursive/res/nb.po cursive/res/ripasso-cursive.pot touch cursive/res/nn.po && msgmerge --update cursive/res/nn.po cursive/res/ripasso-cursive.pot touch cursive/res/fr.po && msgmerge --update cursive/res/fr.po cursive/res/ripasso-cursive.pot touch cursive/res/it.po && msgmerge --update cursive/res/it.po cursive/res/ripasso-cursive.pot touch cursive/res/de.po && msgmerge --update cursive/res/de.po cursive/res/ripasso-cursive.pot touch cursive/res/ru.po && msgmerge --update cursive/res/ru.po cursive/res/ripasso-cursive.pot touch cursive/res/es.po && msgmerge --update cursive/res/es.po cursive/res/ripasso-cursive.pot ripasso-0.8.0/src/crypto.rs000064400000000000000000000767341046102023000140070ustar 00000000000000use std::{ collections::HashMap, fmt::{Display, Formatter, Write}, fs, fs::File, io::Write as IoWrite, path::Path, sync::Arc, }; use hex::FromHex; use sequoia_openpgp::{ Cert, Fingerprint, KeyHandle, KeyID, crypto::SessionKey, parse::{ Parse, stream::{ DecryptionHelper, DecryptorBuilder, DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper, }, }, policy::Policy, serialize::{ Serialize, stream::{Armorer, Encryptor, LiteralWriter, Message, Signer}, }, types::{RevocationStatus, SymmetricAlgorithm}, }; use zeroize::Zeroize; pub use crate::error::{Error, Result}; use crate::{ crypto::VerificationError::InfrastructureError, pass::OwnerTrustLevel, signature::{KeyRingStatus, Recipient, SignatureStatus}, }; /// The different pgp implementations we support #[non_exhaustive] #[derive(PartialEq, Eq, Debug)] pub enum CryptoImpl { /// Implemented with the help of the gpgme crate GpgMe, /// Implemented with the help of the sequoia crate Sequoia, } impl TryFrom<&str> for CryptoImpl { type Error = Error; fn try_from(value: &str) -> Result { match value { "gpg" => Ok(Self::GpgMe), "sequoia" => Ok(Self::Sequoia), _ => Err(Error::Generic( "unknown pgp implementation value, valid values are 'gpg' and 'sequoia'", )), } } } impl Display for CryptoImpl { fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match self { Self::GpgMe => write!(f, "gpg"), Self::Sequoia => write!(f, "sequoia"), }?; Ok(()) } } /// The different types of errors that can occur when doing a signature verification #[non_exhaustive] #[derive(Debug)] pub enum VerificationError { /// Error message from the pgp library. InfrastructureError(String), /// The data was signed, but not from one of the supplied recipients. SignatureFromWrongRecipient, /// The signature was invalid, BadSignature, /// No signature found. MissingSignatures, /// More than one signature, this shouldn't happen and can indicate that someone have tried /// to trick the process by appending an additional signature. TooManySignatures, } impl From for VerificationError { fn from(err: std::io::Error) -> Self { InfrastructureError(format!("{err:?}")) } } impl From for VerificationError { fn from(err: Error) -> Self { InfrastructureError(format!("{err:?}")) } } impl From for VerificationError { fn from(err: anyhow::Error) -> Self { InfrastructureError(format!("{err:?}")) } } /// The strategy for finding the gpg key to sign with can either be to look at the git /// config, or ask gpg. #[non_exhaustive] pub enum FindSigningFingerprintStrategy { /// Will look at the git configuration to find the users fingerprint GIT, /// Will ask gpg to find the users fingerprint GPG, } /// Models the interactions that can be done on a pgp key pub trait Key { /// returns a list of names associated with the key fn user_id_names(&self) -> Vec; /// returns the keys fingerprint fn fingerprint(&self) -> Result<[u8; 20]>; /// returns if the key isn't usable fn is_not_usable(&self) -> bool; } /// A key gotten from gpgme pub struct GpgMeKey { /// The key, gotten from gpgme. key: gpgme::Key, } impl Key for GpgMeKey { fn user_id_names(&self) -> Vec { self.key .user_ids() .map(|user_id| user_id.name().unwrap_or("?").to_owned()) .collect() } fn fingerprint(&self) -> Result<[u8; 20]> { let fp = self.key.fingerprint()?; Ok(<[u8; 20]>::from_hex(fp)?) } fn is_not_usable(&self) -> bool { self.key.is_bad() || self.key.is_revoked() || self.key.is_expired() || self.key.is_disabled() || self.key.is_invalid() } } /// All operations that can be done through pgp, either with gpgme or sequoia. pub trait Crypto { /// Reads a file and decrypts it /// # Errors /// Will return `Err` if decryption fails, for example if the current user isn't the /// recipient of the message. fn decrypt_string(&self, ciphertext: &[u8]) -> Result; /// Encrypts a string /// # Errors /// Will return `Err` if encryption fails, for example if the current users key /// isn't capable of encrypting. fn encrypt_string(&self, plaintext: &str, recipients: &[Recipient]) -> Result>; /// Returns a gpg signature for the supplied string. Suitable to add to a gpg commit. /// # Errors /// Will return `Err` if signing fails, for example if the current users key /// isn't capable of signing. fn sign_string( &self, to_sign: &str, valid_gpg_signing_keys: &[[u8; 20]], strategy: &FindSigningFingerprintStrategy, ) -> Result; /// Verifies is a signature is valid /// # Errors /// Will return `Err` if the verification fails. fn verify_sign( &self, data: &[u8], sig: &[u8], valid_signing_keys: &[[u8; 20]], ) -> std::result::Result; /// Returns true if a recipient is in the user's keyring. fn is_key_in_keyring(&self, recipient: &Recipient) -> Result; /// Pull keys from the keyserver for those recipients. /// # Errors /// Will return `Err` on network errors and similar. fn pull_keys(&mut self, recipients: &[&Recipient], config_path: &Path) -> Result; /// Import a key from text. /// # Errors /// Will return `Err` if the import of `key` as a key failed. fn import_key(&mut self, key: &str, config_path: &Path) -> Result; /// Return a key corresponding to the given key id. /// # Errors /// Will return `Err` if `key_id` didn't correspond to a key. fn get_key(&self, key_id: &str) -> Result>; /// Returns a map from key fingerprints to OwnerTrustLevel's /// # Errors /// Will return `Err` on failure to obtain trust levels. fn get_all_trust_items(&self) -> Result>; /// Returns the type of this `CryptoImpl`, useful for serializing the store config fn implementation(&self) -> CryptoImpl; /// Returns the fingerprint of the user using ripasso fn own_fingerprint(&self) -> Option<[u8; 20]>; } /// Used when the user configures gpgme to be used as a pgp backend. #[non_exhaustive] pub struct GpgMe {} impl Crypto for GpgMe { fn decrypt_string(&self, ciphertext: &[u8]) -> Result { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; let mut output = Vec::new(); ctx.decrypt(ciphertext, &mut output)?; let result = String::from_utf8(output.to_vec())?; output.zeroize(); Ok(result) } fn encrypt_string(&self, plaintext: &str, recipients: &[Recipient]) -> Result> { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; ctx.set_armor(false); let mut keys = Vec::new(); for recipient in recipients { if recipient.key_ring_status == KeyRingStatus::NotInKeyRing { return Err(Error::RecipientNotInKeyRing(recipient.key_id.clone())); } keys.push(ctx.get_key(recipient.key_id.clone())?); } let mut output = Vec::new(); ctx.encrypt_with_flags( &keys, plaintext, &mut output, gpgme::EncryptFlags::NO_COMPRESS, )?; Ok(output) } fn sign_string( &self, to_sign: &str, valid_gpg_signing_keys: &[[u8; 20]], strategy: &FindSigningFingerprintStrategy, ) -> Result { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; let config = git2::Config::open_default()?; let signing_key = match strategy { FindSigningFingerprintStrategy::GIT => config.get_string("user.signingkey")?, FindSigningFingerprintStrategy::GPG => { let mut key_opt: Option = None; for key_id in valid_gpg_signing_keys { let key_res = ctx.get_key(hex::encode_upper(key_id)); if let Ok(r) = key_res { key_opt = Some(r); } } if let Some(key) = key_opt { key.fingerprint()?.to_owned() } else { return Err(Error::Generic("no valid signing key")); } } }; ctx.set_armor(true); let key = ctx.get_secret_key(signing_key)?; ctx.add_signer(&key)?; let mut output = Vec::new(); let signature = ctx.sign_detached(to_sign, &mut output); if let Err(e) = signature { return Err(Error::Gpg(e)); } Ok(String::from_utf8(output)?) } fn verify_sign( &self, data: &[u8], sig: &[u8], valid_signing_keys: &[[u8; 20]], ) -> std::result::Result { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp) .map_err(|e| InfrastructureError(format!("{e:?}")))?; let result = ctx .verify_detached(sig, data) .map_err(|e| InfrastructureError(format!("{e:?}")))?; let mut sig_sum = None; for (i, s) in result.signatures().enumerate() { let fpr = s .fingerprint() .map_err(|e| InfrastructureError(format!("{e:?}")))?; let fpr = <[u8; 20]>::from_hex(fpr).map_err(|e| InfrastructureError(format!("{e:?}")))?; if !valid_signing_keys.contains(&fpr) { return Err(VerificationError::SignatureFromWrongRecipient); } if i == 0 { sig_sum = Some(s.summary()); } else { return Err(VerificationError::TooManySignatures); } } match sig_sum { None => Err(VerificationError::MissingSignatures), Some(sig_sum) => { let sig_status: SignatureStatus = sig_sum.into(); match sig_status { SignatureStatus::Bad => Err(VerificationError::BadSignature), SignatureStatus::Good | SignatureStatus::AlmostGood => Ok(sig_status), } } } } fn is_key_in_keyring(&self, recipient: &Recipient) -> Result { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; if let Some(fingerprint) = recipient.fingerprint { Ok(ctx.get_key(hex::encode(fingerprint)).is_ok()) } else { Ok(false) } } fn pull_keys(&mut self, recipients: &[&Recipient], _config_path: &Path) -> Result { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; let mut result_str = String::new(); for recipient in recipients { let response = download_keys(&recipient.key_id)?; let result = ctx.import(response)?; write!( result_str, "{}: import result: {:?}\n\n", recipient.key_id, result )?; } Ok(result_str) } fn import_key(&mut self, key: &str, _config_path: &Path) -> Result { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; let result = ctx.import(key)?; let result_str = format!("Import result: {result:?}\n\n"); Ok(result_str) } fn get_key(&self, key_id: &str) -> Result> { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; Ok(Box::new(GpgMeKey { key: ctx.get_key(key_id)?, })) } fn get_all_trust_items(&self) -> Result> { let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; ctx.set_key_list_mode(gpgme::KeyListMode::SIGS)?; let keys = ctx.find_keys(vec![String::new()])?; let mut trusts = HashMap::new(); for key_res in keys { let key = key_res?; trusts.insert( <[u8; 20]>::from_hex(key.fingerprint()?)?, crate::signature::OwnerTrustLevel::from(&key.owner_trust()), ); } Ok(trusts) } fn implementation(&self) -> CryptoImpl { CryptoImpl::GpgMe } fn own_fingerprint(&self) -> Option<[u8; 20]> { None } } /// Tries to download keys from keys.openpgp.org fn download_keys(recipient_key_id: &str) -> Result { let url = match recipient_key_id.len() { 16 => format!("https://keys.openpgp.org/vks/v1/by-keyid/{recipient_key_id}"), 18 if recipient_key_id.starts_with("0x") => format!( "https://keys.openpgp.org/vks/v1/by-keyid/{}", &recipient_key_id[2..] ), 40 => format!("https://keys.openpgp.org/vks/v1/by-fingerprint/{recipient_key_id}"), 42 if recipient_key_id.starts_with("0x") => format!( "https://keys.openpgp.org/vks/v1/by-fingerprint/{}", &recipient_key_id[2..] ), _ => return Err(Error::Generic("key id is not 16 or 40 hex chars")), }; Ok(reqwest::blocking::get(url)?.text()?) } /// Internal helper struct for sequoia implementation. struct Helper<'a> { /// A sequoia policy to use in various operations policy: &'a dyn Policy, /// the users cert secret: Option>, /// all certs key_ring: &'a HashMap<[u8; 20], Arc>, /// This is all the certificates that are allowed to sign something public_keys: Vec>, /// context if talking to gpg_agent for example ctx: Option, /// to do verification or not do_signature_verification: bool, } impl VerificationHelper for Helper<'_> { fn get_certs(&mut self, handles: &[KeyHandle]) -> sequoia_openpgp::Result> { let mut certs = vec![]; for handle in handles { for cert in &self.public_keys { for c in cert.keys() { if c.key().keyid().aliases(handle) { certs.push(cert.as_ref().clone()); } } } } // Return public keys for signature verification here. Ok(certs) } fn check(&mut self, structure: MessageStructure) -> sequoia_openpgp::Result<()> { if !self.do_signature_verification { return Ok(()); } for layer in structure { if let MessageLayer::SignatureGroup { results } = layer { if results.iter().any(std::result::Result::is_ok) { return Ok(()); } } } Err(anyhow::anyhow!("No valid signature")) } } fn find( key_ring: &HashMap<[u8; 20], Arc>, recipient: &Option, ) -> Result> { let recipient = recipient.as_ref().ok_or(Error::Generic("No recipient"))?; match recipient { KeyHandle::Fingerprint(fpr) => { match fpr { Fingerprint::V6(_v6) => { return Err(Error::Generic("v6 keys not supported yet")); } Fingerprint::V4(v4) => { for (key, value) in key_ring { if key == v4 { return Ok(value.clone()); } } } Fingerprint::Unknown { .. } => { return Err(Error::Generic("unknown fingerprint version")); } _ => {} }; } KeyHandle::KeyID(key_id) => match key_id { KeyID::Long(bytes) => { for (key, value) in key_ring { if key[0..8] == *bytes { return Ok(value.clone()); } } } KeyID::Invalid(_) => { return Err(Error::Generic("Invalid key ID")); } _ => {} }, } Err(Error::Generic("key not found in keyring")) } impl DecryptionHelper for Helper<'_> { fn decrypt( &mut self, pkesks: &[sequoia_openpgp::packet::PKESK], _skesks: &[sequoia_openpgp::packet::SKESK], sym_algo: Option, decrypt: &mut dyn FnMut(Option, &SessionKey) -> bool, ) -> sequoia_openpgp::Result> { if self.secret.is_none() { // we don't know which key is the users own key, so lets try them all let mut selected_fingerprint: Option> = None; for pkesk in pkesks { if let Ok(cert) = find(self.key_ring, &pkesk.recipient()) { let key = cert.primary_key().key(); let mut pair = sequoia_gpg_agent::KeyPair::new_for_gnupg_context( self.ctx .as_ref() .ok_or_else(|| anyhow::anyhow!("no context configured"))?, key, )?; if pkesk .decrypt(&mut pair, sym_algo) .is_some_and(|(algo, session_key)| decrypt(algo, &session_key)) { selected_fingerprint = Some(cert); break; } } } return Ok(selected_fingerprint.map(|f| f.as_ref().clone())); } // The encryption key is the first and only subkey. let key = self .secret .as_ref() .ok_or_else(|| anyhow::anyhow!("no user secret"))? .keys() .unencrypted_secret() .with_policy(self.policy, None) .for_transport_encryption() .next() .ok_or_else(|| anyhow::anyhow!("no keys capable of encryption"))? .key() .clone(); // The secret key is not encrypted. let mut pair = key.into_keypair()?; for pkesk in pkesks { if pkesk .decrypt(&mut pair, sym_algo) .map(|(algo, sk)| decrypt(algo, &sk)) .unwrap_or(false) { return Ok(Some( (*self .secret .clone() .ok_or_else(|| anyhow::anyhow!("no user secret"))?) .clone(), )); } } Err(anyhow::anyhow!( "no pkesks managed to decrypt the ciphertext" )) } } /// Intended for usage with slices containing a v4 fingerprint. pub fn slice_to_20_bytes(b: &[u8]) -> Result<[u8; 20]> { if b.len() != 20 { return Err(Error::Generic("slice isn't 20 bytes")); } let mut f: [u8; 20] = [0; 20]; f.copy_from_slice(&b[0..20]); Ok(f) } /// A pgp key produced with sequoia. pub struct SequoiaKey { /// The pgp key cert: Cert, } impl Key for SequoiaKey { fn user_id_names(&self) -> Vec { self.cert .userids() .map(|ui| ui.userid().to_string()) .collect() } fn fingerprint(&self) -> Result<[u8; 20]> { slice_to_20_bytes(self.cert.fingerprint().as_bytes()) } fn is_not_usable(&self) -> bool { let p = sequoia_openpgp::policy::StandardPolicy::new(); let policy = match self.cert.with_policy(&p, None) { Err(_) => return true, Ok(p) => p, }; self.cert.revocation_status(&p, None) != RevocationStatus::NotAsFarAsWeKnow || policy.alive().is_err() } } /// If the user selects to use sequoia as their pgp implementation. pub struct Sequoia { /// key id of the user. user_key_id: [u8; 20], /// All certs in the keys directory key_ring: HashMap<[u8; 20], Arc>, /// The home directory of the user, for gnupg context user_home: std::path::PathBuf, } impl Sequoia { /// creates the sequoia object /// # Errors /// If there is any problems reading the keys directory pub fn new(config_path: &Path, own_fingerprint: [u8; 20], user_home: &Path) -> Result { let mut key_ring: HashMap<[u8; 20], Arc> = HashMap::new(); let dir = config_path.join("share").join("ripasso").join("keys"); if dir.exists() { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.is_file() { let data = fs::read(path)?; let cert = Cert::from_bytes(&data)?; let fingerprint = slice_to_20_bytes(cert.fingerprint().as_bytes())?; key_ring.insert(fingerprint, Arc::new(cert)); } } } Ok(Self { user_key_id: own_fingerprint, key_ring, user_home: user_home.to_path_buf(), }) } pub fn from_values( user_key_id: [u8; 20], key_ring: HashMap<[u8; 20], Arc>, user_home: &Path, ) -> Self { Self { user_key_id, key_ring, user_home: user_home.to_path_buf(), } } /// Converts a list of recipients to their sequoia certs /// # Errors /// The function errors if any recipient doesn't have a cert fn convert_recipients(&self, input: &[Recipient]) -> Result>> { let mut result = vec![]; for recipient in input { match recipient.fingerprint { Some(fp) => match self.key_ring.get(&fp) { Some(cert) => result.push(cert.clone()), None => { return Err(Error::GenericDyn(format!( "Recipient with key id {} not found", recipient.key_id ))); } }, None => { let kh: KeyHandle = recipient.key_id.parse()?; for cert in self.key_ring.values() { if cert.key_handle().aliases(&kh) { result.push(cert.clone()); } } } }; } Ok(result) } /// Download keys from the internet and write them to the keys-dir. /// # Errors /// The function errors on download problems. fn pull_and_write(&mut self, key_id: &str, keys_dir: &Path) -> Result { let response = download_keys(key_id)?; self.write_cert(&response, keys_dir) } /// Writes a key to the keys directory, imported from a string. /// # Errors /// The function errors if the string can't be parsed as a cert. fn write_cert(&mut self, cert_str: &str, keys_dir: &Path) -> Result { let cert = Cert::from_bytes(cert_str.as_bytes())?; let fingerprint = slice_to_20_bytes(cert.fingerprint().as_bytes())?; let mut file = File::create(keys_dir.join(hex::encode(fingerprint)))?; cert.serialize(&mut file)?; self.key_ring.insert(fingerprint, Arc::new(cert)); Ok("Downloaded ok".to_owned()) } } impl Crypto for Sequoia { fn decrypt_string(&self, ciphertext: &[u8]) -> Result { let p = sequoia_openpgp::policy::StandardPolicy::new(); let mut sink: Vec = vec![]; let decrypt_key = self .key_ring .get(&self.user_key_id) .ok_or(Error::Generic("no key for user found"))?; if decrypt_key.is_tsk() { // Make a helper that that feeds the recipient's secret key to the // decryptor. let helper = Helper { policy: &p, secret: Some(decrypt_key.clone()), key_ring: &self.key_ring, public_keys: vec![], ctx: None, do_signature_verification: false, }; // Now, create a decryptor with a helper using the given Certs. let mut decryptor = DecryptorBuilder::from_bytes(ciphertext)?.with_policy(&p, None, helper)?; // Decrypt the data. std::io::copy(&mut decryptor, &mut sink)?; let result = std::str::from_utf8(&sink)?.to_owned(); sink.zeroize(); Ok(result) } else { // Make a helper that that feeds the recipient's secret key to the // decryptor. let helper = Helper { policy: &p, secret: Some(decrypt_key.clone()), key_ring: &self.key_ring, public_keys: vec![], ctx: Some( sequoia_gpg_agent::gnupg::Context::with_homedir(&self.user_home) .map_err(anyhow::Error::from)?, ), do_signature_verification: false, }; // Now, create a decryptor with a helper using the given Certs. let mut decryptor = DecryptorBuilder::from_bytes(ciphertext)?.with_policy(&p, None, helper)?; // Decrypt the data. std::io::copy(&mut decryptor, &mut sink)?; let result = std::str::from_utf8(&sink)?.to_owned(); sink.zeroize(); Ok(result) } } fn encrypt_string(&self, plaintext: &str, recipients: &[Recipient]) -> Result> { let p = sequoia_openpgp::policy::StandardPolicy::new(); let mut recipient_keys = vec![]; let cr = self.convert_recipients(recipients)?; for r in &cr { for k in r .keys() .with_policy(&p, None) .supported() .alive() .revoked(false) .for_transport_encryption() { recipient_keys.push(k); } } let mut sink: Vec = vec![]; // Start streaming an OpenPGP message. let message = Message::new(&mut sink); // We want to encrypt a literal data packet. let message = Encryptor::for_recipients(message, recipient_keys).build()?; // Emit a literal data packet. let mut message = LiteralWriter::new(message).build()?; // Encrypt the data. message.write_all(plaintext.as_bytes())?; // Finalize the OpenPGP message to make sure that all data is // written. message.finalize()?; Ok(sink) } fn sign_string( &self, to_sign: &str, _valid_gpg_signing_keys: &[[u8; 20]], _strategy: &FindSigningFingerprintStrategy, ) -> Result { let p = sequoia_openpgp::policy::StandardPolicy::new(); let tsk = self .key_ring .get(&self.user_key_id) .ok_or(Error::Generic("no key for user found"))?; // Get the keypair to do the signing from the Cert. let keypair = tsk .keys() .unencrypted_secret() .with_policy(&p, None) .alive() .revoked(false) .for_signing() .next() .ok_or_else(|| anyhow::anyhow!("no cert valid for signing"))? .key() .clone() .into_keypair()?; let mut sink: Vec = vec![]; // Start streaming an OpenPGP message. let message = Message::new(&mut sink); let message = Armorer::new(message) .kind(sequoia_openpgp::armor::Kind::Signature) .build()?; // We want to sign a literal data packet. let mut message = Signer::new(message, keypair)?.detached().build()?; // Sign the data. message.write_all(to_sign.as_bytes())?; // Finalize the OpenPGP message to make sure that all data is // written. message.finalize()?; Ok(std::str::from_utf8(&sink)?.to_owned()) } fn verify_sign( &self, data: &[u8], sig: &[u8], valid_signing_keys: &[[u8; 20]], ) -> std::result::Result { let p = sequoia_openpgp::policy::StandardPolicy::new(); let recipients: Vec = if valid_signing_keys.is_empty() { self.key_ring .keys() .map(|k| Recipient::from(&hex::encode(k), &[], None, self)) .collect::>>()? } else { valid_signing_keys .iter() .map(|k| Recipient::from(&hex::encode_upper(k), &[], None, self)) .collect::>>()? }; let senders = self.convert_recipients(&recipients)?; // Make a helper that that feeds the sender's public key to the // verifier. let helper = Helper { policy: &p, secret: None, key_ring: &self.key_ring, public_keys: senders, ctx: None, do_signature_verification: true, }; // Now, create a verifier with a helper using the given Certs. let mut verifier = DetachedVerifierBuilder::from_bytes(sig)?.with_policy(&p, None, helper)?; // Verify the data. verifier.verify_bytes(data)?; Ok(SignatureStatus::Good) } fn is_key_in_keyring(&self, recipient: &Recipient) -> Result { if let Some(fingerprint) = recipient.fingerprint { Ok(self.key_ring.contains_key(&fingerprint)) } else { Ok(false) } } fn pull_keys(&mut self, recipients: &[&Recipient], config_path: &Path) -> Result { let p = config_path.join("share").join("ripasso").join("keys"); fs::create_dir_all(&p)?; let mut ret = String::new(); for recipient in recipients { let res = self.pull_and_write(&recipient.key_id, &p); write!(ret, "{}: ", &recipient.key_id)?; match res { Ok(s) => ret.push_str(&s), Err(err) => write!(ret, "{err:?}")?, } ret.push('\n'); } Ok(ret) } fn import_key(&mut self, key: &str, config_path: &Path) -> Result { let p = config_path.join("share").join("ripasso").join("keys"); fs::create_dir_all(&p)?; self.write_cert(key, &p) } fn get_key(&self, key_id: &str) -> Result> { let kh: KeyHandle = key_id.parse()?; for c in self.key_ring.values() { if c.key_handle() == kh { return Ok(Box::new(SequoiaKey { cert: c.as_ref().clone(), })); } } Err(Error::GenericDyn(format!("no key found for {key_id}"))) } fn get_all_trust_items(&self) -> Result> { let mut res: HashMap<[u8; 20], OwnerTrustLevel> = HashMap::new(); for k in self.key_ring.keys() { res.insert(*k, OwnerTrustLevel::Ultimate); } Ok(res) } fn implementation(&self) -> CryptoImpl { CryptoImpl::Sequoia } fn own_fingerprint(&self) -> Option<[u8; 20]> { Some(self.user_key_id) } } #[cfg(test)] #[path = "tests/crypto.rs"] mod crypto_tests; ripasso-0.8.0/src/error.rs000064400000000000000000000140341046102023000136010ustar 00000000000000use std::{ io, path, string, sync::{Arc, Mutex, MutexGuard, PoisonError}, }; use hex::FromHexError; use crate::pass::PasswordStore; /// An enum that contains the different types of errors that the library returns as part of Result's. #[non_exhaustive] #[derive(Debug)] pub enum Error { Clipboard(arboard::Error), Io(io::Error), Git(git2::Error), Gpg(gpgme::Error), Utf8(string::FromUtf8Error), Generic(&'static str), GenericDyn(String), PathError(path::StripPrefixError), PatternError(glob::PatternError), GlobError(glob::GlobError), Utf8Error(std::str::Utf8Error), RecipientNotInKeyRing(String), ConfigError(config::ConfigError), SerError(toml::ser::Error), ReqwestError(reqwest::Error), AnyhowError(anyhow::Error), NoneError, HexError(FromHexError), FmtError(std::fmt::Error), TotpUrlError(totp_rs::TotpUrlError), SystemTimeError(std::time::SystemTimeError), } impl From for Error { fn from(err: arboard::Error) -> Self { Self::Clipboard(err) } } impl From for Error { fn from(err: io::Error) -> Self { Self::Io(err) } } impl From for Error { fn from(err: gpgme::Error) -> Self { Self::Gpg(err) } } impl From for Error { fn from(err: git2::Error) -> Self { Self::Git(err) } } impl From for Error { fn from(err: string::FromUtf8Error) -> Self { Self::Utf8(err) } } impl From for Error { fn from(err: path::StripPrefixError) -> Self { Self::PathError(err) } } impl From for Error { fn from(err: glob::PatternError) -> Self { Self::PatternError(err) } } impl From for Error { fn from(err: glob::GlobError) -> Self { Self::GlobError(err) } } impl From for Error { fn from(err: std::str::Utf8Error) -> Self { Self::Utf8Error(err) } } impl From> for Error { fn from(err: Option) -> Self { match err { None => Self::Generic("gpgme error with None"), Some(e) => Self::Utf8Error(e), } } } impl From> for Error { fn from(err: Box) -> Self { Self::GenericDyn(err.to_string()) } } impl From for Error { fn from(err: config::ConfigError) -> Self { Self::ConfigError(err) } } impl From for Error { fn from(err: toml::ser::Error) -> Self { Self::SerError(err) } } impl From<&str> for Error { fn from(err: &str) -> Self { Self::GenericDyn(err.to_owned()) } } impl From>> for Error { fn from(_err: PoisonError>) -> Self { Self::Generic("thread error") } } impl From for Error { fn from(err: reqwest::Error) -> Self { Self::ReqwestError(err) } } impl From for Error { fn from(err: anyhow::Error) -> Self { Self::AnyhowError(err) } } impl From>>> for Error { fn from(_err: PoisonError>>) -> Self { Self::Generic("thread error") } } impl From for Error { fn from(err: FromHexError) -> Self { Self::HexError(err) } } impl From for Error { fn from(err: std::fmt::Error) -> Self { Self::FmtError(err) } } impl From for Error { fn from(err: totp_rs::TotpUrlError) -> Self { Self::TotpUrlError(err) } } impl From for Error { fn from(err: std::time::SystemTimeError) -> Self { Self::SystemTimeError(err) } } impl From>>>>> for Error { fn from(_err: PoisonError>>>>) -> Self { Self::Generic("Error obtaining lock") } } impl From>>>> for Error { fn from(_err: PoisonError>>>) -> Self { Self::Generic("Error obtaining lock") } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Clipboard(err) => write!(f, "{err}"), Self::Io(err) => write!(f, "{err}"), Self::Git(err) => write!(f, "{err}"), Self::Gpg(err) => write!(f, "{err}"), Self::Utf8(err) => write!(f, "{err}"), Self::Generic(err) => write!(f, "{err}"), Self::GenericDyn(err) => write!(f, "{err}"), Self::PathError(err) => write!(f, "{err}"), Self::PatternError(err) => write!(f, "{err}"), Self::GlobError(err) => write!(f, "{err}"), Self::Utf8Error(err) => write!(f, "{err}"), Self::RecipientNotInKeyRing(err) => write!(f, "{err}"), Self::ConfigError(err) => write!(f, "{err}"), Self::SerError(err) => write!(f, "{err}"), Self::ReqwestError(err) => write!(f, "{err}"), Self::AnyhowError(err) => write!(f, "{err}"), Self::NoneError => write!(f, "NoneError"), Self::HexError(err) => write!(f, "{err}"), Self::FmtError(err) => write!(f, "{err}"), Self::TotpUrlError(_err) => write!(f, "TOTP url error"), Self::SystemTimeError(err) => write!(f, "{err}"), } } } /// Convenience type for Results pub type Result = std::result::Result; pub fn to_result( res: chrono::LocalResult>, ) -> Result> { match res { chrono::LocalResult::None => Err(Error::Generic("no timezone")), chrono::LocalResult::Single(t) => Ok(t), chrono::LocalResult::Ambiguous(_, _) => Err(Error::Generic("too many timezones")), } } ripasso-0.8.0/src/git.rs000064400000000000000000000335521046102023000132410ustar 00000000000000use std::{ fmt::Display, path::{Path, PathBuf}, str, }; use chrono::{DateTime, Local, TimeZone}; use git2::{Oid, Repository}; use crate::{ crypto::{Crypto, FindSigningFingerprintStrategy, VerificationError}, error::{Error, Result}, pass::{PasswordEntry, PasswordStore, RepositoryStatus, to_result}, signature::SignatureStatus, }; fn git_branch_name(repo: &Repository) -> Result { let head = repo.find_reference("HEAD")?; let symbolic = head .symbolic_target() .ok_or(Error::Generic("no symbolic target"))?; let mut parts = symbolic.split('/'); Ok(parts .nth(2) .ok_or(Error::Generic( "symbolic target name should be on format 'refs/heads/main'", ))? .to_owned()) } /// Apply the changes to the git repository. pub fn commit( repo: &Repository, signature: &git2::Signature, message: &str, tree: &git2::Tree, parents: &[&git2::Commit], crypto: &(dyn Crypto + Send), ) -> Result { if should_sign(repo) { let commit_buf = repo.commit_create_buffer( signature, // author signature, // committer message, // commit message tree, // tree parents, // parents )?; let commit_as_str = str::from_utf8(&commit_buf)?; let sig = crypto.sign_string(commit_as_str, &[], &FindSigningFingerprintStrategy::GIT)?; let commit = repo.commit_signed(commit_as_str, &sig, Some("gpgsig"))?; if let Ok(mut head) = repo.head() { head.set_target(commit, "added a signed commit using ripasso")?; } else { repo.branch(&git_branch_name(repo)?, &repo.find_commit(commit)?, false)?; } Ok(commit) } else { let commit = repo.commit( Some("HEAD"), // point HEAD to our new commit signature, // author signature, // committer message, // commit message tree, // tree parents, // parents )?; Ok(commit) } } pub fn find_last_commit(repo: &Repository) -> Result { let obj = repo.head()?.resolve()?.peel(git2::ObjectType::Commit)?; obj.into_commit() .map_err(|_| Error::Generic("Couldn't find commit")) } /// Returns if a git commit should be gpg signed or not. fn should_sign(repo: &Repository) -> bool { repo.config() .is_ok_and(|config| config.get_bool("commit.gpgsign").unwrap_or(false)) } /// returns true if the diff between the two commits contains the path that the `DiffOptions` /// have been prepared with pub fn match_with_parent( repo: &Repository, commit: &git2::Commit, parent: &git2::Commit, opts: &mut git2::DiffOptions, ) -> Result { let a = parent.tree()?; let b = commit.tree()?; let diff = repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))?; Ok(diff.deltas().len() > 0) } /// Add a file to the store, and commit it to the supplied git repository. pub fn add_and_commit_internal( repo: &Repository, paths: &[PathBuf], message: &str, crypto: &(dyn Crypto + Send), ) -> Result { let mut index = repo.index()?; for path in paths { index.add_path(path)?; index.write()?; } let signature = repo.signature()?; let mut parents = vec![]; let parent_commit; if let Ok(pc) = find_last_commit(repo) { parent_commit = pc; parents.push(&parent_commit); } let oid = index.write_tree()?; let tree = repo.find_tree(oid)?; let oid = commit(repo, &signature, message, &tree, &parents, crypto)?; Ok(oid) } /// Remove a file from the store, and commit the deletion to the supplied git repository. pub fn remove_and_commit(store: &PasswordStore, paths: &[PathBuf], message: &str) -> Result { let repo = store .repo() .map_err(|_| Error::Generic("must have a repository"))?; let mut index = repo.index()?; for path in paths { index.remove_path(path)?; index.write()?; } let oid = index.write_tree()?; let signature = repo.signature()?; let parent_commit_res = find_last_commit(&repo); let mut parents = vec![]; let parent_commit; if parent_commit_res.is_ok() { parent_commit = parent_commit_res?; parents.push(&parent_commit); } index.write_tree()?; let tree = repo.find_tree(oid)?; let oid = commit( &repo, &signature, message, &tree, &parents, store.get_crypto(), )?; Ok(oid) } /// Move a file to a new place in the store, and commit the move to the supplied git repository. pub fn move_and_commit( store: &PasswordStore, old_name: &Path, new_name: &Path, message: &str, ) -> Result { let repo = store .repo() .map_err(|_| Error::Generic("must have a repository"))?; let mut index = repo.index()?; index.remove_path(old_name)?; index.add_path(new_name)?; index.write()?; let oid = index.write_tree()?; let signature = repo.signature()?; let parent_commit_res = find_last_commit(&repo); let mut parents = vec![]; let parent_commit; if parent_commit_res.is_ok() { parent_commit = parent_commit_res?; parents.push(&parent_commit); } let tree = repo.find_tree(oid)?; let oid = commit( &repo, &signature, message, &tree, &parents, store.get_crypto(), )?; Ok(oid) } /// find the origin of the git repo, with the following strategy: /// find the branch that HEAD points to, and read the remote configured for that branch /// returns the remote and the name of the local branch fn find_origin(repo: &Repository) -> Result<(git2::Remote, String)> { for branch in repo.branches(Some(git2::BranchType::Local))? { let b = branch?.0; if b.is_head() { let upstream_name_buf = repo.branch_upstream_remote(&format!( "refs/heads/{}", &b.name()?.ok_or("no branch name")? ))?; let upstream_name = upstream_name_buf .as_str() .ok_or("Can't convert to string")?; let origin = repo.find_remote(upstream_name)?; return Ok((origin, b.name()?.ok_or("no branch name")?.to_owned())); } } Err(Error::Generic("no remotes configured")) } /// function that can be used for callback handling of the ssh interaction in git2 fn cred( tried_sshkey: &mut bool, _url: &str, username: Option<&str>, allowed: git2::CredentialType, ) -> std::result::Result { let sys_username = whoami::username(); let user: &str = username.map_or(&sys_username, |name| name); if allowed.contains(git2::CredentialType::USERNAME) { return git2::Cred::username(user); } if *tried_sshkey { return Err(git2::Error::from_str("no authentication available")); } *tried_sshkey = true; git2::Cred::ssh_key_from_agent(user) } /// Push your changes to the remote git repository. /// # Errors /// Returns an `Err` if the repository doesn't exist or if a git operation fails pub fn push(store: &PasswordStore) -> Result<()> { let repo = store .repo() .map_err(|_| Error::Generic("must have a repository"))?; let mut ref_status = None; let (mut origin, branch_name) = find_origin(&repo)?; let res = { let mut callbacks = git2::RemoteCallbacks::new(); let mut tried_ssh_key = false; callbacks.credentials(|_url, username, allowed| { cred(&mut tried_ssh_key, _url, username, allowed) }); callbacks.push_update_reference(|_refname, status| { ref_status = status.map(std::borrow::ToOwned::to_owned); Ok(()) }); let mut opts = git2::PushOptions::new(); opts.remote_callbacks(callbacks); origin.push(&[format!("refs/heads/{branch_name}")], Some(&mut opts)) }; match res { Ok(()) if ref_status.is_none() => Ok(()), Ok(()) => Err(Error::GenericDyn(format!( "failed to push a ref: {ref_status:?}", ))), Err(e) => Err(Error::GenericDyn(format!("failure to push: {e}"))), } } /// Pull new changes from the remote git repository. /// # Errors /// Returns an `Err` if the repository doesn't exist or if a git operation fails pub fn pull(store: &PasswordStore) -> Result<()> { let repo = store .repo() .map_err(|_| Error::Generic("must have a repository"))?; let (mut origin, branch_name) = find_origin(&repo)?; let mut cb = git2::RemoteCallbacks::new(); let mut tried_ssh_key = false; cb.credentials(|_url, username, allowed| cred(&mut tried_ssh_key, _url, username, allowed)); let mut opts = git2::FetchOptions::new(); opts.remote_callbacks(cb); origin.fetch(&[branch_name], Some(&mut opts), None)?; let remote_oid = repo.refname_to_id("FETCH_HEAD")?; let head_oid = repo.refname_to_id("HEAD")?; let (_, behind) = repo.graph_ahead_behind(head_oid, remote_oid)?; if behind == 0 { return Ok(()); } let remote_annotated_commit = repo.find_annotated_commit(remote_oid)?; let remote_commit = repo.find_commit(remote_oid)?; repo.merge(&[&remote_annotated_commit], None, None)?; //commit it let mut index = repo.index()?; let oid = index.write_tree()?; let signature = repo.signature()?; let parent_commit = find_last_commit(&repo)?; let tree = repo.find_tree(oid)?; let message = "pull and merge by ripasso"; let _commit = repo.commit( Some("HEAD"), // point HEAD to our new commit &signature, // author &signature, // committer message, // commit message &tree, // tree &[&parent_commit, &remote_commit], )?; // parents //cleanup repo.cleanup_state()?; Ok(()) } fn triple( e: &T, ) -> ( Result>, Result, Result, ) { ( Err(Error::GenericDyn(format!("{e}"))), Err(Error::GenericDyn(format!("{e}"))), Err(Error::GenericDyn(format!("{e}"))), ) } pub fn read_git_meta_data( base: &Path, path: &Path, repo: &Repository, store: &PasswordStore, ) -> ( Result>, Result, Result, ) { let path_res = path.strip_prefix(base); if let Err(e) = path_res { return triple(&e); } let blame_res = repo.blame_file(path_res.unwrap(), None); if let Err(e) = blame_res { return triple(&e); } let blame = blame_res.unwrap(); let id_res = blame .get_line(1) .ok_or(Error::Generic("no git history found")); if let Err(e) = id_res { return triple(&e); } let id = id_res.unwrap().orig_commit_id(); let commit_res = repo.find_commit(id); if let Err(e) = commit_res { return triple(&e); } let commit = commit_res.unwrap(); let time = commit.time(); let time_return = to_result(Local.timestamp_opt(time.seconds(), 0)); let name_return = name_from_commit(&commit); let signature_return = verify_git_signature(repo, &id, store); (time_return, name_return, signature_return) } pub fn verify_git_signature( repo: &Repository, id: &Oid, store: &PasswordStore, ) -> Result { let (signature, signed_data) = repo.extract_signature(id, Some("gpgsig"))?; let signature_str = str::from_utf8(&signature)?.to_owned(); let signed_data_str = str::from_utf8(&signed_data)?.to_owned(); if store.get_valid_gpg_signing_keys().is_empty() { return Err(Error::Generic( "signature not checked as PASSWORD_STORE_SIGNING_KEY is not configured", )); } match store.get_crypto().verify_sign( &signed_data_str.into_bytes(), &signature_str.into_bytes(), store.get_valid_gpg_signing_keys(), ) { Ok(r) => Ok(r), Err(VerificationError::InfrastructureError(message)) => Err(Error::GenericDyn(message)), Err(VerificationError::SignatureFromWrongRecipient) => Err(Error::Generic( "the commit wasn't signed by one of the keys specified in the environmental variable PASSWORD_STORE_SIGNING_KEY", )), Err(VerificationError::BadSignature) => Err(Error::Generic("Bad signature for commit")), Err(VerificationError::MissingSignatures) => { Err(Error::Generic("Missing signature for commit")) } Err(VerificationError::TooManySignatures) => Err(Error::Generic( "If a git commit contains more than one signature, something is fishy", )), } } /// Initialize a git repository for the store. /// # Errors /// Returns an `Err` if the git init fails pub fn init_git_repo(base: &Path) -> Result { Ok(Repository::init(base)?) } pub fn push_password_if_match( target: &Path, found: &Path, commit: &git2::Commit, repo: &Repository, passwords: &mut Vec, oid: &Oid, store: &PasswordStore, ) -> bool { if *target == *found { let time = commit.time(); let time_return = to_result(Local.timestamp_opt(time.seconds(), 0)); let name_return = name_from_commit(commit); let signature_return = verify_git_signature(repo, oid, store); passwords.push(PasswordEntry::new( &store.get_store_path(), target, time_return, name_return, signature_return, RepositoryStatus::InRepo, )); return false; } true } /// Find the name of the committer, or an error message fn name_from_commit(commit: &git2::Commit) -> Result { commit .committer() .name() .map_or(Err(Error::Generic("missing committer name")), |s| { Ok(s.to_owned()) }) } #[cfg(test)] #[path = "tests/git.rs"] mod git_tests; ripasso-0.8.0/src/lib.rs000064400000000000000000000016121046102023000132140ustar 00000000000000//! This implements a handling of a pass directory compatible with . //! The encryption is handled by `GPGme` or `sequoia` and the git integration is with libgit2. /// This is the library part that handles all encryption and decryption pub mod crypto; /// All functions and structs related to error handling pub(crate) mod error; /// All git related operations. pub mod git; /// This is the library part of ripasso, it implements the functions needed to manipulate a pass /// directory. pub mod pass; /// All functions and structs related to handling the identity and signing of things pub(crate) mod signature; /// This is the library that handles password generation, based on the long word list from EFF /// pub mod words; #[cfg(test)] #[path = "tests/test_helpers.rs"] pub mod test_helpers; ripasso-0.8.0/src/pass.rs000064400000000000000000001431661046102023000134270ustar 00000000000000/* Ripasso - a simple password manager Copyright (C) 2019-2020 Joakim Lundborg, Alexander Kjäll This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ use std::{ collections::HashMap, fs, fs::{File, create_dir_all}, io::prelude::*, path::{Path, PathBuf}, str, sync::{Arc, Mutex}, }; use chrono::prelude::*; use config::Config; use totp_rs::TOTP; use zeroize::Zeroize; use crate::{ crypto::{Crypto, CryptoImpl, GpgMe, Sequoia, VerificationError}, git::{ add_and_commit_internal, commit, find_last_commit, init_git_repo, match_with_parent, move_and_commit, push_password_if_match, read_git_meta_data, remove_and_commit, verify_git_signature, }, }; pub use crate::{ error::{Error, Result, to_result}, signature::{ Comment, KeyRingStatus, OwnerTrustLevel, Recipient, SignatureStatus, parse_signing_keys, }, }; /// Represents a complete password store directory pub struct PasswordStore { /// Name given to the store in a config file name: String, /// The absolute path to the root directory of the password store root: PathBuf, /// A list of fingerprints of keys that are allowed to sign the .gpg-id file, obtained from the environmental /// variable `PASSWORD_STORE_SIGNING_KEY` or from the configuration file valid_gpg_signing_keys: Vec<[u8; 20]>, /// a list of password files with metadata pub passwords: Vec, /// A file that describes the style of the store style_file: Option, /// The gpg implementation crypto: Box, /// The home dir of the user, if it exists user_home: Option, } impl Default for PasswordStore { fn default() -> Self { Self { name: "default".to_owned(), root: PathBuf::from("/tmp/"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(GpgMe {}), user_home: None, } } } impl PasswordStore { /// Constructs a `PasswordStore` object. If `password_store_signing_key` is present, /// the function verifies that the .gpg-id file is signed correctly /// # Errors /// If the configuration or the on disk setup is incorrect pub fn new( store_name: &str, password_store_dir: &Option, password_store_signing_key: &Option, home: &Option, style_file: &Option, crypto_impl: &CryptoImpl, own_fingerprint: &Option<[u8; 20]>, ) -> Result { let pass_home = password_dir_raw(password_store_dir, home); if !pass_home.exists() { return Err(Error::Generic("failed to locate password directory")); } let crypto: Box = match crypto_impl { CryptoImpl::GpgMe => Box::new(GpgMe {}), CryptoImpl::Sequoia => { let home: PathBuf = home.clone().ok_or(Error::Generic( "no home, required for using Sequoia as pgp implementation", ))?; Box::new(Sequoia::new( &home.join(".local"), own_fingerprint.ok_or_else(|| Error::Generic("own_fingerprint is not configured, required for using Sequoia as pgp implementation"))?, &home, )?) } }; let signing_keys = parse_signing_keys(password_store_signing_key, crypto.as_ref())?; let store = Self { name: store_name.to_owned(), root: pass_home.canonicalize()?, valid_gpg_signing_keys: signing_keys, passwords: [].to_vec(), style_file: style_file.to_owned(), crypto, user_home: home.clone(), }; if !store.valid_gpg_signing_keys.is_empty() { store.verify_gpg_id_files()?; } Ok(store) } /// Creates a `PasswordStore`, including creating directories and initializing the .gpg-id file /// # Errors /// Returns an `Err` if the directory exists, no recipients are empty or a full fingerprint /// wasn't specified. pub fn create( store_name: &str, password_store_dir: &Option, recipients: &[Recipient], recipients_as_signers: bool, home: &Option, style_file: &Option, ) -> Result { let pass_home = password_dir_raw(password_store_dir, home); if pass_home.exists() { return Err(Error::Generic( "trying to create a pass store in an existing directory", )); } if recipients.is_empty() { return Err(Error::Generic( "password store must have at least one member", )); } for recipient in recipients { if recipient.key_id.len() != 40 && recipient.key_id.len() != 42 { return Err(Error::Generic( "member specification wasn't a full pgp fingerprint", )); } } let crypto = Box::new(GpgMe {}); let signing_keys = { if recipients_as_signers { let mut fingerprints = vec![]; for r in recipients { fingerprints.push(r.fingerprint.ok_or_else(|| { Error::GenericDyn(format!( "recipient {} ({}) doesn't have a fingerprint", r.name, r.key_id )) })?); } fingerprints } else { vec![] } }; create_dir_all(&pass_home)?; Recipient::write_recipients_file( recipients, &pass_home.join(".gpg-id"), &signing_keys, crypto.as_ref(), )?; let repo = init_git_repo(&pass_home)?; if recipients_as_signers { add_and_commit_internal( &repo, &[PathBuf::from(".gpg-id"), PathBuf::from(".gpg-id.sig")], "initial commit by Ripasso", crypto.as_ref(), )?; } else { add_and_commit_internal( &repo, &[PathBuf::from(".gpg-id")], "initial commit by Ripasso", crypto.as_ref(), )?; } let store = Self { name: store_name.to_owned(), root: pass_home.canonicalize()?, valid_gpg_signing_keys: signing_keys, passwords: [].to_vec(), style_file: style_file.to_owned(), crypto, user_home: home.clone(), }; Ok(store) } /// Returns the name of the store, configured to the configuration file pub fn get_name(&self) -> &String { &self.name } /// Returns a vec with the keys that are allowed to sign the .gpg-id file pub fn get_valid_gpg_signing_keys(&self) -> &Vec<[u8; 20]> { &self.valid_gpg_signing_keys } /// returns the path to the directory where the store is located. pub fn get_store_path(&self) -> PathBuf { self.root.clone() } pub fn get_user_home(&self) -> Option { self.user_home.clone() } /// returns the style file for the store pub fn get_style_file(&self) -> Option { self.style_file.clone() } /// returns the crypto implementation for the store pub fn get_crypto(&self) -> &(dyn Crypto + Send) { &*self.crypto } pub fn repo(&self) -> Result { Ok(git2::Repository::open(&self.root)?) } fn verify_gpg_id_files(&self) -> Result { let mut result = SignatureStatus::Good; for gpg_id_file in self.recipients_files()? { let mut gpg_id_sig_file = self.root.clone(); gpg_id_sig_file.push(".gpg-id.sig"); let gpg_id = fs::read(gpg_id_file)?; let gpg_id_sig = match fs::read(gpg_id_sig_file) { Ok(c) => c, Err(_) => { return Err(Error::Generic( "problem reading .gpg-id.sig, and strict signature checking was asked for", )); } }; match self .crypto .verify_sign(&gpg_id, &gpg_id_sig, &self.valid_gpg_signing_keys) { Ok(r) => match r { SignatureStatus::Good => {} SignatureStatus::AlmostGood => result = SignatureStatus::AlmostGood, SignatureStatus::Bad => return Ok(SignatureStatus::Bad), }, Err(VerificationError::InfrastructureError(message)) => { return Err(Error::GenericDyn(message)); } Err(VerificationError::SignatureFromWrongRecipient) => { return Err(Error::Generic( "the .gpg-id file wasn't signed by one of the keys specified in the environmental variable PASSWORD_STORE_SIGNING_KEY", )); } Err(VerificationError::BadSignature) => { return Err(Error::Generic("Bad signature for .gpg-id file")); } Err(VerificationError::MissingSignatures) => { return Err(Error::Generic( "Missing signature for .gpg-id file, and PASSWORD_STORE_SIGNING_KEY specified", )); } Err(VerificationError::TooManySignatures) => { return Err(Error::Generic( "Signature for .gpg-id file contained more than one signature, something is fishy", )); } } } Ok(result) } fn verify_gpg_id_file_for_path(&self, path: &Path) -> Result { let gpg_id_file = self.recipients_file_for_dir(path)?; let gpg_id_sig_file = { let mut sig = gpg_id_file.clone(); sig.pop(); sig.join(".gpg-id.sig") }; let gpg_id = fs::read(gpg_id_file)?; let gpg_id_sig = match fs::read(gpg_id_sig_file) { Ok(c) => c, Err(_) => { return Err(Error::Generic( "problem reading .gpg-id.sig, and strict signature checking was asked for", )); } }; match self .crypto .verify_sign(&gpg_id, &gpg_id_sig, &self.valid_gpg_signing_keys) { Ok(r) => Ok(r), Err(VerificationError::InfrastructureError(message)) => Err(Error::GenericDyn(message)), Err(VerificationError::SignatureFromWrongRecipient) => Err(Error::Generic( "the .gpg-id file wasn't signed by one of the keys specified in the environmental variable PASSWORD_STORE_SIGNING_KEY", )), Err(VerificationError::BadSignature) => { Err(Error::Generic("Bad signature for .gpg-id file")) } Err(VerificationError::MissingSignatures) => Err(Error::Generic( "Missing signature for .gpg-id file, and PASSWORD_STORE_SIGNING_KEY specified", )), Err(VerificationError::TooManySignatures) => Err(Error::Generic( "Signature for .gpg-id file contained more than one signature, something is fishy", )), } } /// Creates a new password file in the store. /// # Errors /// Returns an `Err` if the path points to a file outside the password store or the file already exists. pub fn new_password_file(&mut self, path_end: &str, content: &str) -> Result { let mut path = self.root.clone(); let c_path = fs::canonicalize(path.as_path())?; let path_iter = &mut path_end.split('/').peekable(); while let Some(p) = path_iter.next() { if path_iter.peek().is_some() { path.push(p); let c_file_res = fs::canonicalize(path.as_path()); if let Ok(c_file) = c_file_res { if !c_file.starts_with(c_path.as_path()) { return Err(Error::Generic( "trying to write outside of password store directory", )); } } if !path.exists() { fs::create_dir(&path)?; } } else { path.push(format!("{p}.gpg")); } } if path.exists() { return Err(Error::Generic("file already exist")); } match self.new_password_file_internal(&path, path_end, content) { Ok(pe) => Ok(pe), Err(err) => { // try to remove the file we created, as cleanup let _ = fs::remove_file(path); // but always return the original error Err(err) } } } fn new_password_file_internal( &mut self, path: &Path, path_end: &str, content: &str, ) -> Result { let mut file = File::create(path)?; if !self.valid_gpg_signing_keys.is_empty() { self.verify_gpg_id_files()?; } let recipients = self.recipients_for_path(path)?; let output = self.crypto.encrypt_string(content, &recipients)?; if let Err(why) = file.write_all(&output) { return Err(Error::from(why)); } match self.repo() { Err(_) => { self.passwords.push(PasswordEntry::load_from_filesystem( &self.root, &append_extension(PathBuf::from(path_end), ".gpg"), )); Ok(PasswordEntry::load_from_filesystem( &self.root, &append_extension(PathBuf::from(path_end), ".gpg"), )) } Ok(repo) => { let message = format!("Add password for {path_end} using ripasso"); add_and_commit_internal( &repo, &[append_extension(PathBuf::from(path_end), ".gpg")], &message, self.crypto.as_ref(), )?; self.passwords .push(PasswordEntry::load_from_git(&self.root, path, &repo, self)); Ok(PasswordEntry::load_from_git(&self.root, path, &repo, self)) } } } /// loads the list of passwords from disk again /// # Errors /// Returns an error if any of the passwords contain non-utf8 bytes pub fn reload_password_list(&mut self) -> Result<()> { let mut new_passwords = self.all_passwords()?; self.passwords.clear(); self.passwords.append(&mut new_passwords); Ok(()) } /// checks if there is a username configured in git pub fn has_configured_username(&self) -> bool { if self.repo().is_err() { return true; } if let Ok(repo) = self.repo() { if let Ok(config) = repo.config() { let user_name = config.get_string("user.name"); if user_name.is_ok() { return true; } } } match git2::Config::open_default() { Err(_) => false, Ok(config) => { let user_name = config.get_string("user.name"); if user_name.is_err() { return false; } true } } } /// Read the password store directory and return a list of all the password files. /// # Errors /// Returns an error if any of the passwords contain non-utf8 bytes pub fn all_passwords(&self) -> Result> { let mut passwords = vec![]; let repo = self.repo(); // Not a git repository if repo.is_err() { let password_path_glob = self.root.join("**/*.gpg"); let existing_iter = glob::glob(&password_path_glob.to_string_lossy())?; for existing_file in existing_iter { let relpath = existing_file?.strip_prefix(&self.root)?.to_path_buf(); passwords.push(PasswordEntry::load_from_filesystem(&self.root, &relpath)); } return Ok(passwords); } let repo = repo?; // First, collect all files we need to find the first commit for let password_path_glob = self.root.join("**/*.gpg"); let existing_iter = glob::glob(&password_path_glob.to_string_lossy())?; let mut files_to_find: Vec = vec![]; for existing_file in existing_iter { files_to_find.push(existing_file?.strip_prefix(&self.root)?.to_path_buf()); } if files_to_find.is_empty() { return Ok(vec![]); } // Walk through all commits in reverse order, if the commit contains // the file, mark it let mut walk = repo.revwalk()?; walk.push(repo.head()?.target().ok_or("missing Oid on head")?)?; let mut last_tree = repo .find_commit(repo.head()?.target().ok_or("missing Oid on head")?)? .tree()?; let mut last_commit = repo.head()?.peel_to_commit()?; for rev in walk { if rev.is_err() { continue; } let oid = rev?; let commit = repo.find_commit(oid)?; let tree = commit.tree()?; let diff = repo.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?; diff.foreach( &mut |delta: git2::DiffDelta, _f: f32| { if let Some(found) = delta.new_file().path() { files_to_find.retain(|target| { push_password_if_match( target, found, &commit, &repo, &mut passwords, &oid, self, ) }); } true }, None, None, None, )?; last_tree = tree; last_commit = commit; } // When we have checked all the diffs, we also need to consider what // files was checked in to the first commit last_tree.walk(git2::TreeWalkMode::PreOrder, |path, entry| { if let Some(entry_name) = entry.name() { let found = Path::new(path).join(entry_name); files_to_find.retain(|target| { push_password_if_match( target, &found, &last_commit, &repo, &mut passwords, &last_commit.id(), self, ) }); } git2::TreeWalkResult::Ok })?; // If there are any files we couldn't find, add them to the list anyway for not_found in files_to_find { passwords.push(PasswordEntry::new( &self.root, ¬_found.clone(), Err(Error::Generic("")), Err(Error::Generic("")), Err(Error::Generic("")), RepositoryStatus::NotInRepo, )); } Ok(passwords) } /// Return a list of all the Recipients in the `$PASSWORD_STORE_DIR/.gpg-id` file. /// # Errors /// Returns an `Err` if the gpg_id file should be verified and it can't be pub fn all_recipients(&self) -> Result> { if !self.valid_gpg_signing_keys.is_empty() { self.verify_gpg_id_files()?; } let mut recipients = vec![]; for file in self.recipients_files()? { for r in Recipient::all_recipients(&file, self.crypto.as_ref())? { if !recipients.contains(&r) { recipients.push(r); } } } Ok(recipients) } /// Return a list of all the Recipients in the `.gpg-id` file that is the /// closest parent to `path`. /// # Errors /// Returns an `Err` if the gpg_id file should be verified and it can't be pub fn recipients_for_path(&self, path: &Path) -> Result> { if !self.valid_gpg_signing_keys.is_empty() { self.verify_gpg_id_file_for_path(path)?; } Recipient::all_recipients(&self.recipients_file_for_dir(path)?, self.crypto.as_ref()) } fn recipients_file_for_dir(&self, path: &Path) -> Result { let mut new_dir = fs::canonicalize(self.root.join(path))?; let root = fs::canonicalize(&self.root)?; if !new_dir.starts_with(&root) { return Err(Error::Generic("path traversal is not allowed")); } while new_dir.starts_with(&root) { let f = new_dir.join(".gpg-id"); if f.exists() { return Ok(f); } new_dir.pop(); } Err(Error::Generic("No .gpg-id file found")) } fn visit_dirs(dir: &Path, result: &mut Vec) -> Result<()> { if dir.is_dir() { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { Self::visit_dirs(&path, result)?; } else if entry.file_name() == ".gpg-id" { result.push(entry.path()); } } } Ok(()) } fn recipients_files(&self) -> Result> { let mut results = vec![]; Self::visit_dirs(&self.root, &mut results)?; Ok(results) } fn remove_recipient_inner(&self, r: &Recipient, path: &Path) -> Result<()> { Recipient::remove_recipient_from_file( r, &self.recipients_file_for_dir(path)?, &self.root, &self.valid_gpg_signing_keys, self.crypto.as_ref(), )?; self.reencrypt_all_password_entries() } /// Removes a key from the .gpg-id file and re-encrypts all the passwords /// # Errors /// Returns an `Err` if the gpg_id file can't be verified when it should /// or if the recipient is the last one. pub fn remove_recipient(&self, r: &Recipient, path: &Path) -> Result<()> { let gpg_id_file = &self.recipients_file_for_dir(path)?; let gpg_id_file_content = fs::read_to_string(gpg_id_file)?; let res = self.remove_recipient_inner(r, path); if res.is_err() { fs::write(gpg_id_file, gpg_id_file_content)?; } res } /// Adds a key to the .gpg-id file in the path directory and re-encrypts all the passwords /// # Errors /// Returns an `Err` if the gpg_id file can't be verified when it should or there is some problem with /// the encryption. pub fn add_recipient(&mut self, r: &Recipient, path: &Path, config_path: &Path) -> Result<()> { if !self.crypto.is_key_in_keyring(r)? { self.crypto.pull_keys(&[r], config_path)?; } if !self.crypto.is_key_in_keyring(r)? { return Err(Error::Generic( "Key isn't in keyring and couldn't be downloaded from keyservers", )); } let dir = self.root.join(path); if !dir.exists() { return Err(Error::Generic("path doesn't exist")); } let dir = fs::canonicalize(self.root.join(path))?; let root = fs::canonicalize(&self.root)?; if !dir.starts_with(root) { return Err(Error::Generic("path traversal not allowed")); } if !dir.join(".gpg-id").exists() { File::create(dir.join(".gpg-id"))?; } Recipient::add_recipient_to_file( r, &self.recipients_file_for_dir(path)?, &self.valid_gpg_signing_keys, self.crypto.as_ref(), )?; self.reencrypt_all_password_entries() } /// Reencrypt all the entries in the store, for example when a new collaborator is added /// to the team. /// # Errors /// Returns an `Err` if the gpg_id file can't be verified when it should or there is some problem with /// the encryption. fn reencrypt_all_password_entries(&self) -> Result<()> { let mut names: Vec = Vec::new(); for entry in self.all_passwords()? { let mut secret = entry.secret(self)?; entry.update_internal(&secret, self)?; secret.zeroize(); names.push(append_extension(PathBuf::from(&entry.name), ".gpg")); } names.push(PathBuf::from(".gpg-id")); if self.repo().is_err() { return Ok(()); } let keys = self .all_recipients()? .into_iter() .fold(String::new(), |mut acc, r| { use std::fmt::Write; let _ = write!(acc, ", 0x{}", r.key_id); acc }); let message = format!("Reencrypt password store with new GPG ids {keys}"); self.add_and_commit(&names, &message)?; Ok(()) } /// Add a file to the store, and commit it to the supplied git repository. /// # Errors /// Returns an `Err` if there is any problems with git. pub fn add_and_commit(&self, paths: &[PathBuf], message: &str) -> Result { let repo = self.repo(); if repo.is_err() { return Err(Error::Generic("must have a repository")); } let repo = repo?; let mut index = repo.index()?; for path in paths { index.add_path(path)?; } let oid = index.write_tree()?; let signature = repo.signature()?; let parent_commit_res = find_last_commit(&repo); let mut parents = vec![]; let parent_commit; if parent_commit_res.is_ok() { parent_commit = parent_commit_res?; parents.push(&parent_commit); } let tree = repo.find_tree(oid)?; let oid = commit( &repo, &signature, message, &tree, &parents, self.crypto.as_ref(), )?; let obj = repo.find_object(oid, None)?; repo.reset(&obj, git2::ResetType::Hard, None)?; Ok(oid) } ///Renames a password file to a new name ///returns the index in the password vec of the renamed `PasswordEntry` /// # Errors /// Returns an `Err` if the file is missing, or the target already exists. pub fn rename_file(&mut self, old_name: &str, new_name: &str) -> Result { if new_name.starts_with('/') || new_name.contains("..") { return Err(Error::Generic("directory traversal not allowed")); } let mut old_path = self.root.clone(); old_path.push(PathBuf::from(old_name)); let old_path = append_extension(old_path, ".gpg"); let mut new_path = self.root.clone(); new_path.push(PathBuf::from(new_name)); let new_path = append_extension(new_path, ".gpg"); if !old_path.exists() { return Err(Error::Generic("source file is missing")); } if new_path.exists() { return Err(Error::Generic("can't target file already exists")); } let mut new_path_dir = new_path.clone(); new_path_dir.pop(); create_dir_all(&new_path_dir)?; let mut file = File::create(&new_path)?; let mut secret = self.crypto.decrypt_string(&fs::read(&old_path)?)?; let new_recipients = Recipient::all_recipients( &self.recipients_file_for_dir(&new_path)?, self.crypto.as_ref(), )?; file.write_all(&self.crypto.encrypt_string(&secret, &new_recipients)?)?; secret.zeroize(); fs::remove_file(&old_path)?; if self.repo().is_ok() { let old_file_name = append_extension(PathBuf::from(old_name), ".gpg"); let new_file_name = append_extension(PathBuf::from(new_name), ".gpg"); move_and_commit(self, &old_file_name, &new_file_name, "moved file")?; } let passwords = &mut self.passwords; let mut index = usize::MAX; for (i, entry) in passwords.iter().enumerate() { if entry.name == old_name { index = i; } } if index != usize::MAX { let old_entry = passwords.swap_remove(index); let relpath = new_path.strip_prefix(&self.root)?.to_path_buf(); let new_entry = PasswordEntry::with_new_name(old_entry, &self.root, &relpath); passwords.push(new_entry); } Ok(passwords.len() - 1) } /// Creates a `Recipient` their key_id. /// # Errors /// Returns an `Err` if there is anything wrong with the `Recipient` pub fn recipient_from( &self, key_id: &str, pre_comment: &[String], post_comment: Option, ) -> Result { Recipient::from(key_id, pre_comment, post_comment, self.crypto.as_ref()) } } /// Return all `Recipient` across all different stores in the list. /// # Errors /// Returns an `Err` if there is a problem locking the mutex pub fn all_recipients_from_stores( stores: Arc>>>>, ) -> Result> { let all_recipients: Vec = { let mut ar: HashMap = HashMap::new(); let stores = stores .lock() .map_err(|_e| Error::Generic("problem locking the mutex"))?; #[allow(clippy::significant_drop_in_scrutinee)] for store in stores.iter() { let store = store .lock() .map_err(|_e| Error::Generic("problem locking the mutex"))?; #[allow(clippy::significant_drop_in_scrutinee)] for recipient in store.all_recipients()? { let key = match recipient.fingerprint.as_ref() { None => recipient.key_id.clone(), Some(fingerprint) => hex::encode_upper(fingerprint), }; ar.insert(key, recipient); } } ar.into_values().collect() }; Ok(all_recipients) } /// Describes one log line in the history of a file #[non_exhaustive] pub struct GitLogLine { /// the git commit message pub message: String, /// the timestamp of the commit pub commit_time: DateTime, /// the commit signature status pub signature_status: Option, } impl GitLogLine { /// creates a `GitLogLine` pub fn new( message: String, commit_time: DateTime, signature_status: Option, ) -> Self { Self { message, commit_time, signature_status, } } } /// The state of a password, in the context of git #[derive(Clone, Debug, PartialEq, Eq, Default)] #[non_exhaustive] pub enum RepositoryStatus { /// The password is in git InRepo, /// The password isn't in git NotInRepo, /// The passwordstore isn't backed by a git repo #[default] NoRepo, } /// One password in the password store #[derive(Clone, Debug, Default)] pub struct PasswordEntry { /// Name of the entry, (from relative path to password) pub name: String, /// Absolute path to the password file pub path: PathBuf, /// if we have a git repo, then commit time pub updated: Option>, /// if we have a git repo, then the name of the committer pub committed_by: Option, /// if we have a git repo, and the commit was signed pub signature_status: Option, /// describes if the file is in a repository or not pub is_in_git: RepositoryStatus, } fn to_name(relpath: &Path) -> String { let mut s = relpath.to_string_lossy().to_string(); if relpath .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("gpg")) { s.truncate(s.len() - 4); s } else { s } } impl PasswordEntry { /// constructs a `PasswordEntry` from the supplied parts pub fn new( base: &Path, // Root of the password directory relpath: &Path, // Relative path to the password. update_time: Result>, committed_by: Result, signature_status: Result, is_in_git: RepositoryStatus, ) -> Self { Self { name: to_name(relpath), path: base.join(relpath), updated: update_time.ok(), committed_by: committed_by.ok(), signature_status: signature_status.ok(), is_in_git, } } /// Consumes an `PasswordEntry`, and returns a new one with a new name pub fn with_new_name(old: Self, base: &Path, relpath: &Path) -> Self { Self { name: to_name(relpath), path: base.join(relpath), updated: old.updated, committed_by: old.committed_by, signature_status: old.signature_status, is_in_git: old.is_in_git, } } /// creates a `PasswordEntry` by running git blame on the specified path pub fn load_from_git( base: &Path, path: &Path, repo: &git2::Repository, store: &PasswordStore, ) -> Self { let (update_time, committed_by, signature_status) = read_git_meta_data(base, path, repo, store); let relpath = path .strip_prefix(base) .expect("base was not a prefix of path") .to_path_buf(); Self::new( base, &relpath, update_time, committed_by, signature_status, RepositoryStatus::InRepo, ) } /// creates a `PasswordEntry` based on data in the filesystem pub fn load_from_filesystem(base: &Path, relpath: &Path) -> Self { Self { name: to_name(relpath), path: base.join(relpath), updated: None, committed_by: None, signature_status: None, is_in_git: RepositoryStatus::NoRepo, } } /// Decrypts and returns the full content of the `PasswordEntry` /// # Errors /// Returns an `Err` if the path is empty pub fn secret(&self, store: &PasswordStore) -> Result { let s = fs::metadata(&self.path)?; if s.len() == 0 { return Err(Error::Generic("empty password file")); } let content = fs::read(&self.path)?; store.crypto.decrypt_string(&content) } /// Decrypts and returns the first line of the `PasswordEntry` /// # Errors /// Returns an `Err` if the decryption fails pub fn password(&self, store: &PasswordStore) -> Result { let mut secret = self.secret(store)?; let password: String = secret.split('\n').take(1).collect(); secret.zeroize(); Ok(password) } /// decrypts and returns a TOTP code if the entry contains an otpauth:// url /// # Errors /// Returns an `Err` if the code generation fails pub fn mfa(&self, store: &PasswordStore) -> Result { let mut secret = self.secret(store)?; if let Some(start_pos) = secret.find("otpauth://") { let end_pos = { let mut end_pos = secret.len(); for (pos, c) in secret.chars().skip(start_pos).enumerate() { if c.is_whitespace() { end_pos = pos + start_pos; break; } } end_pos }; // Use unchecked for sites like Discord, GitHub that still use 80 // bit secrets. https://github.com/constantoine/totp-rs/issues/46 let totp = TOTP::from_url_unchecked(&secret[start_pos..end_pos])?; secret.zeroize(); Ok(totp.generate_current()?) } else { secret.zeroize(); Err(Error::Generic("No otpauth:// url in secret")) } } /// All calls to this function must be followed by secret.zeroize() fn update_internal(&self, secret: &str, store: &PasswordStore) -> Result<()> { if !store.valid_gpg_signing_keys.is_empty() { store.verify_gpg_id_files()?; } let recipients = store.recipients_for_path(&self.path)?; let ciphertext = store.crypto.encrypt_string(secret, &recipients)?; let mut output = File::create(&self.path)?; output.write_all(&ciphertext)?; Ok(()) } /// Updates the password store entry with new content, and commits those to git if a repository /// is supplied. /// # Errors /// Returns an `Err` if the update fails. pub fn update(&self, mut secret: String, store: &PasswordStore) -> Result<()> { self.update_internal(&secret, store)?; secret.zeroize(); if store.repo().is_err() { return Ok(()); } let message = format!("Edit password for {} using ripasso", &self.name); store.add_and_commit( &[append_extension(PathBuf::from(&self.name), ".gpg")], &message, )?; Ok(()) } /// Removes this entry from the filesystem and commit that to git if a repository is supplied. /// # Errors /// Returns an `Err` if the remove fails. pub fn delete_file(&self, store: &PasswordStore) -> Result<()> { fs::remove_file(&self.path)?; if store.repo().is_err() { return Ok(()); } let message = format!("Removed password file for {} using ripasso", &self.name); remove_and_commit( store, &[append_extension(PathBuf::from(&self.name), ".gpg")], &message, )?; Ok(()) } /// Returns a list of log lines for the password, one line for each commit that have changed /// that password in some way /// # Errors /// Returns an `Err` if any of the git operation fails. pub fn get_history(&self, store: &PasswordStore) -> Result> { let repo = { let repo_res = store.repo(); if repo_res.is_err() { return Ok(vec![]); } repo_res? }; let mut revwalk = repo.revwalk()?; revwalk.set_sorting(git2::Sort::REVERSE)?; revwalk.set_sorting(git2::Sort::TIME)?; revwalk.push_head()?; let p = self.path.strip_prefix(&store.root)?; let ps = git2::Pathspec::new(vec![&p])?; let mut diffopts = git2::DiffOptions::new(); diffopts.pathspec(p); let walk_res: Vec = revwalk .filter_map(|id| { if let Ok(oid) = id { if let Ok(commit) = repo.find_commit(oid) { if commit.parents().len() == 0 { if let Ok(tree) = commit.tree() { let flags = git2::PathspecFlags::NO_MATCH_ERROR; ps.match_tree(&tree, flags).ok()?; } else { return None; } } else { let m = commit.parents().all(|parent| { match_with_parent(&repo, &commit, &parent, &mut diffopts) .unwrap_or(false) }); if !m { return None; } } let time = commit.time(); let dt = to_result(Local.timestamp_opt(time.seconds(), 0)).ok()?; let signature_status = verify_git_signature(&repo, &oid, store); Some(GitLogLine::new( commit.message().unwrap_or("").to_owned(), dt, signature_status.ok(), )) } else { None } } else { None } }) .collect(); Ok(walk_res) } } /// Import the key_ids from the signature file from a keyserver. /// # Errors /// Returns an `Err` if the download fails pub fn pgp_pull(store: &mut PasswordStore, config_path: &Path) -> Result { let recipients = store.all_recipients()?; let recipients_refs: Vec<&Recipient> = recipients.iter().collect(); let result = store.crypto.pull_keys(&recipients_refs, config_path)?; Ok(result) } /// Import a key from a string. /// # Errors /// Returns an `Err` if the import fails pub fn pgp_import(store: &mut PasswordStore, text: &str, config_path: &Path) -> Result { store.crypto.import_key(text, config_path) } /// Return a list of all passwords whose name contains `query`. pub fn search(store: &PasswordStore, query: &str) -> Vec { let passwords = &store.passwords; fn normalized(s: &str) -> String { s.to_lowercase() } fn matches(s: &str, q: &str) -> bool { normalized(s).as_str().contains(normalized(q).as_str()) } let matching = passwords.iter().filter(|p| matches(&p.name, query)); matching.cloned().collect() } /// Determine password directory pub fn password_dir( password_store_dir: &Option, home: &Option, ) -> Result { let pass_home = password_dir_raw(password_store_dir, home); if !pass_home.exists() { return Err(Error::Generic("failed to locate password directory")); } Ok(pass_home) } /// Determine password directory pub fn password_dir_raw(password_store_dir: &Option, home: &Option) -> PathBuf { // If a directory is provided via env var, use it match password_store_dir.as_ref() { Some(p) => p.clone(), None => match home { Some(h) => h.join(".password-store"), None => PathBuf::new().join(".password-store"), }, } } fn home_exists(home: &Option, settings: &Config) -> bool { if home.is_none() { return false; } let home = home.as_ref().unwrap(); let default_password_store_dir = home.join(".password-store"); if !default_password_store_dir.exists() { return false; } if !default_password_store_dir.is_dir() { return false; } let stores_res = settings.get("stores"); if let Ok(stores) = stores_res { let stores: HashMap = stores; for store_name in stores.keys() { let store: HashMap = stores[store_name].clone().into_table().unwrap(); let password_store_dir_opt = store.get("path"); if let Some(p) = password_store_dir_opt { let p_path = PathBuf::from(p.clone().into_string().unwrap()); let c1 = fs::canonicalize(default_password_store_dir.clone()); let c2 = fs::canonicalize(p_path); if c1.is_ok() && c2.is_ok() && c1.unwrap() == c2.unwrap() { return false; } } } } true } fn env_var_exists(store_dir: &Option, signing_keys: &Option) -> bool { store_dir.is_some() || signing_keys.is_some() } fn settings_file_exists(home: &Option, xdg_config_home: &Option) -> bool { if home.is_none() { return false; } let home = home.as_ref().unwrap(); let xdg_config_file = match xdg_config_home.as_ref() { Some(p) => p.join("ripasso/settings.toml"), None => home.join(".config/ripasso/settings.toml"), }; let xdg_config_file_dir = Path::new(&xdg_config_file); if xdg_config_file_dir.exists() { return fs::metadata(xdg_config_file_dir).is_ok_and(|config_file| config_file.len() != 0); } false } fn home_settings(home: &Option) -> Result { let mut default_store = HashMap::new(); let home = home.as_ref().ok_or("no home directory set")?; default_store.insert( "path".to_owned(), home.join(".password-store/").to_string_lossy().to_string(), ); let mut stores_map = HashMap::new(); stores_map.insert("default".to_owned(), default_store); let mut new_settings = config::ConfigBuilder::default(); new_settings = new_settings.set_default("stores", stores_map)?; Ok(config::ConfigBuilder::::build(new_settings)?) } fn var_settings(store_dir: &Option, signing_keys: &Option) -> Result { let mut default_store = HashMap::new(); if let Some(dir) = store_dir { if dir.ends_with('/') { default_store.insert("path".to_owned(), dir.clone()); } else { default_store.insert("path".to_owned(), dir.clone() + "/"); } } if let Some(keys) = signing_keys { default_store.insert("valid_signing_keys".to_owned(), keys.clone()); } else { default_store.insert("valid_signing_keys".to_owned(), "-1".to_owned()); } let mut stores_map = HashMap::new(); stores_map.insert("default".to_owned(), default_store); let mut new_settings = config::ConfigBuilder::default(); new_settings = new_settings.set_default("stores", stores_map)?; Ok(config::ConfigBuilder::::build(new_settings)?) } fn xdg_config_file_location( home: &Option, xdg_config_home: &Option, ) -> Result { match xdg_config_home.as_ref() { Some(p) => Ok(p.join("ripasso/settings.toml")), None => { if let Some(h) = home { Ok(h.join(".config/ripasso/settings.toml")) } else { Err(Error::Generic("no home directory")) } } } } fn file_settings( xdg_config_file: &Path, ) -> config::File { config::File::from(xdg_config_file.to_path_buf()) } fn append_extension(path: PathBuf, extension: &str) -> PathBuf { let mut str = path.into_os_string(); str.push(extension); PathBuf::from(str) } /// reads ripassos config file, in `$XDG_CONFIG_HOME/ripasso/settings.toml` pub fn read_config( store_dir: &Option, signing_keys: &Option, home: &Option, xdg_config_home: &Option, ) -> Result<(Config, PathBuf)> { let mut settings = config::ConfigBuilder::default(); let config_file_location = xdg_config_file_location(home, xdg_config_home)?; if settings_file_exists(home, xdg_config_home) { settings = config::ConfigBuilder::::add_source( settings, file_settings(&config_file_location), ) } if home_exists(home, &settings.clone().build()?) { settings = settings.add_source(home_settings(home)?); } if env_var_exists(store_dir, signing_keys) { settings = settings.add_source(var_settings(store_dir, signing_keys)?); } Ok((settings.build()?, config_file_location)) } pub fn save_config( stores: Arc>>>>, config_file_location: &Path, ) -> Result<()> { let mut stores_map = HashMap::new(); let stores_borrowed = stores .lock() .map_err(|_e| Error::Generic("problem locking the mutex"))?; #[allow(clippy::significant_drop_in_scrutinee)] for store in stores_borrowed.iter() { let store = store .lock() .map_err(|_e| Error::Generic("problem locking the mutex"))?; let mut store_map = HashMap::new(); store_map.insert( "path", store .get_store_path() .to_string_lossy() .into_owned() .to_string(), ); if !store.get_valid_gpg_signing_keys().is_empty() { store_map.insert( "valid_signing_keys", store .get_valid_gpg_signing_keys() .iter() .map(hex::encode_upper) .collect::>() .join(","), ); } if let Some(style_file) = store.get_style_file() { store_map.insert("style_path", style_file.display().to_string()); } store_map.insert( "pgp_implementation", store.crypto.implementation().to_string(), ); if let Some(fp) = store.crypto.own_fingerprint() { store_map.insert("own_fingerprint", hex::encode_upper(fp)); } stores_map.insert(store.get_name().clone(), store_map); } let mut settings = HashMap::new(); settings.insert("stores", stores_map); let f = File::create(config_file_location)?; let mut f = std::io::BufWriter::new(f); f.write_all(toml::ser::to_string_pretty(&settings)?.as_bytes())?; Ok(()) } #[cfg(test)] #[path = "tests/pass.rs"] mod pass_tests; ripasso-0.8.0/src/signature.rs000064400000000000000000000406571046102023000144630ustar 00000000000000use std::{ cmp::PartialEq, collections::{HashMap, HashSet}, fs, io::prelude::*, path::{Path, PathBuf}, }; use hex::FromHex; use crate::crypto::FindSigningFingerprintStrategy; pub use crate::error::{Error, Result}; /// A git commit for a password might be signed by a gpg key, and this signature's verification /// state is one of these values. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum SignatureStatus { /// Everything is fine with the signature, corresponds to the gpg status of VALID Good, /// There was a non-critical failure in the verification, corresponds to the gpg status of GREEN AlmostGood, /// Verification failed, corresponds to the gpg status of RED Bad, } impl From for SignatureStatus { fn from(s: gpgme::SignatureSummary) -> Self { if s.contains(gpgme::SignatureSummary::VALID) { Self::Good } else if s.contains(gpgme::SignatureSummary::GREEN) { Self::AlmostGood } else { Self::Bad } } } /// Turns an optional string into a vec of parsed gpg fingerprints in the form of strings. /// If any of the fingerprints isn't a full 40 chars or if they haven't been imported to /// the gpg keyring yet, this function instead returns an error. pub fn parse_signing_keys( password_store_signing_key: &Option, crypto: &(dyn crate::crypto::Crypto + Send), ) -> Result> { if password_store_signing_key.is_none() { return Ok(vec![]); } let mut signing_keys = vec![]; for key in password_store_signing_key.as_ref().unwrap().split(',') { let trimmed = key.trim().to_owned(); if trimmed.len() != 40 && (trimmed.len() != 42 && trimmed.starts_with("0x")) { return Err(Error::Generic( "signing key isn't in full 40 character id format", )); } let key_res = crypto.get_key(&trimmed); if let Some(err) = key_res.err() { return Err(Error::GenericDyn(format!( "signing key not found in keyring, error: {err}", ))); } if trimmed.len() == 40 { signing_keys.push(<[u8; 20]>::from_hex(trimmed)?); } else { signing_keys.push(<[u8; 20]>::from_hex(&trimmed[2..])?); } } Ok(signing_keys) } /// the GPG trust level for a key #[derive(Clone, PartialEq, Eq, Debug)] #[non_exhaustive] pub enum OwnerTrustLevel { /// is only used for your own keys. You trust this key 'per se'. Any message signed with that key, /// will be trusted. This is also the reason why any key from a friend, that is signed by you, will /// also show as valid (green), even though you did not change the ownertrust of the signed key. /// The signed key will be valid due to the ultimate ownertrust of your own key. Ultimate, /// is used for keys, which you trust to sign other keys. That means, if Alice's key is signed by /// your Buddy Bob, whose key you set the ownertrust to Full, Alice's key will be trusted. You /// should only be using Full ownertrust after verifying and signing Bob's key. Full, /// will make a key show as valid, if it has been signed by at least three keys which you set to /// 'Marginal' trust-level. Example: If you set Alice's, Bob's and Peter's key to 'Marginal' and /// they all sign Ed's key, Ed's key will be valid. Due to the complexity of this status, we /// do not recommend using it. Marginal, /// Trust-level is identical to 'Unknown / Undefined' i.e. the key is not trusted. But in this case, /// you actively state, to never trust the key in question. That means, you know that the key /// owner is not accurately verifying other keys before signing them. Never, /// has the same meaning as 'Unknown' but differs, since it has actually been set by the user. /// That could mean, that this is a key you want to process at a later point in time. Undefined, /// is the default state. It means, no ownertrust has been set yet. The key is not trusted. Unknown, } impl From<&gpgme::Validity> for OwnerTrustLevel { fn from(level: &gpgme::Validity) -> Self { match level { gpgme::Validity::Unknown => Self::Unknown, gpgme::Validity::Undefined => Self::Undefined, gpgme::Validity::Never => Self::Never, gpgme::Validity::Marginal => Self::Marginal, gpgme::Validity::Full => Self::Full, gpgme::Validity::Ultimate => Self::Ultimate, } } } /// A Recipient can either be in the GPG keyring, or not. #[derive(Clone, PartialEq, Eq, Debug)] #[non_exhaustive] pub enum KeyRingStatus { /// it's in the ring InKeyRing, /// it's not in the ring NotInKeyRing, } /// internal holder of a user id row and the comments that belong to it struct IdComment { /// the id string pub id: String, /// an optional comment before the id string pub pre_comment: Vec, /// an optional comment after the id string pub post_comment: Option, } impl std::hash::Hash for IdComment { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl PartialEq for IdComment { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for IdComment {} /// Describes a comment around a gpg id / fingerprint. See this commit for source: /// #[derive(Clone, Debug)] pub struct Comment { /// The comment field from the .gpg-id file, above the user fingerprint /// not including the leading '#' characters. pub pre_comment: Option, /// The comment field from the .gpg-id file, after the user fingerprint /// not including the leading '#' characters. pub post_comment: Option, } /// Represents one person on the team. /// /// All secrets are encrypted with the key_id of the recipients. #[derive(Clone, Debug)] pub struct Recipient { /// Human-readable name of the person. pub name: String, /// The comment field from the .gpg-id file, not including the leading '#' characters. pub comment: Comment, /// Machine-readable identity taken from the .gpg-id file, in the form of a gpg key id /// (16 hex chars) or a fingerprint (40 hex chars). pub key_id: String, /// The fingerprint of the pgp key, as 20 bytes, /// if the fingerprint of the key is not known, this will be None. pub fingerprint: Option<[u8; 20]>, /// The status of the key in GPG's keyring pub key_ring_status: KeyRingStatus, /// The trust level the owner of the key ring has placed in this person pub trust_level: OwnerTrustLevel, /// If the key isn't usable for any reason, i.e. if any of the gpg function /// `is_bad`, `is_revoked`, `is_expired`, `is_disabled` or `is_invalid` returns true pub not_usable: bool, } impl Recipient { /// Constructs a `Recipient` object. fn new( name: String, comment: Comment, key_id: String, fingerprint: Option<[u8; 20]>, key_ring_status: KeyRingStatus, trust_level: OwnerTrustLevel, not_usable: bool, ) -> Self { Self { name, comment, key_id, fingerprint, key_ring_status, trust_level, not_usable, } } /// Creates a `Recipient` from a gpg key id string /// # Errors /// Returns an `Err` if the trust levels can't be retrieved or there is something wrong with the fingerprint. pub fn from( key_id: &str, pre_comment: &[String], post_comment: Option, crypto: &(dyn crate::crypto::Crypto + Send), ) -> Result { let comment_opt = match pre_comment.len() { 0 => None, _ => Some(pre_comment.join("\n")), }; let comment = Comment { pre_comment: comment_opt, post_comment, }; let key_result = crypto.get_key(key_id); if key_result.is_err() { return Ok(Recipient::new( "key id not in keyring".to_owned(), comment, key_id.to_owned(), None, KeyRingStatus::NotInKeyRing, OwnerTrustLevel::Unknown, true, )); } let real_key = key_result?; let mut names = real_key.user_id_names(); let name = match names.len() { 0 => "?".to_owned(), _ => names.pop().unwrap(), }; let trusts: HashMap<[u8; 20], OwnerTrustLevel> = crypto.get_all_trust_items()?; let fingerprint = real_key.fingerprint()?; Ok(Self::new( name, comment, key_id.to_owned(), Some(fingerprint), KeyRingStatus::InKeyRing, (*trusts .get(&real_key.fingerprint()?) .unwrap_or(&OwnerTrustLevel::Unknown)) .clone(), real_key.is_not_usable(), )) } /// Return a list of all the Recipients in the supplied file. /// # Errors /// Returns an `Err` if there is a problem reading the .gpg_id file pub fn all_recipients( recipients_file: &Path, crypto: &(dyn crate::crypto::Crypto + Send), ) -> Result> { let contents = fs::read_to_string(recipients_file)?; let mut recipients: Vec = Vec::new(); let mut unique_recipients_keys: HashSet = HashSet::new(); let mut comment_buf = vec![]; for key in contents.split('\n') { if key.len() > 1 { if key.starts_with('#') { comment_buf.push(key.chars().skip(1).collect()); } else if key.contains('#') { let mut splitter = key.splitn(2, '#'); let key = splitter.next().unwrap().trim(); let comment = splitter.next().unwrap(); unique_recipients_keys.insert(IdComment { id: key.to_owned(), pre_comment: comment_buf.clone(), post_comment: Some(comment.to_owned()), }); comment_buf.clear(); } else { unique_recipients_keys.insert(IdComment { id: key.to_owned(), pre_comment: comment_buf.clone(), post_comment: None, }); comment_buf.clear(); } } } for key in unique_recipients_keys { let recipient = match Self::from(&key.id, &key.pre_comment, key.post_comment.clone(), crypto) { Ok(r) => r, Err(err) => { let comment_opt = match key.pre_comment.len() { 0 => None, _ => Some(key.pre_comment.join("\n")), }; Self::new( err.to_string(), Comment { pre_comment: comment_opt, post_comment: key.post_comment, }, key.id.clone(), None, KeyRingStatus::NotInKeyRing, OwnerTrustLevel::Unknown, true, ) } }; recipients.push(recipient) } Ok(recipients) } /// write the .gpg-id.sig file /// # Errors /// Returns an `Err` if the file writing fails pub fn write_recipients_file( recipients: &[Self], recipients_file: &Path, valid_gpg_signing_keys: &[[u8; 20]], crypto: &(dyn crate::crypto::Crypto + Send), ) -> Result<()> { let mut file = fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(recipients_file)?; let mut file_content = String::new(); let mut sorted_recipients = recipients.to_owned(); sorted_recipients.sort_by(|a, b| a.fingerprint.cmp(&b.fingerprint)); for recipient in sorted_recipients { let to_add = match recipient.fingerprint { Some(f) => hex::encode_upper(f), None => recipient.key_id, }; if recipient.comment.pre_comment.is_some() { for line in recipient.comment.pre_comment.as_ref().unwrap().split('\n') { file_content.push('#'); file_content.push_str(line); file_content.push('\n'); } } if !to_add.starts_with("0x") { file_content.push_str("0x"); } file_content.push_str(&to_add); if recipient.comment.post_comment.is_some() { file_content.push_str(" #"); file_content.push_str(recipient.comment.post_comment.as_ref().unwrap()); } file_content.push('\n'); } file.write_all(file_content.as_bytes())?; if !valid_gpg_signing_keys.is_empty() { let output = crypto.sign_string( &file_content, valid_gpg_signing_keys, &FindSigningFingerprintStrategy::GPG, )?; let recipient_sig_filename: PathBuf = { let rf = recipients_file.to_path_buf(); let mut sig = rf.into_os_string(); sig.push(".sig"); sig.into() }; let mut recipient_sig_file = fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(recipient_sig_filename)?; recipient_sig_file.write_all(output.as_bytes())?; } Ok(()) } /// Delete one of the persons from the list of team members to encrypt the passwords for. /// # Errors /// Return an `Err` if there is an error reading the gpg_id file pub fn remove_recipient_from_file( s: &Self, recipients_file: &Path, store_root_path: &Path, valid_gpg_signing_keys: &[[u8; 20]], crypto: &(dyn crate::crypto::Crypto + Send), ) -> Result<()> { let mut recipients: Vec = Self::all_recipients(recipients_file, crypto)?; recipients.retain(|vs| { if vs.fingerprint.is_some() && s.fingerprint.is_some() { vs.fingerprint != s.fingerprint } else { vs.key_id != s.key_id } }); if recipients.is_empty() { if recipients_file == store_root_path.join(".gpg_id") { Err(Error::Generic("Can't delete the last encryption key")) } else { Ok(fs::remove_file(recipients_file)?) } } else { Recipient::write_recipients_file( &recipients, recipients_file, valid_gpg_signing_keys, crypto, ) } } /// Add a new person to the list of team members to encrypt the passwords for. /// # Errors /// Return an `Err` if there is an error reading the gpg_id file pub fn add_recipient_to_file( recipient: &Self, recipients_file: &Path, valid_gpg_signing_keys: &[[u8; 20]], crypto: &(dyn crate::crypto::Crypto + Send), ) -> Result<()> { let mut recipients: Vec = Self::all_recipients(recipients_file, crypto)?; for r in &recipients { if r == recipient { return Err(Error::Generic( "Team member is already in the list of key ids", )); } } recipients.push((*recipient).clone()); Recipient::write_recipients_file( &recipients, recipients_file, valid_gpg_signing_keys, crypto, ) } } impl PartialEq for Recipient { fn eq(&self, other: &Self) -> bool { if self.fingerprint.is_none() || other.fingerprint.is_none() { return false; } self.fingerprint.as_ref().unwrap() == other.fingerprint.as_ref().unwrap() } } #[cfg(test)] #[path = "tests/signature.rs"] mod signature_tests; ripasso-0.8.0/src/tests/crypto.rs000064400000000000000000000252461046102023000151410ustar 00000000000000use std::{collections::HashMap, sync::Arc}; use hex::FromHex; use sequoia_openpgp::{Cert, cert::CertBuilder, parse::Parse, serialize::Serialize}; use tempfile::tempdir; use crate::{ crypto::{Crypto, CryptoImpl, Sequoia, slice_to_20_bytes}, signature::Recipient, }; #[test] pub fn crypto_impl_from() { assert_eq!(CryptoImpl::GpgMe, CryptoImpl::try_from("gpg").unwrap()); assert_eq!( CryptoImpl::Sequoia, CryptoImpl::try_from("sequoia").unwrap() ); } #[test] pub fn crypto_impl_from_error() { assert!(CryptoImpl::try_from("random").is_err()); } #[test] pub fn crypto_impl_display() { assert_eq!("gpg", format!("{}", CryptoImpl::GpgMe)); assert_eq!("sequoia", format!("{}", CryptoImpl::Sequoia)); } #[test] pub fn slice_to_20_bytes_failure() { let input = [3; 16]; let result = slice_to_20_bytes(&input); assert!(result.is_err()); } #[test] pub fn slice_to_20_bytes_success() { let input = [3; 20]; let result = slice_to_20_bytes(&input).unwrap(); assert_eq!(input, result); } #[test] pub fn new_one_cert() { let dir = tempdir().unwrap(); let user_home = tempdir().unwrap(); let (cert, _) = CertBuilder::new() .add_userid("someone@example.org") .add_signing_subkey() .generate() .unwrap(); let f = slice_to_20_bytes(cert.fingerprint().as_bytes()).unwrap(); let p = dir.path().join("share").join("ripasso").join("keys"); std::fs::create_dir_all(&p).unwrap(); let file = p.join(hex::encode(f)); let mut file = std::fs::File::create(file).expect("Unable to create file"); cert.serialize(&mut file).unwrap(); let sequoia = Sequoia::new(dir.path(), f, user_home.path()).unwrap(); assert_eq!(1, sequoia.key_ring.len()); assert_eq!( "someone@example.org", sequoia .key_ring .get(&f) .unwrap() .userids() .next() .unwrap() .userid() .email() .unwrap() .unwrap() ); } fn cert() -> Arc { Arc::new( Cert::from_bytes( b"-----BEGIN PGP PUBLIC KEY BLOCK----- mDMEXkLj2xYJKwYBBAHaRw8BAQdAHUsNSgCBZ9wSRCyVciyLF/dT+mf9ezwXY0RA 9PAb3L20LEFsZXhhbmRlciBLasOkbGwgPGFsZXhhbmRlci5ramFsbEBnbWFpbC5j b20+iJYEExYIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQR+BoBw1e95 SwDIqdkdEI5sB8vEBgUCXkMBZwUJA8KEjAAKCRAdEI5sB8vEBq/IAQCgQ2OtjHP0 sJKzAJoUl5vnsIWI0aW8FZSOUzdK0YiDqwD8DYW01fAimGrKGT+hHIexihikx1tx REOpVMS3s8ZLsgWJATMEEAEKAB0WIQTbB9rFs4guq2WeHS/fDD0xa3MS1QUCXkMC ogAKCRDfDD0xa3MS1RasB/9HTBth2WOcbettCgOFzvlMFaaH+zAnilsmoVWgwg2h N2mmhEgzGpMDTR/JdRga4pDZEhKCHNtWTz0cur/CT+ZqTErCrmwqpFqXVAZ374Iy 4y+MBxe2iVuTcmzlx6VYgndTxYHr5KzPFhtSk8vfV0mSteUvID2WJLVX6pPN4LzI bVXqSFuW0gUohh15/1EIhq75phDPZJCEPhNngQwp288wgwGc/LTbfAyq9Y5yaRCJ GjnDiNA1iUydAqLz27YoqaeDFAFo4yJ08Kp64UkjUL/l+3SkV7FofsnkEEhpfNkY E4UYSU/i/BzQrDdDevJtK0PTNx5CaCUpapWFNuy+k3+UiQIzBBABCAAdFiEEd2Ex Dj4Xy6xMTJekIg4YDldt4vsFAl5EA7sACgkQIg4YDldt4vtp/w//SNT8nHCH4bbX 17PQE+B/Y3WDqHf4JpoeNcdBRXaUIL2W08qa6vi5sE2zWhhfy2xa9JbggpP/jx3u cjKHwuNm+zs4wOqWRibTNfcdXyDn1ZMztmiwJMZvGA7WyUR3IhVbFWOg8UWVFJeE 8lY5PgEm3GrqYl41IWZksjzcd3+QkWnz7XN234zTPuxjsWB2v6Dl8wrl+bph2TDk 0e0pFvy1txnKSzJhQkLS4HVrEyE2ef6yLwJqOwAyob2UJtNVxplvgEWo4bt+pSor seRJ2k5volSlsxXrIzqGOTj2rhtqPe8TbppCod8HMVatuNCn/Opkolvx0PIuOI55 aRR8bt+UtFAO3NYg7QO79gmI43gyS88UdgLhXqhm0aRo+X7dtUTavfE5SwK9alwh XAP/gNSVQJYdyfkDzesw6ZQwCZog/zT37Tb2pHelXr5X/toTeX1QM8O8Bq7X6yxn IuICoX7f3j2E+A0xx0NwafpVpR14uJxhQsyDmv0CDpAgaOBYg8FbvOcIyam4tB95 MzPapUHJtwC959tzYYzjDWyks65wnTlGBZna9kLQbrURHqTx8hV06+W2blYPnUEH S1D/QMKORMYTkSS2KQEUd7TAHm0rXnb0JS8nin+HPNcmeshDJADifQ2TRQFChWy6 XwbJNI76eTBVSPNCY5sih7GizQ2raQOJAjMEEAEKAB0WIQSihBGllhkxcTMYAsC2 WkhxyhnXFwUCXrhZRwAKCRC2WkhxyhnXF9MqEACMz0P3KKTRpPm/mB9X0ilQ+s4v zsYe1NyAksIWj3JbckdGtqwtOvKZP5BsFhNDX4D7ftE4pcvcozRcH2oWKzQeN8nd OAWfdtmL9cS6ZRxy7gwfoDsTLKtVX5hcgnmjTztYUsu3SCT1tYYe5wKPvErZCcB1 bMEoGkOl8Z9iXHsZPctNpHhRViMi9LOSE5dmwAV10hRnFr1E8BF+tsCFJiTX4fgB gVWx7CBgn8l501BdwXPgv3ahtcfq7WCjmyorhkkpvYJ+BteEuNflBfubs9Ah4Bw9 HS+IM8+4yYDkv5SBh3ZR/dAs7B6Z/e5XnNMGYBSvCdVzYE7EhBkiIzq5aC5Wh78J 6BUxT9FMheuYYaE8hwyCmFV1ziWhfjNmRzOdi9ZWgcFo7Iehak0WoW+jgay85g7y g6TrDlV36P3qW2D5FTeZ3raOSwHtSIB9tKW5wQ94FSCvMV8uKRVHfrNH8v+NL4h2 eXpUly/RFu0hyUlqutDBHqOpPx1hKXinwP5BmKKndWsm73C5FF7x6wrIGllqha+6 dw0G+b4roFJ8c7MgKpcH3C3xSjsvtqyplaePmy35YUcvEp5KI1AMxxNy5+dHgvFJ NFb7Zt63zL+P0pEhbvY/WCaOCUr8Vl5J9p1i3Js2wOJxqcez+HGAqpihnaQe4CyQ I6PjCIUD/nKjf1M+sIh1BBAWCgAdFiEEUjd9ZeA66cunylPJni8YFQ+6WogFAl64 Yw4ACgkQni8YFQ+6Woj2ewD9H9wBVTdGhFAPTVShFjrul0M8pc41HXqZMnzAcuBF j8ABAN2t+UtjUtE5+kDUkJgk3xtse6SPsP39z3o+A/fuNe0AuDgEXkLj2xIKKwYB BAGXVQEFAQEHQCz43WbDx9rjXKCf9SoafNMct807+toxFSLWVJrJ6i5ZAwEIB4h+ BBgWCAAmAhsMFiEEfgaAcNXveUsAyKnZHRCObAfLxAYFAl5DAXsFCQPChKAACgkQ HRCObAfLxAb07QD9FxvNNG1SDh3jzbvQZdL59p1ehgEniMmzGSALeBYbdtQBAILa 6WPhrYsadEMuxiR3qqLEhkI2nT0ya3USqhRzzL4A =+GpQ -----END PGP PUBLIC KEY BLOCK----- ", ) .unwrap(), ) } fn fingerprint() -> [u8; 20] { [ 0x7E, 0x06, 0x80, 0x70, 0xD5, 0xEF, 0x79, 0x4B, 0x00, 0xC8, 0xA9, 0xD9, 0x1D, 0x10, 0x8E, 0x6C, 0x07, 0xCB, 0xC4, 0x06, ] } #[test] pub fn verify_sign_sequoia_git_commit() { let user_home = tempdir().unwrap(); let mut c = Sequoia { user_key_id: fingerprint(), key_ring: HashMap::new(), user_home: user_home.path().to_path_buf(), }; c.key_ring.insert(fingerprint(), cert()); let data: Vec = vec![0x74, 0x65, 0x73, 0x74, 0x0a]; let sig: Vec = vec![ 0x88, 0x75, 0x04, 0x00, 0x16, 0x0a, 0x00, 0x1d, 0x16, 0x21, 0x04, 0x7e, 0x06, 0x80, 0x70, 0xd5, 0xef, 0x79, 0x4b, 0x00, 0xc8, 0xa9, 0xd9, 0x1d, 0x10, 0x8e, 0x6c, 0x07, 0xcb, 0xc4, 0x06, 0x05, 0x02, 0x61, 0xac, 0xa5, 0x1f, 0x00, 0x0a, 0x09, 0x10, 0x1d, 0x10, 0x8e, 0x6c, 0x07, 0xcb, 0xc4, 0x06, 0x0c, 0x03, 0x01, 0x00, 0xd3, 0xc7, 0x54, 0xd3, 0xed, 0xfc, 0x6b, 0x42, 0x8a, 0xf5, 0x70, 0x8d, 0x83, 0xeb, 0xf3, 0x75, 0xea, 0x72, 0x07, 0x40, 0x71, 0x17, 0xe6, 0xe2, 0xfb, 0xc9, 0xc5, 0x20, 0x24, 0xb5, 0x40, 0x65, 0x01, 0x00, 0x98, 0xb8, 0x6b, 0xf1, 0xbb, 0x9b, 0xdd, 0xf6, 0x17, 0x09, 0x69, 0x73, 0xee, 0xce, 0xf2, 0x4c, 0xfe, 0xbb, 0x99, 0xf3, 0xe1, 0x0c, 0x37, 0x53, 0xb9, 0xf6, 0x5e, 0x46, 0xdf, 0x57, 0x78, 0x06, ]; let result = c.verify_sign( &data, &sig, &[<[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap()], ); assert!(result.is_ok()); } #[test] pub fn verify_sign_sequoia_git_commit_invalid_signing_key() { let user_home = tempdir().unwrap(); let mut c = Sequoia { user_key_id: fingerprint(), key_ring: HashMap::new(), user_home: user_home.path().to_path_buf(), }; c.key_ring.insert(fingerprint(), cert()); let data: Vec = vec![0x74, 0x65, 0x73, 0x74, 0x0a]; let sig: Vec = vec![ 0x88, 0x75, 0x04, 0x00, 0x16, 0x0a, 0x00, 0x1d, 0x16, 0x21, 0x04, 0x7e, 0x06, 0x80, 0x70, 0xd5, 0xef, 0x79, 0x4b, 0x00, 0xc8, 0xa9, 0xd9, 0x1d, 0x10, 0x8e, 0x6c, 0x07, 0xcb, 0xc4, 0x06, 0x05, 0x02, 0x61, 0xac, 0xa5, 0x1f, 0x00, 0x0a, 0x09, 0x10, 0x1d, 0x10, 0x8e, 0x6c, 0x07, 0xcb, 0xc4, 0x06, 0x0c, 0x03, 0x01, 0x00, 0xd3, 0xc7, 0x54, 0xd3, 0xed, 0xfc, 0x6b, 0x42, 0x8a, 0xf5, 0x70, 0x8d, 0x83, 0xeb, 0xf3, 0x75, 0xea, 0x72, 0x07, 0x40, 0x71, 0x17, 0xe6, 0xe2, 0xfb, 0xc9, 0xc5, 0x20, 0x24, 0xb5, 0x40, 0x65, 0x01, 0x00, 0x98, 0xb8, 0x6b, 0xf1, 0xbb, 0x9b, 0xdd, 0xf6, 0x17, 0x09, 0x69, 0x73, 0xee, 0xce, 0xf2, 0x4c, 0xfe, 0xbb, 0x99, 0xf3, 0xe1, 0x0c, 0x37, 0x53, 0xb9, 0xf6, 0x5e, 0x46, 0xdf, 0x57, 0x78, 0x06, ]; let result = c.verify_sign( &data, &sig, &[<[u8; 20]>::from_hex("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap()], ); assert!(result.is_err()); } #[test] pub fn sign_string_sequoia() { let user_home = tempdir().unwrap(); let (cert, _) = CertBuilder::new() .add_userid("someone@example.org") .add_signing_subkey() .generate() .unwrap(); let f = slice_to_20_bytes(cert.fingerprint().as_bytes()).unwrap(); let mut c = Sequoia { user_key_id: f, key_ring: HashMap::new(), user_home: user_home.path().to_path_buf(), }; c.key_ring.insert(f, Arc::new(cert)); let result = c.sign_string( "test", &[], &crate::crypto::FindSigningFingerprintStrategy::GPG, ); assert!(result.is_ok()); assert!(result.unwrap().contains("-----BEGIN PGP SIGNATURE-----")); } #[test] pub fn sign_then_verify_sequoia_with_signing_keys() { let user_home = tempdir().unwrap(); let (cert, _) = CertBuilder::new() .add_userid("someone@example.org") .add_signing_subkey() .generate() .unwrap(); let f = slice_to_20_bytes(cert.fingerprint().as_bytes()).unwrap(); let mut c = Sequoia { user_key_id: f, key_ring: HashMap::new(), user_home: user_home.path().to_path_buf(), }; c.key_ring.insert(f, Arc::new(cert)); let sig = c .sign_string( "test", &[], &crate::crypto::FindSigningFingerprintStrategy::GPG, ) .unwrap(); let result = c.verify_sign("test".as_bytes(), sig.as_bytes(), &[f]); assert!(result.is_ok()); } #[test] pub fn sign_then_verify_sequoia_without_signing_keys() { let user_home = tempdir().unwrap(); let (cert, _) = CertBuilder::new() .add_userid("someone@example.org") .add_signing_subkey() .generate() .unwrap(); let f = slice_to_20_bytes(cert.fingerprint().as_bytes()).unwrap(); let mut c = Sequoia { user_key_id: f, key_ring: HashMap::new(), user_home: user_home.path().to_path_buf(), }; c.key_ring.insert(f, Arc::new(cert)); let sig = c .sign_string( "test", &[], &crate::crypto::FindSigningFingerprintStrategy::GPG, ) .unwrap(); let result = c.verify_sign("test".as_bytes(), sig.as_bytes(), &[]); assert!(result.is_ok()); } #[test] pub fn encrypt_then_decrypt_sequoia() { let user_home = tempdir().unwrap(); let (cert, _) = CertBuilder::new() .add_userid("someone@example.org") .add_transport_encryption_subkey() .generate() .unwrap(); let f = slice_to_20_bytes(cert.fingerprint().as_bytes()).unwrap(); let mut c = Sequoia { user_key_id: f, key_ring: HashMap::new(), user_home: user_home.path().to_path_buf(), }; c.key_ring.insert(f, Arc::new(cert)); let r = Recipient::from(&hex::encode(f), &[], None, &c).unwrap(); let result = c.encrypt_string("test", &[r]).unwrap(); let result = c.decrypt_string(&result).unwrap(); assert_eq!("test", result); } ripasso-0.8.0/src/tests/git.rs000064400000000000000000000010461046102023000143740ustar 00000000000000use crate::{error::Result, git::should_sign, test_helpers::UnpackedDir}; #[test] fn test_should_sign_true() -> Result<()> { let dir = UnpackedDir::new("test_should_sign_true")?; let repo = git2::Repository::open(dir.path())?; let result = should_sign(&repo); assert!(result); Ok(()) } #[test] fn test_should_sign_false() -> Result<()> { let dir = UnpackedDir::new("test_should_sign_false")?; let repo = git2::Repository::open(dir.path())?; let result = should_sign(&repo); assert!(!result); Ok(()) } ripasso-0.8.0/src/tests/pass.rs000064400000000000000000002117371046102023000145710ustar 00000000000000use config::ConfigBuilder; use git2::Repository; use hex::FromHex; use sequoia_openpgp::{ cert::CertBuilder, serialize::{ Serialize, stream::{Armorer, Message, Signer}, }, }; use std::{env, fs::File, path::PathBuf}; use tempfile::tempdir; use super::*; use crate::{ crypto::slice_to_20_bytes, test_helpers::{ MockCrypto, UnpackedDir, count_recipients, generate_sequoia_cert, generate_sequoia_cert_without_private_key, }, }; impl PartialEq for Error { fn eq(&self, other: &Error) -> bool { format!("{:?}", self) == format!("{:?}", *other) } } pub fn setup_store( td: &tempfile::TempDir, user_home: &Path, ) -> Result<(PasswordStore, Vec>)> { let users = vec![ Arc::new(generate_sequoia_cert("alice@example.org")), Arc::new(generate_sequoia_cert("bob@example.org")), Arc::new(generate_sequoia_cert("carlos@example.org")), Arc::new(generate_sequoia_cert_without_private_key( "daniel@example.com", )), ]; let mut key_ring = HashMap::new(); for u in &users { key_ring.insert(slice_to_20_bytes(u.fingerprint().as_bytes())?, u.clone()); } let store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: [].to_vec(), style_file: None, crypto: Box::new(Sequoia::from_values( slice_to_20_bytes(users[0].fingerprint().as_bytes())?, key_ring, user_home, )), user_home: None, }; Ok((store, users)) } #[test] fn get_password_dir_no_env() { let dir = tempdir().unwrap(); // ensure that the test run isn't polluted by outside env. // "safe" as it's just a unit test unsafe { env::remove_var("PASSWORD_STORE_DIR"); } let path = password_dir(&None, &Some(dir.into_path())); assert_eq!( path.unwrap_err(), Error::Generic("failed to locate password directory") ); } #[test] fn get_password_dir_raw_none_none() { let result = password_dir_raw(&None, &None); assert_eq!(PathBuf::new().join(".password-store"), result); } #[test] fn get_password_dir_raw_some_none() { let result = password_dir_raw(&Some(PathBuf::from("/tmp/")), &None); assert_eq!(PathBuf::from("/tmp/"), result); } #[test] fn get_password_dir_raw_none_some() { let result = password_dir_raw(&None, &Some(PathBuf::from("/tmp/"))); assert_eq!(PathBuf::from("/tmp/.password-store"), result); } #[test] fn get_password_dir_raw_some_some() { let result = password_dir_raw(&Some(PathBuf::from("/tmp/")), &Some(PathBuf::from("/tmp/"))); assert_eq!(PathBuf::from("/tmp/"), result); } #[test] fn populate_password_list_small_repo() -> Result<()> { let dir = UnpackedDir::new("populate_password_list_small_repo")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 1); assert_eq!(results[0].name, "test"); assert_eq!(results[0].committed_by, Some("Alexander Kjäll".to_owned())); assert!(results[0].signature_status.is_none()); Ok(()) } #[test] fn populate_password_list_repo_with_deleted_files() -> Result<()> { let dir = UnpackedDir::new("populate_password_list_repo_with_deleted_files")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 1); assert_eq!(results[0].name, "10"); assert_eq!(results[0].committed_by, Some("Alexander Kjäll".to_owned())); assert!(results[0].signature_status.is_none()); Ok(()) } #[test] fn populate_password_list_directory_without_git() -> Result<()> { let dir = UnpackedDir::new("populate_password_list_directory_without_git")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 3); assert_eq!(results[0].name, "first"); assert!(results[0].committed_by.is_none()); assert!(results[0].updated.is_none()); assert!(results[0].signature_status.is_none()); assert_eq!(results[1].name, "second"); assert!(results[1].committed_by.is_none()); assert!(results[1].updated.is_none()); assert!(results[1].signature_status.is_none()); assert_eq!(results[2].name, "third"); assert!(results[2].committed_by.is_none()); assert!(results[2].updated.is_none()); assert!(results[2].signature_status.is_none()); Ok(()) } #[test] fn password_store_with_files_in_initial_commit() -> Result<()> { let dir = UnpackedDir::new("password_store_with_files_in_initial_commit")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; let expected = ["3", "A/1", "B/2"]; assert_eq!(results.len(), expected.len()); for (i, e) in expected.iter().enumerate() { assert_eq!(results[i].name, e.to_string()); assert!(results[i].committed_by.is_some()); assert!(results[i].updated.is_some()); } Ok(()) } #[test] fn password_store_with_relative_path() -> Result<()> { let dir = UnpackedDir::new("password_store_with_relative_path")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 3); assert_eq!(results[0].name, "3"); assert!(results[0].committed_by.is_some()); assert!(results[0].updated.is_some()); assert_eq!(results[1].name, "2"); assert!(results[1].committed_by.is_some()); assert!(results[1].updated.is_some()); assert_eq!(results[2].name, "1"); assert!(results[2].committed_by.is_some()); assert!(results[2].updated.is_some()); Ok(()) } #[test] fn password_store_with_shallow_checkout() -> Result<()> { let dir = UnpackedDir::new("password_store_with_shallow_checkout")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 1); assert_eq!(results[0].name, "1"); assert!(results[0].committed_by.is_some()); assert!(results[0].updated.is_some()); Ok(()) } #[test] fn password_store_with_sparse_checkout() -> Result<()> { let dir = UnpackedDir::new("password_store_with_sparse_checkout")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 3); assert_eq!(results[0].name, "3/1"); assert!(results[0].committed_by.is_some()); assert!(results[0].updated.is_some()); assert_eq!(results[1].name, "2/1"); assert!(results[1].committed_by.is_some()); assert!(results[1].updated.is_some()); assert_eq!(results[2].name, "1/1"); assert!(results[2].committed_by.is_some()); assert!(results[2].updated.is_some()); Ok(()) } #[cfg(target_family = "unix")] #[test] fn password_store_with_symlink() -> Result<()> { let dir = UnpackedDir::new("password_store_with_symlink")?; let link_dir = dir .path() .parent() .unwrap() .join("password_store_with_symlink_link"); std::os::unix::fs::symlink(dir.path(), link_dir.clone())?; let store = PasswordStore::new( "default", &Some(link_dir.clone()), &None, &Some(link_dir.clone()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; fs::remove_file(link_dir)?; assert_eq!(results.len(), 3); assert_eq!(results[0].name, "3"); assert!(results[0].committed_by.is_some()); assert!(results[0].updated.is_some()); assert_eq!(results[1].name, "2"); assert!(results[1].committed_by.is_some()); assert!(results[1].updated.is_some()); assert_eq!(results[2].name, "1"); assert!(results[2].committed_by.is_some()); assert!(results[2].updated.is_some()); Ok(()) } #[test] fn home_exists_missing_home_env() { assert!(!home_exists(&None, &Config::default())); } #[test] fn home_exists_home_dir_without_config_dir() { let dir = tempdir().unwrap(); let result = home_exists(&Some(dir.into_path()), &Config::default()); assert!(!result); } #[test] fn home_exists_home_dir_with_file_instead_of_dir() -> Result<()> { let dir = tempdir()?; File::create(dir.path().join(".password-store"))?; let result = home_exists(&Some(dir.into_path()), &Config::default()); assert!(!result); Ok(()) } #[test] fn home_exists_home_dir_with_config_dir() -> Result<()> { let dir = tempdir()?; fs::create_dir(dir.path().join(".password-store"))?; let result = home_exists(&Some(dir.into_path()), &Config::default()); assert!(result); Ok(()) } #[test] fn env_var_exists_test_none() { assert!(!env_var_exists(&None, &None)); } #[test] fn env_var_exists_test_without_dir() { let dir = tempdir().unwrap(); assert!(env_var_exists( &Some( dir.path() .join(".password-store") .to_str() .unwrap() .to_owned() ), &None )); } #[test] fn env_var_exists_test_with_dir() { let dir = tempdir().unwrap(); assert!(env_var_exists( &Some(dir.path().to_str().unwrap().to_owned()), &None )); } #[test] fn home_settings_missing() { assert_eq!( Error::GenericDyn("no home directory set".to_owned()), home_settings(&None).err().unwrap() ); } #[test] fn home_settings_dir_exists() -> Result<()> { let dir = tempdir()?; fs::create_dir(dir.path().join(".password-store"))?; let settings = home_settings(&Some(PathBuf::from(dir.path())))?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; assert_eq!( dir.path() .join(".password-store/") .to_str() .unwrap() .to_owned(), path ); Ok(()) } /// this works due to that it's the function `home_exists` that checks if it exists #[test] fn home_settings_dir_doesnt_exists() -> Result<()> { let dir = tempdir()?; let settings = home_settings(&Some(PathBuf::from(dir.path())))?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; assert_eq!( dir.path() .join(".password-store/") .to_str() .unwrap() .to_owned(), path ); Ok(()) } #[test] fn var_settings_test() -> Result<()> { let settings = var_settings( &Some("/home/user/.password-store".to_owned()), &Some("E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F".to_owned()), )?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; let valid_signing_keys = work["valid_signing_keys"].clone().into_string()?; assert_eq!("/home/user/.password-store/", path); assert_eq!( "E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F", valid_signing_keys ); Ok(()) } #[test] fn file_settings_simple_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".config").join("ripasso"))?; let mut file = File::create( dir.path() .join(".config") .join("ripasso") .join("settings.toml"), )?; writeln!( &file, "[stores]\n [stores.work]\n path = \"/home/user/.password-store\"\n" )?; file.flush()?; let mut settings = ConfigBuilder::default(); settings = config::ConfigBuilder::::add_source( settings, file_settings(&xdg_config_file_location(&Some(dir.into_path()), &None)?), ); let settings = settings.build()?; let stores = settings.get_table("stores")?; let work = stores["work"].clone().into_table()?; let path = work["path"].clone().into_string()?; assert_eq!("/home/user/.password-store", path); Ok(()) } #[test] fn file_settings_file_in_xdg_config_home() -> Result<()> { let dir = tempdir()?; let dir2 = tempdir()?; create_dir_all(dir2.path().join(".random_config").join("ripasso"))?; let mut file = File::create( dir2.path() .join(".random_config") .join("ripasso") .join("settings.toml"), )?; writeln!( &file, "[stores]\n [stores.work]\n path = \"/home/user/.password-store\"\n" )?; file.flush()?; let mut settings = ConfigBuilder::default(); settings = config::ConfigBuilder::::add_source( settings, file_settings(&xdg_config_file_location( &Some(dir.into_path()), &Some(dir2.path().join(".random_config")), )?), ); let settings = settings.build()?; let stores = settings.get_table("stores")?; let work = stores["work"].clone().into_table()?; let path = work["path"].clone().into_string()?; assert_eq!("/home/user/.password-store", path); Ok(()) } #[test] fn read_config_empty_config_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".config").join("ripasso"))?; create_dir_all(dir.path().join(".password-store"))?; File::create( dir.path() .join(".config") .join("ripasso") .join("settings.toml"), )?; let (settings, _) = read_config(&None, &None, &Some(PathBuf::from(dir.path())), &None)?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; assert_eq!( dir.path() .join(".password-store/") .to_str() .unwrap() .to_owned(), path ); Ok(()) } #[test] fn read_config_empty_config_file_with_keys_env() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let (settings, _) = read_config( &None, &Some("E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F".to_owned()), &Some(PathBuf::from(dir.path())), &None, )?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; let valid_signing_keys = work["valid_signing_keys"].clone().into_string()?; assert_eq!( dir.path() .join(".password-store/") .to_str() .unwrap() .to_owned(), path ); assert_eq!( "E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F", valid_signing_keys ); Ok(()) } #[test] fn read_config_env_vars() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join("env_var").join(".password-store"))?; let (settings, _) = read_config( &Some( dir.path() .join("env_var") .join(".password-store") .to_str() .unwrap() .to_owned(), ), &Some("E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F".to_owned()), &Some(PathBuf::from(dir.path())), &None, )?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; let valid_signing_keys = work["valid_signing_keys"].clone().into_string()?; assert_eq!( dir.path() .join("env_var") .join(".password-store/") .to_str() .unwrap() .to_owned(), path ); assert_eq!( "E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F", valid_signing_keys ); Ok(()) } #[test] fn read_config_home_and_env_vars() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; create_dir_all(dir.path().join("env_var").join(".password-store"))?; let (settings, _) = read_config( &Some( dir.path() .join("env_var") .join(".password-store") .to_str() .unwrap() .to_owned(), ), &Some("E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F".to_owned()), &Some(PathBuf::from(dir.path())), &None, )?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; let valid_signing_keys = work["valid_signing_keys"].clone().into_string()?; assert_eq!( dir.path() .join("env_var") .join(".password-store/") .to_str() .unwrap() .to_owned(), path ); assert_eq!( "E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F", valid_signing_keys ); Ok(()) } #[test] fn read_config_default_path_in_config_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; create_dir_all(dir.path().join(".config").join("ripasso"))?; let mut file = File::create( dir.path() .join(".config") .join("ripasso") .join("settings.toml"), )?; writeln!( &file, "[stores]\n [stores.work]\n path = \"{}\"\n", dir.path().join(".password-store").to_str().unwrap() )?; file.flush()?; let (settings, _) = read_config(&None, &None, &Some(PathBuf::from(dir.path())), &None)?; let stores = settings.get_table("stores")?; let work = stores["work"].clone().into_table()?; let path = work["path"].clone().into_string()?; assert_eq!(dir.path().join(".password-store").to_str().unwrap(), path); assert!(!stores.contains_key("default")); Ok(()) } #[test] fn read_config_default_path_in_env_var() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; create_dir_all(dir.path().join(".config").join("ripasso"))?; let mut file = File::create( dir.path() .join(".config") .join("ripasso") .join("settings.toml"), )?; writeln!( &file, "[stores]\n [stores.default]\n path = \"{}\"\n valid_signing_keys = \"7E068070D5EF794B00C8A9D91D108E6C07CBC406\"\n", dir.path().join(".password-store").to_str().unwrap() )?; file.flush()?; let (settings, _) = read_config( &Some("/tmp/t2".to_owned()), &None, &Some(dir.into_path()), &None, )?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; let keys = work["valid_signing_keys"].clone().into_string()?; assert_eq!("/tmp/t2/", path); assert_eq!("-1", keys); assert!(!stores.contains_key("work")); Ok(()) } #[test] fn read_config_default_path_in_env_var_with_pgp_setting() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; create_dir_all(dir.path().join(".config").join("ripasso"))?; let mut file = File::create( dir.path() .join(".config") .join("ripasso") .join("settings.toml"), )?; writeln!( &file, "[stores.default]\npath = \"{}\"\nvalid_signing_keys = \"7E068070D5EF794B00C8A9D91D108E6C07CBC406\"\npgp_implementation = 'gpg'\nown_fingerprint = \"7E068070D5EF794B00C8A9D91D108E6C07CBC406\"", dir.path().join(".password-store").to_str().unwrap() )?; file.flush()?; let (settings, _) = read_config( &Some("/tmp/t2".to_owned()), &None, &Some(dir.into_path()), &None, )?; let stores = settings.get_table("stores")?; let work = stores["default"].clone().into_table()?; let path = work["path"].clone().into_string()?; let keys = work["valid_signing_keys"].clone().into_string()?; assert_eq!("/tmp/t2/", path); assert_eq!("-1", keys); assert_eq!("gpg", work["pgp_implementation"].clone().into_string()?); assert_eq!( "7E068070D5EF794B00C8A9D91D108E6C07CBC406", work["own_fingerprint"].clone().into_string()? ); assert!(!stores.contains_key("work")); Ok(()) } #[test] fn save_config_one_store() { let config_file = tempfile::NamedTempFile::new().unwrap(); let style_file = tempfile::NamedTempFile::new().unwrap(); let passdir = tempdir().unwrap(); let home = tempdir().unwrap(); let s1 = PasswordStore::new( "s1 name", &Some(passdir.path().to_path_buf()), &None, &Some(home.path().to_path_buf()), &Some(style_file.path().to_path_buf()), &CryptoImpl::Sequoia, &Some([0; 20]), ) .unwrap(); save_config( Arc::new(Mutex::new(vec![Arc::new(Mutex::new(s1))])), config_file.path(), ) .unwrap(); let config = fs::read_to_string(config_file.path()).unwrap(); assert!(config.contains("[stores.\"s1 name\"]\n")); assert!(config.contains(&format!("path = \"{}\"\n", passdir.path().display()))); assert!(config.contains(&format!( "style_path = \"{}\"\n", style_file.path().display() ))); assert!(config.contains("pgp_implementation = \"sequoia\"\n")); assert!(config.contains("own_fingerprint = \"0000000000000000000000000000000000000000\"\n")); } #[test] fn save_config_one_store_with_pgp_impl() { let dir = tempdir().unwrap(); let store = PasswordStore::new( "default", &Some(dir.path().to_path_buf()), &None, &Some(dir.path().to_path_buf()), &None, &CryptoImpl::GpgMe, &None, ) .unwrap(); save_config( Arc::new(Mutex::new(vec![Arc::new(Mutex::new(store))])), &dir.path().join("file.toml"), ) .unwrap(); let data = fs::read_to_string(dir.path().join("file.toml")).unwrap(); assert!(data.contains("[stores.default]")); assert!(data.contains("pgp_implementation = \"gpg\"")); assert!(data.contains(&format!("path = \"{}\"\n", &dir.path().display()))); } #[test] fn save_config_one_store_with_fingerprint() { let dir = tempdir().unwrap(); let store = PasswordStore::new( "default", &Some(dir.path().to_path_buf()), &None, &Some(dir.path().to_path_buf()), &None, &CryptoImpl::Sequoia, &Some(<[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap()), ) .unwrap(); save_config( Arc::new(Mutex::new(vec![Arc::new(Mutex::new(store))])), &dir.path().join("file.toml"), ) .unwrap(); let data = fs::read_to_string(dir.path().join("file.toml")).unwrap(); assert!(data.contains("[stores.default]")); assert!(data.contains("pgp_implementation = \"sequoia\"")); assert!(data.contains("own_fingerprint = \"7E068070D5EF794B00C8A9D91D108E6C07CBC406\"")); assert!(data.contains(&format!("path = \"{}\"\n", &dir.path().display()))); } #[test] fn append_extension_with_dot() { let result = append_extension(PathBuf::from("foo.txt"), ".gpg"); assert_eq!(result, PathBuf::from("foo.txt.gpg")); } #[test] fn rename_file() -> Result<()> { let dir = UnpackedDir::new("rename_file")?; let mut config_location = dir.dir(); config_location.push(".git"); config_location.push("config"); let mut config = git2::Config::open(&config_location)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; config.set_str("commit.gpgsign", "false")?; let mut store = PasswordStore { name: "default".to_owned(), root: dir.dir(), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; store.reload_password_list()?; let index = store.rename_file("1/test", "2/test")?; assert_eq!(1, index); assert_eq!(store.passwords.len(), 2); assert_eq!(store.passwords[0].name, "test"); assert!(store.passwords[0].committed_by.is_some()); assert!(store.passwords[0].updated.is_some()); assert_eq!(store.passwords[1].name, "2/test"); assert!(store.passwords[1].committed_by.is_some()); assert!(store.passwords[1].updated.is_some()); Ok(()) } #[test] fn rename_file_absolute_path() -> Result<()> { let dir = UnpackedDir::new("rename_file_absolute_path")?; let mut store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; store.reload_password_list()?; let res = store.rename_file("1/test", "/2/test"); assert!(res.is_err()); Ok(()) } #[test] fn rename_file_git_index_clean() -> Result<()> { let dir = UnpackedDir::new("rename_file_git_index_clean")?; let mut config_location = dir.dir(); config_location.push(".git"); config_location.push("config"); let mut config = git2::Config::open(&config_location)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; config.set_str("commit.gpgsign", "false")?; let mut store = PasswordStore { name: "default".to_owned(), root: dir.dir(), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; store.reload_password_list()?; store.rename_file("1/test", "2/test")?; let repo = Repository::open(dir.path())?; assert!(repo.statuses(None)?.is_empty()); Ok(()) } #[test] fn decrypt_secret_empty_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(GpgMe {}), user_home: None, }; let res = pe.secret(&store); assert!(res.is_err()); assert_eq!("empty password file", format!("{}", res.err().unwrap())); Ok(()) } #[test] fn decrypt_secret_missing_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(GpgMe {}), user_home: None, }; let res = pe.secret(&store); assert!(res.is_err()); assert_eq!( "No such file or directory (os error 2)", format!("{}", res.err().unwrap()) ); Ok(()) } #[test] fn decrypt_secret() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.write_all("dummy data".as_bytes())?; pass_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let crypto = MockCrypto::new().with_decrypt_string_return("decrypt_secret unit test".to_owned()); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(crypto), user_home: None, }; let res = pe.secret(&store)?; assert_eq!("decrypt_secret unit test", res); Ok(()) } #[test] fn decrypt_password_empty_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(GpgMe {}), user_home: None, }; let res = pe.password(&store); assert!(res.is_err()); assert_eq!("empty password file", format!("{}", res.err().unwrap())); Ok(()) } #[test] fn decrypt_password_multiline() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.write_all("dummy data".as_bytes())?; pass_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let crypto = MockCrypto::new().with_decrypt_string_return("row one\nrow two\nrow three".to_owned()); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(crypto), user_home: None, }; let mut res = pe.password(&store)?; assert_eq!("row one", res); res.zeroize(); Ok(()) } fn mfa_setup(payload: String) -> Result<(tempfile::TempDir, PasswordEntry, PasswordStore)> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.write_all("dummy data".as_bytes())?; pass_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let crypto = MockCrypto::new().with_decrypt_string_return(payload); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(crypto), user_home: None, }; Ok((dir, pe, store)) } #[test] fn mfa_example1() -> Result<()> { let (_dir, pe, store) = mfa_setup("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXPAAAAAAAAAAAA&issuer=Example".to_owned())?; let res = pe.mfa(&store)?; assert_eq!(6, res.len()); assert_eq!(6, res.chars().filter(|c| c.is_ascii_digit()).count()); Ok(()) } #[test] fn mfa_example2() -> Result<()> { let (_dir, pe, store) = mfa_setup("some text\n otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXPAAAAAAAAAAAA&issuer=Example\nmore txt\n\n".to_owned())?; let res = pe.mfa(&store)?; assert_eq!(6, res.len()); assert_eq!(6, res.chars().filter(|c| c.is_ascii_digit()).count()); Ok(()) } #[test] fn mfa_example3() -> Result<()> { let (_dir, pe, store) = mfa_setup("lots and lots and lots and lots and lots and lots and lots and lots and lots and lots of text\n otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXPAAAAAAAAAAAA&issuer=Example\nmore txt\n\n".to_owned())?; let res = pe.mfa(&store)?; assert_eq!(6, res.len()); assert_eq!(6, res.chars().filter(|c| c.is_ascii_digit()).count()); Ok(()) } #[test] fn mfa_no_otpauth_url() -> Result<()> { let (_dir, pe, store) = mfa_setup("password".to_owned())?; let res = pe.mfa(&store); assert_eq!(Err(Error::Generic("No otpauth:// url in secret")), res); Ok(()) } #[test] fn update() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.write_all("dummy data".as_bytes())?; pass_file.flush()?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let crypto = MockCrypto::new().with_encrypt_string_return(vec![1, 2, 3]); let store = PasswordStore { name: "store_name".to_owned(), root: dir.path().join(".password-store"), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(crypto), user_home: None, }; let res = pe.update("new content".to_owned(), &store); assert!(res.is_ok()); let mut pass_file = File::open(dir.path().join(".password-store").join("file.gpg"))?; let mut data = Vec::new(); pass_file.read_to_end(&mut data)?; assert_eq!(vec![1, 2, 3], data); Ok(()) } #[test] fn delete_file() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.flush()?; let store = PasswordStore::new( "test", &Some(dir.path().join(".password-store")), &None, &None, &None, &CryptoImpl::GpgMe, &None, )?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let res = pe.delete_file(&store); assert!(res.is_ok()); let stat = fs::metadata(dir.path().join(".password-store").join("file.gpg")); assert!(stat.is_err()); Ok(()) } #[test] fn get_history_no_repo() -> Result<()> { let dir = tempdir()?; create_dir_all(dir.path().join(".password-store"))?; let mut gpg_file = File::create(dir.path().join(".password-store").join(".gpg-id"))?; writeln!(&gpg_file, "0xDF0C3D316B7312D5\n")?; gpg_file.flush()?; let mut pass_file = File::create(dir.path().join(".password-store").join("file.gpg"))?; pass_file.flush()?; let store = PasswordStore::new( "test", &Some(dir.path().join(".password-store")), &None, &None, &None, &CryptoImpl::GpgMe, &None, )?; let pe = PasswordEntry::new( &dir.path().join(".password-store"), &PathBuf::from("file.gpg"), Ok(Local::now()), Ok(String::new()), Ok(SignatureStatus::Good), RepositoryStatus::NoRepo, ); let history = pe.get_history(&store)?; assert_eq!(0, history.len()); Ok(()) } #[test] fn get_history_with_repo() -> Result<()> { let dir = UnpackedDir::new("get_history_with_repo")?; let store = PasswordStore::new( "default", &Some(dir.dir()), &None, &Some(dir.dir()), &None, &CryptoImpl::GpgMe, &None, )?; let results = store.all_passwords()?; assert_eq!(results.len(), 1); assert_eq!(results[0].name, "test"); assert_eq!(results[0].committed_by, Some("default".to_owned())); assert!(results[0].signature_status.is_none()); let pw = &results[0]; let history = pw.get_history(&store)?; assert_eq!(history.len(), 3); assert_eq!(history[0].message, "commit 3\n"); assert_eq!(history[0].signature_status, None); assert_eq!(history[1].message, "commit 2\n"); assert_eq!(history[1].signature_status, None); assert_eq!(history[2].message, "commit 1\n"); assert_eq!(history[2].signature_status, None); Ok(()) } #[test] fn test_format_error() { assert_eq!( format!("{}", Error::from(std::io::Error::from_raw_os_error(3))), "No such process (os error 3)" ); assert_eq!( format!("{}", Error::from(gpgme::Error::from_errno(3))), "No such process (gpg error 32895)" ); assert_eq!( format!("{}", Error::from(git2::Error::from_str("git error"))), "git error" ); #[allow(invalid_from_utf8)] let utf8_error = String::from_utf8(vec![255]).err().unwrap(); #[allow(invalid_from_utf8)] let str_utf8_error = str::from_utf8(&[255]).err().unwrap(); assert_eq!( format!("{}", Error::from(utf8_error.clone())), "invalid utf-8 sequence of 1 bytes from index 0" ); let path = Path::new("/test/haha/foo.txt"); assert_eq!( format!("{}", Error::from(path.strip_prefix("test").err().unwrap())), "prefix not found" ); assert_eq!( format!("{}", Error::from(glob::glob("****").err().unwrap())), "Pattern syntax error near position 2: wildcards are either regular `*` or recursive `**`" ); assert_eq!( format!("{}", Error::from(utf8_error)), "invalid utf-8 sequence of 1 bytes from index 0" ); assert_eq!( format!("{}", Error::from(Some(str_utf8_error))), "invalid utf-8 sequence of 1 bytes from index 0" ); assert_eq!( format!("{}", Error::from(config::ConfigError::Frozen)), "configuration is frozen" ); assert_eq!( format!( "{}", Error::from(toml::ser::to_string_pretty(&None::).err().unwrap()) ), "unsupported None value" ); assert_eq!( format!("{}", Error::from("custom error message")), "custom error message" ); assert_eq!(format!("{}", Error::NoneError), "NoneError"); } #[test] fn test_commit_unsigned() -> Result<()> { let td = tempdir()?; let repo = Repository::init(td.path())?; let mut config = repo.config()?; config.set_bool("commit.gpgsign", false)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; let mut index = repo.index()?; let path = td.path().join("password-to-add"); let mut f = File::create(path)?; f.write_all("some data".as_bytes())?; index.add_path(Path::new("password-to-add"))?; index.write()?; let oid = index.write_tree()?; let tree = repo.find_tree(oid)?; let parents = vec![]; let crypto = MockCrypto::new(); let c_oid = commit(&repo, &repo.signature()?, "test", &tree, &parents, &crypto)?; assert!(!(*crypto.sign_called.borrow())); assert_eq!("test", repo.find_commit(c_oid)?.message().unwrap()); Ok(()) } #[test] fn test_commit_signed() -> Result<()> { let td = tempdir()?; let repo = Repository::init(td.path())?; let mut config = repo.config()?; config.set_bool("commit.gpgsign", true)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; let mut index = repo.index()?; let path = td.path().join("password-to-add"); let mut f = File::create(path)?; f.write_all("some data".as_bytes())?; index.add_path(Path::new("password-to-add"))?; index.write()?; let oid = index.write_tree()?; let tree = repo.find_tree(oid)?; let parents = vec![]; let crypto = MockCrypto::new(); let c_oid = commit(&repo, &repo.signature()?, "test", &tree, &parents, &crypto)?; assert!(*crypto.sign_called.borrow()); assert_eq!("test", repo.find_commit(c_oid)?.message().unwrap()); Ok(()) } #[test] fn test_move_and_commit_signed() -> Result<()> { let dir = UnpackedDir::new("test_move_and_commit_signed")?; let repo = Repository::init(dir.path())?; let mut config = repo.config()?; config.set_bool("commit.gpgsign", true)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; fs::rename( dir.path().join("first_pass.gpg"), dir.path().join("second_pass.gpg"), )?; let store = PasswordStore { name: "store_name".to_owned(), root: dir.dir(), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; let c_oid = move_and_commit( &store, Path::new("first_pass.gpg"), Path::new("second_pass.gpg"), "unit test", )?; assert_eq!("unit test", repo.find_commit(c_oid)?.message().unwrap()); Ok(()) } #[test] fn test_search() -> Result<()> { let p1 = PasswordEntry { name: "no/match/check".to_owned(), path: Default::default(), updated: None, committed_by: None, signature_status: None, is_in_git: RepositoryStatus::InRepo, }; let p2 = PasswordEntry { name: "dir/test/middle".to_owned(), path: Default::default(), updated: None, committed_by: None, signature_status: None, is_in_git: RepositoryStatus::InRepo, }; let p3 = PasswordEntry { name: " space test ".to_owned(), path: Default::default(), updated: None, committed_by: None, signature_status: None, is_in_git: RepositoryStatus::InRepo, }; let store = PasswordStore { name: "store_name".to_owned(), root: env::temp_dir(), valid_gpg_signing_keys: vec![], passwords: vec![p1, p2, p3], style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; let store = store; let result = search(&store, "test"); assert_eq!(2, result.len()); assert_eq!("dir/test/middle", result[0].name); assert_eq!(" space test ", result[1].name); Ok(()) } #[test] fn test_verify_git_signature() -> Result<()> { let dir = UnpackedDir::new("test_verify_git_signature")?; let repo = Repository::open(dir.path())?; let oid = repo.head()?.target().unwrap(); let store = PasswordStore { name: "store_name".to_owned(), root: dir.dir(), valid_gpg_signing_keys: vec![<[u8; 20]>::from_hex( "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; let result = verify_git_signature(&repo, &oid, &store); assert_eq!( Error::Generic( "the commit wasn\'t signed by one of the keys specified in the environmental variable PASSWORD_STORE_SIGNING_KEY" ), result.err().unwrap() ); Ok(()) } #[test] fn test_add_and_commit_internal() -> Result<()> { let dir = UnpackedDir::new("test_add_and_commit_internal")?; let repo = Repository::init(dir.path())?; let mut config = repo.config()?; config.set_bool("commit.gpgsign", true)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; let crypto = MockCrypto::new(); let new_password = dir.path().join("new_password"); File::create(new_password)?.write_all("swordfish".as_bytes())?; let c_oid = add_and_commit_internal( &repo, &[PathBuf::from("new_password")], "unit test", &crypto, )?; assert_eq!("unit test", repo.find_commit(c_oid)?.message().unwrap()); Ok(()) } #[test] fn test_remove_and_commit() -> Result<()> { let dir = UnpackedDir::new("test_remove_and_commit")?; let store = PasswordStore { name: "store_name".to_owned(), root: dir.dir(), valid_gpg_signing_keys: vec![<[u8; 20]>::from_hex( "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; let repo = Repository::open(dir.path())?; let mut config = repo.config()?; config.set_bool("commit.gpgsign", true)?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; let c_oid = remove_and_commit(&store, &[PathBuf::from("pass_to_be_deleted")], "unit test")?; assert_eq!("unit test", repo.find_commit(c_oid)?.message().unwrap()); assert!(!dir.path().join("pass_to_be_deleted").is_file()); Ok(()) } #[test] fn test_to_name() { assert_eq!("name", to_name(&PathBuf::from("name.gpg"))); assert_eq!("dir/name", to_name(&PathBuf::from("dir/name.gpg"))); assert_eq!( "dir/name without gpg on end", to_name(&PathBuf::from("dir/name without gpg on end")) ); } #[test] fn test_verify_gpg_id_files_missing_sig_file() -> Result<()> { let td = tempdir()?; let store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![<[u8; 20]>::from_hex( "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; let result = store.verify_gpg_id_files(); assert!(result.is_err()); assert_eq!( Error::Generic("problem reading .gpg-id.sig, and strict signature checking was asked for"), result.err().unwrap() ); Ok(()) } #[test] fn test_verify_gpg_id_files() -> Result<()> { let td = tempdir()?; let store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![<[u8; 20]>::from_hex( "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; fs::write( td.path().join(".gpg-id.sig"), "here there should be gpg data", )?; let result = store.verify_gpg_id_files(); assert!(result.is_err()); assert_eq!( Error::Generic( "the .gpg-id file wasn't signed by one of the keys specified in the environmental variable PASSWORD_STORE_SIGNING_KEY" ), result.err().unwrap() ); Ok(()) } fn sign(to_sign: &str, tsk: &sequoia_openpgp::Cert) -> String { let p = sequoia_openpgp::policy::StandardPolicy::new(); let keypair = tsk .keys() .unencrypted_secret() .with_policy(&p, None) .alive() .revoked(false) .for_signing() .next() .unwrap() .key() .clone() .into_keypair() .unwrap(); let mut sink: Vec = vec![]; // Start streaming an OpenPGP message. let message = Message::new(&mut sink); let message = Armorer::new(message) .kind(sequoia_openpgp::armor::Kind::Signature) .build() .unwrap(); // We want to sign a literal data packet. let mut message = Signer::new(message, keypair) .unwrap() .detached() .build() .unwrap(); // Sign the data. message.write_all(to_sign.as_bytes()).unwrap(); // Finalize the OpenPGP message to make sure that all data is // written. message.finalize().unwrap(); str::from_utf8(&sink).unwrap().to_owned() } #[test] fn test_verify_gpg_id_files_untrusted_key_in_keyring() { let td = tempdir().unwrap(); let (store_owner, _) = CertBuilder::new() .add_userid("store_owner@example.org") .add_signing_subkey() .generate() .unwrap(); let sofp = slice_to_20_bytes(store_owner.fingerprint().as_bytes()).unwrap(); let (unrelated_user, _) = CertBuilder::new() .add_userid("unrelated_user@example.org") .add_signing_subkey() .generate() .unwrap(); let keys_dir = td .path() .join("local") .join("share") .join("ripasso") .join("keys"); create_dir_all(&keys_dir).unwrap(); let password_store_dir = td.path().join(".password_store"); create_dir_all(&password_store_dir).unwrap(); let mut file = File::create(keys_dir.join(hex::encode(store_owner.fingerprint().as_bytes()))).unwrap(); store_owner.serialize(&mut file).unwrap(); let mut file = File::create(keys_dir.join(hex::encode(unrelated_user.fingerprint().as_bytes()))).unwrap(); unrelated_user.serialize(&mut file).unwrap(); fs::write(password_store_dir.join(".gpg-id"), hex::encode_upper(sofp)).unwrap(); fs::write( password_store_dir.join(".gpg-id.sig"), sign(&hex::encode_upper(sofp), &unrelated_user), ) .unwrap(); let store = PasswordStore { name: "store_name".to_owned(), root: password_store_dir.to_path_buf(), valid_gpg_signing_keys: vec![sofp], passwords: [].to_vec(), style_file: None, crypto: Box::new(Sequoia::new(&td.path().join("local"), sofp, td.path()).unwrap()), user_home: None, }; let result = store.verify_gpg_id_files(); assert!(result.is_err()); assert_eq!( Error::GenericDyn("No valid signature".to_owned()), result.err().unwrap() ); } #[test] fn test_new_password_file() -> Result<()> { let td = tempdir()?; let mut store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; assert_eq!(0, store.passwords.len()); let result = store.new_password_file("test/file", "password")?; assert_eq!(1, store.passwords.len()); assert_eq!("test/file", store.passwords[0].name); assert_eq!(RepositoryStatus::NoRepo, result.is_in_git); assert!(result.signature_status.is_none()); assert!(result.committed_by.is_none()); assert!(result.updated.is_none()); assert_eq!("test/file", result.name); assert_eq!(td.path().join("test").join("file.gpg"), result.path); Ok(()) } #[test] fn test_new_password_file_in_git_repo() -> Result<()> { let td = tempdir()?; let mut store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new().with_encrypt_string_return(vec![32, 32, 32, 32])), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; let repo = Repository::init(td.path())?; let mut config = repo.config()?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; assert_eq!(0, store.passwords.len()); let result = store.new_password_file("test/file", "password")?; assert_eq!(1, store.passwords.len()); assert_eq!("test/file", store.passwords[0].name); assert_eq!(RepositoryStatus::InRepo, result.is_in_git); assert!(result.signature_status.is_none()); assert!(result.committed_by.is_some()); assert!(result.updated.is_some()); assert_eq!("test/file", result.name); Ok(()) } #[test] fn test_new_password_file_encryption_failure() -> Result<()> { let td = tempdir()?; let mut store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new().with_encrypt_error("unit test error".to_owned())), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; let repo = Repository::init(td.path())?; let mut config = repo.config()?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; assert_eq!(0, store.passwords.len()); let err = store.new_password_file("test/file", "password"); assert_eq!(0, store.passwords.len()); assert!(err.is_err()); assert!(!td.path().join("test").join("file.gpg").exists()); Ok(()) } #[test] fn test_new_password_file_twice() -> Result<()> { let td = tempdir()?; let mut store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new().with_encrypt_string_return(vec![32, 32, 32, 32])), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; let repo = Repository::init(td.path())?; let mut config = repo.config()?; config.set_str("user.name", "default")?; config.set_str("user.email", "default@example.com")?; assert_eq!(0, store.passwords.len()); let result = store.new_password_file("test/file", "password")?; assert_eq!(1, store.passwords.len()); assert_eq!("test/file", store.passwords[0].name); assert_eq!(RepositoryStatus::InRepo, result.is_in_git); assert!(result.signature_status.is_none()); assert!(result.committed_by.is_some()); assert!(result.updated.is_some()); assert_eq!("test/file", result.name); let result = store.new_password_file("test/file", "password"); assert_eq!(1, store.passwords.len()); assert_eq!("test/file", store.passwords[0].name); assert!(result.is_err()); assert!(td.path().join("test").join("file.gpg").exists()); Ok(()) } #[test] fn test_new_password_file_outside_pass_dir() -> Result<()> { let td = tempdir()?; let mut store = PasswordStore { name: "store_name".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: [].to_vec(), style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; assert_eq!(0, store.passwords.len()); let result = store.new_password_file("../file", "password"); assert_eq!(0, store.passwords.len()); assert!(result.is_err()); Ok(()) } #[test] fn test_new_password_file_different_sub_permissions() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n" + &hex::encode(users[1].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; fs::write( td.path().join("dir").join(".gpg-id"), hex::encode(users[1].fingerprint().as_bytes()), )?; assert_eq!(0, store.passwords.len()); store.new_password_file("dir/file", "password")?; assert_eq!(1, store.passwords.len()); let content = fs::read(td.path().join("dir").join("file.gpg"))?; assert_eq!(1, count_recipients(&content)); Ok(()) } #[test] fn test_rename_file_different_sub_permissions() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n" + &hex::encode(users[1].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; fs::write( td.path().join("dir").join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()), )?; assert_eq!(0, store.passwords.len()); store.new_password_file("dir/file", "password")?; store.rename_file("dir/file", "file")?; assert_eq!(1, store.passwords.len()); let content = fs::read(td.path().join("file.gpg"))?; assert_eq!(2, count_recipients(&content)); Ok(()) } #[test] fn test_add_recipient_different_sub_permissions() -> Result<()> { let td = tempdir()?; let config_path = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n" + &hex::encode(users[1].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; fs::write( td.path().join("dir").join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n", )?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; store.new_password_file("dir/file", "password")?; store.add_recipient( &crate::test_helpers::recipient_from_cert(&users[2]), &PathBuf::from("./"), config_path.path(), )?; assert_eq!(2, store.passwords.len()); let content = fs::read(td.path().join("file.gpg"))?; assert_eq!(3, count_recipients(&content)); let content = fs::read(td.path().join("dir/file.gpg"))?; assert_eq!(1, count_recipients(&content)); Ok(()) } #[test] fn test_add_recipient_to_sub_dir() -> Result<()> { let td = tempdir()?; let config_path = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n" + &hex::encode(users[1].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; store.new_password_file("dir/file", "password")?; store.add_recipient( &crate::test_helpers::recipient_from_cert(&users[2]), &PathBuf::from("dir/"), config_path.path(), )?; assert_eq!(2, store.passwords.len()); let content = fs::read(td.path().join("file.gpg"))?; assert_eq!(2, count_recipients(&content)); let content = fs::read(td.path().join("dir/file.gpg"))?; assert_eq!(1, count_recipients(&content)); Ok(()) } #[test] fn test_add_recipient_to_sub_dir_path_traversal() -> Result<()> { let td = tempdir()?; let config_path = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; let res = store.add_recipient( &crate::test_helpers::recipient_from_cert(&users[2]), &PathBuf::from("/tmp/"), config_path.path(), ); assert!(res.is_err()); assert_eq!( "Generic(\"path traversal not allowed\")", format!("{:?}", res.err().unwrap()) ); Ok(()) } #[test] fn test_add_recipient_to_sub_dir_unknown_path() -> Result<()> { let td = tempdir()?; let config_path = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; let res = store.add_recipient( &crate::test_helpers::recipient_from_cert(&users[2]), &PathBuf::from("path_that_doesnt_exist/"), config_path.path(), ); assert!(res.is_err()); assert_eq!( "Generic(\"path doesn't exist\")", format!("{:?}", res.err().unwrap()) ); Ok(()) } #[test] fn test_add_recipient_not_in_key_ring() -> Result<()> { let td = tempdir()?; let config_path = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; let external_user = generate_sequoia_cert_without_private_key("bob@example.com"); let external_user_recipient = crate::test_helpers::recipient_from_cert(&external_user); fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n", )?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; let gpg_id_file_pre = fs::read_to_string(td.path().join(".gpg-id"))?; let res = store.add_recipient( &external_user_recipient, &PathBuf::from("./"), config_path.path(), ); let gpg_id_file_post = fs::read_to_string(td.path().join(".gpg-id"))?; assert!(res.is_err()); assert_eq!(gpg_id_file_pre, gpg_id_file_post); Ok(()) } #[test] fn test_remove_last_recipient_with_decryption_rights() -> Result<()> { let td = tempdir()?; let config_path = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; let user0_recipient = crate::test_helpers::recipient_from_cert(&users[0]); let user3_recipient = crate::test_helpers::recipient_from_cert(&users[3]); fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n", )?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; store.add_recipient(&user3_recipient, &PathBuf::from("./"), config_path.path())?; let gpg_id_file_pre = fs::read_to_string(td.path().join(".gpg-id"))?; let res = store.remove_recipient(&user0_recipient, &PathBuf::from("./")); let gpg_id_file_post = fs::read_to_string(td.path().join(".gpg-id"))?; assert!(res.is_ok()); assert_ne!(gpg_id_file_pre, gpg_id_file_post); Ok(()) } #[test] fn test_remove_last_recipient_from_sub_folder() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; let user0_recipient = crate::test_helpers::recipient_from_cert(&users[0]); fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; fs::write( td.path().join("dir").join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n", )?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; store.new_password_file("dir/file", "password")?; let gpg_id_file_pre = fs::read_to_string(td.path().join(".gpg-id"))?; let res = store.remove_recipient(&user0_recipient, &PathBuf::from("dir")); let gpg_id_file_post = fs::read_to_string(td.path().join(".gpg-id"))?; assert!(res.is_ok()); assert!(!td.path().join("dir").join(".gpg-id").exists()); assert_eq!(gpg_id_file_pre, gpg_id_file_post); Ok(()) } #[test] fn test_add_password_without_decryption_rights() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[3].fingerprint().as_bytes()) + "\n", )?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; assert_eq!(1, store.passwords.len()); Ok(()) } #[test] fn test_remove_recipient_root() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (mut store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n" + &hex::encode(users[1].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; fs::write( td.path().join("dir").join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n", )?; assert_eq!(0, store.passwords.len()); store.new_password_file("file", "password")?; store.new_password_file("dir/file", "password")?; store.remove_recipient( &crate::test_helpers::recipient_from_cert(&users[1]), &PathBuf::from("./"), )?; assert_eq!(2, store.passwords.len()); let content = fs::read(td.path().join("file.gpg"))?; assert_eq!(1, count_recipients(&content)); let content = fs::read(td.path().join("dir/file.gpg"))?; assert_eq!(1, count_recipients(&content)); Ok(()) } #[test] fn test_recipients_file_for_dir() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (store, _) = setup_store(&td, user_home.path())?; File::create(td.path().join(".gpg-id"))?; assert_eq!( td.path().join(".gpg-id"), store.recipients_file_for_dir(&store.get_store_path())? ); Ok(()) } #[test] fn test_recipient_files() -> Result<()> { let td = tempdir()?; let user_home = tempdir()?; let (store, users) = setup_store(&td, user_home.path())?; fs::write( td.path().join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()) + "\n" + &hex::encode(users[1].fingerprint().as_bytes()) + "\n", )?; fs::create_dir(td.path().join("dir"))?; fs::write( td.path().join("dir").join(".gpg-id"), hex::encode(users[0].fingerprint().as_bytes()), )?; let result = store.recipients_files()?; assert_eq!(2, result.len()); assert!(result.contains(&td.path().join(".gpg-id"))); assert!(result.contains(&td.path().join("dir").join(".gpg-id"))); Ok(()) } #[test] fn init_git_repo_success() -> Result<()> { let td = tempdir()?; assert!(!td.path().join(".git").exists()); init_git_repo(td.path())?; assert!(td.path().join(".git").exists()); Ok(()) } #[test] fn all_recipients_from_stores_plain() -> Result<()> { let td = tempdir()?; fs::write( td.path().join(".gpg-id"), "7E068070D5EF794B00C8A9D91D108E6C07CBC406", )?; let s1 = PasswordStore { name: "unit test store".to_owned(), root: td.path().to_path_buf(), valid_gpg_signing_keys: vec![], passwords: vec![], style_file: None, crypto: Box::new(MockCrypto::new()), user_home: None, }; let result = all_recipients_from_stores(Arc::new(Mutex::new(vec![Arc::new(Mutex::new(s1))])))?; assert_eq!(1, result.len()); assert_eq!("7E068070D5EF794B00C8A9D91D108E6C07CBC406", result[0].key_id); Ok(()) } ripasso-0.8.0/src/tests/signature.rs000064400000000000000000000606741046102023000156260ustar 00000000000000use hex::FromHex; use crate::{ pass::{KeyRingStatus, OwnerTrustLevel, Recipient}, signature::{Comment, parse_signing_keys}, test_helpers::{MockCrypto, MockKey, append_file_name, recipient_alex, recipient_alex_old}, }; #[test] fn test_parse_signing_keys_two_keys() { let crypto = MockCrypto::new() .with_get_key_result( "7E068070D5EF794B00C8A9D91D108E6C07CBC406".to_owned(), MockKey::new(), ) .with_get_key_result( "E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F".to_owned(), MockKey::new(), ); let file_content = "7E068070D5EF794B00C8A9D91D108E6C07CBC406,E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F" .to_owned(); let result = parse_signing_keys(&Some(file_content), &crypto).unwrap(); assert_eq!(2, result.len()); assert!( result.contains(&<[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap()) ); assert!( result.contains(&<[u8; 20]>::from_hex("E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F").unwrap()) ); } #[test] fn test_parse_signing_keys_two_keys_with_0x() { let crypto = MockCrypto::new() .with_get_key_result( "0x7E068070D5EF794B00C8A9D91D108E6C07CBC406".to_owned(), MockKey::new(), ) .with_get_key_result( "0xE6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F".to_owned(), MockKey::new(), ); let file_content = "0x7E068070D5EF794B00C8A9D91D108E6C07CBC406,0xE6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F" .to_owned(); let result = parse_signing_keys(&Some(file_content), &crypto).unwrap(); assert_eq!(2, result.len()); assert!( result.contains(&<[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap()) ); assert!( result.contains(&<[u8; 20]>::from_hex("E6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F").unwrap()) ); } #[test] fn parse_signing_keys_key_error() { let crypto = MockCrypto::new().with_get_key_error("unit test error".to_owned()); let file_content = "0x7E068070D5EF794B00C8A9D91D108E6C07CBC406,0xE6A7D758338EC2EF2A8A9F4EE7E3DB4B3217482F" .to_owned(); let result = parse_signing_keys(&Some(file_content), &crypto); assert!(result.is_err()); } #[test] fn parse_signing_keys_empty() { let crypto = MockCrypto::new(); let result = parse_signing_keys(&None, &crypto).unwrap(); assert_eq!(result.len(), 0); } #[test] fn parse_signing_keys_short() { let crypto = MockCrypto::new(); let result = parse_signing_keys(&Some("0x1D108E6C07CBC406".to_string()), &crypto); assert!(result.is_err()); } #[test] fn recipient_from_key_error() { let crypto = MockCrypto::new().with_get_key_error("unit test error".to_owned()); let result = Recipient::from("0x1D108E6C07CBC406", &[], None, &crypto); assert!(result.is_ok()); let result = result.unwrap(); assert_eq!("key id not in keyring", result.name); } #[test] fn all_recipients() { let crypto = MockCrypto::new().with_get_key_result( "0x1D108E6C07CBC406".to_owned(), MockKey::from_args( <[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap(), vec!["Alexander Kjäll ".to_owned()], ), ); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join(".gpg-id"); std::fs::File::create(&file).unwrap(); std::fs::write(&file, "0x1D108E6C07CBC406").unwrap(); let result = Recipient::all_recipients(&file, &crypto).unwrap(); assert_eq!(1, result.len()); assert_eq!( "Alexander Kjäll ", result[0].name ); assert_eq!("0x1D108E6C07CBC406", result[0].key_id); assert_eq!(KeyRingStatus::InKeyRing, result[0].key_ring_status); } #[test] fn all_recipients_with_one_comment_line() { let crypto = MockCrypto::new().with_get_key_result( "0x1D108E6C07CBC406".to_owned(), MockKey::from_args( <[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap(), vec!["Alexander Kjäll ".to_owned()], ), ); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join(".gpg-id"); std::fs::File::create(&file).unwrap(); std::fs::write(&file, "# first comment\n0x1D108E6C07CBC406").unwrap(); let result = Recipient::all_recipients(&file, &crypto).unwrap(); assert_eq!(1, result.len()); assert_eq!( "Alexander Kjäll ", result[0].name ); assert_eq!( " first comment", result[0].comment.pre_comment.as_ref().unwrap() ); assert_eq!(None, result[0].comment.post_comment); assert_eq!("0x1D108E6C07CBC406", result[0].key_id); assert_eq!(KeyRingStatus::InKeyRing, result[0].key_ring_status); } #[test] fn all_recipients_with_multiple_comment_lines() { let crypto = MockCrypto::new().with_get_key_result( "0x1D108E6C07CBC406".to_owned(), MockKey::from_args( <[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap(), vec!["Alexander Kjäll ".to_owned()], ), ); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join(".gpg-id"); std::fs::File::create(&file).unwrap(); std::fs::write( &file, "# first comment\n\n#comment two\n0x1D108E6C07CBC406\n# end comment\n", ) .unwrap(); let result = Recipient::all_recipients(&file, &crypto).unwrap(); assert_eq!(1, result.len()); assert_eq!( "Alexander Kjäll ", result[0].name ); assert_eq!( " first comment\ncomment two", result[0].comment.pre_comment.as_ref().unwrap() ); assert_eq!(None, result[0].comment.post_comment); assert_eq!("0x1D108E6C07CBC406", result[0].key_id); assert_eq!(KeyRingStatus::InKeyRing, result[0].key_ring_status); } #[test] fn all_recipients_with_comment_lines_pre_and_post() { let crypto = MockCrypto::new().with_get_key_result( "0x1D108E6C07CBC406".to_owned(), MockKey::from_args( <[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap(), vec!["Alexander Kjäll ".to_owned()], ), ); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join(".gpg-id"); std::fs::File::create(&file).unwrap(); std::fs::write( &file, "# first comment\n\n#comment two\n0x1D108E6C07CBC406 # post comment\n", ) .unwrap(); let result = Recipient::all_recipients(&file, &crypto).unwrap(); assert_eq!(1, result.len()); assert_eq!( "Alexander Kjäll ", result[0].name ); assert_eq!( " first comment\ncomment two", result[0].comment.pre_comment.as_ref().unwrap() ); assert_eq!("0x1D108E6C07CBC406", result[0].key_id); assert_eq!(KeyRingStatus::InKeyRing, result[0].key_ring_status); } #[test] fn all_recipients_error() { let crypto = MockCrypto::new().with_get_key_error("unit test error".to_owned()); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join(".gpg-id"); std::fs::File::create(&file).unwrap(); std::fs::write(&file, "0x1D108E6C07CBC406").unwrap(); let result = Recipient::all_recipients(&file, &crypto).unwrap(); assert_eq!(1, result.len()); assert_eq!("key id not in keyring", result[0].name); assert_eq!("0x1D108E6C07CBC406", result[0].key_id); assert_eq!(KeyRingStatus::NotInKeyRing, result[0].key_ring_status); } #[test] fn all_recipients_no_file_error() { let crypto = MockCrypto::new(); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join(".gpg-id"); let result = Recipient::all_recipients(&file, &crypto); assert!(result.is_err()); } #[test] fn write_recipients_file_empty() { let recipients = vec![]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new(); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); assert!(recipients_file.exists()); assert!(!recipients_file.join(".sig").exists()); let contents = std::fs::read_to_string(recipients_file).unwrap(); assert_eq!("", contents); assert!(!signature_file.exists()); } #[test] fn write_recipients_file_one() { let recipients = vec![recipient_alex()]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new(); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); assert!(recipients_file.exists()); let recipient_sig_filename = append_file_name(&recipients_file); assert!(!recipient_sig_filename.exists()); let contents = std::fs::read_to_string(recipients_file).unwrap(); assert_eq!("0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n", contents); assert!(!signature_file.exists()); } #[test] fn write_recipients_file_one_with_pre_comment() { let mut r = recipient_alex(); r.comment = Comment { pre_comment: Some("comment line".to_owned()), post_comment: None, }; let recipients = vec![r]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new(); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); assert!(recipients_file.exists()); let recipient_sig_filename = append_file_name(&recipients_file); assert!(!recipient_sig_filename.exists()); let contents = std::fs::read_to_string(recipients_file).unwrap(); assert_eq!( "#comment line\n0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n", contents ); assert!(!signature_file.exists()); } #[test] fn write_recipients_file_one_with_multi_line_comment() { let mut r = recipient_alex(); r.comment = Comment { pre_comment: Some("comment one\ncomment two".to_owned()), post_comment: None, }; let recipients = vec![r]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new(); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); assert!(recipients_file.exists()); let recipient_sig_filename = append_file_name(&recipients_file); assert!(!recipient_sig_filename.exists()); let contents = std::fs::read_to_string(recipients_file).unwrap(); assert_eq!( "#comment one\n#comment two\n0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n", contents ); assert!(!signature_file.exists()); } #[test] fn write_recipients_file_one_comment_pre_and_post() { let mut r = recipient_alex(); r.comment = Comment { pre_comment: Some("pre comment".to_owned()), post_comment: Some("post comment".to_owned()), }; let recipients = vec![r]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new(); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); assert!(recipients_file.exists()); let recipient_sig_filename = append_file_name(&recipients_file); assert!(!recipient_sig_filename.exists()); let contents = std::fs::read_to_string(recipients_file).unwrap(); assert_eq!( "#pre comment\n0x7E068070D5EF794B00C8A9D91D108E6C07CBC406 #post comment\n", contents ); assert!(!signature_file.exists()); } #[test] fn write_recipients_file_one_and_signed() { let recipients = vec![recipient_alex()]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![<[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap()]; let crypto = MockCrypto::new().with_sign_string_return("unit test sign string".to_owned()); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); assert!(recipients_file.exists()); let recipient_sig_filename = append_file_name(&recipients_file); assert!(recipient_sig_filename.exists()); let contents = std::fs::read_to_string(recipients_file).unwrap(); assert_eq!("0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n", contents); assert!(signature_file.exists()); let contents = std::fs::read_to_string(&signature_file).unwrap(); assert_eq!("unit test sign string", contents); } #[test] fn remove_recipient_from_file_last() { let r = recipient_alex(); let recipients = vec![r.clone()]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new(); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!("0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n", contents); let result = Recipient::remove_recipient_from_file( &r, &recipients_file, dir.path(), &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!("0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n", contents); assert!(!signature_file.exists()); } #[test] fn remove_recipient_from_file_two() { let r = recipient_alex(); let r2 = recipient_alex_old(); let recipients = vec![r.clone(), r2.clone()]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new() .with_get_key_result( r.key_id.clone(), MockKey::from_args(r.fingerprint.unwrap(), vec![r.name.clone()]), ) .with_get_key_result( r2.key_id.clone(), MockKey::from_args(r2.fingerprint.unwrap(), vec![r2.name.clone()]), ); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!( "0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n0xDB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5\n", contents ); let result = Recipient::remove_recipient_from_file( &r, &recipients_file, dir.path(), &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!(86, contents.len()); assert!(contents.contains("0xDB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5")); assert!(contents.contains("0x7E068070D5EF794B00C8A9D91D108E6C07CBC406")); assert!(!signature_file.exists()); } #[test] fn remove_recipient_from_file_same_key_id_different_fingerprint() { let r = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; let r2 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("88283D2EF664DD5F6AEBB51CDF0C3D316B7312D5").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; let recipients = vec![r.clone(), r2.clone()]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new() .with_get_key_result( "0xDB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5".to_owned(), MockKey::from_args( <[u8; 20]>::from_hex("DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5").unwrap(), vec!["Alexander Kjäll ".to_owned()], ), ) .with_get_key_result( "0x88283D2EF664DD5F6AEBB51CDF0C3D316B7312D5".to_owned(), MockKey::from_args( <[u8; 20]>::from_hex("88283D2EF664DD5F6AEBB51CDF0C3D316B7312D5").unwrap(), vec!["Alexander Kjäll ".to_owned()], ), ); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!( "0x88283D2EF664DD5F6AEBB51CDF0C3D316B7312D5\n0xDB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5\n", contents ); let result = Recipient::remove_recipient_from_file( &r, &recipients_file, dir.path(), &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!(43, contents.len()); assert!(contents.contains("0x88283D2EF664DD5F6AEBB51CDF0C3D316B7312D5")); assert!(!signature_file.exists()); } #[test] fn add_recipient_from_file_one_plus_one() { let r = recipient_alex(); let r2 = recipient_alex_old(); let recipients = vec![r.clone()]; let dir = tempfile::tempdir().unwrap(); let recipients_file = dir.path().join(".gpg-id"); let signature_file = dir.path().join(".gpg-id.sig"); let valid_gpg_signing_keys = vec![]; let crypto = MockCrypto::new() .with_get_key_result( r.key_id.clone(), MockKey::from_args(r.fingerprint.unwrap(), vec![r.name.clone()]), ) .with_get_key_result( r2.key_id.clone(), MockKey::from_args(r2.fingerprint.unwrap(), vec![r2.name.clone()]), ); assert!(!recipients_file.exists()); assert!(!signature_file.exists()); let result = Recipient::write_recipients_file( &recipients, &recipients_file, &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let result = Recipient::add_recipient_to_file(&r2, &recipients_file, &valid_gpg_signing_keys, &crypto); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!( "0x7E068070D5EF794B00C8A9D91D108E6C07CBC406\n0xDB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5\n", contents ); let result = Recipient::remove_recipient_from_file( &r, &recipients_file, dir.path(), &valid_gpg_signing_keys, &crypto, ); assert!(result.is_ok()); let contents = std::fs::read_to_string(&recipients_file).unwrap(); assert_eq!(86, contents.len()); assert!(contents.contains("0xDB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5")); assert!(contents.contains("0x7E068070D5EF794B00C8A9D91D108E6C07CBC406")); assert!(!signature_file.exists()); } #[test] fn recipient_both_none() { let r1 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: None, key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; let r2 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: None, key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; assert_ne!(r1, r2); assert_ne!(r2, r1); } #[test] fn recipient_one_none() { let r1 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; let r2 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: None, key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; assert_ne!(r1, r2); assert_ne!(r2, r1); } #[test] fn recipient_same_fingerprint_different_key_id() { let r1 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; let r2 = Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, }; assert_eq!(r1, r2); assert_eq!(r2, r1); } ripasso-0.8.0/src/tests/test_helpers.rs000064400000000000000000000272751046102023000163260ustar 00000000000000use std::{ cell::RefCell, collections::HashMap, fs::File, path::{Path, PathBuf}, }; use flate2::read::GzDecoder; use hex::FromHex; use sequoia_openpgp::crypto::SessionKey; use sequoia_openpgp::packet::UserID; use sequoia_openpgp::types::SymmetricAlgorithm; use sequoia_openpgp::{ Cert, KeyHandle, KeyID, cert::CertBuilder, parse::{ Parse, stream::{DecryptionHelper, DecryptorBuilder, MessageStructure, VerificationHelper}, }, policy::StandardPolicy, }; use tar::Archive; use crate::{ crypto::{Crypto, CryptoImpl, FindSigningFingerprintStrategy, Key, VerificationError}, error::{Error, Result}, pass::{KeyRingStatus, OwnerTrustLevel, SignatureStatus}, signature::{Comment, Recipient}, }; pub struct UnpackedDir { dir: PathBuf, } impl Drop for UnpackedDir { fn drop(&mut self) { std::fs::remove_dir_all(&self.dir).unwrap(); } } impl UnpackedDir { pub fn new(name: &str) -> Result { let base_path: PathBuf = get_testres_path(); let packed_file = base_path.join(name.to_owned() + ".tar.gz"); let tar_gz = File::open(packed_file)?; let tar = GzDecoder::new(tar_gz); let mut archive = Archive::new(tar); archive.unpack(base_path.clone())?; Ok(UnpackedDir { dir: base_path.join(name), }) } pub fn path(&self) -> &Path { self.dir.as_path() } pub fn dir(&self) -> PathBuf { self.dir.clone() } } fn get_testres_path() -> PathBuf { let mut base_path: PathBuf = std::env::current_exe().unwrap(); base_path.pop(); base_path.pop(); base_path.pop(); base_path.pop(); base_path.push("testres"); base_path } #[derive(Clone)] pub struct MockKey { fingerprint: [u8; 20], user_id_names: Vec, } impl Key for MockKey { fn user_id_names(&self) -> Vec { self.user_id_names.clone() } fn fingerprint(&self) -> Result<[u8; 20]> { Ok(self.fingerprint) } fn is_not_usable(&self) -> bool { false } } impl Default for MockKey { fn default() -> Self { Self::new() } } impl MockKey { pub fn new() -> MockKey { MockKey { fingerprint: <[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap(), user_id_names: vec!["Alexander Kjäll ".to_owned()], } } pub fn from_args(fingerprint: [u8; 20], user_id_names: Vec) -> MockKey { MockKey { user_id_names, fingerprint, } } } #[derive(Clone)] pub struct MockCrypto { pub decrypt_called: RefCell, pub encrypt_called: RefCell, pub sign_called: RefCell, pub verify_called: RefCell, encrypt_string_return: Vec, decrypt_string_return: Option, sign_string_return: Option, encrypt_string_error: Option, get_key_string_error: Option, get_key_answers: HashMap, } impl Default for MockCrypto { fn default() -> Self { Self::new() } } impl MockCrypto { pub fn new() -> MockCrypto { MockCrypto { decrypt_called: RefCell::new(false), encrypt_called: RefCell::new(false), sign_called: RefCell::new(false), verify_called: RefCell::new(false), encrypt_string_return: vec![], decrypt_string_return: None, sign_string_return: None, encrypt_string_error: None, get_key_string_error: None, get_key_answers: HashMap::new(), } } pub fn with_encrypt_string_return(mut self, data: Vec) -> MockCrypto { self.encrypt_string_return = data; self } pub fn with_decrypt_string_return(mut self, data: String) -> MockCrypto { self.decrypt_string_return = Some(data); self } pub fn with_encrypt_error(mut self, err_str: String) -> MockCrypto { self.encrypt_string_error = Some(err_str); self } pub fn with_get_key_error(mut self, err_str: String) -> MockCrypto { self.get_key_string_error = Some(err_str); self } pub fn with_get_key_result(mut self, key_id: String, key: MockKey) -> MockCrypto { self.get_key_answers.insert(key_id, key); self } pub fn with_sign_string_return(mut self, sign_str: String) -> MockCrypto { self.sign_string_return = Some(sign_str); self } } impl Crypto for MockCrypto { fn decrypt_string(&self, _: &[u8]) -> Result { self.decrypt_called.replace(true); match &self.decrypt_string_return { Some(s) => Ok(s.clone()), None => Ok(String::new()), } } fn encrypt_string(&self, _: &str, _: &[Recipient]) -> Result> { self.encrypt_called.replace(true); if self.encrypt_string_error.is_some() { Err(Error::GenericDyn( self.encrypt_string_error.clone().unwrap(), )) } else { Ok(self.encrypt_string_return.clone()) } } fn sign_string( &self, _: &str, _: &[[u8; 20]], _: &FindSigningFingerprintStrategy, ) -> Result { self.sign_called.replace(true); Ok(match self.sign_string_return.as_ref() { Some(s) => s.to_owned(), None => String::new(), }) } fn verify_sign( &self, _: &[u8], _: &[u8], _: &[[u8; 20]], ) -> std::result::Result { self.verify_called.replace(true); Err(VerificationError::SignatureFromWrongRecipient) } fn is_key_in_keyring(&self, _recipient: &Recipient) -> Result { Ok(true) } fn pull_keys(&mut self, _recipients: &[&Recipient], _config_path: &Path) -> Result { Ok("dummy implementation".to_owned()) } fn import_key(&mut self, _key: &str, _config_path: &Path) -> Result { Ok("dummy implementation".to_owned()) } fn get_key(&self, key_id: &str) -> Result> { if self.get_key_string_error.is_some() { Err(Error::GenericDyn( self.get_key_string_error.clone().unwrap(), )) } else if self.get_key_answers.contains_key(key_id) { Ok(Box::new(self.get_key_answers.get(key_id).unwrap().clone())) } else { Err(Error::Generic("no key configured")) } } fn get_all_trust_items(&self) -> Result> { Ok(HashMap::new()) } fn implementation(&self) -> CryptoImpl { CryptoImpl::GpgMe } fn own_fingerprint(&self) -> Option<[u8; 20]> { None } } pub fn recipient_alex() -> Recipient { Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "1D108E6C07CBC406".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("7E068070D5EF794B00C8A9D91D108E6C07CBC406").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, } } pub fn recipient_alex_old() -> Recipient { Recipient { name: "Alexander Kjäll ".to_owned(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: "DF0C3D316B7312D5".to_owned(), fingerprint: Some( <[u8; 20]>::from_hex("DB07DAC5B3882EAB659E1D2FDF0C3D316B7312D5").unwrap(), ), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, } } pub fn recipient_from_cert(cert: &Cert) -> Recipient { Recipient { name: String::from_utf8(cert.userids().next().unwrap().userid().value().to_vec()).unwrap(), comment: Comment { pre_comment: None, post_comment: None, }, key_id: cert.fingerprint().to_hex(), fingerprint: Some(<[u8; 20]>::from_hex(cert.fingerprint().to_hex()).unwrap()), key_ring_status: KeyRingStatus::InKeyRing, trust_level: OwnerTrustLevel::Ultimate, not_usable: false, } } pub fn append_file_name(file: &Path) -> PathBuf { let rf = file.to_path_buf(); let mut sig = rf.into_os_string(); sig.push(".sig"); sig.into() } pub fn generate_sequoia_cert(email: &str) -> Cert { let (cert, _) = CertBuilder::general_purpose([UserID::from(email)]) .generate() .unwrap(); cert } pub fn generate_sequoia_cert_without_private_key(email: &str) -> Cert { let (cert, _) = CertBuilder::general_purpose([UserID::from(email)]) .generate() .unwrap(); cert.strip_secret_key_material() } struct KeyLister { pub ids: Vec, } impl VerificationHelper for &mut KeyLister { fn get_certs(&mut self, _: &[KeyHandle]) -> std::result::Result, anyhow::Error> { Ok(vec![]) } fn check(&mut self, _structure: MessageStructure) -> std::result::Result<(), anyhow::Error> { Ok(()) } } impl DecryptionHelper for &mut KeyLister { fn decrypt( &mut self, pkesks: &[sequoia_openpgp::packet::PKESK], _: &[sequoia_openpgp::packet::SKESK], _: Option, _: &mut dyn FnMut(Option, &SessionKey) -> bool, ) -> std::result::Result, anyhow::Error> { self.ids.extend( pkesks .iter() .map(|p| { return match p.recipient().clone().unwrap() { KeyHandle::Fingerprint(fpr) => Ok(fpr.into()), KeyHandle::KeyID(key_id) => Ok(key_id), }; }) .collect::, anyhow::Error>>()?, ); Ok(None) } } pub fn count_recipients(data: &[u8]) -> usize { let p = StandardPolicy::new(); let mut h = KeyLister { ids: vec![] }; // result ignored since it's always an error, as we are not decrypting for real let _ = DecryptorBuilder::from_bytes(&data) .unwrap() .with_policy(&p, None, &mut h); h.ids.len() } #[test] fn test_count_recipients() { let data = vec![ 0xc1, 0x6c, 0x06, 0x15, 0x04, 0x08, 0x6f, 0x6c, 0x69, 0x12, 0x24, 0xad, 0x6e, 0x3c, 0x0c, 0x86, 0xfc, 0xa2, 0x26, 0xb7, 0x82, 0xd7, 0xfc, 0xd2, 0x44, 0x12, 0x01, 0x07, 0x40, 0x72, 0xb0, 0x2f, 0x8b, 0x35, 0x5a, 0x34, 0xe1, 0x05, 0xbf, 0x6f, 0x35, 0x2d, 0xc8, 0x33, 0xed, 0xaa, 0xdf, 0x76, 0xbf, 0xfb, 0x54, 0x8b, 0x73, 0x2c, 0xac, 0x7d, 0xd4, 0xd8, 0xc9, 0xdf, 0x1b, 0x30, 0x0d, 0x09, 0x53, 0x11, 0x31, 0x03, 0x99, 0xfb, 0x77, 0xa0, 0xa1, 0x1a, 0x0d, 0x9a, 0xb2, 0xf0, 0x22, 0xe6, 0xf1, 0x63, 0x90, 0x29, 0xb8, 0x37, 0xd4, 0x75, 0xd8, 0x03, 0xc7, 0x22, 0xdb, 0xe3, 0x9d, 0x62, 0xea, 0x70, 0x69, 0xfa, 0x29, 0x4b, 0x00, 0x11, 0x49, 0x0c, 0xbf, 0x96, 0x39, 0xa9, 0xd2, 0x54, 0x02, 0x09, 0x02, 0x06, 0x55, 0x14, 0xe8, 0x76, 0xdd, 0x0f, 0x25, 0x13, 0x16, 0xe5, 0xfd, 0xb4, 0x57, 0x3b, 0xce, 0xa0, 0x3c, 0x81, 0x3d, 0xc1, 0x82, 0x27, 0x46, 0x91, 0xf1, 0x9e, 0xc1, 0x09, 0x94, 0x9b, 0xbb, 0x55, 0xd4, 0xa4, 0x26, 0x31, 0xb8, 0x17, 0xef, 0xd8, 0x48, 0xbd, 0x1b, 0x3a, 0xbd, 0x40, 0xec, 0xc6, 0x0b, 0x33, 0xb0, 0x2f, 0x8c, 0x71, 0xb1, 0x90, 0xf6, 0xda, 0x35, 0xe5, 0x8b, 0xb5, 0x3e, 0x23, 0xa3, 0x80, 0x35, 0x11, 0x83, 0x79, 0xf4, 0x79, 0x09, 0x71, 0xac, 0xee, 0xc5, 0x65, 0x0e, 0xb8, ]; assert_eq!(1, count_recipients(&data)); } ripasso-0.8.0/src/tests/words.rs000064400000000000000000000004421046102023000147460ustar 00000000000000use crate::words::generate_password; #[test] fn do_not_generate_passwords_that_ends_in_space() { let pass = generate_password(3); assert!(!pass.ends_with(' ')); } #[test] fn generate_long_enough_passwords() { let pass = generate_password(3); assert!(pass.len() > 10); } ripasso-0.8.0/src/words.rs000064400000000000000000003447021046102023000136160ustar 00000000000000use rand::distributions::{Distribution, Uniform}; /// Generate a random password, consisting of `number_of_words` words. pub fn generate_password(number_of_words: isize) -> String { let mut rng = rand::thread_rng(); let die = Uniform::from(0..7776); let mut rand_words = vec![]; for _ in 0..number_of_words { rand_words.push(WORDS[die.sample(&mut rng)]); } rand_words.join(" ") } /// The large wordlist from /// const WORDS: &[&str] = &[ "abacus", "abdomen", "abdominal", "abide", "abiding", "ability", "ablaze", "able", "abnormal", "abrasion", "abrasive", "abreast", "abridge", "abroad", "abruptly", "absence", "absentee", "absently", "absinthe", "absolute", "absolve", "abstain", "abstract", "absurd", "accent", "acclaim", "acclimate", "accompany", "account", "accuracy", "accurate", "accustom", "acetone", "achiness", "aching", "acid", "acorn", "acquaint", "acquire", "acre", "acrobat", "acronym", "acting", "action", "activate", "activator", "active", "activism", "activist", "activity", "actress", "acts", "acutely", "acuteness", "aeration", "aerobics", "aerosol", "aerospace", "afar", "affair", "affected", "affecting", "affection", "affidavit", "affiliate", "affirm", "affix", "afflicted", "affluent", "afford", "affront", "aflame", "afloat", "aflutter", "afoot", "afraid", "afterglow", "afterlife", "aftermath", "aftermost", "afternoon", "aged", "ageless", "agency", "agenda", "agent", "aggregate", "aghast", "agile", "agility", "aging", "agnostic", "agonize", "agonizing", "agony", "agreeable", "agreeably", "agreed", "agreeing", "agreement", "aground", "ahead", "ahoy", "aide", "aids", "aim", "ajar", "alabaster", "alarm", "albatross", "album", "alfalfa", "algebra", "algorithm", "alias", "alibi", "alienable", "alienate", "aliens", "alike", "alive", "alkaline", "alkalize", "almanac", "almighty", "almost", "aloe", "aloft", "aloha", "alone", "alongside", "aloof", "alphabet", "alright", "although", "altitude", "alto", "aluminum", "alumni", "always", "amaretto", "amaze", "amazingly", "amber", "ambiance", "ambiguity", "ambiguous", "ambition", "ambitious", "ambulance", "ambush", "amendable", "amendment", "amends", "amenity", "amiable", "amicably", "amid", "amigo", "amino", "amiss", "ammonia", "ammonium", "amnesty", "amniotic", "among", "amount", "amperage", "ample", "amplifier", "amplify", "amply", "amuck", "amulet", "amusable", "amused", "amusement", "amuser", "amusing", "anaconda", "anaerobic", "anagram", "anatomist", "anatomy", "anchor", "anchovy", "ancient", "android", "anemia", "anemic", "aneurism", "anew", "angelfish", "angelic", "anger", "angled", "angler", "angles", "angling", "angrily", "angriness", "anguished", "angular", "animal", "animate", "animating", "animation", "animator", "anime", "animosity", "ankle", "annex", "annotate", "announcer", "annoying", "annually", "annuity", "anointer", "another", "answering", "antacid", "antarctic", "anteater", "antelope", "antennae", "anthem", "anthill", "anthology", "antibody", "antics", "antidote", "antihero", "antiquely", "antiques", "antiquity", "antirust", "antitoxic", "antitrust", "antiviral", "antivirus", "antler", "antonym", "antsy", "anvil", "anybody", "anyhow", "anymore", "anyone", "anyplace", "anything", "anytime", "anyway", "anywhere", "aorta", "apache", "apostle", "appealing", "appear", "appease", "appeasing", "appendage", "appendix", "appetite", "appetizer", "applaud", "applause", "apple", "appliance", "applicant", "applied", "apply", "appointee", "appraisal", "appraiser", "apprehend", "approach", "approval", "approve", "apricot", "april", "apron", "aptitude", "aptly", "aqua", "aqueduct", "arbitrary", "arbitrate", "ardently", "area", "arena", "arguable", "arguably", "argue", "arise", "armadillo", "armband", "armchair", "armed", "armful", "armhole", "arming", "armless", "armoire", "armored", "armory", "armrest", "army", "aroma", "arose", "around", "arousal", "arrange", "array", "arrest", "arrival", "arrive", "arrogance", "arrogant", "arson", "art", "ascend", "ascension", "ascent", "ascertain", "ashamed", "ashen", "ashes", "ashy", "aside", "askew", "asleep", "asparagus", "aspect", "aspirate", "aspire", "aspirin", "astonish", "astound", "astride", "astrology", "astronaut", "astronomy", "astute", "atlantic", "atlas", "atom", "atonable", "atop", "atrium", "atrocious", "atrophy", "attach", "attain", "attempt", "attendant", "attendee", "attention", "attentive", "attest", "attic", "attire", "attitude", "attractor", "attribute", "atypical", "auction", "audacious", "audacity", "audible", "audibly", "audience", "audio", "audition", "augmented", "august", "authentic", "author", "autism", "autistic", "autograph", "automaker", "automated", "automatic", "autopilot", "available", "avalanche", "avatar", "avenge", "avenging", "avenue", "average", "aversion", "avert", "aviation", "aviator", "avid", "avoid", "await", "awaken", "award", "aware", "awhile", "awkward", "awning", "awoke", "awry", "axis", "babble", "babbling", "babied", "baboon", "backache", "backboard", "backboned", "backdrop", "backed", "backer", "backfield", "backfire", "backhand", "backing", "backlands", "backlash", "backless", "backlight", "backlit", "backlog", "backpack", "backpedal", "backrest", "backroom", "backshift", "backside", "backslid", "backspace", "backspin", "backstab", "backstage", "backtalk", "backtrack", "backup", "backward", "backwash", "backwater", "backyard", "bacon", "bacteria", "bacterium", "badass", "badge", "badland", "badly", "badness", "baffle", "baffling", "bagel", "bagful", "baggage", "bagged", "baggie", "bagginess", "bagging", "baggy", "bagpipe", "baguette", "baked", "bakery", "bakeshop", "baking", "balance", "balancing", "balcony", "balmy", "balsamic", "bamboo", "banana", "banish", "banister", "banjo", "bankable", "bankbook", "banked", "banker", "banking", "banknote", "bankroll", "banner", "bannister", "banshee", "banter", "barbecue", "barbed", "barbell", "barber", "barcode", "barge", "bargraph", "barista", "baritone", "barley", "barmaid", "barman", "barn", "barometer", "barrack", "barracuda", "barrel", "barrette", "barricade", "barrier", "barstool", "bartender", "barterer", "bash", "basically", "basics", "basil", "basin", "basis", "basket", "batboy", "batch", "bath", "baton", "bats", "battalion", "battered", "battering", "battery", "batting", "battle", "bauble", "bazooka", "blabber", "bladder", "blade", "blah", "blame", "blaming", "blanching", "blandness", "blank", "blaspheme", "blasphemy", "blast", "blatancy", "blatantly", "blazer", "blazing", "bleach", "bleak", "bleep", "blemish", "blend", "bless", "blighted", "blimp", "bling", "blinked", "blinker", "blinking", "blinks", "blip", "blissful", "blitz", "blizzard", "bloated", "bloating", "blob", "blog", "bloomers", "blooming", "blooper", "blot", "blouse", "blubber", "bluff", "bluish", "blunderer", "blunt", "blurb", "blurred", "blurry", "blurt", "blush", "blustery", "boaster", "boastful", "boasting", "boat", "bobbed", "bobbing", "bobble", "bobcat", "bobsled", "bobtail", "bodacious", "body", "bogged", "boggle", "bogus", "boil", "bok", "bolster", "bolt", "bonanza", "bonded", "bonding", "bondless", "boned", "bonehead", "boneless", "bonelike", "boney", "bonfire", "bonnet", "bonsai", "bonus", "bony", "boogeyman", "boogieman", "book", "boondocks", "booted", "booth", "bootie", "booting", "bootlace", "bootleg", "boots", "boozy", "borax", "boring", "borough", "borrower", "borrowing", "boss", "botanical", "botanist", "botany", "botch", "both", "bottle", "bottling", "bottom", "bounce", "bouncing", "bouncy", "bounding", "boundless", "bountiful", "bovine", "boxcar", "boxer", "boxing", "boxlike", "boxy", "breach", "breath", "breeches", "breeching", "breeder", "breeding", "breeze", "breezy", "brethren", "brewery", "brewing", "briar", "bribe", "brick", "bride", "bridged", "brigade", "bright", "brilliant", "brim", "bring", "brink", "brisket", "briskly", "briskness", "bristle", "brittle", "broadband", "broadcast", "broaden", "broadly", "broadness", "broadside", "broadways", "broiler", "broiling", "broken", "broker", "bronchial", "bronco", "bronze", "bronzing", "brook", "broom", "brought", "browbeat", "brownnose", "browse", "browsing", "bruising", "brunch", "brunette", "brunt", "brush", "brussels", "brute", "brutishly", "bubble", "bubbling", "bubbly", "buccaneer", "bucked", "bucket", "buckle", "buckshot", "buckskin", "bucktooth", "buckwheat", "buddhism", "buddhist", "budding", "buddy", "budget", "buffalo", "buffed", "buffer", "buffing", "buffoon", "buggy", "bulb", "bulge", "bulginess", "bulgur", "bulk", "bulldog", "bulldozer", "bullfight", "bullfrog", "bullhorn", "bullion", "bullish", "bullpen", "bullring", "bullseye", "bullwhip", "bully", "bunch", "bundle", "bungee", "bunion", "bunkbed", "bunkhouse", "bunkmate", "bunny", "bunt", "busboy", "bush", "busily", "busload", "bust", "busybody", "buzz", "cabana", "cabbage", "cabbie", "cabdriver", "cable", "caboose", "cache", "cackle", "cacti", "cactus", "caddie", "caddy", "cadet", "cadillac", "cadmium", "cage", "cahoots", "cake", "calamari", "calamity", "calcium", "calculate", "calculus", "caliber", "calibrate", "calm", "caloric", "calorie", "calzone", "camcorder", "cameo", "camera", "camisole", "camper", "campfire", "camping", "campsite", "campus", "canal", "canary", "cancel", "candied", "candle", "candy", "cane", "canine", "canister", "cannabis", "canned", "canning", "cannon", "cannot", "canola", "canon", "canopener", "canopy", "canteen", "canyon", "capable", "capably", "capacity", "cape", "capillary", "capital", "capitol", "capped", "capricorn", "capsize", "capsule", "caption", "captivate", "captive", "captivity", "capture", "caramel", "carat", "caravan", "carbon", "cardboard", "carded", "cardiac", "cardigan", "cardinal", "cardstock", "carefully", "caregiver", "careless", "caress", "caretaker", "cargo", "caring", "carless", "carload", "carmaker", "carnage", "carnation", "carnival", "carnivore", "carol", "carpenter", "carpentry", "carpool", "carport", "carried", "carrot", "carrousel", "carry", "cartel", "cartload", "carton", "cartoon", "cartridge", "cartwheel", "carve", "carving", "carwash", "cascade", "case", "cash", "casing", "casino", "casket", "cassette", "casually", "casualty", "catacomb", "catalog", "catalyst", "catalyze", "catapult", "cataract", "catatonic", "catcall", "catchable", "catcher", "catching", "catchy", "caterer", "catering", "catfight", "catfish", "cathedral", "cathouse", "catlike", "catnap", "catnip", "catsup", "cattail", "cattishly", "cattle", "catty", "catwalk", "caucasian", "caucus", "causal", "causation", "cause", "causing", "cauterize", "caution", "cautious", "cavalier", "cavalry", "caviar", "cavity", "cedar", "celery", "celestial", "celibacy", "celibate", "celtic", "cement", "census", "ceramics", "ceremony", "certainly", "certainty", "certified", "certify", "cesarean", "cesspool", "chafe", "chaffing", "chain", "chair", "chalice", "challenge", "chamber", "chamomile", "champion", "chance", "change", "channel", "chant", "chaos", "chaperone", "chaplain", "chapped", "chaps", "chapter", "character", "charbroil", "charcoal", "charger", "charging", "chariot", "charity", "charm", "charred", "charter", "charting", "chase", "chasing", "chaste", "chastise", "chastity", "chatroom", "chatter", "chatting", "chatty", "cheating", "cheddar", "cheek", "cheer", "cheese", "cheesy", "chef", "chemicals", "chemist", "chemo", "cherisher", "cherub", "chess", "chest", "chevron", "chevy", "chewable", "chewer", "chewing", "chewy", "chief", "chihuahua", "childcare", "childhood", "childish", "childless", "childlike", "chili", "chill", "chimp", "chip", "chirping", "chirpy", "chitchat", "chivalry", "chive", "chloride", "chlorine", "choice", "chokehold", "choking", "chomp", "chooser", "choosing", "choosy", "chop", "chosen", "chowder", "chowtime", "chrome", "chubby", "chuck", "chug", "chummy", "chump", "chunk", "churn", "chute", "cider", "cilantro", "cinch", "cinema", "cinnamon", "circle", "circling", "circular", "circulate", "circus", "citable", "citadel", "citation", "citizen", "citric", "citrus", "city", "civic", "civil", "clad", "claim", "clambake", "clammy", "clamor", "clamp", "clamshell", "clang", "clanking", "clapped", "clapper", "clapping", "clarify", "clarinet", "clarity", "clash", "clasp", "class", "clatter", "clause", "clavicle", "claw", "clay", "clean", "clear", "cleat", "cleaver", "cleft", "clench", "clergyman", "clerical", "clerk", "clever", "clicker", "client", "climate", "climatic", "cling", "clinic", "clinking", "clip", "clique", "cloak", "clobber", "clock", "clone", "cloning", "closable", "closure", "clothes", "clothing", "cloud", "clover", "clubbed", "clubbing", "clubhouse", "clump", "clumsily", "clumsy", "clunky", "clustered", "clutch", "clutter", "coach", "coagulant", "coastal", "coaster", "coasting", "coastland", "coastline", "coat", "coauthor", "cobalt", "cobbler", "cobweb", "cocoa", "coconut", "cod", "coeditor", "coerce", "coexist", "coffee", "cofounder", "cognition", "cognitive", "cogwheel", "coherence", "coherent", "cohesive", "coil", "coke", "cola", "cold", "coleslaw", "coliseum", "collage", "collapse", "collar", "collected", "collector", "collide", "collie", "collision", "colonial", "colonist", "colonize", "colony", "colossal", "colt", "coma", "come", "comfort", "comfy", "comic", "coming", "comma", "commence", "commend", "comment", "commerce", "commode", "commodity", "commodore", "common", "commotion", "commute", "commuting", "compacted", "compacter", "compactly", "compactor", "companion", "company", "compare", "compel", "compile", "comply", "component", "composed", "composer", "composite", "compost", "composure", "compound", "compress", "comprised", "computer", "computing", "comrade", "concave", "conceal", "conceded", "concept", "concerned", "concert", "conch", "concierge", "concise", "conclude", "concrete", "concur", "condense", "condiment", "condition", "condone", "conducive", "conductor", "conduit", "cone", "confess", "confetti", "confidant", "confident", "confider", "confiding", "configure", "confined", "confining", "confirm", "conflict", "conform", "confound", "confront", "confused", "confusing", "confusion", "congenial", "congested", "congrats", "congress", "conical", "conjoined", "conjure", "conjuror", "connected", "connector", "consensus", "consent", "console", "consoling", "consonant", "constable", "constant", "constrain", "constrict", "construct", "consult", "consumer", "consuming", "contact", "container", "contempt", "contend", "contented", "contently", "contents", "contest", "context", "contort", "contour", "contrite", "control", "contusion", "convene", "convent", "copartner", "cope", "copied", "copier", "copilot", "coping", "copious", "copper", "copy", "coral", "cork", "cornball", "cornbread", "corncob", "cornea", "corned", "corner", "cornfield", "cornflake", "cornhusk", "cornmeal", "cornstalk", "corny", "coronary", "coroner", "corporal", "corporate", "corral", "correct", "corridor", "corrode", "corroding", "corrosive", "corsage", "corset", "cortex", "cosigner", "cosmetics", "cosmic", "cosmos", "cosponsor", "cost", "cottage", "cotton", "couch", "cough", "could", "countable", "countdown", "counting", "countless", "country", "county", "courier", "covenant", "cover", "coveted", "coveting", "coyness", "cozily", "coziness", "cozy", "crabbing", "crabgrass", "crablike", "crabmeat", "cradle", "cradling", "crafter", "craftily", "craftsman", "craftwork", "crafty", "cramp", "cranberry", "crane", "cranial", "cranium", "crank", "crate", "crave", "craving", "crawfish", "crawlers", "crawling", "crayfish", "crayon", "crazed", "crazily", "craziness", "crazy", "creamed", "creamer", "creamlike", "crease", "creasing", "creatable", "create", "creation", "creative", "creature", "credible", "credibly", "credit", "creed", "creme", "creole", "crepe", "crept", "crescent", "crested", "cresting", "crestless", "crevice", "crewless", "crewman", "crewmate", "crib", "cricket", "cried", "crier", "crimp", "crimson", "cringe", "cringing", "crinkle", "crinkly", "crisped", "crisping", "crisply", "crispness", "crispy", "criteria", "critter", "croak", "crock", "crook", "croon", "crop", "cross", "crouch", "crouton", "crowbar", "crowd", "crown", "crucial", "crudely", "crudeness", "cruelly", "cruelness", "cruelty", "crumb", "crummiest", "crummy", "crumpet", "crumpled", "cruncher", "crunching", "crunchy", "crusader", "crushable", "crushed", "crusher", "crushing", "crust", "crux", "crying", "cryptic", "crystal", "cubbyhole", "cube", "cubical", "cubicle", "cucumber", "cuddle", "cuddly", "cufflink", "culinary", "culminate", "culpable", "culprit", "cultivate", "cultural", "culture", "cupbearer", "cupcake", "cupid", "cupped", "cupping", "curable", "curator", "curdle", "cure", "curfew", "curing", "curled", "curler", "curliness", "curling", "curly", "curry", "curse", "cursive", "cursor", "curtain", "curtly", "curtsy", "curvature", "curve", "curvy", "cushy", "cusp", "cussed", "custard", "custodian", "custody", "customary", "customer", "customize", "customs", "cut", "cycle", "cyclic", "cycling", "cyclist", "cylinder", "cymbal", "cytoplasm", "cytoplast", "dab", "dad", "daffodil", "dagger", "daily", "daintily", "dainty", "dairy", "daisy", "dallying", "dance", "dancing", "dandelion", "dander", "dandruff", "dandy", "danger", "dangle", "dangling", "daredevil", "dares", "daringly", "darkened", "darkening", "darkish", "darkness", "darkroom", "darling", "darn", "dart", "darwinism", "dash", "dastardly", "data", "datebook", "dating", "daughter", "daunting", "dawdler", "dawn", "daybed", "daybreak", "daycare", "daydream", "daylight", "daylong", "dayroom", "daytime", "dazzler", "dazzling", "deacon", "deafening", "deafness", "dealer", "dealing", "dealmaker", "dealt", "dean", "debatable", "debate", "debating", "debit", "debrief", "debtless", "debtor", "debug", "debunk", "decade", "decaf", "decal", "decathlon", "decay", "deceased", "deceit", "deceiver", "deceiving", "december", "decency", "decent", "deception", "deceptive", "decibel", "decidable", "decimal", "decimeter", "decipher", "deck", "declared", "decline", "decode", "decompose", "decorated", "decorator", "decoy", "decrease", "decree", "dedicate", "dedicator", "deduce", "deduct", "deed", "deem", "deepen", "deeply", "deepness", "deface", "defacing", "defame", "default", "defeat", "defection", "defective", "defendant", "defender", "defense", "defensive", "deferral", "deferred", "defiance", "defiant", "defile", "defiling", "define", "definite", "deflate", "deflation", "deflator", "deflected", "deflector", "defog", "deforest", "defraud", "defrost", "deftly", "defuse", "defy", "degraded", "degrading", "degrease", "degree", "dehydrate", "deity", "dejected", "delay", "delegate", "delegator", "delete", "deletion", "delicacy", "delicate", "delicious", "delighted", "delirious", "delirium", "deliverer", "delivery", "delouse", "delta", "deluge", "delusion", "deluxe", "demanding", "demeaning", "demeanor", "demise", "democracy", "democrat", "demote", "demotion", "demystify", "denatured", "deniable", "denial", "denim", "denote", "dense", "density", "dental", "dentist", "denture", "deny", "deodorant", "deodorize", "departed", "departure", "depict", "deplete", "depletion", "deplored", "deploy", "deport", "depose", "depraved", "depravity", "deprecate", "depress", "deprive", "depth", "deputize", "deputy", "derail", "deranged", "derby", "derived", "desecrate", "deserve", "deserving", "designate", "designed", "designer", "designing", "deskbound", "desktop", "deskwork", "desolate", "despair", "despise", "despite", "destiny", "destitute", "destruct", "detached", "detail", "detection", "detective", "detector", "detention", "detergent", "detest", "detonate", "detonator", "detoxify", "detract", "deuce", "devalue", "deviancy", "deviant", "deviate", "deviation", "deviator", "device", "devious", "devotedly", "devotee", "devotion", "devourer", "devouring", "devoutly", "dexterity", "dexterous", "diabetes", "diabetic", "diabolic", "diagnoses", "diagnosis", "diagram", "dial", "diameter", "diaper", "diaphragm", "diary", "dice", "dicing", "dictate", "dictation", "dictator", "difficult", "diffused", "diffuser", "diffusion", "diffusive", "dig", "dilation", "diligence", "diligent", "dill", "dilute", "dime", "diminish", "dimly", "dimmed", "dimmer", "dimness", "dimple", "diner", "dingbat", "dinghy", "dinginess", "dingo", "dingy", "dining", "dinner", "diocese", "dioxide", "diploma", "dipped", "dipper", "dipping", "directed", "direction", "directive", "directly", "directory", "direness", "dirtiness", "disabled", "disagree", "disallow", "disarm", "disarray", "disaster", "disband", "disbelief", "disburse", "discard", "discern", "discharge", "disclose", "discolor", "discount", "discourse", "discover", "discuss", "disdain", "disengage", "disfigure", "disgrace", "dish", "disinfect", "disjoin", "disk", "dislike", "disliking", "dislocate", "dislodge", "disloyal", "dismantle", "dismay", "dismiss", "dismount", "disobey", "disorder", "disown", "disparate", "disparity", "dispatch", "dispense", "dispersal", "dispersed", "disperser", "displace", "display", "displease", "disposal", "dispose", "disprove", "dispute", "disregard", "disrupt", "dissuade", "distance", "distant", "distaste", "distill", "distinct", "distort", "distract", "distress", "district", "distrust", "ditch", "ditto", "ditzy", "dividable", "divided", "dividend", "dividers", "dividing", "divinely", "diving", "divinity", "divisible", "divisibly", "division", "divisive", "divorcee", "dizziness", "dizzy", "doable", "docile", "dock", "doctrine", "document", "dodge", "dodgy", "doily", "doing", "dole", "dollar", "dollhouse", "dollop", "dolly", "dolphin", "domain", "domelike", "domestic", "dominion", "dominoes", "donated", "donation", "donator", "donor", "donut", "doodle", "doorbell", "doorframe", "doorknob", "doorman", "doormat", "doornail", "doorpost", "doorstep", "doorstop", "doorway", "doozy", "dork", "dormitory", "dorsal", "dosage", "dose", "dotted", "doubling", "douche", "dove", "down", "dowry", "doze", "drab", "dragging", "dragonfly", "dragonish", "dragster", "drainable", "drainage", "drained", "drainer", "drainpipe", "dramatic", "dramatize", "drank", "drapery", "drastic", "draw", "dreaded", "dreadful", "dreadlock", "dreamboat", "dreamily", "dreamland", "dreamless", "dreamlike", "dreamt", "dreamy", "drearily", "dreary", "drench", "dress", "drew", "dribble", "dried", "drier", "drift", "driller", "drilling", "drinkable", "drinking", "dripping", "drippy", "drivable", "driven", "driver", "driveway", "driving", "drizzle", "drizzly", "drone", "drool", "droop", "drop-down", "dropbox", "dropkick", "droplet", "dropout", "dropper", "drove", "drown", "drowsily", "drudge", "drum", "dry", "dubbed", "dubiously", "duchess", "duckbill", "ducking", "duckling", "ducktail", "ducky", "duct", "dude", "duffel", "dugout", "duh", "duke", "duller", "dullness", "duly", "dumping", "dumpling", "dumpster", "duo", "dupe", "duplex", "duplicate", "duplicity", "durable", "durably", "duration", "duress", "during", "dusk", "dust", "dutiful", "duty", "duvet", "dwarf", "dweeb", "dwelled", "dweller", "dwelling", "dwindle", "dwindling", "dynamic", "dynamite", "dynasty", "dyslexia", "dyslexic", "each", "eagle", "earache", "eardrum", "earflap", "earful", "earlobe", "early", "earmark", "earmuff", "earphone", "earpiece", "earplugs", "earring", "earshot", "earthen", "earthlike", "earthling", "earthly", "earthworm", "earthy", "earwig", "easeful", "easel", "easiest", "easily", "easiness", "easing", "eastbound", "eastcoast", "easter", "eastward", "eatable", "eaten", "eatery", "eating", "eats", "ebay", "ebony", "ebook", "ecard", "eccentric", "echo", "eclair", "eclipse", "ecologist", "ecology", "economic", "economist", "economy", "ecosphere", "ecosystem", "edge", "edginess", "edging", "edgy", "edition", "editor", "educated", "education", "educator", "eel", "effective", "effects", "efficient", "effort", "eggbeater", "egging", "eggnog", "eggplant", "eggshell", "egomaniac", "egotism", "egotistic", "either", "eject", "elaborate", "elastic", "elated", "elbow", "eldercare", "elderly", "eldest", "electable", "election", "elective", "elephant", "elevate", "elevating", "elevation", "elevator", "eleven", "elf", "eligible", "eligibly", "eliminate", "elite", "elitism", "elixir", "elk", "ellipse", "elliptic", "elm", "elongated", "elope", "eloquence", "eloquent", "elsewhere", "elude", "elusive", "elves", "email", "embargo", "embark", "embassy", "embattled", "embellish", "ember", "embezzle", "emblaze", "emblem", "embody", "embolism", "emboss", "embroider", "emcee", "emerald", "emergency", "emission", "emit", "emote", "emoticon", "emotion", "empathic", "empathy", "emperor", "emphases", "emphasis", "emphasize", "emphatic", "empirical", "employed", "employee", "employer", "emporium", "empower", "emptier", "emptiness", "empty", "emu", "enable", "enactment", "enamel", "enchanted", "enchilada", "encircle", "enclose", "enclosure", "encode", "encore", "encounter", "encourage", "encroach", "encrust", "encrypt", "endanger", "endeared", "endearing", "ended", "ending", "endless", "endnote", "endocrine", "endorphin", "endorse", "endowment", "endpoint", "endurable", "endurance", "enduring", "energetic", "energize", "energy", "enforced", "enforcer", "engaged", "engaging", "engine", "engorge", "engraved", "engraver", "engraving", "engross", "engulf", "enhance", "enigmatic", "enjoyable", "enjoyably", "enjoyer", "enjoying", "enjoyment", "enlarged", "enlarging", "enlighten", "enlisted", "enquirer", "enrage", "enrich", "enroll", "enslave", "ensnare", "ensure", "entail", "entangled", "entering", "entertain", "enticing", "entire", "entitle", "entity", "entomb", "entourage", "entrap", "entree", "entrench", "entrust", "entryway", "entwine", "enunciate", "envelope", "enviable", "enviably", "envious", "envision", "envoy", "envy", "enzyme", "epic", "epidemic", "epidermal", "epidermis", "epidural", "epilepsy", "epileptic", "epilogue", "epiphany", "episode", "equal", "equate", "equation", "equator", "equinox", "equipment", "equity", "equivocal", "eradicate", "erasable", "erased", "eraser", "erasure", "ergonomic", "errand", "errant", "erratic", "error", "erupt", "escalate", "escalator", "escapable", "escapade", "escapist", "escargot", "eskimo", "esophagus", "espionage", "espresso", "esquire", "essay", "essence", "essential", "establish", "estate", "esteemed", "estimate", "estimator", "estranged", "estrogen", "etching", "eternal", "eternity", "ethanol", "ether", "ethically", "ethics", "euphemism", "evacuate", "evacuee", "evade", "evaluate", "evaluator", "evaporate", "evasion", "evasive", "even", "everglade", "evergreen", "everybody", "everyday", "everyone", "evict", "evidence", "evident", "evil", "evoke", "evolution", "evolve", "exact", "exalted", "example", "excavate", "excavator", "exceeding", "exception", "excess", "exchange", "excitable", "exciting", "exclaim", "exclude", "excluding", "exclusion", "exclusive", "excretion", "excretory", "excursion", "excusable", "excusably", "excuse", "exemplary", "exemplify", "exemption", "exerciser", "exert", "exes", "exfoliate", "exhale", "exhaust", "exhume", "exile", "existing", "exit", "exodus", "exonerate", "exorcism", "exorcist", "expand", "expanse", "expansion", "expansive", "expectant", "expedited", "expediter", "expel", "expend", "expenses", "expensive", "expert", "expire", "expiring", "explain", "expletive", "explicit", "explode", "exploit", "explore", "exploring", "exponent", "exporter", "exposable", "expose", "exposure", "express", "expulsion", "exquisite", "extended", "extending", "extent", "extenuate", "exterior", "external", "extinct", "extortion", "extradite", "extras", "extrovert", "extrude", "extruding", "exuberant", "fable", "fabric", "fabulous", "facebook", "facecloth", "facedown", "faceless", "facelift", "faceplate", "faceted", "facial", "facility", "facing", "facsimile", "faction", "factoid", "factor", "factsheet", "factual", "faculty", "fade", "fading", "failing", "falcon", "fall", "false", "falsify", "fame", "familiar", "family", "famine", "famished", "fanatic", "fancied", "fanciness", "fancy", "fanfare", "fang", "fanning", "fantasize", "fantastic", "fantasy", "fascism", "fastball", "faster", "fasting", "fastness", "faucet", "favorable", "favorably", "favored", "favoring", "favorite", "fax", "feast", "federal", "fedora", "feeble", "feed", "feel", "feisty", "feline", "felt-tip", "feminine", "feminism", "feminist", "feminize", "femur", "fence", "fencing", "fender", "ferment", "fernlike", "ferocious", "ferocity", "ferret", "ferris", "ferry", "fervor", "fester", "festival", "festive", "festivity", "fetal", "fetch", "fever", "fiber", "fiction", "fiddle", "fiddling", "fidelity", "fidgeting", "fidgety", "fifteen", "fifth", "fiftieth", "fifty", "figment", "figure", "figurine", "filing", "filled", "filler", "filling", "film", "filter", "filth", "filtrate", "finale", "finalist", "finalize", "finally", "finance", "financial", "finch", "fineness", "finer", "finicky", "finished", "finisher", "finishing", "finite", "finless", "finlike", "fiscally", "fit", "five", "flaccid", "flagman", "flagpole", "flagship", "flagstick", "flagstone", "flail", "flakily", "flaky", "flame", "flammable", "flanked", "flanking", "flannels", "flap", "flaring", "flashback", "flashbulb", "flashcard", "flashily", "flashing", "flashy", "flask", "flatbed", "flatfoot", "flatly", "flatness", "flatten", "flattered", "flatterer", "flattery", "flattop", "flatware", "flatworm", "flavored", "flavorful", "flavoring", "flaxseed", "fled", "fleshed", "fleshy", "flick", "flier", "flight", "flinch", "fling", "flint", "flip", "flirt", "float", "flock", "flogging", "flop", "floral", "florist", "floss", "flounder", "flyable", "flyaway", "flyer", "flying", "flyover", "flypaper", "foam", "foe", "fog", "foil", "folic", "folk", "follicle", "follow", "fondling", "fondly", "fondness", "fondue", "font", "food", "fool", "footage", "football", "footbath", "footboard", "footer", "footgear", "foothill", "foothold", "footing", "footless", "footman", "footnote", "footpad", "footpath", "footprint", "footrest", "footsie", "footsore", "footwear", "footwork", "fossil", "foster", "founder", "founding", "fountain", "fox", "foyer", "fraction", "fracture", "fragile", "fragility", "fragment", "fragrance", "fragrant", "frail", "frame", "framing", "frantic", "fraternal", "frayed", "fraying", "frays", "freckled", "freckles", "freebase", "freebee", "freebie", "freedom", "freefall", "freehand", "freeing", "freeload", "freely", "freemason", "freeness", "freestyle", "freeware", "freeway", "freewill", "freezable", "freezing", "freight", "french", "frenzied", "frenzy", "frequency", "frequent", "fresh", "fretful", "fretted", "friction", "friday", "fridge", "fried", "friend", "frighten", "frightful", "frigidity", "frigidly", "frill", "fringe", "frisbee", "frisk", "fritter", "frivolous", "frolic", "from", "front", "frostbite", "frosted", "frostily", "frosting", "frostlike", "frosty", "froth", "frown", "frozen", "fructose", "frugality", "frugally", "fruit", "frustrate", "frying", "gab", "gaffe", "gag", "gainfully", "gaining", "gains", "gala", "gallantly", "galleria", "gallery", "galley", "gallon", "gallows", "gallstone", "galore", "galvanize", "gambling", "game", "gaming", "gamma", "gander", "gangly", "gangrene", "gangway", "gap", "garage", "garbage", "garden", "gargle", "garland", "garlic", "garment", "garnet", "garnish", "garter", "gas", "gatherer", "gathering", "gating", "gauging", "gauntlet", "gauze", "gave", "gawk", "gazing", "gear", "gecko", "geek", "geiger", "gem", "gender", "generic", "generous", "genetics", "genre", "gentile", "gentleman", "gently", "gents", "geography", "geologic", "geologist", "geology", "geometric", "geometry", "geranium", "gerbil", "geriatric", "germicide", "germinate", "germless", "germproof", "gestate", "gestation", "gesture", "getaway", "getting", "getup", "giant", "gibberish", "giblet", "giddily", "giddiness", "giddy", "gift", "gigabyte", "gigahertz", "gigantic", "giggle", "giggling", "giggly", "gigolo", "gilled", "gills", "gimmick", "girdle", "giveaway", "given", "giver", "giving", "gizmo", "gizzard", "glacial", "glacier", "glade", "gladiator", "gladly", "glamorous", "glamour", "glance", "glancing", "glandular", "glare", "glaring", "glass", "glaucoma", "glazing", "gleaming", "gleeful", "glider", "gliding", "glimmer", "glimpse", "glisten", "glitch", "glitter", "glitzy", "gloater", "gloating", "gloomily", "gloomy", "glorified", "glorifier", "glorify", "glorious", "glory", "gloss", "glove", "glowing", "glowworm", "glucose", "glue", "gluten", "glutinous", "glutton", "gnarly", "gnat", "goal", "goatskin", "goes", "goggles", "going", "goldfish", "goldmine", "goldsmith", "golf", "goliath", "gonad", "gondola", "gone", "gong", "good", "gooey", "goofball", "goofiness", "goofy", "google", "goon", "gopher", "gore", "gorged", "gorgeous", "gory", "gosling", "gossip", "gothic", "gotten", "gout", "gown", "grab", "graceful", "graceless", "gracious", "gradation", "graded", "grader", "gradient", "grading", "gradually", "graduate", "graffiti", "grafted", "grafting", "grain", "granddad", "grandkid", "grandly", "grandma", "grandpa", "grandson", "granite", "granny", "granola", "grant", "granular", "grape", "graph", "grapple", "grappling", "grasp", "grass", "gratified", "gratify", "grating", "gratitude", "gratuity", "gravel", "graveness", "graves", "graveyard", "gravitate", "gravity", "gravy", "gray", "grazing", "greasily", "greedily", "greedless", "greedy", "green", "greeter", "greeting", "grew", "greyhound", "grid", "grief", "grievance", "grieving", "grievous", "grill", "grimace", "grimacing", "grime", "griminess", "grimy", "grinch", "grinning", "grip", "gristle", "grit", "groggily", "groggy", "groin", "groom", "groove", "grooving", "groovy", "grope", "ground", "grouped", "grout", "grove", "grower", "growing", "growl", "grub", "grudge", "grudging", "grueling", "gruffly", "grumble", "grumbling", "grumbly", "grumpily", "grunge", "grunt", "guacamole", "guidable", "guidance", "guide", "guiding", "guileless", "guise", "gulf", "gullible", "gully", "gulp", "gumball", "gumdrop", "gumminess", "gumming", "gummy", "gurgle", "gurgling", "guru", "gush", "gusto", "gusty", "gutless", "guts", "gutter", "guy", "guzzler", "gyration", "habitable", "habitant", "habitat", "habitual", "hacked", "hacker", "hacking", "hacksaw", "had", "haggler", "haiku", "half", "halogen", "halt", "halved", "halves", "hamburger", "hamlet", "hammock", "hamper", "hamster", "hamstring", "handbag", "handball", "handbook", "handbrake", "handcart", "handclap", "handclasp", "handcraft", "handcuff", "handed", "handful", "handgrip", "handgun", "handheld", "handiness", "handiwork", "handlebar", "handled", "handler", "handling", "handmade", "handoff", "handpick", "handprint", "handrail", "handsaw", "handset", "handsfree", "handshake", "handstand", "handwash", "handwork", "handwoven", "handwrite", "handyman", "hangnail", "hangout", "hangover", "hangup", "hankering", "hankie", "hanky", "haphazard", "happening", "happier", "happiest", "happily", "happiness", "happy", "harbor", "hardcopy", "hardcore", "hardcover", "harddisk", "hardened", "hardener", "hardening", "hardhat", "hardhead", "hardiness", "hardly", "hardness", "hardship", "hardware", "hardwired", "hardwood", "hardy", "harmful", "harmless", "harmonica", "harmonics", "harmonize", "harmony", "harness", "harpist", "harsh", "harvest", "hash", "hassle", "haste", "hastily", "hastiness", "hasty", "hatbox", "hatchback", "hatchery", "hatchet", "hatching", "hatchling", "hate", "hatless", "hatred", "haunt", "haven", "hazard", "hazelnut", "hazily", "haziness", "hazing", "hazy", "headache", "headband", "headboard", "headcount", "headdress", "headed", "header", "headfirst", "headgear", "heading", "headlamp", "headless", "headlock", "headphone", "headpiece", "headrest", "headroom", "headscarf", "headset", "headsman", "headstand", "headstone", "headway", "headwear", "heap", "heat", "heave", "heavily", "heaviness", "heaving", "hedge", "hedging", "heftiness", "hefty", "helium", "helmet", "helper", "helpful", "helping", "helpless", "helpline", "hemlock", "hemstitch", "hence", "henchman", "henna", "herald", "herbal", "herbicide", "herbs", "heritage", "hermit", "heroics", "heroism", "herring", "herself", "hertz", "hesitancy", "hesitant", "hesitate", "hexagon", "hexagram", "hubcap", "huddle", "huddling", "huff", "hug", "hula", "hulk", "hull", "human", "humble", "humbling", "humbly", "humid", "humiliate", "humility", "humming", "hummus", "humongous", "humorist", "humorless", "humorous", "humpback", "humped", "humvee", "hunchback", "hundredth", "hunger", "hungrily", "hungry", "hunk", "hunter", "hunting", "huntress", "huntsman", "hurdle", "hurled", "hurler", "hurling", "hurray", "hurricane", "hurried", "hurry", "hurt", "husband", "hush", "husked", "huskiness", "hut", "hybrid", "hydrant", "hydrated", "hydration", "hydrogen", "hydroxide", "hyperlink", "hypertext", "hyphen", "hypnoses", "hypnosis", "hypnotic", "hypnotism", "hypnotist", "hypnotize", "hypocrisy", "hypocrite", "ibuprofen", "ice", "iciness", "icing", "icky", "icon", "icy", "idealism", "idealist", "idealize", "ideally", "idealness", "identical", "identify", "identity", "ideology", "idiocy", "idiom", "idly", "igloo", "ignition", "ignore", "iguana", "illicitly", "illusion", "illusive", "image", "imaginary", "imagines", "imaging", "imbecile", "imitate", "imitation", "immature", "immerse", "immersion", "imminent", "immobile", "immodest", "immorally", "immortal", "immovable", "immovably", "immunity", "immunize", "impaired", "impale", "impart", "impatient", "impeach", "impeding", "impending", "imperfect", "imperial", "impish", "implant", "implement", "implicate", "implicit", "implode", "implosion", "implosive", "imply", "impolite", "important", "importer", "impose", "imposing", "impotence", "impotency", "impotent", "impound", "imprecise", "imprint", "imprison", "impromptu", "improper", "improve", "improving", "improvise", "imprudent", "impulse", "impulsive", "impure", "impurity", "iodine", "iodize", "ion", "ipad", "iphone", "ipod", "irate", "irk", "iron", "irregular", "irrigate", "irritable", "irritably", "irritant", "irritate", "islamic", "islamist", "isolated", "isolating", "isolation", "isotope", "issue", "issuing", "italicize", "italics", "item", "itinerary", "itunes", "ivory", "ivy", "jab", "jackal", "jacket", "jackknife", "jackpot", "jailbird", "jailbreak", "jailer", "jailhouse", "jalapeno", "jam", "janitor", "january", "jargon", "jarring", "jasmine", "jaundice", "jaunt", "java", "jawed", "jawless", "jawline", "jaws", "jaybird", "jaywalker", "jazz", "jeep", "jeeringly", "jellied", "jelly", "jersey", "jester", "jet", "jiffy", "jigsaw", "jimmy", "jingle", "jingling", "jinx", "jitters", "jittery", "job", "jockey", "jockstrap", "jogger", "jogging", "john", "joining", "jokester", "jokingly", "jolliness", "jolly", "jolt", "jot", "jovial", "joyfully", "joylessly", "joyous", "joyride", "joystick", "jubilance", "jubilant", "judge", "judgingly", "judicial", "judiciary", "judo", "juggle", "juggling", "jugular", "juice", "juiciness", "juicy", "jujitsu", "jukebox", "july", "jumble", "jumbo", "jump", "junction", "juncture", "june", "junior", "juniper", "junkie", "junkman", "junkyard", "jurist", "juror", "jury", "justice", "justifier", "justify", "justly", "justness", "juvenile", "kabob", "kangaroo", "karaoke", "karate", "karma", "kebab", "keenly", "keenness", "keep", "keg", "kelp", "kennel", "kept", "kerchief", "kerosene", "kettle", "kick", "kiln", "kilobyte", "kilogram", "kilometer", "kilowatt", "kilt", "kimono", "kindle", "kindling", "kindly", "kindness", "kindred", "kinetic", "kinfolk", "king", "kinship", "kinsman", "kinswoman", "kissable", "kisser", "kissing", "kitchen", "kite", "kitten", "kitty", "kiwi", "kleenex", "knapsack", "knee", "knelt", "knickers", "knoll", "koala", "kooky", "kosher", "krypton", "kudos", "kung", "labored", "laborer", "laboring", "laborious", "labrador", "ladder", "ladies", "ladle", "ladybug", "ladylike", "lagged", "lagging", "lagoon", "lair", "lake", "lance", "landed", "landfall", "landfill", "landing", "landlady", "landless", "landline", "landlord", "landmark", "landmass", "landmine", "landowner", "landscape", "landside", "landslide", "language", "lankiness", "lanky", "lantern", "lapdog", "lapel", "lapped", "lapping", "laptop", "lard", "large", "lark", "lash", "lasso", "last", "latch", "late", "lather", "latitude", "latrine", "latter", "latticed", "launch", "launder", "laundry", "laurel", "lavender", "lavish", "laxative", "lazily", "laziness", "lazy", "lecturer", "left", "legacy", "legal", "legend", "legged", "leggings", "legible", "legibly", "legislate", "lego", "legroom", "legume", "legwarmer", "legwork", "lemon", "lend", "length", "lens", "lent", "leotard", "lesser", "letdown", "lethargic", "lethargy", "letter", "lettuce", "level", "leverage", "levers", "levitate", "levitator", "liability", "liable", "liberty", "librarian", "library", "licking", "licorice", "lid", "life", "lifter", "lifting", "liftoff", "ligament", "likely", "likeness", "likewise", "liking", "lilac", "lilly", "lily", "limb", "limeade", "limelight", "limes", "limit", "limping", "limpness", "line", "lingo", "linguini", "linguist", "lining", "linked", "linoleum", "linseed", "lint", "lion", "lip", "liquefy", "liqueur", "liquid", "lisp", "list", "litigate", "litigator", "litmus", "litter", "little", "livable", "lived", "lively", "liver", "livestock", "lividly", "living", "lizard", "lubricant", "lubricate", "lucid", "luckily", "luckiness", "luckless", "lucrative", "ludicrous", "lugged", "lukewarm", "lullaby", "lumber", "luminance", "luminous", "lumpiness", "lumping", "lumpish", "lunacy", "lunar", "lunchbox", "luncheon", "lunchroom", "lunchtime", "lung", "lurch", "lure", "luridness", "lurk", "lushly", "lushness", "luster", "lustfully", "lustily", "lustiness", "lustrous", "lusty", "luxurious", "luxury", "lying", "lyrically", "lyricism", "lyricist", "lyrics", "macarena", "macaroni", "macaw", "mace", "machine", "machinist", "magazine", "magenta", "maggot", "magical", "magician", "magma", "magnesium", "magnetic", "magnetism", "magnetize", "magnifier", "magnify", "magnitude", "magnolia", "mahogany", "maimed", "majestic", "majesty", "majorette", "majority", "makeover", "maker", "makeshift", "making", "malformed", "malt", "mama", "mammal", "mammary", "mammogram", "manager", "managing", "manatee", "mandarin", "mandate", "mandatory", "mandolin", "manger", "mangle", "mango", "mangy", "manhandle", "manhole", "manhood", "manhunt", "manicotti", "manicure", "manifesto", "manila", "mankind", "manlike", "manliness", "manly", "manmade", "manned", "mannish", "manor", "manpower", "mantis", "mantra", "manual", "many", "map", "marathon", "marauding", "marbled", "marbles", "marbling", "march", "mardi", "margarine", "margarita", "margin", "marigold", "marina", "marine", "marital", "maritime", "marlin", "marmalade", "maroon", "married", "marrow", "marry", "marshland", "marshy", "marsupial", "marvelous", "marxism", "mascot", "masculine", "mashed", "mashing", "massager", "masses", "massive", "mastiff", "matador", "matchbook", "matchbox", "matcher", "matching", "matchless", "material", "maternal", "maternity", "math", "mating", "matriarch", "matrimony", "matrix", "matron", "matted", "matter", "maturely", "maturing", "maturity", "mauve", "maverick", "maximize", "maximum", "maybe", "mayday", "mayflower", "moaner", "moaning", "mobile", "mobility", "mobilize", "mobster", "mocha", "mocker", "mockup", "modified", "modify", "modular", "modulator", "module", "moisten", "moistness", "moisture", "molar", "molasses", "mold", "molecular", "molecule", "molehill", "mollusk", "mom", "monastery", "monday", "monetary", "monetize", "moneybags", "moneyless", "moneywise", "mongoose", "mongrel", "monitor", "monkhood", "monogamy", "monogram", "monologue", "monopoly", "monorail", "monotone", "monotype", "monoxide", "monsieur", "monsoon", "monstrous", "monthly", "monument", "moocher", "moodiness", "moody", "mooing", "moonbeam", "mooned", "moonlight", "moonlike", "moonlit", "moonrise", "moonscape", "moonshine", "moonstone", "moonwalk", "mop", "morale", "morality", "morally", "morbidity", "morbidly", "morphine", "morphing", "morse", "mortality", "mortally", "mortician", "mortified", "mortify", "mortuary", "mosaic", "mossy", "most", "mothball", "mothproof", "motion", "motivate", "motivator", "motive", "motocross", "motor", "motto", "mountable", "mountain", "mounted", "mounting", "mourner", "mournful", "mouse", "mousiness", "moustache", "mousy", "mouth", "movable", "move", "movie", "moving", "mower", "mowing", "much", "muck", "mud", "mug", "mulberry", "mulch", "mule", "mulled", "mullets", "multiple", "multiply", "multitask", "multitude", "mumble", "mumbling", "mumbo", "mummified", "mummify", "mummy", "mumps", "munchkin", "mundane", "municipal", "muppet", "mural", "murkiness", "murky", "murmuring", "muscular", "museum", "mushily", "mushiness", "mushroom", "mushy", "music", "musket", "muskiness", "musky", "mustang", "mustard", "muster", "mustiness", "musty", "mutable", "mutate", "mutation", "mute", "mutilated", "mutilator", "mutiny", "mutt", "mutual", "muzzle", "myself", "myspace", "mystified", "mystify", "myth", "nacho", "nag", "nail", "name", "naming", "nanny", "nanometer", "nape", "napkin", "napped", "napping", "nappy", "narrow", "nastily", "nastiness", "national", "native", "nativity", "natural", "nature", "naturist", "nautical", "navigate", "navigator", "navy", "nearby", "nearest", "nearly", "nearness", "neatly", "neatness", "nebula", "nebulizer", "nectar", "negate", "negation", "negative", "neglector", "negligee", "negligent", "negotiate", "nemeses", "nemesis", "neon", "nephew", "nerd", "nervous", "nervy", "nest", "net", "neurology", "neuron", "neurosis", "neurotic", "neuter", "neutron", "never", "next", "nibble", "nickname", "nicotine", "niece", "nifty", "nimble", "nimbly", "nineteen", "ninetieth", "ninja", "nintendo", "ninth", "nuclear", "nuclei", "nucleus", "nugget", "nullify", "number", "numbing", "numbly", "numbness", "numeral", "numerate", "numerator", "numeric", "numerous", "nuptials", "nursery", "nursing", "nurture", "nutcase", "nutlike", "nutmeg", "nutrient", "nutshell", "nuttiness", "nutty", "nuzzle", "nylon", "oaf", "oak", "oasis", "oat", "obedience", "obedient", "obituary", "object", "obligate", "obliged", "oblivion", "oblivious", "oblong", "obnoxious", "oboe", "obscure", "obscurity", "observant", "observer", "observing", "obsessed", "obsession", "obsessive", "obsolete", "obstacle", "obstinate", "obstruct", "obtain", "obtrusive", "obtuse", "obvious", "occultist", "occupancy", "occupant", "occupier", "occupy", "ocean", "ocelot", "octagon", "octane", "october", "octopus", "ogle", "oil", "oink", "ointment", "okay", "old", "olive", "olympics", "omega", "omen", "ominous", "omission", "omit", "omnivore", "onboard", "oncoming", "ongoing", "onion", "online", "onlooker", "only", "onscreen", "onset", "onshore", "onslaught", "onstage", "onto", "onward", "onyx", "oops", "ooze", "oozy", "opacity", "opal", "open", "operable", "operate", "operating", "operation", "operative", "operator", "opium", "opossum", "opponent", "oppose", "opposing", "opposite", "oppressed", "oppressor", "opt", "opulently", "osmosis", "other", "otter", "ouch", "ought", "ounce", "outage", "outback", "outbid", "outboard", "outbound", "outbreak", "outburst", "outcast", "outclass", "outcome", "outdated", "outdoors", "outer", "outfield", "outfit", "outflank", "outgoing", "outgrow", "outhouse", "outing", "outlast", "outlet", "outline", "outlook", "outlying", "outmatch", "outmost", "outnumber", "outplayed", "outpost", "outpour", "output", "outrage", "outrank", "outreach", "outright", "outscore", "outsell", "outshine", "outshoot", "outsider", "outskirts", "outsmart", "outsource", "outspoken", "outtakes", "outthink", "outward", "outweigh", "outwit", "oval", "ovary", "oven", "overact", "overall", "overarch", "overbid", "overbill", "overbite", "overblown", "overboard", "overbook", "overbuilt", "overcast", "overcoat", "overcome", "overcook", "overcrowd", "overdraft", "overdrawn", "overdress", "overdrive", "overdue", "overeager", "overeater", "overexert", "overfed", "overfeed", "overfill", "overflow", "overfull", "overgrown", "overhand", "overhang", "overhaul", "overhead", "overhear", "overheat", "overhung", "overjoyed", "overkill", "overlabor", "overlaid", "overlap", "overlay", "overload", "overlook", "overlord", "overlying", "overnight", "overpass", "overpay", "overplant", "overplay", "overpower", "overprice", "overrate", "overreach", "overreact", "override", "overripe", "overrule", "overrun", "overshoot", "overshot", "oversight", "oversized", "oversleep", "oversold", "overspend", "overstate", "overstay", "overstep", "overstock", "overstuff", "oversweet", "overtake", "overthrow", "overtime", "overtly", "overtone", "overture", "overturn", "overuse", "overvalue", "overview", "overwrite", "owl", "oxford", "oxidant", "oxidation", "oxidize", "oxidizing", "oxygen", "oxymoron", "oyster", "ozone", "paced", "pacemaker", "pacific", "pacifier", "pacifism", "pacifist", "pacify", "padded", "padding", "paddle", "paddling", "padlock", "pagan", "pager", "paging", "pajamas", "palace", "palatable", "palm", "palpable", "palpitate", "paltry", "pampered", "pamperer", "pampers", "pamphlet", "panama", "pancake", "pancreas", "panda", "pandemic", "pang", "panhandle", "panic", "panning", "panorama", "panoramic", "panther", "pantomime", "pantry", "pants", "pantyhose", "paparazzi", "papaya", "paper", "paprika", "papyrus", "parabola", "parachute", "parade", "paradox", "paragraph", "parakeet", "paralegal", "paralyses", "paralysis", "paralyze", "paramedic", "parameter", "paramount", "parasail", "parasite", "parasitic", "parcel", "parched", "parchment", "pardon", "parish", "parka", "parking", "parkway", "parlor", "parmesan", "parole", "parrot", "parsley", "parsnip", "partake", "parted", "parting", "partition", "partly", "partner", "partridge", "party", "passable", "passably", "passage", "passcode", "passenger", "passerby", "passing", "passion", "passive", "passivism", "passover", "passport", "password", "pasta", "pasted", "pastel", "pastime", "pastor", "pastrami", "pasture", "pasty", "patchwork", "patchy", "paternal", "paternity", "path", "patience", "patient", "patio", "patriarch", "patriot", "patrol", "patronage", "patronize", "pauper", "pavement", "paver", "pavestone", "pavilion", "paving", "pawing", "payable", "payback", "paycheck", "payday", "payee", "payer", "paying", "payment", "payphone", "payroll", "pebble", "pebbly", "pecan", "pectin", "peculiar", "peddling", "pediatric", "pedicure", "pedigree", "pedometer", "pegboard", "pelican", "pellet", "pelt", "pelvis", "penalize", "penalty", "pencil", "pendant", "pending", "penholder", "penknife", "pennant", "penniless", "penny", "penpal", "pension", "pentagon", "pentagram", "pep", "perceive", "percent", "perch", "percolate", "perennial", "perfected", "perfectly", "perfume", "periscope", "perish", "perjurer", "perjury", "perkiness", "perky", "perm", "peroxide", "perpetual", "perplexed", "persecute", "persevere", "persuaded", "persuader", "pesky", "peso", "pessimism", "pessimist", "pester", "pesticide", "petal", "petite", "petition", "petri", "petroleum", "petted", "petticoat", "pettiness", "petty", "petunia", "phantom", "phobia", "phoenix", "phonebook", "phoney", "phonics", "phoniness", "phony", "phosphate", "photo", "phrase", "phrasing", "placard", "placate", "placidly", "plank", "planner", "plant", "plasma", "plaster", "plastic", "plated", "platform", "plating", "platinum", "platonic", "platter", "platypus", "plausible", "plausibly", "playable", "playback", "player", "playful", "playgroup", "playhouse", "playing", "playlist", "playmaker", "playmate", "playoff", "playpen", "playroom", "playset", "plaything", "playtime", "plaza", "pleading", "pleat", "pledge", "plentiful", "plenty", "plethora", "plexiglas", "pliable", "plod", "plop", "plot", "plow", "ploy", "pluck", "plug", "plunder", "plunging", "plural", "plus", "plutonium", "plywood", "poach", "pod", "poem", "poet", "pogo", "pointed", "pointer", "pointing", "pointless", "pointy", "poise", "poison", "poker", "poking", "polar", "police", "policy", "polio", "polish", "politely", "polka", "polo", "polyester", "polygon", "polygraph", "polymer", "poncho", "pond", "pony", "popcorn", "pope", "poplar", "popper", "poppy", "popsicle", "populace", "popular", "populate", "porcupine", "pork", "porous", "porridge", "portable", "portal", "portfolio", "porthole", "portion", "portly", "portside", "poser", "posh", "posing", "possible", "possibly", "possum", "postage", "postal", "postbox", "postcard", "posted", "poster", "posting", "postnasal", "posture", "postwar", "pouch", "pounce", "pouncing", "pound", "pouring", "pout", "powdered", "powdering", "powdery", "power", "powwow", "pox", "praising", "prance", "prancing", "pranker", "prankish", "prankster", "prayer", "praying", "preacher", "preaching", "preachy", "preamble", "precinct", "precise", "precision", "precook", "precut", "predator", "predefine", "predict", "preface", "prefix", "preflight", "preformed", "pregame", "pregnancy", "pregnant", "preheated", "prelaunch", "prelaw", "prelude", "premiere", "premises", "premium", "prenatal", "preoccupy", "preorder", "prepaid", "prepay", "preplan", "preppy", "preschool", "prescribe", "preseason", "preset", "preshow", "president", "presoak", "press", "presume", "presuming", "preteen", "pretended", "pretender", "pretense", "pretext", "pretty", "pretzel", "prevail", "prevalent", "prevent", "preview", "previous", "prewar", "prewashed", "prideful", "pried", "primal", "primarily", "primary", "primate", "primer", "primp", "princess", "print", "prior", "prism", "prison", "prissy", "pristine", "privacy", "private", "privatize", "prize", "proactive", "probable", "probably", "probation", "probe", "probing", "probiotic", "problem", "procedure", "process", "proclaim", "procreate", "procurer", "prodigal", "prodigy", "produce", "product", "profane", "profanity", "professed", "professor", "profile", "profound", "profusely", "progeny", "prognosis", "program", "progress", "projector", "prologue", "prolonged", "promenade", "prominent", "promoter", "promotion", "prompter", "promptly", "prone", "prong", "pronounce", "pronto", "proofing", "proofread", "proofs", "propeller", "properly", "property", "proponent", "proposal", "propose", "props", "prorate", "protector", "protegee", "proton", "prototype", "protozoan", "protract", "protrude", "proud", "provable", "proved", "proven", "provided", "provider", "providing", "province", "proving", "provoke", "provoking", "provolone", "prowess", "prowler", "prowling", "proximity", "proxy", "prozac", "prude", "prudishly", "prune", "pruning", "pry", "psychic", "public", "publisher", "pucker", "pueblo", "pug", "pull", "pulmonary", "pulp", "pulsate", "pulse", "pulverize", "puma", "pumice", "pummel", "punch", "punctual", "punctuate", "punctured", "pungent", "punisher", "punk", "pupil", "puppet", "puppy", "purchase", "pureblood", "purebred", "purely", "pureness", "purgatory", "purge", "purging", "purifier", "purify", "purist", "puritan", "purity", "purple", "purplish", "purposely", "purr", "purse", "pursuable", "pursuant", "pursuit", "purveyor", "pushcart", "pushchair", "pusher", "pushiness", "pushing", "pushover", "pushpin", "pushup", "pushy", "putdown", "putt", "puzzle", "puzzling", "pyramid", "pyromania", "python", "quack", "quadrant", "quail", "quaintly", "quake", "quaking", "qualified", "qualifier", "qualify", "quality", "qualm", "quantum", "quarrel", "quarry", "quartered", "quarterly", "quarters", "quartet", "quench", "query", "quicken", "quickly", "quickness", "quicksand", "quickstep", "quiet", "quill", "quilt", "quintet", "quintuple", "quirk", "quit", "quiver", "quizzical", "quotable", "quotation", "quote", "rabid", "race", "racing", "racism", "rack", "racoon", "radar", "radial", "radiance", "radiantly", "radiated", "radiation", "radiator", "radio", "radish", "raffle", "raft", "rage", "ragged", "raging", "ragweed", "raider", "railcar", "railing", "railroad", "railway", "raisin", "rake", "raking", "rally", "ramble", "rambling", "ramp", "ramrod", "ranch", "rancidity", "random", "ranged", "ranger", "ranging", "ranked", "ranking", "ransack", "ranting", "rants", "rare", "rarity", "rascal", "rash", "rasping", "ravage", "raven", "ravine", "raving", "ravioli", "ravishing", "reabsorb", "reach", "reacquire", "reaction", "reactive", "reactor", "reaffirm", "ream", "reanalyze", "reappear", "reapply", "reappoint", "reapprove", "rearrange", "rearview", "reason", "reassign", "reassure", "reattach", "reawake", "rebalance", "rebate", "rebel", "rebirth", "reboot", "reborn", "rebound", "rebuff", "rebuild", "rebuilt", "reburial", "rebuttal", "recall", "recant", "recapture", "recast", "recede", "recent", "recess", "recharger", "recipient", "recital", "recite", "reckless", "reclaim", "recliner", "reclining", "recluse", "reclusive", "recognize", "recoil", "recollect", "recolor", "reconcile", "reconfirm", "reconvene", "recopy", "record", "recount", "recoup", "recovery", "recreate", "rectal", "rectangle", "rectified", "rectify", "recycled", "recycler", "recycling", "reemerge", "reenact", "reenter", "reentry", "reexamine", "referable", "referee", "reference", "refill", "refinance", "refined", "refinery", "refining", "refinish", "reflected", "reflector", "reflex", "reflux", "refocus", "refold", "reforest", "reformat", "reformed", "reformer", "reformist", "refract", "refrain", "refreeze", "refresh", "refried", "refueling", "refund", "refurbish", "refurnish", "refusal", "refuse", "refusing", "refutable", "refute", "regain", "regalia", "regally", "reggae", "regime", "region", "register", "registrar", "registry", "regress", "regretful", "regroup", "regular", "regulate", "regulator", "rehab", "reheat", "rehire", "rehydrate", "reimburse", "reissue", "reiterate", "rejoice", "rejoicing", "rejoin", "rekindle", "relapse", "relapsing", "relatable", "related", "relation", "relative", "relax", "relay", "relearn", "release", "relenting", "reliable", "reliably", "reliance", "reliant", "relic", "relieve", "relieving", "relight", "relish", "relive", "reload", "relocate", "relock", "reluctant", "rely", "remake", "remark", "remarry", "rematch", "remedial", "remedy", "remember", "reminder", "remindful", "remission", "remix", "remnant", "remodeler", "remold", "remorse", "remote", "removable", "removal", "removed", "remover", "removing", "rename", "renderer", "rendering", "rendition", "renegade", "renewable", "renewably", "renewal", "renewed", "renounce", "renovate", "renovator", "rentable", "rental", "rented", "renter", "reoccupy", "reoccur", "reopen", "reorder", "repackage", "repacking", "repaint", "repair", "repave", "repaying", "repayment", "repeal", "repeated", "repeater", "repent", "rephrase", "replace", "replay", "replica", "reply", "reporter", "repose", "repossess", "repost", "repressed", "reprimand", "reprint", "reprise", "reproach", "reprocess", "reproduce", "reprogram", "reps", "reptile", "reptilian", "repugnant", "repulsion", "repulsive", "repurpose", "reputable", "reputably", "request", "require", "requisite", "reroute", "rerun", "resale", "resample", "rescuer", "reseal", "research", "reselect", "reseller", "resemble", "resend", "resent", "reset", "reshape", "reshoot", "reshuffle", "residence", "residency", "resident", "residual", "residue", "resigned", "resilient", "resistant", "resisting", "resize", "resolute", "resolved", "resonant", "resonate", "resort", "resource", "respect", "resubmit", "result", "resume", "resupply", "resurface", "resurrect", "retail", "retainer", "retaining", "retake", "retaliate", "retention", "rethink", "retinal", "retired", "retiree", "retiring", "retold", "retool", "retorted", "retouch", "retrace", "retract", "retrain", "retread", "retreat", "retrial", "retrieval", "retriever", "retry", "return", "retying", "retype", "reunion", "reunite", "reusable", "reuse", "reveal", "reveler", "revenge", "revenue", "reverb", "revered", "reverence", "reverend", "reversal", "reverse", "reversing", "reversion", "revert", "revisable", "revise", "revision", "revisit", "revivable", "revival", "reviver", "reviving", "revocable", "revoke", "revolt", "revolver", "revolving", "reward", "rewash", "rewind", "rewire", "reword", "rework", "rewrap", "rewrite", "rhyme", "ribbon", "ribcage", "rice", "riches", "richly", "richness", "rickety", "ricotta", "riddance", "ridden", "ride", "riding", "rifling", "rift", "rigging", "rigid", "rigor", "rimless", "rimmed", "rind", "rink", "rinse", "rinsing", "riot", "ripcord", "ripeness", "ripening", "ripping", "ripple", "rippling", "riptide", "rise", "rising", "risk", "risotto", "ritalin", "ritzy", "rival", "riverbank", "riverbed", "riverboat", "riverside", "riveter", "riveting", "roamer", "roaming", "roast", "robbing", "robe", "robin", "robotics", "robust", "rockband", "rocker", "rocket", "rockfish", "rockiness", "rocking", "rocklike", "rockslide", "rockstar", "rocky", "rogue", "roman", "romp", "rope", "roping", "roster", "rosy", "rotten", "rotting", "rotunda", "roulette", "rounding", "roundish", "roundness", "roundup", "roundworm", "routine", "routing", "rover", "roving", "royal", "rubbed", "rubber", "rubbing", "rubble", "rubdown", "ruby", "ruckus", "rudder", "rug", "ruined", "rule", "rumble", "rumbling", "rummage", "rumor", "runaround", "rundown", "runner", "running", "runny", "runt", "runway", "rupture", "rural", "ruse", "rush", "rust", "rut", "sabbath", "sabotage", "sacrament", "sacred", "sacrifice", "sadden", "saddlebag", "saddled", "saddling", "sadly", "sadness", "safari", "safeguard", "safehouse", "safely", "safeness", "saffron", "saga", "sage", "sagging", "saggy", "said", "saint", "sake", "salad", "salami", "salaried", "salary", "saline", "salon", "saloon", "salsa", "salt", "salutary", "salute", "salvage", "salvaging", "salvation", "same", "sample", "sampling", "sanction", "sanctity", "sanctuary", "sandal", "sandbag", "sandbank", "sandbar", "sandblast", "sandbox", "sanded", "sandfish", "sanding", "sandlot", "sandpaper", "sandpit", "sandstone", "sandstorm", "sandworm", "sandy", "sanitary", "sanitizer", "sank", "santa", "sapling", "sappiness", "sappy", "sarcasm", "sarcastic", "sardine", "sash", "sasquatch", "sassy", "satchel", "satiable", "satin", "satirical", "satisfied", "satisfy", "saturate", "saturday", "sauciness", "saucy", "sauna", "savage", "savanna", "saved", "savings", "savior", "savor", "saxophone", "say", "scabbed", "scabby", "scalded", "scalding", "scale", "scaling", "scallion", "scallop", "scalping", "scam", "scandal", "scanner", "scanning", "scant", "scapegoat", "scarce", "scarcity", "scarecrow", "scared", "scarf", "scarily", "scariness", "scarring", "scary", "scavenger", "scenic", "schedule", "schematic", "scheme", "scheming", "schilling", "schnapps", "scholar", "science", "scientist", "scion", "scoff", "scolding", "scone", "scoop", "scooter", "scope", "scorch", "scorebook", "scorecard", "scored", "scoreless", "scorer", "scoring", "scorn", "scorpion", "scotch", "scoundrel", "scoured", "scouring", "scouting", "scouts", "scowling", "scrabble", "scraggly", "scrambled", "scrambler", "scrap", "scratch", "scrawny", "screen", "scribble", "scribe", "scribing", "scrimmage", "script", "scroll", "scrooge", "scrounger", "scrubbed", "scrubber", "scruffy", "scrunch", "scrutiny", "scuba", "scuff", "sculptor", "sculpture", "scurvy", "scuttle", "secluded", "secluding", "seclusion", "second", "secrecy", "secret", "sectional", "sector", "secular", "securely", "security", "sedan", "sedate", "sedation", "sedative", "sediment", "seduce", "seducing", "segment", "seismic", "seizing", "seldom", "selected", "selection", "selective", "selector", "self", "seltzer", "semantic", "semester", "semicolon", "semifinal", "seminar", "semisoft", "semisweet", "senate", "senator", "send", "senior", "senorita", "sensation", "sensitive", "sensitize", "sensually", "sensuous", "sepia", "september", "septic", "septum", "sequel", "sequence", "sequester", "series", "sermon", "serotonin", "serpent", "serrated", "serve", "service", "serving", "sesame", "sessions", "setback", "setting", "settle", "settling", "setup", "sevenfold", "seventeen", "seventh", "seventy", "severity", "shabby", "shack", "shaded", "shadily", "shadiness", "shading", "shadow", "shady", "shaft", "shakable", "shakily", "shakiness", "shaking", "shaky", "shale", "shallot", "shallow", "shame", "shampoo", "shamrock", "shank", "shanty", "shape", "shaping", "share", "sharpener", "sharper", "sharpie", "sharply", "sharpness", "shawl", "sheath", "shed", "sheep", "sheet", "shelf", "shell", "shelter", "shelve", "shelving", "sherry", "shield", "shifter", "shifting", "shiftless", "shifty", "shimmer", "shimmy", "shindig", "shine", "shingle", "shininess", "shining", "shiny", "ship", "shirt", "shivering", "shock", "shone", "shoplift", "shopper", "shopping", "shoptalk", "shore", "shortage", "shortcake", "shortcut", "shorten", "shorter", "shorthand", "shortlist", "shortly", "shortness", "shorts", "shortwave", "shorty", "shout", "shove", "showbiz", "showcase", "showdown", "shower", "showgirl", "showing", "showman", "shown", "showoff", "showpiece", "showplace", "showroom", "showy", "shrank", "shrapnel", "shredder", "shredding", "shrewdly", "shriek", "shrill", "shrimp", "shrine", "shrink", "shrivel", "shrouded", "shrubbery", "shrubs", "shrug", "shrunk", "shucking", "shudder", "shuffle", "shuffling", "shun", "shush", "shut", "shy", "siamese", "siberian", "sibling", "siding", "sierra", "siesta", "sift", "sighing", "silenced", "silencer", "silent", "silica", "silicon", "silk", "silliness", "silly", "silo", "silt", "silver", "similarly", "simile", "simmering", "simple", "simplify", "simply", "sincere", "sincerity", "singer", "singing", "single", "singular", "sinister", "sinless", "sinner", "sinuous", "sip", "siren", "sister", "sitcom", "sitter", "sitting", "situated", "situation", "sixfold", "sixteen", "sixth", "sixties", "sixtieth", "sixtyfold", "sizable", "sizably", "size", "sizing", "sizzle", "sizzling", "skater", "skating", "skedaddle", "skeletal", "skeleton", "skeptic", "sketch", "skewed", "skewer", "skid", "skied", "skier", "skies", "skiing", "skilled", "skillet", "skillful", "skimmed", "skimmer", "skimming", "skimpily", "skincare", "skinhead", "skinless", "skinning", "skinny", "skintight", "skipper", "skipping", "skirmish", "skirt", "skittle", "skydiver", "skylight", "skyline", "skype", "skyrocket", "skyward", "slab", "slacked", "slacker", "slacking", "slackness", "slacks", "slain", "slam", "slander", "slang", "slapping", "slapstick", "slashed", "slashing", "slate", "slather", "slaw", "sled", "sleek", "sleep", "sleet", "sleeve", "slept", "sliceable", "sliced", "slicer", "slicing", "slick", "slider", "slideshow", "sliding", "slighted", "slighting", "slightly", "slimness", "slimy", "slinging", "slingshot", "slinky", "slip", "slit", "sliver", "slobbery", "slogan", "sloped", "sloping", "sloppily", "sloppy", "slot", "slouching", "slouchy", "sludge", "slug", "slum", "slurp", "slush", "sly", "small", "smartly", "smartness", "smasher", "smashing", "smashup", "smell", "smelting", "smile", "smilingly", "smirk", "smite", "smith", "smitten", "smock", "smog", "smoked", "smokeless", "smokiness", "smoking", "smoky", "smolder", "smooth", "smother", "smudge", "smudgy", "smuggler", "smuggling", "smugly", "smugness", "snack", "snagged", "snaking", "snap", "snare", "snarl", "snazzy", "sneak", "sneer", "sneeze", "sneezing", "snide", "sniff", "snippet", "snipping", "snitch", "snooper", "snooze", "snore", "snoring", "snorkel", "snort", "snout", "snowbird", "snowboard", "snowbound", "snowcap", "snowdrift", "snowdrop", "snowfall", "snowfield", "snowflake", "snowiness", "snowless", "snowman", "snowplow", "snowshoe", "snowstorm", "snowsuit", "snowy", "snub", "snuff", "snuggle", "snugly", "snugness", "speak", "spearfish", "spearhead", "spearman", "spearmint", "species", "specimen", "specked", "speckled", "specks", "spectacle", "spectator", "spectrum", "speculate", "speech", "speed", "spellbind", "speller", "spelling", "spendable", "spender", "spending", "spent", "spew", "sphere", "spherical", "sphinx", "spider", "spied", "spiffy", "spill", "spilt", "spinach", "spinal", "spindle", "spinner", "spinning", "spinout", "spinster", "spiny", "spiral", "spirited", "spiritism", "spirits", "spiritual", "splashed", "splashing", "splashy", "splatter", "spleen", "splendid", "splendor", "splice", "splicing", "splinter", "splotchy", "splurge", "spoilage", "spoiled", "spoiler", "spoiling", "spoils", "spoken", "spokesman", "sponge", "spongy", "sponsor", "spoof", "spookily", "spooky", "spool", "spoon", "spore", "sporting", "sports", "sporty", "spotless", "spotlight", "spotted", "spotter", "spotting", "spotty", "spousal", "spouse", "spout", "sprain", "sprang", "sprawl", "spray", "spree", "sprig", "spring", "sprinkled", "sprinkler", "sprint", "sprite", "sprout", "spruce", "sprung", "spry", "spud", "spur", "sputter", "spyglass", "squabble", "squad", "squall", "squander", "squash", "squatted", "squatter", "squatting", "squeak", "squealer", "squealing", "squeamish", "squeegee", "squeeze", "squeezing", "squid", "squiggle", "squiggly", "squint", "squire", "squirt", "squishier", "squishy", "stability", "stabilize", "stable", "stack", "stadium", "staff", "stage", "staging", "stagnant", "stagnate", "stainable", "stained", "staining", "stainless", "stalemate", "staleness", "stalling", "stallion", "stamina", "stammer", "stamp", "stand", "stank", "staple", "stapling", "starboard", "starch", "stardom", "stardust", "starfish", "stargazer", "staring", "stark", "starless", "starlet", "starlight", "starlit", "starring", "starry", "starship", "starter", "starting", "startle", "startling", "startup", "starved", "starving", "stash", "state", "static", "statistic", "statue", "stature", "status", "statute", "statutory", "staunch", "stays", "steadfast", "steadier", "steadily", "steadying", "steam", "steed", "steep", "steerable", "steering", "steersman", "stegosaur", "stellar", "stem", "stench", "stencil", "step", "stereo", "sterile", "sterility", "sterilize", "sterling", "sternness", "sternum", "stew", "stick", "stiffen", "stiffly", "stiffness", "stifle", "stifling", "stillness", "stilt", "stimulant", "stimulate", "stimuli", "stimulus", "stinger", "stingily", "stinging", "stingray", "stingy", "stinking", "stinky", "stipend", "stipulate", "stir", "stitch", "stock", "stoic", "stoke", "stole", "stomp", "stonewall", "stoneware", "stonework", "stoning", "stony", "stood", "stooge", "stool", "stoop", "stoplight", "stoppable", "stoppage", "stopped", "stopper", "stopping", "stopwatch", "storable", "storage", "storeroom", "storewide", "storm", "stout", "stove", "stowaway", "stowing", "straddle", "straggler", "strained", "strainer", "straining", "strangely", "stranger", "strangle", "strategic", "strategy", "stratus", "straw", "stray", "streak", "stream", "street", "strength", "strenuous", "strep", "stress", "stretch", "strewn", "stricken", "strict", "stride", "strife", "strike", "striking", "strive", "striving", "strobe", "strode", "stroller", "strongbox", "strongly", "strongman", "struck", "structure", "strudel", "struggle", "strum", "strung", "strut", "stubbed", "stubble", "stubbly", "stubborn", "stucco", "stuck", "student", "studied", "studio", "study", "stuffed", "stuffing", "stuffy", "stumble", "stumbling", "stump", "stung", "stunned", "stunner", "stunning", "stunt", "stupor", "sturdily", "sturdy", "styling", "stylishly", "stylist", "stylized", "stylus", "suave", "subarctic", "subatomic", "subdivide", "subdued", "subduing", "subfloor", "subgroup", "subheader", "subject", "sublease", "sublet", "sublevel", "sublime", "submarine", "submerge", "submersed", "submitter", "subpanel", "subpar", "subplot", "subprime", "subscribe", "subscript", "subsector", "subside", "subsiding", "subsidize", "subsidy", "subsoil", "subsonic", "substance", "subsystem", "subtext", "subtitle", "subtly", "subtotal", "subtract", "subtype", "suburb", "subway", "subwoofer", "subzero", "succulent", "such", "suction", "sudden", "sudoku", "suds", "sufferer", "suffering", "suffice", "suffix", "suffocate", "suffrage", "sugar", "suggest", "suing", "suitable", "suitably", "suitcase", "suitor", "sulfate", "sulfide", "sulfite", "sulfur", "sulk", "sullen", "sulphate", "sulphuric", "sultry", "superbowl", "superglue", "superhero", "superior", "superjet", "superman", "supermom", "supernova", "supervise", "supper", "supplier", "supply", "support", "supremacy", "supreme", "surcharge", "surely", "sureness", "surface", "surfacing", "surfboard", "surfer", "surgery", "surgical", "surging", "surname", "surpass", "surplus", "surprise", "surreal", "surrender", "surrogate", "surround", "survey", "survival", "survive", "surviving", "survivor", "sushi", "suspect", "suspend", "suspense", "sustained", "sustainer", "swab", "swaddling", "swagger", "swampland", "swan", "swapping", "swarm", "sway", "swear", "sweat", "sweep", "swell", "swept", "swerve", "swifter", "swiftly", "swiftness", "swimmable", "swimmer", "swimming", "swimsuit", "swimwear", "swinger", "swinging", "swipe", "swirl", "switch", "swivel", "swizzle", "swooned", "swoop", "swoosh", "swore", "sworn", "swung", "sycamore", "sympathy", "symphonic", "symphony", "symptom", "synapse", "syndrome", "synergy", "synopses", "synopsis", "synthesis", "synthetic", "syrup", "system", "t-shirt", "tabasco", "tabby", "tableful", "tables", "tablet", "tableware", "tabloid", "tackiness", "tacking", "tackle", "tackling", "tacky", "taco", "tactful", "tactical", "tactics", "tactile", "tactless", "tadpole", "taekwondo", "tag", "tainted", "take", "taking", "talcum", "talisman", "tall", "talon", "tamale", "tameness", "tamer", "tamper", "tank", "tanned", "tannery", "tanning", "tantrum", "tapeless", "tapered", "tapering", "tapestry", "tapioca", "tapping", "taps", "tarantula", "target", "tarmac", "tarnish", "tarot", "tartar", "tartly", "tartness", "task", "tassel", "taste", "tastiness", "tasting", "tasty", "tattered", "tattle", "tattling", "tattoo", "taunt", "tavern", "thank", "that", "thaw", "theater", "theatrics", "thee", "theft", "theme", "theology", "theorize", "thermal", "thermos", "thesaurus", "these", "thesis", "thespian", "thicken", "thicket", "thickness", "thieving", "thievish", "thigh", "thimble", "thing", "think", "thinly", "thinner", "thinness", "thinning", "thirstily", "thirsting", "thirsty", "thirteen", "thirty", "thong", "thorn", "those", "thousand", "thrash", "thread", "threaten", "threefold", "thrift", "thrill", "thrive", "thriving", "throat", "throbbing", "throng", "throttle", "throwaway", "throwback", "thrower", "throwing", "thud", "thumb", "thumping", "thursday", "thus", "thwarting", "thyself", "tiara", "tibia", "tidal", "tidbit", "tidiness", "tidings", "tidy", "tiger", "tighten", "tightly", "tightness", "tightrope", "tightwad", "tigress", "tile", "tiling", "till", "tilt", "timid", "timing", "timothy", "tinderbox", "tinfoil", "tingle", "tingling", "tingly", "tinker", "tinkling", "tinsel", "tinsmith", "tint", "tinwork", "tiny", "tipoff", "tipped", "tipper", "tipping", "tiptoeing", "tiptop", "tiring", "tissue", "trace", "tracing", "track", "traction", "tractor", "trade", "trading", "tradition", "traffic", "tragedy", "trailing", "trailside", "train", "traitor", "trance", "tranquil", "transfer", "transform", "translate", "transpire", "transport", "transpose", "trapdoor", "trapeze", "trapezoid", "trapped", "trapper", "trapping", "traps", "trash", "travel", "traverse", "travesty", "tray", "treachery", "treading", "treadmill", "treason", "treat", "treble", "tree", "trekker", "tremble", "trembling", "tremor", "trench", "trend", "trespass", "triage", "trial", "triangle", "tribesman", "tribunal", "tribune", "tributary", "tribute", "triceps", "trickery", "trickily", "tricking", "trickle", "trickster", "tricky", "tricolor", "tricycle", "trident", "tried", "trifle", "trifocals", "trillion", "trilogy", "trimester", "trimmer", "trimming", "trimness", "trinity", "trio", "tripod", "tripping", "triumph", "trivial", "trodden", "trolling", "trombone", "trophy", "tropical", "tropics", "trouble", "troubling", "trough", "trousers", "trout", "trowel", "truce", "truck", "truffle", "trump", "trunks", "trustable", "trustee", "trustful", "trusting", "trustless", "truth", "try", "tubby", "tubeless", "tubular", "tucking", "tuesday", "tug", "tuition", "tulip", "tumble", "tumbling", "tummy", "turban", "turbine", "turbofan", "turbojet", "turbulent", "turf", "turkey", "turmoil", "turret", "turtle", "tusk", "tutor", "tutu", "tux", "tweak", "tweed", "tweet", "tweezers", "twelve", "twentieth", "twenty", "twerp", "twice", "twiddle", "twiddling", "twig", "twilight", "twine", "twins", "twirl", "twistable", "twisted", "twister", "twisting", "twisty", "twitch", "twitter", "tycoon", "tying", "tyke", "udder", "ultimate", "ultimatum", "ultra", "umbilical", "umbrella", "umpire", "unabashed", "unable", "unadorned", "unadvised", "unafraid", "unaired", "unaligned", "unaltered", "unarmored", "unashamed", "unaudited", "unawake", "unaware", "unbaked", "unbalance", "unbeaten", "unbend", "unbent", "unbiased", "unbitten", "unblended", "unblessed", "unblock", "unbolted", "unbounded", "unboxed", "unbraided", "unbridle", "unbroken", "unbuckled", "unbundle", "unburned", "unbutton", "uncanny", "uncapped", "uncaring", "uncertain", "unchain", "unchanged", "uncharted", "uncheck", "uncivil", "unclad", "unclaimed", "unclamped", "unclasp", "uncle", "unclip", "uncloak", "unclog", "unclothed", "uncoated", "uncoiled", "uncolored", "uncombed", "uncommon", "uncooked", "uncork", "uncorrupt", "uncounted", "uncouple", "uncouth", "uncover", "uncross", "uncrown", "uncrushed", "uncured", "uncurious", "uncurled", "uncut", "undamaged", "undated", "undaunted", "undead", "undecided", "undefined", "underage", "underarm", "undercoat", "undercook", "undercut", "underdog", "underdone", "underfed", "underfeed", "underfoot", "undergo", "undergrad", "underhand", "underline", "underling", "undermine", "undermost", "underpaid", "underpass", "underpay", "underrate", "undertake", "undertone", "undertook", "undertow", "underuse", "underwear", "underwent", "underwire", "undesired", "undiluted", "undivided", "undocked", "undoing", "undone", "undrafted", "undress", "undrilled", "undusted", "undying", "unearned", "unearth", "unease", "uneasily", "uneasy", "uneatable", "uneaten", "unedited", "unelected", "unending", "unengaged", "unenvied", "unequal", "unethical", "uneven", "unexpired", "unexposed", "unfailing", "unfair", "unfasten", "unfazed", "unfeeling", "unfiled", "unfilled", "unfitted", "unfitting", "unfixable", "unfixed", "unflawed", "unfocused", "unfold", "unfounded", "unframed", "unfreeze", "unfrosted", "unfrozen", "unfunded", "unglazed", "ungloved", "unglue", "ungodly", "ungraded", "ungreased", "unguarded", "unguided", "unhappily", "unhappy", "unharmed", "unhealthy", "unheard", "unhearing", "unheated", "unhelpful", "unhidden", "unhinge", "unhitched", "unholy", "unhook", "unicorn", "unicycle", "unified", "unifier", "uniformed", "uniformly", "unify", "unimpeded", "uninjured", "uninstall", "uninsured", "uninvited", "union", "uniquely", "unisexual", "unison", "unissued", "unit", "universal", "universe", "unjustly", "unkempt", "unkind", "unknotted", "unknowing", "unknown", "unlaced", "unlatch", "unlawful", "unleaded", "unlearned", "unleash", "unless", "unleveled", "unlighted", "unlikable", "unlimited", "unlined", "unlinked", "unlisted", "unlit", "unlivable", "unloaded", "unloader", "unlocked", "unlocking", "unlovable", "unloved", "unlovely", "unloving", "unluckily", "unlucky", "unmade", "unmanaged", "unmanned", "unmapped", "unmarked", "unmasked", "unmasking", "unmatched", "unmindful", "unmixable", "unmixed", "unmolded", "unmoral", "unmovable", "unmoved", "unmoving", "unnamable", "unnamed", "unnatural", "unneeded", "unnerve", "unnerving", "unnoticed", "unopened", "unopposed", "unpack", "unpadded", "unpaid", "unpainted", "unpaired", "unpaved", "unpeeled", "unpicked", "unpiloted", "unpinned", "unplanned", "unplanted", "unpleased", "unpledged", "unplowed", "unplug", "unpopular", "unproven", "unquote", "unranked", "unrated", "unraveled", "unreached", "unread", "unreal", "unreeling", "unrefined", "unrelated", "unrented", "unrest", "unretired", "unrevised", "unrigged", "unripe", "unrivaled", "unroasted", "unrobed", "unroll", "unruffled", "unruly", "unrushed", "unsaddle", "unsafe", "unsaid", "unsalted", "unsaved", "unsavory", "unscathed", "unscented", "unscrew", "unsealed", "unseated", "unsecured", "unseeing", "unseemly", "unseen", "unselect", "unselfish", "unsent", "unsettled", "unshackle", "unshaken", "unshaved", "unshaven", "unsheathe", "unshipped", "unsightly", "unsigned", "unskilled", "unsliced", "unsmooth", "unsnap", "unsocial", "unsoiled", "unsold", "unsolved", "unsorted", "unspoiled", "unspoken", "unstable", "unstaffed", "unstamped", "unsteady", "unsterile", "unstirred", "unstitch", "unstopped", "unstuck", "unstuffed", "unstylish", "unsubtle", "unsubtly", "unsuited", "unsure", "unsworn", "untagged", "untainted", "untaken", "untamed", "untangled", "untapped", "untaxed", "unthawed", "unthread", "untidy", "untie", "until", "untimed", "untimely", "untitled", "untoasted", "untold", "untouched", "untracked", "untrained", "untreated", "untried", "untrimmed", "untrue", "untruth", "unturned", "untwist", "untying", "unusable", "unused", "unusual", "unvalued", "unvaried", "unvarying", "unveiled", "unveiling", "unvented", "unviable", "unvisited", "unvocal", "unwanted", "unwarlike", "unwary", "unwashed", "unwatched", "unweave", "unwed", "unwelcome", "unwell", "unwieldy", "unwilling", "unwind", "unwired", "unwitting", "unwomanly", "unworldly", "unworn", "unworried", "unworthy", "unwound", "unwoven", "unwrapped", "unwritten", "unzip", "upbeat", "upchuck", "upcoming", "upcountry", "update", "upfront", "upgrade", "upheaval", "upheld", "uphill", "uphold", "uplifted", "uplifting", "upload", "upon", "upper", "upright", "uprising", "upriver", "uproar", "uproot", "upscale", "upside", "upstage", "upstairs", "upstart", "upstate", "upstream", "upstroke", "upswing", "uptake", "uptight", "uptown", "upturned", "upward", "upwind", "uranium", "urban", "urchin", "urethane", "urgency", "urgent", "urging", "urologist", "urology", "usable", "usage", "useable", "used", "uselessly", "user", "usher", "usual", "utensil", "utility", "utilize", "utmost", "utopia", "utter", "vacancy", "vacant", "vacate", "vacation", "vagabond", "vagrancy", "vagrantly", "vaguely", "vagueness", "valiant", "valid", "valium", "valley", "valuables", "value", "vanilla", "vanish", "vanity", "vanquish", "vantage", "vaporizer", "variable", "variably", "varied", "variety", "various", "varmint", "varnish", "varsity", "varying", "vascular", "vaseline", "vastly", "vastness", "veal", "vegan", "veggie", "vehicular", "velcro", "velocity", "velvet", "vendetta", "vending", "vendor", "veneering", "vengeful", "venomous", "ventricle", "venture", "venue", "venus", "verbalize", "verbally", "verbose", "verdict", "verify", "verse", "version", "versus", "vertebrae", "vertical", "vertigo", "very", "vessel", "vest", "veteran", "veto", "vexingly", "viability", "viable", "vibes", "vice", "vicinity", "victory", "video", "viewable", "viewer", "viewing", "viewless", "viewpoint", "vigorous", "village", "villain", "vindicate", "vineyard", "vintage", "violate", "violation", "violator", "violet", "violin", "viper", "viral", "virtual", "virtuous", "virus", "visa", "viscosity", "viscous", "viselike", "visible", "visibly", "vision", "visiting", "visitor", "visor", "vista", "vitality", "vitalize", "vitally", "vitamins", "vivacious", "vividly", "vividness", "vixen", "vocalist", "vocalize", "vocally", "vocation", "voice", "voicing", "void", "volatile", "volley", "voltage", "volumes", "voter", "voting", "voucher", "vowed", "vowel", "voyage", "wackiness", "wad", "wafer", "waffle", "waged", "wager", "wages", "waggle", "wagon", "wake", "waking", "walk", "walmart", "walnut", "walrus", "waltz", "wand", "wannabe", "wanted", "wanting", "wasabi", "washable", "washbasin", "washboard", "washbowl", "washcloth", "washday", "washed", "washer", "washhouse", "washing", "washout", "washroom", "washstand", "washtub", "wasp", "wasting", "watch", "water", "waviness", "waving", "wavy", "whacking", "whacky", "wham", "wharf", "wheat", "whenever", "whiff", "whimsical", "whinny", "whiny", "whisking", "whoever", "whole", "whomever", "whoopee", "whooping", "whoops", "why", "wick", "widely", "widen", "widget", "widow", "width", "wieldable", "wielder", "wife", "wifi", "wikipedia", "wildcard", "wildcat", "wilder", "wildfire", "wildfowl", "wildland", "wildlife", "wildly", "wildness", "willed", "willfully", "willing", "willow", "willpower", "wilt", "wimp", "wince", "wincing", "wind", "wing", "winking", "winner", "winnings", "winter", "wipe", "wired", "wireless", "wiring", "wiry", "wisdom", "wise", "wish", "wisplike", "wispy", "wistful", "wizard", "wobble", "wobbling", "wobbly", "wok", "wolf", "wolverine", "womanhood", "womankind", "womanless", "womanlike", "womanly", "womb", "woof", "wooing", "wool", "woozy", "word", "work", "worried", "worrier", "worrisome", "worry", "worsening", "worshiper", "worst", "wound", "woven", "wow", "wrangle", "wrath", "wreath", "wreckage", "wrecker", "wrecking", "wrench", "wriggle", "wriggly", "wrinkle", "wrinkly", "wrist", "writing", "written", "wrongdoer", "wronged", "wrongful", "wrongly", "wrongness", "wrought", "xbox", "xerox", "yahoo", "yam", "yanking", "yapping", "yard", "yarn", "yeah", "yearbook", "yearling", "yearly", "yearning", "yeast", "yelling", "yelp", "yen", "yesterday", "yiddish", "yield", "yin", "yippee", "yo-yo", "yodel", "yoga", "yogurt", "yonder", "yoyo", "yummy", "zap", "zealous", "zebra", "zen", "zeppelin", "zero", "zestfully", "zesty", "zigzagged", "zipfile", "zipping", "zippy", "zips", "zit", "zodiac", "zombie", "zone", "zoning", "zookeeper", "zoologist", "zoology", "zoom", ]; #[cfg(test)] #[path = "tests/words.rs"] mod words_tests; ripasso-0.8.0/testres/get_history_with_repo.tar.gz000064400000000000000000000262071046102023000205600ustar 00000000000000<[UM>?nMsx7^۱8coWv=fܶ'BB!BBBD ~|!$$HDH(?D<~;(˔4U{ys{np{ύ{ۡszZYYR=is啕zciaeX:]4_IPsq?Ww*AOi dr~0 K '奅9Q?Z}b}t7FcQxmyqucl=>k*ˋbf]ctG 7?OY~ޮѹ}~?񝧿sv_}懿zGiݯ\?λX?,^{_~csCT廿̛7o>~?ǟ+~kߺ_<_ܷO?Q|K Wq^D!'r?W*#߉3+{2vz}W#xcF?ƙ$ k-ׯ TYV)šīt'C~{ټ5Pe͊+POIJ"ǪXZlyǃYuZ嬗nP3U+ CoN4P-hX91`p|'v\xq=UYx~OFb J |Qt0T~ |@x2|1hv/{Fwuă$ 5" J)ZAkH`z>f..H' 4 J/@*Tg8 J <7{(kti+; #x0B(m4g0K^E^lp^;1` Kܓ I20 IBx.]&5x$ʢ^POl˞ll 3wM\d\ofEC2MejFS9ezlhA >P+m>v- giU1A&-ʗ6}BWm]mU\2zO% l^N4=`^Rи)%Tg~z(Oڣ絏`fD&Ϋ0> z# /:YtMs޲/wrJ2G& !qtbs@e"qMSJIoO#$<.`wqCHلGM-擶D`pxK86v-r?4AqRC bHpGv.M'f&7+ uۉTb)Ⱦh3$N~SH>5'N4 ĖG0=g 90awiZ]a|[{mͬ_qXڪmЮ 偤?' pj&{f s{UOud z'fXw~aJש/r;CLhp>RCBl亣 ⃅:xc ف XOF*gcʻAf'YbFR𱐩'vF\pR#qQ+ThP 8!q'0mԾ)DMVh@/nbT`<{=8m)|];7n aя{Hpp%%t32&1E\Kz64(`v ƀas oS$u*09X:>|ш#* x!b`v1&$z3Uut/>$$KqmQDo IqjY]hVzp3o[ֳZe =l )XюiNC} Ov@Ҧ6F60DfStrO u3@D+IÁjb FkyT0a0$7KpQL@\0CZq9<% ^c h@{1q(! !rzX??R@ BQHYWOkM=ʈ=XE}vU)qQˋc ps_ŕٗ X.bcA ɱMy)B-%SR'tԏ"^r&|h'm0GCX7<1>&Lx>pÓY/ncMCSp;fAQ'5| K2n8 J%dM@;qQ= x,@OֽSkqiRI<ۊ ^A$rb|`q^EƓ\hko,GF ^:ڠm}7rBE… mǁ]IOFl X̬Z*N* E\`iBƨZG"yf<^`YS0][j-Y_{z~M,5/4d\Wb@Sب\ H3.K](J'~esCC\aKVxfg3Y5c\l\0QIeQlVnGe|@ MH<#9&l WbPFai`I*ᑄ4 f|(8_X>%d;o2UW ݚ_?0kasLklCAE){8 dUFq-0(F0n!0a#|tH$&IkqCoM0M&\Ǡ~I_`|Z-OWBel3N\E{>9$,[dJKbԜ3v)rc-wqEP<Ʈc%F.Ru#KߥYU\Y*C DJ_)Ԏ xfJ{L)B@es,8 =1^-ٹq ¦{Xx]xP xQˍ3רoav{(:%1/w ĆxG lga{)D;n]F,/V6D:O(2#էlӀbI7w(h+a?>ܿ(U-FǂgEUR>6{QDݱT=jl!nЙ%*E<竧,ҐvI'&$-BHl G.TQ3>0Yм6MgΥ*XrhoT0}ڕ ]Y( }a23ې<.r/oT!pr r!H.dIt)m%=#7x7*( wǂ/#;"hQ<6q̀x4HE!bUd|wf n$ c}z$8D&\e u|> 7KQzpl+W%|jtdVqHj:A.Sktƙ%qbv bsܹP/F(aP6輮;:6vn'#ßsBǹtϮ)3edCiMޥeHiyIT 0}fv*k]dpG gΌҴ=SZ<МGtViA^53LhHc|κ|>:x=*>Wސ/)Z[ /ږ_+k-Ќ'] xC.žҾ׹8' 9ӀuҾn{Vȹn|$~K}k- 2gQ'M~ҥ[t.9eMd؎I*n .^hnZ n1ρ!^y5n2RF+x褋GuU:fDχ7yu!E_WF D,Y;**_ZLj˫ᠣ Al5U/]|`1, ;J p& |D`0#g+YI t)S)FzפeZp87/ki^ <' m* J/=.%!eJ__\a>Ɉ5sz"Uwd Sx*Ad- ) , e&:`=4\5(s"&3e3 *꠷&1D~>Avy"7iÿh5qpwo(^]*`_Qy LHTשY'UO.l*'*Vh^Ro>iMTߪ7ߗ+=U)G?ݥ!VE8")4@(RX E?CZbiK#R3bsq,86HJ%\'[%L!i^/ o -;C[ Afv":F;t":" 'v`?jk=SE[|…/QH!* M\@a8Z&_6jDj;P8 H{kӂWVL.X#uKM5,gcǢC1mWPjyEH;Xb PTՆI( Gcw>a7UDҬL ̖mb&tݏp]_]+` Ŷ9\8 (p 1PL[kA:0` %WHr5Dxthw9Cx6%J Ǘ—KDza|L2߅"6T\]]@yy3&Z`Ab|z.aACMؽ&wm@  *.A#eRj"Ңh}Zܖ -5UiVuqH8SPBW3P jP"b1v ¡i[{7p¹@SUdnS0A#S7P>9†YDQ|9d!}1pdm?ct/m0Ķ 1nޚzmKYø 𝎬^MpYƗ5.^98'۟Jq &ԣ6P.D$4 v_T!o7![,=zHT A8mZf܀DaVT>6Xq,ܚd@ DFc6ZsWO倛3U~arS B,zX럤85YAuxwn._ X=k#(*Z;yoڗLvD&=b"1+ªjr&XjpqфI=h#d j`DzqF AogZ(G-бOKB_#|0LF?In6b(O4gi&O/w~+S+/JXN$XfeŽ)_-}cf~dk^?ْ訩{%pA֭n>9hyf9όe3fFd˯>b^TnX}ho_]ܭ/vk_{~x9#r0bk0zC$` tA`sCCqa?)p M|O7*$ˌ(n$yYb$K"< ùyr tv(<4%]XZI鄡Mɞ+%s~X-x[ m~W/.ہ6: Wn hܼ)[%n ;F&Ntcg,r}u'+4˓exx^h^]$%$猢?0d=?%=̪ރ(6GQ GݲSO*<*#,ψ 鬍?MOCДeCP@}Gs݌' NHx{ʉ5ʮ6|k{,jg`ۑDr?\kw r#j=zʎEo_Z[= !k'-^Vo>:;e{>{YO3[Sg2}Or\҄Znܖs3MB/A}`9}gܔKPSI|Hʬ']iWIq2A7?[/عwk2$nzs,}F&@?Q<>$n Ųeef89ͺ)6bx*,E!hJ޲$% YQ@zy9&w?|[G.\Q?YFtSy@ &7:4t@4;eR<,(0\RgmĴS934%4>c 'el1g{κrE.GÊ,\=rȇ_XyC*]f i&[_xb TMGؒZ|FarfWo=7k}M<}ЈϨ(o>TcK_LaGSH 9S`x\ +Ir [pIFݝG?˘A כlgg_uml>y5}t&usZlw޶K>x_ѯ\ܛ)GXaN_p3?#8 ]mIc\ܔ+7> :0~'TgHuݨK.q@$#?4e > EJnJ`X€?$9R(I*7CXX;>=+;J}IՒ[ɵk s޴j_ME}+F}O4k{29G;.{v27疁ߞuױڪ/nb=g=C^{q/1%A5Se_!ЄS<$$KI))(Srf1yF|{гr~}F&⁎10Ϛ?KHZ`=#E'MyhY%wyhg/ _mY2!aɚ+'e?d׸NlL豌~[vywbO|v'hT߶^=g|UjH_z4eJ q_ 5?7bӃO 9fOy c,Hu?Ҵa/غSy|[5}=n?Xh|͙?)2<#.")VrSI2(KxxGLo >M*:ju8sup̢r9;G>J3 kti}N}ʽ+K'+~3>憖QхW} zDonIkbѿ7+E1Q,SIVF_S(c(%Y'1H2I-@Ň"ro㑭$}n*}=Ms)hA_=rnʧG.Gq?hэ4]GuOьYA|4\7ڈ[i'cS3?S B?tm bѿBhִ}C#I6GɛCW!0P(n &L0a„ &L0aDTP1ripasso-0.8.0/testres/password_store_with_files_in_initial_commit.tar.gz000064400000000000000000000562541046102023000252070ustar 00000000000000{ 8Um7O*Ry^{u[7I $2{bpG Mc? ‘C""[VYQ߫B'+,,$##G'#'('C(S?#A:$HF{qwRqCD(ʲ +/+6+)(+*ɃWRTWTq"'D/OyO1vV5ogѶ\4*OyrDDbw$ܨFVd\fud%KV5%&*M>q+8aɅnJmg_=8gwɰMkNvP.0kSeT;H2{ q8k6d!DNglEa{g1*1-.n%>od,[^G 3Ro7/˛iWY|E`wSsO>o6^s~]OFMP2J\,٣CQn"ZIk]|>I/(iJwn0'꫾{oO*,}DNܧm؍5%"dpDb3L08/f *u ǥkBFnf h&9x#^}rQh|4Q*EJ_K2=I/H19u\ǷLE6쨼Ӭũ;v77m"؀i*9S>?yPnňn{B!.=Ԅkr 7dzHrekvZŠmV7>sܞ!qyh%Q SԺI\ƵS jxE7_[C: ]XnOw /2+dcfuMƵ;xb'+-ul,Vr8ӛab;X7<8wa'N,7#̘yv`iZ ElZ_kF>'mRK=qޓ@5[FL/kU\N-r9YlG7*ӑ L2.o 5 S;Vs8tC1UINRwI+ER 4VmulZ˶,Ǧ┛` oqX MD^mj68m`\XLdEmj_y N32gޡҐSg&v7|T2O׳/VgtW?40Z;{u AY6թ~.j8Øw`1] ōB 3w2\G'8戅e;ץAH"=ID{Kw\ƍݗRzkVfS9$$5qBD>-{$Uhϥ5|Kuxro,,TmrԤK;{e-VӵQ1a;8{9En1?_gQ40+C5i7$0,)Eˤ/d[7ǣ>|sF 6%%&6R]5eV~J\9] ONkwsq`@xզM@нBuJ[;cIbZ83tYa+J\Oc&z{UĶ+ʽZ<G$^h֨/}.ƵmR^? >吽Q)TsN؉³2cj>3*,|J8BXR_pxcuK' >=W}bsU;l _NJP_ʑYbkn8VzGKMӦgFXOǺo7x!+X46ǵ9=/dռ+ ]-6)q`f=NtrRQ٠'~LGxDŽS9_\kЪi8@^ }AXVF,RYc=C$ <^·o zKm1Jg4'#b;Q Y:ށ1t|Y-*$~AG3EXhvA-Kt*]\sZP5ަ)R1NR= 065ڸ&=*o^up\[RcaG &f&;rS]mֿxJ0-PzbK,9 }axYC K.wonӦoNZ[R 4KVyDZ̗c|c^˞''4p<9f91f|>٪bKr[Oϲ|KtKE0ܘm<|A ^"+\r=ZSrv%>~Rv(=ò2o>޶6UDG­c0TfE e/^4\Yn+ڻ/+Qg[!Po7Q8}m׎ }[Ă8u''y2~{k5oeOyQË=uEjkьU˪X A'gvmهz[\pdJ,@/G(xŽ>-[EN|iVUG-m{|9n0\SkcJAY 6LCPR n[6)oA~??r QQ}+lBy_"<lۿ]|e37v*AO&QY=$ΆQ%3{Fz^`3IOU. ׁbt)Y|i~pfuSgKxOJ[['ZtƹFfUKz,yorI?lBWU~E'i#i~umWWO]9llSj¼0 ?𛓻[,$_z[ *{{9e_)L'QW4kmh>6)g,3aк1,0ߡ WAcc[J|"oޟ#7}f,I޲ Cz!PDVg3mJb@!( ger(qITBkn3zW<Z0~kܡ#լƭdžnUH;2 k}I4L;Q~AO|GlF39]#8CO4+7:t.cС m֪] ?5f;XRϜ'eF.[RCԌY R<6nwP_Úo:{-Yn_+L<iꟲga 囚2\䟘]dr6fFG"asL/7eNȇZh:D?8~_@eD4\"l| -n_])]WmF.޶w땞t/_~dqi 6=Jlqs ݝ{g4˙v-ټɏ~E!a/'jxdF丨1Ho7zp쀗+ǒL QA,Ļmw(9y5̇{=^ƝQn T^mf;ʃتy] WZ-NSG-c:]q:'bJ<,Ēׯ:`swѵ:EϢU=nf[Fy)Hmn_ǣs0ȓ:jYVw q]gopܑPh}{@ @$@ނ$RߝF$+4戁w+T hiI`Яlv$E&!00v&`PRPhv-͏Wx:x$P$ rIkA6 ܣWT9buAS†,IN+B:Ma9, ͣa+kA@h6)ymCZ@JH –L ` Ѐm7B:BM'{@.jh$DB-fe82 6tEW4 Z q$  Z؎}YVjl3@[dqE<ؤXڀD3ڋ Q DZfWb / zE+FzDR] 0AC#p}#7Z0$Î(6iDz.pO-*+HIIAn4cR0BK[j7 w\w#@U TxvŎ|ܵk;;kOo={>(JD$U -HQJPPDH(RڂTQhU|?{3?^wDf{}~盈|P?w6\jO;T4b3ԲcbE B0ȆhX!.DX9tPR'h1pH$AA(I`7Rc՞"/R0 a`#@;ifG 6}P@VY 6 K3h:mF1 3b,pwe!ie^B;-~fƬ!</~iHeڑe *z$t(&Fe΄i Sh%>m;D= Z3F+p'TF{$OF- ?0ٮ .v j! a#B_d;[(_`~!Pս$5K9G4X- S|Xz[pk6m1‘,"EW6HY(v{Ћz 2,D>)-gM{%l &{;@SҜ -hO/ LmCeo/m1FZV{ݷ8QNqu"-R:c1Hpc:7N%贵:`1J8}%(Nk#i*vъ Q~Җ ᑉ]ld͝4exYPB'XrͲDȟeAL$Bi:-W/*W.̱Q<;:CX3Pcn *HIkKez>k4[K 3۩b1No+FITۢCUX`4X y¦C-F$)f(ڈTWV !wNY@.! njY!ԐUolM-A5"v F a0&у^]tv+BFPS2mma- Pt0G܇n%E04Ns.2ð tA&`2@d6( 9bJ#;gRqXVNA0p1rc~ nMb,*?t,6`.;A+Q}1w*=``ny]H%{i[lB ޺m aKo4ZGƉ"AD 햣mY$"gc(˩bNʽ%ib\րȲX61Αja+Ӳ00| kg|i'&*\YY؇l Dj6m^yv\ K2wfyUd'~녎j q]kq&DdE WA~m [lB]6ݒ/zrB5 ^-P_ { MjO (b{z9Xi6lIv\k^LFauS&#[+YP^izYeUXD@$2,gkHa d7l3m2p78Bֆ3':HW8 WY;ioZoُY++ emy$1~zMRØ> yS H|mFL2yȞzfmnT7W7n\ iꒌI?- age3eߔ~n(וVrLSWm/}uvT3Bv+/\:ssL7WL׵FF @诎;הȓdnu"AF"#}b*6fLMbt|n˂CYlHes1OUl #AVug lٷ'q?'$c|DlրQUWTtIsLZQܲCEqݡpfىtL#T /~`!V$INy&;\#4SBЪ {˥, 8m۴Mq5hlr9*?gC?v?U7ULb^waԞ<]]@>0h3x'^Ao7w ;tۇo BC" 3iڊKoGg% 4=RG$>EC&35-x! }@=/5Wp UhMPC$;dS ^ZP\ ^|d\6roܢaQCJ2 (ʚ;Yn-&2nXWO(ZWwcV" \nH!E4o&VɠLq桜p=;g jLmu!T/VxItXxip2< հ\μ;KdLŖTi喣9&-$$KԿFG1hv/tUΛI$D[s*֜4VL$ynH 9DMv.gy?N֋(qVc%IMGB0DEĄ;"*eŌ9 kʈ{:95ДW='feii%&GL# .pRɩ4f'HJ`n-Ip%4Im:XnHXa3su_xLy9!yTpv|%}TR]&f:E8"zvScKF#JQl1k,gCx-GC$Ɖc\w:|Oσ8ܑ~VY$Ɲ1AV$ 2\ev[ݚ6?YOI@j=O@URMk5\߈>`{ #Eud6q&v FNOҥ]9ss r| Wx5\*43YSk!5AC|D4^7?ϷSNPkU ]4^ ݏ-&b$2ũͣ{ה]53rUpq5ry L*'#[{˞"m<ٛ3Y>\0HwIHsKFGNFZ=.52!lUMjF ︷Hp]`3\nU]K&!YR{IvDϟp8^?dG֨p5Ԛh+[ p ybÂc|z[~'7eιY9{ ^ o \Wbmӛq(.08ުEMi8̆Ma]͓ohciwrʩ3gK̭] D*!/_qI;&"lwZB>OFӅ,6(xԾ.hriŽQrOU|!P `Cl)ѫ؈R>D=G2i&Gܜ*^mYa|TztОUO ;8)!|Rd9*" ߖ6MƺW0J=zt]lzcw؝?Ť'&qycýcWĵw*C=$:\cJT&NJRRx\*;rErrܽr8r4GI|ŗ_8WŸW?f~臋?ݷv^{xgDiPĵ?Tde>U*VtѩW9_t*kTm؎\Yaρ\7zӯWAEQukWz?œ?u+X<յz9q{^ŻO־z~'?y~e5qNͼw~{g黿_ΑO|sx˿{G.q_G~e/}Oї>O=/?x$ߺŭ=ϼѧޙz6>}ϮX~.gwP~sOy?WO~N} /x?gwys?}cYE{.ABBRa$DD!![!;AR@@nxwsp`g}>k)K]oI,}Z0oώU$nt9ރzF< /nUct`s1.1O gniBV*^q?1-*qm0# OH>u1.9hIRd;M/)ƙ1PzD %TC@̙8Bꁻ98'Jg"pDzdߦ5/<1G䦡F!?u,BvZ#QHRøthk/f<6L?2c*Rs'ZF>"|;HJ^0S +:!_*ol'`$86r.OwPUQb 1Bhx|<^rV3ؗm^_AeqVYNFxCͰ4_rr%Vr^W`n5Ngp>d*m%q%3<џK~ ^cֳʎ݌ƶ}f56)bZm<4GG{Hn`'iuզX /_|Iˀ֩9"I! O]촇^1ﺲگM<$%>XBD.B.m ({W`;ך·&؎BrKto9iMΡƾ)')}.`4k )i9E3q }1ig*D]^6v$2&c TnŠÿNS/|7$6rѫq &<   QYS=sϝHJbif8\ 뇼2;VH& kj lBd`-'_?P[Q/(p$N_WzFB,uF #?%woh2w,B1!,#!/KW!>0WQӑw}X.z=5aI  +7.d7 s b5=N5?p[/I>-{ d[\\;O/e{faJ)"UDK[|촛[-xzX%dleO/7?@DKruBa .uC:LeZy@*~(Y~0@4ދ1າvy&̽:ՙٗd;Ķ6}%FYx&7:pXa /2#X=eAH{.*xu[*S(#|b^Xb{9rMx#H3/wgs1 :DhJXFܣ\X ;R9G Mkx}@'n^8c1+;~D5[ C8[]~*p=W>eE I[xCEi`vW _Fmxyj|I@WlA\ZN@`g8w83ZIeKXU.c~iB+H+MOܬĊTH}<giKTK?lc`#@A!}.6n5S4q+Jz5qPB<S]ff<ŝ@/ 6\AJ 7Ҭg鳮bT0"ۀ]G4ON񝖋A{:RBNtzb:G;Ӿղ[TM}:h=v&j5&HyMqSU~$ v6?~ꍸzK\n| I\P8:>vH+=~-}l]5Gd_~x_Dq>;Sh]BBO>)L5u?ep;S"sJ[[ۈ* b#&2 ܻ0kFvY֭A6"pJ M /X_V*ҹ}~2tKsY2T#%ۘ3cTDwML IwqG?I0R\Ia ݖv՝3qwoœKkYi/kʲlGL,UݖԍhȢ@㟟'e1BLPh=Q|^&I<v?Kbq_Dr-JdKHA݉R8gs\ cjXvSP-$L|,6!, (,qQ9ۿdE)Zǽ̳v8TVGR׸?JVwa ziٶ_6oqJVfFHH3z_9M/.blI=Mj~矾'xS_zA>oJ ,bklukN\]&DǕdgdHwͪj7/2;1\ߣv,42j0ХEВq *0\.1g|iӲvU9Uj#! e _HY\ji*?L1"7/mZwtkYm>AopUp9ϓ/XT MYno*ͥ˶u^Pa/O<&-Jղ?s|j9NH_w* {X(4 1ɓ:sF[ޒ]ٟ^=>tJ[hRI&0ȶ(Ƒ:o|m[x] k?y;JZ<ÅH}}ik|8 ϡA ծ=[?kf2;90Kl#C:~̚Q>46NyT {.]㏬+pT{/i]W#$—Rw<;Ba)-]׽!Xb[$Cp<9Z z|V$<+$2۶k"5vFQSQrfWUAշxԩjҰJmH1l͓pዸc {W @~=-[<`ԸC[tf]`ӡh릧Ficu bZLyV/",* 鈴'Sی*5 ]vm^)0:fENhEWQ*FGgMrI7dW wHs%IA ]['n1Ⴧ|*/Zؔc|R,n ߑFdeVZ]Ŷ3d bXҩr`eit|_z~YLC~V5Jq+p8vO+} V-y!$qLTq7)@>S|V/͗p0++ &k{po<|~m6E;h.7_U2jϕNN+=~H@,lz:y&a+xCsRyLԨ6PDmS,FzCCR`R7ߒXѲzwCk=ikEGkL[RoWd*jLrB:R<47i|L;uʂD}Lӎ1jG6E.ZR=?q|3&P{`[eTQ6fmTʤ Ej<ڶ>p^#bz 4"AA A@@vMP\^}ҤX@@,t Cw>w 87<3$kkZQG<+ cBY*+]*iv<zSD,r+Dٕ0s%:mvU7Lt;#r^VފKlu:B(v`%4a~Ceu1%5wD4hJ⋫&K/ Fw{?}y6[)0X[B폎~V6`=h02p mF0x G`VVh O(zzQO-*,5P SLDPKEF Cm'} ,Ȇ3Of8#N80M~ʦf`Mo>1**=0 VMyN~}[i5)ad]Tp1eت æ0&ߣ? GD$ - A"d $(@bd0 YY6Zc%a8HFw}]L_3e1M8M%$Ϸl XLDN-lRb"l4./.qk8M/W##aLB_Mqs;O <=ThM?ZetvrW|`ЂA|X-G]oL>~@_M6@ ެ|hOw ] KK<$v}wۥuR/!ɸDV$2;fDFҋLchD5>U@ OҏyL7CI*?;V!/U$ۓ]aap]{swc0֕gt2ƺ{I8r,GmWX? *,r1r1w0#7-8GQnt}vԫo A&7ɒj sc/_r!g+ɢGOhvwPe7spŷo 37emW^L[j:P'~qmgf.Ĝ|7Cw/v+yneWOv 0 4wի۹7mV]ljv2oJBdܠ#G|lKM<[ hZ+һiK3d_dj$dDQ/.,*\S5o+n(u1>hSM1fH4%-1)s#];_@Zh`'Ǖ_u/3J MOk|T/0x qV`+^M'M<nM_wZs"x}=7%jz8KNJa qx Cq໰*c ;*: )[/jt[7ʵg ^Vjv4?UʀUBawOW^PziSn%L _u+;d :e ˅WZBAV(c=q&+^<ѻLh4ujeU3#|tğJ՝ hH-mmsocGV⧒8(|]Y0>10EYh <{Sq#!3g)2ӹkLF1L|udEbCM2Hn?[c ֺrYriˆ̮-9d1Q6v&&XbV^V1$ /Qm{1ƒj'F'pꆧDdOӗfv#1۞ЧrrzD^GsTNO;ڡt;"ELŻP!䫗MQ﬑ߧnȠ0 V;._\c[}lk }n/\~є"'&|{a{:>"U3DϷ,h#VvQ $|&$3KKIm^*ܭ YR%ϖ*aZ;ZWq׿NV4n^|\tzݥP?b*5$14*$ _z`"\͞E(|t;T*?/80֘pk;FJ,奛SG8 5- RϴZU >ړlhNXű*D=|˶틪Y2(|L"kzM:OqEIS>-AAr",&&LإPqri|:mwZȲձeKXT怹HF"N3#EYii)`G4wGb1ةvcG_{*q3% d_-o\,2 M.u.?/a]bS뚹bt/t&8RgbН]D"VΏ~V6#*|D DC QhDYYec?$* }HE'YD24=X m۽+{ObN%` :lQ8b2 Ec>%E)#'-・G5/I_bstKiߏ ~j66čh8?TAM˖ՃwGlEf$ --E hG<@;\ՉC~}g~=Ұu ĸZچ#6Pd&m]sc=ⓡY`8GlXc_iS$oeL3Qli2/}m'&_k謻l+s3wB۳ FU肐px'ﱲHַkQ4LʒKgM [ns0oٹGAJuL6ţ*pSRϿ<]7|*PG:߰wm|a0?yeH+EK|ʐ96ņzC_A!0ں_x*|\G@!wss'6!_Q2@7]'h r8x8Nւ krpV)%U0, ¡Pϵ $?_y`9A?U`=fsyPEmml2( 'Uf ?p ݡH?U}=Czu`ds"j`]qJn ܄0a76_?\ Ta;[L_7;A@ripasso-0.8.0/testres/password_store_with_relative_path.tar.gz000064400000000000000000000601001046102023000231460ustar 00000000000000}XSI0.w$! URY Kr4rwņb{캊w,X(fM!컯<;̙DdB%j$Kq!Ւ$Bq&q\ńur廂_~Lrx(ƀHο?X_pk3]i*=~i÷հ,SDjϟ{S:TԼ[yԌ^韮wO9aoj[ŷGٯyN{Mqmsǹeng;y|'k;R"omAwJ^옱#1bXm{ ?0:\~<[^]H {1I!9CQSf{~u ny?dZu``߾Y)lGE*BclXө̀[w~Yms#w]I::dx9}kkŏ#-hKLi2 Ϻ}`Im'U[dBz`|S]3~4}r3y>3;m&lsY^I^?/(Lx~'}+&=vpk{xnuqEc-ca;y9=BU[ݞ3mjmX锶N8n#wIu&|mu;'o\*B\g5Yhq-ag0=uu[{Gi70QT0v-62~z>s#TYzCy"f=wEMμSyi M+|=hToa欸aweG~ܢVpǥ(1-#ט5rVyP3 ~> ڬVcnbYi`o󧗯,{ ouWd/D*5 kgB >̷_IL}蠤w΃>b^hħVwԧO53,i^".yq6>؛)m?;ħF9zFOfo=o>LL/EN\7xI~2_ucZgf'ԭo뒬¶33՝ٰ[_ ۷_e<']qĵLj<5s&듸փiVGoN6ӤV{j:6GW$>ޖ]"`{anbZ〶ڛ\+aON=G+Y]Zڽ-gLߐmM&~k#͜4o^_ڨ/4^Owe艎g%z^ן6쁙 Q۷"fu=)O\&&)#'NpvEys}=к[ z u:}X:<7d 8lkN΄G'y s {w n9sݮfW;qm喯'QRcр7Kzl3;8σz!|kޜ,:'6t<$ En5n.ѿ80*ڱCAg}=ev-ZF&ϧ./(y~i>cO4,[vgKL+Jx6[MfwCoaɘkV#y>3a[960ؙ[wWsn5u&ڠԂoYd>ccop{ڇ;,]5f[8qsאF ::,xwwUc짩w\zǻeE넭]0!pN3)mlopy®਱3vĒ7?^q4azхճkk^Oq!eo^.+O<ɪOޯ.U/,FoA;,ezmZ u h_*{. ù ??RWsڵ?̓Kbf }>^ ۰z+GOɔ:ײܟ=z2Ŝ+5ݬws7m%'sЅ:w<0A#0,׹Ev{>hdnx{~zaӷ.v~zɮgް-6~w6{;q!4Szgli}/`xE[%xß]ĴEA]Z״}I^ݻ,mu{؝oikonW={H4mM.yWs gCLgŌ[.IBx?Pk_elK}};̛9ixeԠm]f&p$:2v]}PHk߷>nxgg5c\6>޸ՠ7Oyy^g@Ot1=Vu|fkv{iwߚ{fYUMVeM5n_:{\/83/=6dOoZ6V/Kji}W`aiрӟwgژOZ-Lj?1 wEeY\huqvoþٓ3ޟq~} F-<͵RB5_Ci͟6j+m]2}Ŋ9^vS/x7SS]աWǧ>2up߭RFlg _JYg]oi6Y]+ˋŁ[}u?{.%~c,3;6i//{B}˨{ Ԝ> haG_չ7;(g:5{ud*eoxpMí5|fءӻ+۴B Ƚ][λys[NB~uVuW35~ڿprJB/mZT^ˀek9uɖmUonSdµg7E^[L^':s{ƾC:kaͭ.NM΍^zFDžv\svg~99iaf4SOqW'Ojq,㓇U1$GYG_T_1󛡋w|/Uey//-?f4J88Um7-vm cMczIi>Gx?N];rg'gTz_}ОeOۅi=;.݋w-Eo>J̄fG<=^гi[!{Eqzc_1|eB~Izfl;n:Fܮ:gUϋ&M`^.3u_b,ƾsO.|5qCopi1#sn+hoo"ߙ;,[>bcVwW,r;ܴwj嬳QQry3蕜h>cیݜ{68/lŻF|IlѫN>uљ'M0N8PreGw=\٢ZƻSO:ΐypx#~O-O3 c:=~u].{o8j ͝C?`at]K'ǍZ8eN3O' fɠwla[p[̈)\ѤVwul^h]QgI1pX5;9mo8?_x_>n[QkVO!Cǭ7ٸ&imk#n㱡E1^wHƿܛn0h铯=ʽlNw-^T7j0퍧Ghao/>Tκη?]-xˍ)<Ӳ[bZ,چkVZt7An^"11mFt+Ί7*%X>Ytw 0QΣ|]&GS޶~e^MF\#͝7͒ -̂?&r~~?̚Y3⢸꽛yC[ZpX:;o~Sck7!fYW}߲YXisv)_g#'j^Ly`9?{jn7a@pPNLkX=O&h6]cIcI{'$/W/; |ٸGXɫ~-޷f~1S.[auO38hѾvee~O.6\53Y2=k"TIFfn?oL[v3lNyII<֌Zgxʥ,rܒ$}A1l.C4&q0oּkt |˜T˘) yI|}b7gpZkw.x!./ؔg9>W|TC:K/g7άoӎ}f?+jٴhOMه OplYEwd68즳.%obOo=tjx'OL׊G8Ȱo`8_16T&OU1(?'QgItr!WKOB@]*t/̯Z_Q*bR,ɸZ')_QFTbi!U1PI I`Z%=d\%cI[POP%!{2#R`B0R(՘ZIj"V /M*]Z$=Ÿ mib8Y$B Ja%&"ԄP ȉd 0B$K{DJ%x;EBb[K"TD!1DFp19.WP!B%jؚ F䱨BVj$N-0\* qÓ, u 0$BF`= hiv k 5n7   $!EXL*уF#x pq2 LTK3ՠ[ %bI,"8: mTΊFf'¼ξa}4-j QhW]`p0-!4pR+j  9!W 4) 4r*BQ" py,:A\  cc&Kuȝ?Yd,[e 6HHGG٪D4+|!a8VWi ) E`0` 9l@K9vzB#z;`V׈r[ FTڲ(Ԃ@+Du8 }feQC #E DDKCHX2a ЂO aDgZ@W $h.LWNJH}n@lr&UT fXqP$C@C!`X,!'IC!^>T SǤ@c-"vBf9djR nJZHKS9RƼ]BR"1KSxܜ݃„zDC dQQit R /C J> kTIH"6DRODRZLQA\`mW1zD RHy.hUJ  % g? sI/T<  B+ ~ OW}pybFE9{={iZ8lZY/"tSGJ`\DV BR|9jh(&$Zs\X7= ]!DHҤ;KX mK-\1I#RM3 ?'8ui"HQ;c0Ӵ ƸR4O% )%KT̡؝TtU/P@ZjթLL,c!4%RPxhH7ʘLC+Jna*jG*@#`jj`o8@}$HSsVED:& Mr!TWYo $ !E% ^ CPhyҠݐ/Ja+5dOyRw:n_R}2y I%T,@21 ʨ^+$B0 (g Ht0)8I to B ZShj-`ړ $W8(bIJ@ك *gOzX@ )Q7H)M-#H̊< ,z vz!S  a%tI A 5 !B<> DQ0*WNz8ҽ%I V>xXz61 VOFpp( -B@՚Y 0NKR?$ +9 >&j 93AwJ%~hxՉϫ5(ZP#jd`ZZ'LꁐHE-b'ji=N$"83P&i%>MZ<#%p6PP" !lܘ@yp&$pe 5ET ?AAaH66 .D ! lk_!qSK P FˁjDh5 C #eƐ%0:-@gc+"#*ə]2)GU/BJxY@PNUkW~PЕڦCsDAT R&5Rdg1Sʞ}W 6(S+c2<դ2ڟ`P5wʂS"BJ()+Mg\LG M[O a*f"Z@T|\ G() ىm u*d3(Sx^[Lad3>Ty)?ԂaBY*4H:OB)1DK7Ƭ5K[2 &VPT'eց_ZvP: Ib,8 lH*"cX2^Qkú,1Ek㪥stCd" "Ns怽'"(;`'j2fK5 3Y !NAC&`*3B h9i Ř XO! ƈIJK' qDT%c 0N \hp8:[(BjcsКflDɠ={-I{ϰ ^ hthr _Khl~sZr#dw#<Xi٥$x yZXVCO>A c^a%Z! &ina0*5vKnBa (8 mp@MlEtԮVQ(/RxM Q ՈRČJDu5R쥠N (q@0 pu\#džg;t[0JKBM$ F`T.Uјi@s# EbN/r(xeLeퟂ ӶfU c2MڞWR$0%0;‰B_\,G 5, .L98#MU"fUR/-|%b8P_2_/)dt3uB&2\"jo%qy? `~&zAzIzۇ͆5!`!] F`VCxVi8p_ER18ss_QJBƩ1;! w~% pBL=E0u0Oi@t Dmc"u+bPb6 PH^CnZOeT6(I50өH $ta Kuh#Jf+HȔj(8a( >֦_GQ,%?&Î Lݠ]&xnQP[9BK:x8Ƶ(A;H$*%k=3&FE +~ Ge\G|ezx3 |ڤ;ObR n]|aC1.i 3;E d$*RZjZ=(a@NZ1C((ЖAТ\%TP]iB`,Xr$f:Pv{X|P'.H܉!С0?YXFA Baip CC艋 D 9 oQd,i.a_LAK5]>0j5OfC F^SIg ;L n0-`4HP"@!ڄqtxAxF"Z>VChJÆUA\_i:8AZL7;hz aԥ2+ گ.oDDARh!.Z0mBǛRVHy(f Ɓ)ycVT@n'\F (U^OLhHT9k98F ;lqT2xyF*cс^x&2NץQlձ@g+>_}Xt‚aVA} +& s\ci2v^BT ?!H̲p+2 _*eeDWr^M"0;u!uC\w5h`00{,3wޙ4O-I&4jxx"܎P( $9̈qurWp7 \t0_x]()E1 `kΣL 'pT.%< `+\"8`8iGRC<3B?RD =x`tK %Jk`@!ea8W$%ibZGM{8%84%F-ETp(܁&*\;Sh^L K qzd*^I\;=Ө ^2tu􆠌sfLlГaʞ& o; p8q74(1^VR%(Qt!Gtc  ?DB rHPL֐) 3.d`4ntS%Ta~xpYH+ DP(8 ;_a!z7TЖ>`=cX0D v`YZA2#J"Azj "!h8<ӯF(F 0&\/ U&17b OYV` ;p%wI#k<8]Eѣу;8Ng{)lvhG-iKOgX >AK/-ѳr<7?̯lMGYat=G쯻tDX p䧕"(EKR(Hj9`i6bz#j=pVjJyhg %zX6BR2ńԥlh?]8$! %~e LnH:$6QtA5(?`5Z;*nbBU$IJ@Gia<.з$_Bx-TJxFNmB$Qۚ:M0o Ӆ KCtPb*|ae ʟR'B2+4K`=TTKP% 9tjaS,5Gߛc009pE޶&Ħ@Z2ki1DmK24%@E4MA=BIGk%0hi`+ \%6.\pIb1hTI`% ݵ ,^P,hp0p]ztiF|Fx7_acpb ౥v2hNEkjV,0e R8XAXP( 2c0KZ# A"Ɣ24:nZ!B-xfϢ>h~tc;& C!r̖tD8V9*mc%tFjRC`fKl!⥟0PE!RfRa/kSЗ!]^K *eTN+H5*wFU~M1xT5JR޷:id1 V0T@VZ Az& pX4%o] e8T/)?(\L_,q6CT lSڔ=r@(V7yCC 7%H*wKap[KPT>3" g`ODJ szoUR37=P7E1;LU 2P%A( FjLM [ A]X{钒a9@V$edGyYIł F%m?TJW2-2_S~V uV>CSƂ7)\QN0? -'~T:zTI%dӱ*]* C"τ+8(3/ 0sMѕR]: V6Eme0lIoiݾm^N$aRPg>>.:V!+((!\؂;Ʀ2Kf$*0\1z feσ;lYer}ݿ+uU*N:]D Z%1g:ѸK@PJQ7'`s|7n/)téojoQw2>r_RŸujU9Sq_^nӑ'F%{mA-UR)4N*z<;3ZY뚺ܤ'V˖@H\GR3BIZ mMiZnf<< WV!J'޸-6.?v?kb^Ϗ.Lj p.hHɟtjwQë_3}H[TN~zu4qBFg )w=1Kks_M!rՒcQ+Q~wc=U?Sfo+ǯ5+N|SR7D@ ߥjMp"'3wsvr܎B!Ͻ[G!;bq 3gnnNU/))ժ**b0v96}wo^@j:o_cviIQ&;/Ҋj?0ϟ;J n񹱇돽Xi2.h_Ube̟$,|Üu/vu{my^a豟¦y.>^$Q3N8"X޺`1?"YSk#m$u2^ٰVX$/t~Td{wY4P?qƴjXϩy"~Z)r`j_j-?v+.]iMJI(DRDfHCI "E::tW EQP)wff.,:wq/Yy/IWvԺU`=1#4%zILq|qcg*3(@Q@u%{umgǟ zflU ijbk\Mn67c%IVxÎEL>Ʒʁ&Ӥsj}rTY.|3uBf]K %dxypGq7Gd5WZ.`;Q9wnJժn$}^{R!o]6]PU߇鲋h"y~6F/*N[EMQ\O)Def;SoDkR#JW'%_k \___YJhck%{܆gǫJ(y%ukhztr1SkGg1Gp%jZ aɡVrA9 {J::9|a^Zhݨ9nea8gMo<9QWO6Ȍe m`6_jx?L< !MFw`ʣ D]YhAf;DybڽdWcb/ yѸ҉ &j2 K ;׵Hkg+q}EJfym9˅N8o ӟ^_oCm+q I~:TrOŊ1XZ[0_oTT:RE QЌJK^ؘRWXfʉ"SYCц2)ߞ 7T.{3O Hh((qnzƔYM\*4Hw{:rxH4% ѓ9~̯#zO1:澗f %C4b%i%?%Kl|`*F麃dgj~h.I{ea[֒ Z..[vj՘hlɻ0Y>f|ZqPƈ^~Gm}S0zLWSI!OoQDIt~o VQipcO9Vݜcέ3G=Z|?q+@\*1=KOO;dIO&I({c6nUyT)^??Lt-68}Z!5^\\)6\f0 ֛6vv[H (|1g_c|?ye7Sʿ;To $|r/]NƂQ^uܷ|֡ "w˿v胠!+y(fYwv9඄CUа[W߃r"Wp|`$gKB(,DD =Q00A@!ypDB~K} fx|"Hcp$Og߸@cۛշY;B%+Cwz2tA2/ XW :G|"c';{Fče `sކ3]W#(+`Vv@j2TQ,\f`>5@,w[\Ymg;F)hDfPތr|0R q)4e 1a\n[ 6Hu7DzlGWy[[}nGRXs;P95Vda(/Ir6Uf`Q06-QLp" . ͯS5U޲?58+"Ea8(G BD-aKB 8, XGU=K)n`zM>֥ ~^'#S@$lbk+dϘ={5Ź ʸj8#e'S\dl2n$D>29bfvD->=Py!\;?)ֲKHVzq/"ܐtBJxp$lQ(bh7ψ4] O" [H2=͌,l}NvyAx&2 d vVaޮH8֤Gpc-krNIqᖼ˝5o| .=(>q7Zf9d hyhW+ ?/Q(@$X< Ucm!F[ !?{M{N) )z :}T٪]-݌Ĉ#w@a",ܠP4r^G h m~_GaX(UP,{)PRm*uOaKo`HcB4 *|Kt,Ԯ%;ae!f+Aa~2kS[=oQ"B˴iwַK! MonZ2a*< 0.80 `_Ck.ʻLg- L{rīw󬘸÷IvV/I yŨ }WA%իXw_B;^1u6wie_oCK*?]S8{7[tae*}7|Fְ.4%hEĐM.lbyUz&w&7ʮ2`o+,P(w;'{%ARƲ0K۔ĒSF'm) ͭ7"eokRzbԌbu]txmtXl#:# 6;̩3kw [;(:ZQԛ.m w]k-*&7ݶ-7mqFz;&(!rL?n>BR<:.cub0tՑ^i9 1qɧ{zPwz5J_ /#qc1?V3?8cڛb{C#]~d@{:{?pg'}r6ۻ(ԦӟRޒo}teLOݭIhbvg9M ܠzUH(GVJ9P^X^Z̨+Aݩ"e=\lUNEṊ$&IYz‚Gu& irr†jA]!,UaKA29o/2S?dOAq*S_",gN]']^Bn:*:F;n+Ƈv$b's9T,Wp-R'K w*Lsݰ>lYRhw55XUM ؛/Wc"?s͌dFފ;I0؋qfT:36_T2Fk=m}C"E=Һո3o':FίEt!J:o`֫ ^%K'$1$ΙIu 78c9rt{"Ύ0N'`$_]s!A / ۔[)+InSeQң, \iIVMw|[])MN^9K%?Svj20HG@T)"RC*M"t! *(BUzE H. *( H&Eܙ5YW]ì{<+Y+ds9}v1Mc OPƌY/{߿wPxc^*Z[BE9Lz-+v`nf/$Α]/^e!b̺ɮ[8oPl|Sf\twrmjlݒ+a ^i979=߫7Quð{6-_BVD"?M4׵;43ERPġ\_"YcY7JA<1o8{d'<礅U+.N| &)ܻ@=t+{>aLiㆴ:êQ27&Ō? M_au=4'<O7-'*Y\[vcT=t^Ul@ցnf.6K3'hpn7Pv&BlJ@f{L/UE*r%ES3G)rrN꡽RůoS:"Ԕ۸ljd7Yحi:Z+ۑ׮ i9l骗)RG ~ EtBRPA@Ma ^'R'XB[3߸xzGSŨ%RRku貼ҥF=m=)QmֹWd9'Kj,[J±k8Z\JMߣR`egJgTzbzc5Z[vdgB<丅|y.tYJ*eg$ǡU F<ŵN]RBO@mz %םR/&],$UhoX>w9zvb0@!4̵Jo3!玅IoQo󕁠H/./wYYjs j 22#/uQ_5.m53]3,*sN8se66%>"[6‘[%-6\$&GlqJQdh{ΖIsBjM5,aib.փu*C(NE;|Ci3$!N:~HLm>D4iWknc=ٔ *|!+9)]~H{vN 9TTT}ܩ*ب֦H2f$} jvgotq͙kq]Ӎ41S߄?&#6`, n19Xa G@l[?_% :;4*0\Rrz Ը&Y~GH? ^sǀ!P0 0 cA ŀQ?/(?_I~?d;U7J rck# @-s(!Pp AAy4kA1_'eZgTGP}p*AXI nPpxc X3z`HEFq2v:B myf\ﱄ2̔y7J<692 NHH&`;Mx\ĿC(?83ENrhQybB>]|+'Usa2JL[' BÔ1$jOӂמʔ5%p'ً?[We8$tLT?볡7vD'V*:'{z^kWyq7"|`]t:&M;+2 p˷FEx3bB!j˷7uLQVkNd`x9]m\r /KOK/8tt”ȣ-wf<@R\֣s;[!,⟑u*WP̖]8ɼo-̫‰  7y݁rOcmKÑa\p<z<*QqϤL\'ȕ3s>+i`gG虹 4Ȳ_hvA+.Z!4SLM4_/,J[9Zٸ^d7G6̒*Gcc^$XS)XDGf=F?+w|&5P3.ۦdwSI%+Fv_<`{Mk,$7UqS'] dh=."OAguj"ȑEa}އ]Y ۃ se:RKm{"{eHrzOԀWwByeƝ_i&WljGx~pLfe*I|5Dl:Ack'<].}\9A[mXߑbW0)cqY/:W4fk>'^xnrhպZH|joHCz,c)/+Q(Ic tB*E'<  ?_}n:4۝r] LF!+(U+ar>yً\*ǨOVSB>?p@uԚzyf<l)#sFʽ^7ᧆG ӕ@vB˘#eӂ_m9w 'bbV﹘n7e]!Sfz3z_Zh놳WGrG_y0y_mxwF`r9(X6w랫(5a^/ Gr)֋y(YTs˟~$H:'G@,TA$߾NxGuGaN<[2q8)oXp89˟끋mexP 8 @գrB%+ @AwGՏu Aw+  ;5 w_ Z_a߾Z&; }"'?jLj L/e @D_@\BO|)"s 1cN`? $$H A $H A $H A/k@ripasso-0.8.0/testres/password_store_with_shallow_checkout.tar.gz000064400000000000000000000277501046102023000236730ustar 00000000000000v9Zw$`}$ bA`;JH HaYCWl!'Q` ^U3܃(8zU<Ko%+nҼƂTٻwWgGmtb|b|to}btG^ۻwy7DA}vܳB׾jLo3/v2ud_mbUʅ^lz=3ս3gƪ;sM ϱt7๛}+uN:{M[o|/>򃝯?v۳gGϽ3>_'cg}=χ'}m?W_s7)w>'O>Sx }7=w7vp?ǟ||W~]q/_t>K7 s'+w'7W'gG^~;w?~j/8W{xCOmu'.{-/ӿU/_2}{|yϭOz{Mso[^xޏ⋿s_-x9Io]OOv?uk>t]ᗖxht)b96xml߈gJT<ۚo$/q߱v :6hSԴ]%b?C`[^BYN(>;w J P:QlX*BȠ&TJ%-y0c$i! ZDlέeCBKҦɖDS -Θa(Hj!Y0uKކ/Hy ǧ#.+ ] :E]E4`G]X4yhD0c@ LqI &YLD& ͅF6Vm"%~&sؽb$۷O,yh4Iz_@ I"}R4g<uYI2l0"IsEtٲf$ JT`]$'.\:LGl4,rۊ]S##$4ȮQV97\Tμ|LG-G4@%~z@lZ!et}UF;Wb<\hIz!(MGaҴ в }gUIXsnizY믬 V"7] qڋGV+0@u%# Y%j'Ns8&R=/yx@5@¹T*ϢzQn$&mKA4bܴlg;X }ϑr{?Vϕ6eHu\R+F]c쁵@찧ۈ ".ӕ%{))B]&Is(ɇhƞ[ʋ4L K ~(eK,ؔEZ.!b`z1%"zS1mm xCOӇl6n/i)=Wӵȷ᥮k#cթ&Mڸ n2ZSYT0~Se|J4)@U5ȕ[ #e1e e<fFǬ0T@׏Sc@='G}FeӻdDt(uE4j2w%IL n1Ow]k !$:0=NR/~<)ԃlƒ:~\A*<pHf&|Sa]<0j|PupeLKA@*i@u }N:њ ޭ1KXxmV?"?C[<3^ߎoȳYos\`pTyw?XXXC!eƂ2 h`Hz_ɵMq5S .zDI3 ͦ 0E`(KjCEWMGp qe;؃X#잪95E 0~T_B6({#qv=;Քv' (g&%ph[ 7|[$0ӓ+BEvOx-dKDWG AW*W+$X1Ҕt"aQ=xDؓI&CGR$0,ÏT:c4:@6 {nDHop0&CYi&r="3]{pt:+@V-3蠱ά'@dpL3UlLA[%-h}snnb&aIH9L! w!` ?DIH)˒"ѿ 1{D锷u$ F2Q>,!aq>3)6@+N>6XCa)XJRSP^c[2IDE Z^f聫1kH"Vy[ǶҰjk)ӪqoZ^עG$ :J5l o8+:ؿ]r!a Ak [=܁}"/!Lx !_Aɑ죂DyBdÐ>R~_(j``,#-;҅ƌaA|$trgL/,TZ̋6E JԷ)UCdEKd.L i9Sz&Ҧc6zݦk[F 90{]j6U6 Ov.tď0Eڄl݃f2G+Ct֟$.KSp;TL(2dcAǓNz=vqYx/HmtyF!b.|' DHuKJa+8ӡ@ъpT$5 ȼ˟5=)XEfd\ne62@0TuE$V9Y,SCk O._d ǔV]2[<]ZŹXT^b yT$#f51,>rWLU1T" P83Lt~$լa 4rxL H܃=jSa)cPܯfPs]/sgRC๦Y oq p2T Yjz&(le x!'suZߚg ])v,&,MVTWy1h7-1|XVYbm ._AUw ,l wn\Yb kry#V7h 0-J8a!v9ZJ0)pXU+;(yQ O2"ȟot5O/*a]B6U hq;=lE>eШ%ujC+p-VvZ)k;,xK õ+˘HZL"#ѥR" ,|?(PQ ~薙O `LE )fZzxPl6]GAl@l@ՒPO(E!G !$$AS ף*t0zJtLa{I+Ł؇Fؐ eHHb?V A*.+J]wC"13P,&BʬiE>MT(bE59D; Ҥ1*OxQ'9(!Pw:W=64y-JSBO#**ҋz^HdTFWg;g%/}Vuk3}=U [d(Pg(O7"Ҕ }@˺njS}15GF&/{ՆA7.E PCFCI2TDT֊{AEV 7h=quιBXsGs=[t3GDRS~fBP1|/',%դ$EqA@}oS (p+BWF"O( .4r`?(#"~n84HBD^ #@}A fSm vcV!GrVhD_CeK#*w6(5ˆgapߦ( QQͶK1T%ejo5Z^ZFx*H2#oV/P%PH#'FP Vy7M$B>]hedxTL/$֤S#CQʗD =X!HACo'CI55ü y$Hөn+9l4)7%Csoʃ$Ts "d[/D%BuT ]ͪc@Q{LI $- b&& i2!e824ߊIQ<` J(t\P5yåBU&QC> 9G1GLIwҐ@ p t@ %aC:PG^>.Ӂ62R1iaws#!ƔN(t PkB  WY%(2cu70:qN# *lw2iuZ: RDlOç !^1:os >Caܟ61؂6B1`&=ppC!U)rfXJ‡!^SL|ӺE]5;PoE)P8S--^3hh,e %U$,ka0d. V@%0 K EDU1`%dy?PRߵZ\v7IiG`EF yO`Orj\SlBt fmT*T 6_ 0Ю(DL(n^~ý<>GFI #CM1D !e :׺T)p ]_Vu`UʫPq0%Ua6! aȆxZ)#=1f1-,`B# u mMM>doMS{5@mU/YC u7E-:Bo8YgUgV'?=P{|"RY⮠!d/ /p:D*8 VEH{#&#Եn'?7Fݐ҇:IFID5\9:clQAATḊ]``8?A" sFD#J*QuC7/l&[kfj(_@bB9 T?@byaŘZ+n,(<O(n;OJ !, -W0]Yx *a: gV x$1LD/@^h|8Lf&J| !w FhXPWIHzXX@bz 5rLv-RItN&<&" bDK`G1sU (s Ŋjqbc!D#b]B¿PRݪWdHF: ZFkjrM93sFT +""}"j~Kh>Y4`Q#ƾjpԯh犦_uNd}l5I; Bl@h<5WGY3w kd WU5fY _x&)4c(/er$ sѨg%T~/!m0[+"[akxSϘN8VfN͸#(ɹ|>V ~taM(|ЙuO/)L d>PG^]XnꝐ-/v]u忲4|5,k)JǛ5bajǿ)yRhǿ _~6oBepwtz9r2֬sdr`wBQdʿF</̆pl9l!Vpm9!GX3D" ŢjK)89\x;EcWo*.IfT$5}7ߺbmG8;gc-S|ONZTvԚ73Eq>◲K6I7]J*Q-0Kgg=Z^5b `Nوu=cԄzG&h f;,?`m(,inSohnNj>}89?pYNJGYd.߳rݢmh9}&lZf`rW+R8ɭWE?Ǥ~I7{w^½'7li1]ӧʢ~"L"6kf={%<)FcLeR-zk3*OkuNm7^d,Zjt:CY֦A+C]o6}AK~\$`mC_i~3ˢAK(y;.C~73X3# lߊq33 g{+ sCս5VeT*w볡,Үsdn ٿ)v>F+8'$gۧYm5pCfCIm^[?L升.偯υ`QjR,wۋfj7?سC{C,+ۙ}|c';0`s=.RKV QLoM\nKs?ĵzi/,uAg`W/;6kyr񄷳Ջהnr|>'(;gɪ%r tєlMޜ|]L o1X Z+,R#V9#wMҊ%aW=b];o ,w&$tbg*E~Q[ogF;l|"qL{z[lڃqyJŘ++u)*fX (N\y~~=~tWv]mZL-qXs4{yɿx,]{J7_b][+z;N隐|iיHh汫ّtKyc+brSk>O7j=>]edSj{įn]Pi2b\&V3~s]bWN]qF=ECu[ %3=X!,;NϏ-_|{!FtuϻrOY}_X}Tx|~'{%Q\rh bs?޻هw2ILc։ ~Iq ٣Ԭ׃N螞ff9x3L>qLw2񦬽d}V pX$y`7y[SF5n()z^ߚO,?x7hʐg['|{M{_cDy|ˆm 'oCxzcX-gE-<?[cmxȞ==2ٽ4]ƒ˅]_:FT<{㽸e~tysXr5¥܍-,w-?tW&6 YK ;Vf,o_b[QSNximCގ=p'x%{;7űďWW|NkVz~X(GgnÒim+hM偂=']035njLZg=u EY[D^ߕiܤKNt7ߗw}eѲ2gg:r>,-w^{6yl9W Mǎksm124C?$g=o߽`񸕤u噳iҜW+v558&0:Nķp[sfM+]peꯥ%.VL8G+ιNQRcѮF?$7m+s@sI3M w"nkߣMϰo$Xи|YLI?PD3fD Db$I5qlQVEEIa6JogX^I>c9ςؖYvqěoVOKz'MqG/`畷n-\_jӒ8{>/nyw_$PIko^qwI  ? %t90^?Be‰l?lS6gWf?'? d06 1]ht7]@9y)E Y($D)!t^r~j9;eC KIdt?]9"Aǿ/E;HMMGF vAFon4.$hofdh0TcffF?#?hhO@blt.'SLLe ZX' 12sȌb`@zP- Xcljf6z']G" 1$x >dAz|'s$sKK44C eajndfhfdhbd1:: F(`Q0 F0TJripasso-0.8.0/testres/password_store_with_sparse_checkout.tar.gz000064400000000000000000000402531046102023000235100ustar 00000000000000; xU 8NstQig.p$b@aBuJ:4I]EqEQFǙQ?otuO>o]<q\q;@:ݼTWURU4膪цɈ7IQiC8NMix,ǿr__{ VWp^W(@ʏu|FȀ U>켞dĒ19=PeԒY)?o}tv?_TQ뫬X[poG|(YEzyy G@y񳥃ޛcNx3일|#6ߎO^qkl­HA#|oٻ&>pyCϞE'=vg}Op{~9zSnߔC{}z疘Py+ w2,8_m9qr[^[̫ Oym_3([ݏ|x~#?_bo78{*Y~Wi/m~rkҖko$U]| KN_#OEO~xoW;>>m;[>4Y5>Μuɦ=[n+X}c=_?GL<5E~sWmwrշT]` _|ܲ~%<7KvTmzqoqCwbOx?~|9V녚[Q=sطlzucv].ᶟfۯw¢_᳇yQzOL6>w|+ 6oܰW>;hmImՠ퇶 {3wŮIjN>8OW9_=-^f?4砺gsφmukw]Ͷɗ/UV߸p߿`Řw#)t\qLJ)7yϗ{Gn~qm}h>ܽión}O4|+?PKCW?Vl|9>$u&&n bL{ @}]EAdV w4Ʊ(;u-q|9&ǻ iǝ4xUfP}e9y?z?McjvrLf 51,ݠ^rp_p XQ9`(mڬ+ ̒i]]GDe"HSLnao P,e*G20 B8\Y3@B=m!!)f K,I٨`·1B%(9+ rOgQ2HE2L@n̎[!h $q-. 9:1*$K5!lJLe l%&r~3cV?tt?Aus,OEEE}22h 9Ia"EX&JAO7ɛr2svV&1 .IgI7"PaDnԫI!>ƽmÃe[u(ej3e܆ivH#G:5h+ i#w-^e=4ce|?@hdhCqhj;]{R3Ԍbri &nU #zi\+nеu\ـ 3,Xʒ͐ 0&Qh]Ij5]k:U7=)lSEESfyv93.<=`汦}heg$Dܝ$~2{?'kS]hddA*)v%/Tx*+, JnY5Qm0r*Py҂>\Fx}60/QLu`/#FE+2r*Sa ut*IG^HxJ5Y-aWa`LkC88}Xa NLM=X dȰ8!oՙH2MGص릶'u΢n7a|L:{n#WA:wjx&Z Ӟȸp|/BR}Hi[e)q2B#,f-jFs1\"!lxIt# AÎ:n%i'c*3 G_=.Bn4-.Q7]3\~㱭LZ˺D(;JU$ ùYΉw^15)jt[i78;إˉt%EfwEW?--+b|_'KI(;!Jݨ9ۿhڿk}7CI7^`x}n $[,d )l7n:x> hʆ^l~YYn>VM4)7HQ5ctSTRK QQ,dAHR#|-O"*7v/,* (F’1VHow3N{|!_A<#8J NmAHI'}ZXx5)i;NU2''u)p٘1.ƨad!XE<-,a'wZK֨J) q2K!efaݫ]jY%xgg!GO9BV M=vc;u҄eHU!쌨,=ghf 粕}ksd!0nuȎϑP1}ΥهL(*ʶbA%Ί,D,񤓎m<lj{Q2S $Dry1 Ԇ1˽ \i_ ϗ<~29hS6K[ =Ab.3a db’%ƁhӸ(܁@9('y*Eiko8dbc%TB4J3^M2y !w'2lB|_tJFٳ_A^N2epvڄ؟g4DciFx x5(pg+L:S bADyOSWh L(aO5$OTrWũ,"<DD^g\ݵ=/dJFMFA\Mo4j('Poq <20 xNP]K^PB+?)dFbn 4A`AԿKy$[AN$2h0Aq~J mBVL%&S` OTS|#PaJhUtk Ē(\IiJ^b2,3uDq'J Fs|n/֚EYGU㇭ŏ8TV$ JpxLvaA0p 0n0gtvw1LH\D|DwF6QXPnxC'nAS.}j`G=3P"H ^$ 6RkA$"+ QBt O'r_3XMq?-DRCBn5^<ݿHH!.* {! 5 #`@+/p6k⒯N_8 j9lmnRwLw,XhJ& kDUjǬG j^A[A8v&PPЄR 9Bq9&2*"kKyY: ?5غ~7?V#*V`P{[TMnv G9 R?cyMbWP+QhGD(稆(w6 ȃ#q5uu?"69^E.AJ77|jHZ&_ܢ|JR[ix"1HI! 'X2<('ĢÂVu f\tz<պpC%Ja?rDZc(rp;% cs+\QNLV DI?#@h4EyZ+@vTsX` ‚ N($S~r5n4(9ĈOn?":],/:_~au;4+߸|wc2&f2C/[Nںg>SwR͑dՕJÆn(߸%sߴE4GmG:nݻ/ʗ5g5,tu~6saZmI}p摟=%Ys]} ͝F0& n%;d3$bp?S?ll[wmWc׽t{Y4#ӞN!oOfl=g0nÞK8v6A?OqahZKeFƠxrrG%9/|u{_<<1nb%R> +>X) 3{82CnkzK\Wv]j^Xg2Iz|7jT3]8qRGN{_i^SON}niuO-RR V][jSAzk buF5 F}ghMlQiH4tk9b`p'O=\ <ӻZ/>m|̔wg[=XZcbln[9giFhA Sϱ& ~-1 66>-٧w:8{Y\}mr;ML>\l5?w/v_J>aqO蝝Kfj)뽡nTl~fxc_IPMyʺ"-yK,nFߏo-b2LOi ֘7dL5+h4qznZ,-+3j,>3?Cc4Qӛ$>7IԻt.pwޔgm~eew晷8A2dũ mRf}r6 .xБM_peO YZ<7η[0ghU4gw|i?AhǠ]zZ߈<o?gic֘v.SiF!noD `Z km6b͔^?M1Ovҧ}qη=ȐGm=pxCϬo?v;My]Ő}'?~N|/?T|dыw̟pO<ngMnKOyz;#oyM /+IԾ3wմnΧ9^kNssw/ǡd϶ا/ܧ醵 o,Fn}_p^]w͝c ܟM,<9IW=TBU[f9k>~_no߿wރG.οoKC>:akTlڧ *޿yƞZmd~O1QbƜ?gҙYV-XΔZzVo,k3&>?F_)V?J KYS?/::l?mV]=Oh6ƳW~zܙVڸmT;u!ЪVj}9?l߄ۿn+Rn|pw}_ݭs̞Xvy'~u}6. ;nXudێqO險o_Ӥ>*qis#n95^;IksG[ɬ%T5w;{Ў'ky5?|oeӥ ^:} 眸{s;.z{/e'3l_y@MyKɔ%Y:97IHl96JJ3SQH(6%e"$df11k=n|}پaR}'&i,_W0 Q<s9J]#&`AHX\EX PfX@bX!xaoGZ.XxبP ~h5TZ>22MJNʛ_Q"2マw}k̶A/OϺhFɶSͷU-qyV|JNBP1 Q*zvqyqz#12gGkО8w'g8{CwZMZë ATO+l5Viy^÷g} ͱ@Q~@{}S Wo:PK^u8ڈS<6 ߽) 5v1fuZ m _v}p<7jYgN{6f+OHΨN4gV^ T k ddx'^M [딢]ǍGPk7$*tfمXprft/'q>nĺX׈oߋiy=ozsye#O&\*lO*?s1@b$qD1˥  P$/8?? _QQOѝ)2A"=1 ODZ*]jzٵ?3Yb}U7 4ffzncهϒxp/bJrX FkkNuݑ61N0_JU0$':遚%wsשz[.<88jLWbR]7%%ۜtkeŽ>Np ޻8=ٽ5s;&@Wt& vΕt{Ykkۿ/'Lt@YyE\+BҹXim 7-)qzfc9KD_[dc;%6('wy]D)m<'cʜ"~/U,"R`X@O $/$FHB8R @/}QG?0h|ɢ/J+ןP/h_bLv7:e$m4{Z(,2Uqgj=r*WBn@GтO?hc<՛F׎|-q­)c,J֜#վHk Hv(߬mu]?̊Ztb͖V3f]5_]^?+tVKKɢiZ5iFk/O}ﮢ,2f帼ݞvU ޜj}gI*,zVGX:bv7<ĊūwYM 9a7]Gl菢0c,A18$q90( 3t<8`B_ |Z=vi\m=Ƙ s, :A52<Nj<2 L۳}<*Wf4] i%vwn^q,FLo[>9I~uH6ϡڞ.!Kz^ΪLV:Uv,0!ʆv F66lRD)waqq=Q3MjKguW~mJ6284 8[Nu}-K\,Z)e_]O .#Wgɛ T5U$2JEw׈4krfi1V?hǍ~Wq_W~l9aZҷ}-;I??KB.90pR&`¹45?$pYg R$˒͢$Q7sP<`0@g3eȐ޵#);M_&jݒS< 6ܕ&\Waߍ2wjj ,ͲqlrwIw|pxʉFo;wb¤od߻Qxj9/uk׬)_^RQq.1eCcTd6.rԩP6QEx[ՇEiju{4xz*Ӿfo+g[8\I:(eƫD6zEM^k\ډb+M-Fϭ]q7yѡIeߪ PMׄ4*bΥ/޾l lbjڗEy8J3NW~tL:Ydž_Z6j滅Cl^?gkI|\@/(REL` ŅHQw _>m(1ukGZF~tM*/$>O,_ ? X !1a(!AsQ/ sA FD?!/ ?~PGq8 S4 R0J(2?a_VTj^(Ѷ㺜dxH^eN-S,hPv uUM˙7'eRAy`}W!}^vTſopHr]wZosDFEڶe["J؉?q,vHY8껨Ȯ6yIM̃7(q3]`*Z>,h N nҷyvͅDţ3̈́}x  @qXhabAcY ¸K!h0DC jO _1#i/*Oysu ~g! |0 Ϯ;\@@XfM?~ѼϷTD^;A*Tm2vއcdYn>fT}kRO>zaE֧"xz%S4:c*@aqPB:!ϩу^T;gzEU6ƴMdx0yy1]w9%{X)ize"ݬaq|¸R^t>'Ak8d-fZ#vh]5 /8zBUfx_j$Q9i4g}IofL׬ Ohm25Y=udFɬ6q =k;7{F.*ꇒڗ.PZtVk156;mW8( MB E(á ck C?")/rW`!{?(iCs~u%h@BP.2 ~^c?O?WUE@ ^ rb}<0x8D;U}QWp)G! !Pg py _߾V޲8Wˈ/$B.BJDf0 0I#M`>K,/P<Mw=Yx3VIeBѓg <@C $<)xĐ ?x/?8W`@&(&Qgd,FE $Sd_R6te|u4l\|UY'.Τ]羚 h R(pR(j$ Mi ̈.[enL(]ˣ2J1DZ9i.*6/zX&9hi3wމ_*Ai,ml6mVCڎ616'?C j{}/G ?ϱH1Y+f^w o #Y3ʆS}k ԺڀwOyEhJhripasso-0.8.0/testres/password_store_with_symlink.tar.gz000064400000000000000000000600711046102023000220140ustar 00000000000000}\I0w]B @BENqI6H#{ Ŏ {XbQl8{$SyDd\)*'U|2E*3uuv)F\GggG xuq:a?*jR+1H+*OU(~E [,cvqq9jߑsWa_dF\'ƿ RUGσwhW'Ae~YUͻ#~2`< D;g?BIKh# [tEQ1o^sn3ս-ֱ4_uԓ]~0α~붽5._`,J1U^#yUQc>N)Xhy8Pֿl76aq??qev(>GZ Nhda ;P$[6knӳ^n*9~ö0/]x*{)ϞsC2ycՔ>׼S)?^랖}Q^6KIׇڮiNviskեgY\'K#R,kuꃠAw؅m;Iy]e{v/oj8Y5B|-O}#tJzYdՇodrǁM\x5㾿sb_])$dXf/- \wy3or;!Q^9e}yz,[.qbNzhrjF\[XX[c''[6۫Tyu/;p}YjlԤz721uJRݠqK}gUɪF^Z{k$]5[_=>~8'Nݭb ʽ;FrsOGet oZZ'rfz[b٧=Lˬ7jv$!=Y]2Ñ#-۪&w Y%+h/B36gިo:͞ܯp޷-f?gڦoŷLyM޵͞Ѓtc[ܞ5q U﫫Sׇ ?s$-q'[h~EZ ,p]fX,pYS\`>9b)3n_ǶNvQZqv?1zOO]ϪamxH=^ohkvrm:9 iۮlosw<8]j`u=b:{=ZM>>LF5ք9i2lSmXq޴A[׾~Q W8?mڙ3>]{'?O= D nX?E~'GKltͬ7 Hu6>=zg:i^Νʯ}A<ؽ2'?>}븭{oMKy7tږSMb>k,]&/]5X/ۅd_vimُdm?/{o~92~iIK{5fs÷#s l7n5sʧOJm<4I%j{z ^4fY5<_T!zE^Y9-;j:YU-OgZ^Lg썓j)"m=z{uk,ty[y&trS-x N XvwSVV|ҾK rpf}^W?1z ze\UEr\򱫫=%^2~k=T+E-޻݋kk0]ٗ>wvVpglNK_ƹst𪵉!_#i虽}nT-}o~q[{{Qf{ۘN\%TJ?]c wFfYou~6/Cڒ2ޝ~^ǽrF-׾G85sC6]}i.L4Âj +d]0 mٞ6.x;Sc]Ǧ<<%i7osգ/],#˦[Æ54ܬŅevѕIB>zE k xB4ɤ!ү#Ե֓8n'~1PxIo9qƌ7^5Ӽ/l,W>}X v^b1>oK,|u#ÖjB76I{?wG7=;٥nʼniN\nU7?6y6XVi]^jcqJ >l2ߝw<;Q?J(/ya%wHX?7i$NrA__&.T]wCO4Yf9w+:iU귍S3>8RgC~Nr@{Ύط#]lpu8-co.3j4g2+f}bw] seU9=m{O8p!ijkSTiJ/g}뭵 xB_5IMͶw|]N(]2X6 w!- {5Jx}tՎU-mj z{B38;_:g+~cIl_?1c~mиe7|ORS5oK77OQpKI$5f7y^YMZρ2S{߆;kA-|KʍN^|z+Ɔt\}fݩg~>1ީFff0QMqΒceW&Ml~4cV18{faXG^[U_>ɳ! -Yyi/?d4师eG98ں5-Z6Uٻ&l<$7- ϽCjrЂSNĵu5"7~,mM' .oFz3{M?Mjsj?f>L K8QHnnBw;R[ᭁ~o[X{9tF.YǸ[M1-m<>?ͨk5_7son3w `~ ijj/UMl֙2ή/~Oti1uQc9q٫{riW5h׽GMW~d;+wVG.OOw؃gxj\Dg u>,Q} ?]Pu^r s1ýo,?|kX;`/(H*>=?Luv,q=6z]n}^=gI%6/}%lɭGmQ`毭嚳RVcx]FɥMlWRVm3b#vqe~5r9mG^8qSFeg798]}}FjJY}/);Ca;=w /ΈW ,v:5Ӣ+[4w ɓ.; {')~ ']k{.3i[upֳYo[;nRMؽqOX Ⴖ_/=mWq}՝g{2!L߲im\0֮ͳܭ?8_+ Wm@j^߸GA;"s96&)IYsm-m2S WG7g+׋ܬeûŵݲ'ʮhUqgymevnG ig\T4mwl{g+· ?OF,L=1mu}وYKGŽFn4`nr݋qX|ѝ/RF͚=l'L^j賀ulN YاԄN Qڏ(֎]F>|wy/ 32Do~cfJ~O{U/vӚYv\|7٩mܑmVv|phQ3NgR{zכ_^4)f܏A%Sj9wɽ7`+[cų _ Pv^jf5\0dIet߮:|5?n.#n}{ú7[|dg~{M i:zj]fK}aN|Cb^>y+\5˒Ս4ֵ g+oo3ɺ:m:(jU{:]srʑ+ H^WwpFKʬbӳ?Դq3wjmxW'8çXYuYcrybӍYCܺmlӬ?5iV-loyC { Ihܼ4y9fQU{6E13+Qs6"6X5dV޼'kBJ3'yV)Sgm.Ŧѻ9Hx='{N䰡?=||_Fv[Ԛ&vZc8YSf%װ{.xlNOR ﶎN}QŢN/V-}7\OƼ,~aV!g|a̳Ĥnr00pL>M_h4֋:-{WeI Li08bqׅk&|+ݥ]ol{>B^o/'qF QEXşݕ}%zL'7}w<Ձ5v7U=hB[/)i[gZ=ҕQ"|l u{g+sϙ;oJÍjwՃ˪s\SkO6;e2ܬJq-lh/kR3s1bɗ.NM)Z|o]u{n'ӧK= Wc}x4-j(!QhVt]`^^p3L!4JR+jh9!W4) 4rJBV" bqY :A\ j cc\FFHObcHB!WPf6֚yYs"9 S fW\hLI q8j·A)*YK2<$euy/Ih([e( 6{{*4+|.a8VWi bE`0` )l AK996~zC{a`;Zۈ29[FTX(Ԃ@+DuD8 };faVC !E DDKMHIXa ЂO aDg@W4$Ih.LWF H}n@l29&1bq@$EnGC&`X !#IC!^> Rd$@c-Bv\f9djnJZHIR8R<=]MMR"i1KSxԔݝ„zDC dQit R(!/C H< kHHB96X"wDRT QA\`mW1: RHbY".kTJ  %g?lA/T)1 B# ~ vwO[ypibFy9{={i8loJY/$lSGJxcX-lR)Wf-WzSPL%*1 o[1 "C표I-P#B[&dkعVcG95f6~NH,TӐ/a5D&]l*8|hҭ %fSy\$1 KpMpx< *Akؓ@ªPc%ӂdd0R~08!\O`bJG!#t 0Uиccq3!+@5^/{܁\qnTC0ERk8%P+)K _a@ e@&2P :Qr:]$H*eJtk4ҳued@z : ԷC1i0-R rۊ,X QXhY41QKhq)?ĥ ʼ6*2`SP3_5(P!j``f'LꁐWK-b'ji=N$"83P&i$>MJ<#%p&P/W !lܐ@yp&c$pe ET2?zHVV .D2!Q]\$K3yU/)?@9cj.EUx `\ p'PC'b@+ܡ lDH}vD%9N&~h |`> Тכ2R-K T#k e٩jj Rt`Iӓ$o DZ:MpJBs APо|0h2b p8:&S]MJ. [pEASNM(jw@LYI =`=iڢ}? U)5 H b  /pntdY 4hL01z@}:)3M50jA5$g!a1P{EֆUK ZPQCmED@{OHPDwGTe ̚`gg$KLT fLp1AzmmmT3}( {^}SA- P ) `"ȹxp8u0Qv'Ur]Ǡ/05وA{azMCм뿠W* ,D45e5F6H{ymɱӮK)HJ_#,ӟu}„;KLRMMToA`8fQjT;͖\) &iZoP q@$i]Qra]_HCF-7,(B"_)\JDuՇR쥠N (p%@0o puL #ۊk;tk0JKBM$ B`T.U,ѐi@q# E~~~GKJ3D-Sz,)C|ôxYU~LbIRh66P*y'XWO8QWp݁ɓdH!Ze- X8g麊ebլ*?[*šm Uu6::U%埌`&L(W8[%@A6D4./g!߿ͤP/@oP1)#[gٰ&htYL Z v`Ztf% ܅1sNN_'E)~rERl,D ]-1?.er-3utRRuesЁ`- I'C}\@!1z!c *MZ< RZ^=RXG UL"a$~0Ƈ!+$6֡D*#!U0X~ME8?X ;'0uvE@eDZb Rh0נe$!$Hh 10 CV(r 3|L s鍦a`hcP?HrXVn.>!C蘍a3;rd$JBZjZ=(̡@NZ1!|oKhQ.*t(֮4f0Kh,`Kq3HBS=wct`M$]@dNsK,Pմ48R I!R!tEAىWd \F(2EUӂ4HKTW/&C/A.T'BdqW S#b3Cr T X;w7$N(m@@ǸhV< \hqY4 aC@J ?i+4qTBg iv4԰PRPWt V ){h/-&S]MC)D+$N=ԴHԔ0 xmC\J (U^GLhHTp9K=9)G t`w٢, eq).4TH* 7L#!!K> cӁT<4rE谅,d{,UWM7j6ej31HgL}egWʔ˂5cPDW#`vLAj``X< f9 B3i[@yLh*C;D t !UQ FIs7'jnz#haP[ YPRb:GmJlOJJ]@KxSS06f], LdhK iܨ1.Q B|ಀWD+A:ΝQp076a!z7TЖ>`=cXD v`YZAҟ#J"Azj HkL  3XPs,VXO܈J4`H#g vŔ*50?N(5e;V5"9hzLs'0p=2d`,DQvk7Go`Bs4s "mLMoձv5fFR;yc(F05d m)&iJ i8@i{TB }'8J`ВV@Rm\(za!dE"612Ѩ9*J@@HkI%X< )F.(8X> (e< xn>\/ G $DcM99lQԞ-Y0aߥP9 Q- o;JePa:/F@W`D@ )ehi u=@ܡɡ<18- y@CjػK(?пKpo0k>*†cZؗ"w,kA~!dJ %-Zp~ 3C5JՅHKI W/3 C|H:@TbY/'Uzxg˫:kJe?3<?g``O쒊ԅު%?og oznbԄWDxeJ S-P 1қ J0J&%4dS RIȎ2󲰱c0.AJU忯KqMHɡ5g@{ao?049mWn,yChaed #/lam@L!rGGTBpR ѥ0t/L2èB3MƩ]*Ѧ`eCPiV 6v&޻[_v>.5Q*zkcjлBB".-Cil*ʆD kz`)JP [b\КU&pRmnJߑRVV_Q "Q]41)068J9 t<2u|b̲v⤚|ȃ2mhh-Pl<ҍjjgiWE-Sۿ>a~hm$A'1.\| Hr@%2 8TD H/J9Ib0 `~=zxI@=42$п,t4X@b]B21ܴPDlN S%@p`&cH.HtuhZ%,x(¹D ŤqZh D͗0 :-L7m敦/0/r1/&Ʀ: 9UҤvD"@W t8XdъR]{SU; b:\OLAo)ovKI9185KvjXaN WpodEPUKϭ?Ҁ?]G纺TB%HH6[-l6u|ל ŸWRbu#᎞5/@& *HҾ& p' cSjG* Qa?P2QJ;3VR;hl(#ᇡUAaoQ; ӁPu/)JB՜~߫Tziu =Cu_Qޥ4 ߭FBU㿳TtqAai ;!;;VB=T6Bhxũ%e YHxɮFS?u6 ~bԋZ2;y .(ETVU*.?oP;8syU+`W=jQ4V3kd4" ϨqGWa]ߞ"X]eF81X*DmVEt?[LrGi+Br3Žlⷢt?qoxHӼs3wTdTߏ!b/ݏİ'-v589.)E1wh.OK#' 1wT^ ^[ms>&'՘d24W%7B܊7*99VRSifg+ǭ5oWyN׿_Ǫ52s: 7p(p:v8"3.hpG}KkKJrji&#%huq4e~YUͻ#~2`< D;g?BIKh# [tEQ1o^sn3ս-ֱ4_uԓ]~0α~붽5._`,J1U^#yUQc>N)Xhy8Pֿl76aq??qev(>GZ Nhda ;P$[6knӳ^n*9~ö0/]x*{)ϞsC2ycՔ>׼S)?^9v!-$4рiiH wP"(M*o|3/b͚ggw_j5z!oS3BG6Iŵ=1{\*=n0ՉT=vKdw|~όEMLIfm--dC';efuNa:o;s@ciBWs]J)2F,d 2Bٮ)r|i<3bxz7|)q݇r)D5D~q'ўXUI*2BYʫ/EiK :^2^ݢCVGjnwٮ)r-bB67?TC` k -swݎzr6gM^jhbNful #VV2I4Ukmb{[)ۨ^+3?{ t`pՠ哨{3v>w/9&/#TO _Jsh=TLHϗo СދhMUĈ~4lx\pqQvdL@Pzvg_=ʄߤW{MZNw՟ &= & ̈́#o7mp&.E=]Gw4-*6"S A #83fADx6&/9~+9*c~C ~xP(e:גXH gՏJ DX(u>D}N坞9#{l88)5DIq]2vH_ i8>]x]b2QTy;Z^I%{ds{(4tYZx |asHztk +`Gl_oO9\w!9(ژ`wx ^L*RxK-`T/ɭdz'A5,9lf Է W%S*O ^E{̺t0П{( p\:E=+/=g+3cpCh4\{# 8¿X|'n0ۥ Lt$"8;uy+$9/^򸻽8|d Ε*xasG-F:9SI 1;٨3R'b$o$-7gmX\ .إ~U«bwƁ/jf9x%A@: rǾ/%\w *г^ v1d@'('/.QmLU` Ѓ΀&黲N+jȶ !q >}^9^{AǨuC3c?OA~N_(0P b faP4?ap_CaN?xxa Pϰau?xOM'.nJ2^[C5Y*7n,T,k~d$c'\lsiq?٬Q:+ a1yr;0<$J.u@b#QqrjށTHfrS if,rOVDS!xyZtJXA_QoFX`AkAP;{N#ah _ZƤTœl#m߸i/}Q:% y4f=8)Ek}&~hWA\p? $lNCO@@8<aЊ0 QgC!xȿ?5w"xQoxZ(I-^mjwOS'=!r%CP<iTs+䅞tXqZmnz7YǃOr 3#X4CX(F^ x҆_?_w~I?#%Rmۿ&+s/S1z̸҆( ճS~|| ]\LG&E  Hj3z隉TRh(+"{8>~=H)*"8ۘ)N0K˵#zY˼$4qvAɣi2.|*fuJl̺0򁓒P~޻`^n4N:;9aUhm 9%n0tEsG>9w*0۞hz%uo':F.l̅w)wD[nTUXLVM7Oߕ 9">lDe5(v;g,n6Ɯw` ȗ.(eT87KK8v{8vI` Xxї[B\p!' (hSj%$NFIwWoղ&[6FCޅaXo[trbĹ4 $QCS6@M՚Zm717S x I$sx]xX?Utr7W|U:׹q`!U$%+jl&1ӛu"Ɏ!Ë[%AaAlqc䪁A8*DZU64,2ٌwjWfejņeB.42;뤽lH%)a&_@=7dQlr6W ŔI fU47Nn+IPUkU[,pplW`KWRD+BteUIso|?*3ڦP5Z<ۗ\e>XƩGencg_֔ښ;2W¢xO 9j&;[clɣ5^HZ?vhxa5mC^S՜ quV:M~uaIWދ C. ej[;7V8d.pl5_/ϓ:p0(}eDef]x/`:\ˁsٹ+ WFLM;?ɇ>/ jcliyVE7L5uʊUv}GJ#{Fp10 !h >@^|b}`2mp=-j =fD7ȳɁ-cFe_ F")D&7Kg5qINÈ }c'菀 ֏`JX0ÂL 1/VwQQrsq* 2"<=E+qQoςM/3)cJ}lL s8cy&:)$]Ė)2X$*Kd qB^xpkֆ 3*au Aq|6֍y"Clx"$ NLK9|!SN*)IOHi@0nSmje"5ސ$ ȷ|&+:.YzH{fVTDgDmԡ"РƺH1쪽aѣJ+ހַNy =n˖7BBzBiw*v}˽4[( ,{~FwV\Giy9*aYdA؂ҸVEoAvUY5;%*K%IFՒۧlT@B*̩Ւ 6u8wc)g
    *d^CkM]].Վ UQO;W3mjf 0L۵޷(^0F7sP]Ο1!`{e<8IXm_xnٛwS3M?ݫBpG o ?K(H?*f帳Eڞc|)cCZJ4B{/ \TGfPaיMy zbj=TdJ$:S>uin4ϋq Mw:`ܚnJ*Ťm8g7O%*1 4Xnea;P{KSw\5R6afutq^Oyn„>YDʡq[HťrCi:yTY]=:Rdwqʷ,9)V:g׆s Q 4ߚrK/9£\}}5P:wcoZ7]3Q>ȣ6'g(oy[^dw|Ot~4||X&ZI#fs1QMY13ejpwqu~m[6MA߸+KFh]!PqfMX. L/b xN>{~%ܪܑ/ӑ@X`Вp!֖jԋD<)k ^:^t遏}+S2#;C"HFEsOV={a|5(u_!_+`OWgkS͟I~'؝?7W8mJ ]D,늲`.Y6p\?Dp2EZKqbbbpQ XX8ZpzP:UR?ݵ]1Ji~p_ڝ6 c{ {bA ž={Èb'?$vS}//E~;DO6w;wDLx"D!B"D!B"D!Bj@ripasso-0.8.0/testres/populate_password_list_directory_without_git.tar.gz000064400000000000000000000027371046102023000254620ustar 00000000000000+/(I,I/H,../J,.O,JM.//,/-O,g )6475@aԐĄALH%E ɉ%98K/HL!P#g!(ndlb`Ƞ`@ .n.ƆfNƆF.tt`IFfQ  B7103<3-*09'jkljǿnO{8ײ*+(؝bʏodj]ϰTcf'Xw9=ifJiZi37muT6D*9 *shmyLפ7j癮Mz\bPvUZݟC7I}8wFS oi/ 7 3n8ջK\~}|8)g"{=^eyvg L%}yNgy8ņwYԥ<_m=ò.W6;Jkq$K38Z۴cn׈Z/k 5fp ||o?ohθ~~P8WdB@G?i859?pGj[%wu层W0R\E[ږEˤZd]sx%ɷ>=ǽVHOc _v{Yo<li?vGmt5|D^ήi]f_{׺Gκ1XjAK4f7;uʝ|| o 8ɉbknƉ Vm̰|=dzr r#~$nٱ"*@Ci9RUU3xZfQq -D76CO+VJ sgyaw17 o+L-kQ {'tn܍+=Y'xۻe[??6cUIJߗӕNGO_+Qkxu9)p3s>d/'G`v}?՞U/|u_4SƆPCt"-x1Vw޸HG1q秮JZtFCoGzx$wCӎX;/yҡꞹc OI?u]Gw K.e3iB%)?=漞I_}5Wh/ۄm]zK8]տlJR$s#۝{3 i9K7fx-cuo¶Jm~Q0 F(`Q0 F(`Q0 FuE (ripasso-0.8.0/testres/populate_password_list_large_repo.tar.gz000064400000000000000000000303411046102023000231370ustar 00000000000000^1˫gy7&7ɏM7}ڥ⡟kWs/~/OYyo|;KWk;dnǞ0<o8_;qߞs_-=~T3Уo~o_;𺢜h^Yg^/|^zo/ٷ_gx]O O}:?oy_xͧG.~G/w=)sǟ<}o`=_}c{ӯ ^3sǛ8[_/eCiѽwկMS}z\ssG +}_G6qo]y%_2ߵy*<۱v׷ʫc;[߱ձߎkw\Yx_Nw;Mʿ8ebt|N[-ɿwFPow ג믱'F'';\חB+-vɕi/xf=٦בjcS6,B^$ !¯{Ȗ%N)Uv~TjuPbtJFFD xp|Wʆ]ѐ eWvCt4}Ӵ_Ԇm ixk%c\= R H2B^d4?\a!mff:(rs{}vɻB9xR(lŠA/;¬Ȉh\IH`D(-+2 l1|2.0{ݥlmحؒY`gؾ E'6&6Of؎2lGakSovOD贼.ZҖ`.^" O֥]7E6 4ciUh:x&82;p8|L>B1ĶzEtj0u`*if&FVTã: r@:)@dsdi H%Wkt(Ժ&wC 0lB +dJ0 wP~%$f֜io>tYP6 )Ix<=R :2U)+' uPmIw JyRXhyD}CGNQ|x\z#aҔ Yek N z6h<7+ /J 4\K la%+JgKSJ|^pϷ ˑp#gz]ǎj?}*D<G/HU?s֘}ef?W"<x*̀bf Lb_Cb9Qa>#ʤ0,=VV;P ڨc-<18-4W3zKX XPFLSG" zN 3n(4F`ZTt-,vXyKӀd4Q 0%)znp(K; IM IlsE "~6b3<[c1sC '*Y".O٭>fbBSaYitl1o'ˣ;[$,R[ DL[Es:Bb3ul%Ԏ2 k^WQ:T5ԎF_p߀bR`wpS nHyM Efq, Ŕ/9PjQ\%8|6=AKi܃ Zg{Z~\؟ 0qj[+B6(z#aPZpxWsVL-J@֥#fdAC=nn eVOQNT;/,4SJ }.L4e#@$̄Gh]FS=N5@( GN@, )/j2(?(7"{NH=<՝ƞP bȔ1&ff$ ]~ d!惵uV]h$$c:Km *1#*mhvz6ڷ6jb6Adv@YpGrs#) DCKV73:(,}$tDgqqE%I/)MF=ިk lBҠbR,O|]7U:8*]7Vf+ ]S}p RͯiUp~Dg"D+G!x0䙸JC !Tk^9̲N`$Eˠa/)"RōU"ӷj>jMO ; $p1Ɓ1@ ϒS؍k0N`ԡ>xoJ-ciV!#fAy %Y08(4a)nADekxHČ^*! rEkNRVp~q Ahl +J bD'FP=3yi:;eZEa *lE^)|9wAUkOX˘V{%hzEDa|FM:3qw[|?M9'`;o;]¸-ͥ3#9c#7n6m;f-Ỹ>F'˃?㕝v\;*x1V.%aZQk~&oX:Q8jC> a <}_ ;:鸩mWuiEp ~5CB9#UZ$;}  "t  p u!eafWW7޲{r-׷Jell;^ر<D>"߷[Ct!qhbCi\A-'sb ;K7ȍT1* h[OcB MN{|!_#JxKǨ$PچA(;n[a8:4#m~W3IۑG֭*ȹZ|*kˋA6ŒؒAB0 <>jD,.d>nxTE٢4lWjYQ39>=3QUԛșS5 6huˬ0=|ٳ%ճ)a<ԠS| AU5S@m0[;dMGL1 ! +RT,(RW bA1|'|hnƬC $Ċά׈a6Ĭ…K^ zL&DT#5,nJG|F3%b,k^HcK"3<,FlMҵTIhk#3L է_BfcjMM `T  iyPAߡƐJaP73wuP`N̋3|2ʄV%gSӫ\kLO./R#N+.^-A=KGVI. UW%ȃ.5I1#>TP+ުʈ!*g}E=kX2I"_.OFй'uV%>Q&Z2 5uyK uxc|t&w\)М;"P"!C+w-3ȕR^WWD%;"g|3 x4CT5aWG0:v",VP> ~7Ph7?XVE@e^l4Jjڷonء:D\M 0I7]<}RDYWw5r4M[ȁ)yAiT!~&ґnpxޗቨˬ:7 12oIfjS{)mZ`I-!8_e]2K?dwFg66rh!U<heHG.FQ')Kp>9/xQ#|v 4%Jx6MD*+FZg"ZXkR*#^N_B^V k] w>X2 +>EoUR7Ԇ`G"-vSl5s>` ;Wb MCIl=W2 ·e%5و'g:mC$Rb ɦ#<.J#6[Q5qdYTW:EBpX"I1P(`ohz\%fOm`‹`B܁,eo^ð@JHH8(`iIDLaʼnbrf`XhM4=񅐕)w&WUZGjIcgdQ;>(zȮ&avaojk@#J-eC[63Lh tAڲva&3)6)I eD@J_dGY*(* DE@Aв Rx{$iZl_ I;wg| 9K<1+:_Gg^3< ~A3ዖGzPw-|Akw#^vʨqq4bt#d } =/E[iՅ'ʣPn0Iȿh8`y1 uF٘4D%"4)N4jJg0(f.gCG`QcMP9w A_p׺!Fyj?'tBۍfp9v~V"GA<\=t)/[,GQ?$IH?\BSQq̶+1l 2ÅY8ʃ7VCx/-"<7ç '}kK"+hCYa,c,l.ՑO0xXvQq8_$&VPTP2s 9kȊS80>&d*L[ho q4/ eg1 p!SkCF1vcGő4Zؼjf|cLpVoB  nalkbK5XFa6`!sգ@H iwa#i K`6JI}c#0Ԕ \7 #d2'1F?KR kNBCa~a z\ryȄ%Z DPfẏ0_^ (W>a9J:m  (k뼼Gx@pOMGIw܉icbӠ;v$ee 3|x9F= X">+&= ߂n6.jwVoNvuSVWnJ uz}4efkB|P]\v]Ab?%<$ 4;a h tXEkC"j" Cr_)\p7]B GK/#w0br,qNCrcQnS9{ a@8+~D@C+6짲V;FG6[XPTOG韞7;Z|J2'ғӄ(1B)تG-xy0ӛ⃊OEd84S9 ĥ_xAy\0זp+ &751J0dn/ (c&V&ҖGH@uRYCc v@ׄR/ g7haC/,-l8^Ӟ[Hrx)D)aE0_=7 )7j~ꑹ68 ϑA M>'k4x Kz(FY!ֈRcƩ<=6Z|T)5j pC@ED:_X@T> MQr^#/& a` l]_klm?5OУK5e:;!2!i9̓+tJs6 Y4\q0稓MBVD#Q=!RpHZs =BlM@ipag?_Aj~Gְ}/^-kքcְ×Ⲇ0ʚR\Bb-B` @5`?.VKiV@1QcUv#wdav<f܀DtS>6X9,ܚ`WLVy@4|矈:Le/IՓר0꿸,9&-TOw_F]fwoPZ=w|&G*I'ՓyqlL|ZRjרS?'ո `K_L.kԩ ?Lzh|s̃{1jbe{3W['EދM*7&#UM?-j5R9PLhxgLoҡ;YOz[Ɠnn/pskz{=;)E˙dΆqnCssFY\Qxiz;MYnn?)x׹,o/} ;.wkN wvԯ±a|6;OHi#8\Sibc@מL n:`WeZ:%@+N_pߖ/濞ρ ;cIWoPP|HLWt  5AH ?"HINkY5 ApJ%Jրh%x א)*j~'6=O<}f1v{oٗ?َ"vͲ._[tqIkϒYy~/{&]v?lWw8GyvwᒀE۰sGqFܝ'W1 Rz{%\.ڧZ]1oYnyn&wL=2'mQX7Fq 0wP_c(5jp+1-$Ziw?F8?LAi*jT))R D̯۱ߕ]=>i#ZF=k^M^LJq{wϟusn%S'N"q IDοz6'wZaVkYFGPZJi5j\o`5jSWǔ GoWe "ݦZ/^Hfϧw=qOϞr+-e=St [QٲuKwWg.kx$t_KFfnw4|N˼ o]s9;G%Thg%?fӧnn6tPbrKn8ڃal_ooߟދ4uv}5|q_&Zɰ$큲k>1ҝIμU7'jڠU76ܭ y+OANY7t|kǗ^N[Yݞڔ,3]W(܏9=;w R榼tcL99+燃}wgyU婬?Y8o_`/[Էh{F!L$ 6gG^vIWRڜ/} ۾?>_qk 3ˣoO{W}?w%E|y}U }Jwg]w|˶*#Wgm_kM>O0Wr2;[kwtՋ?uҽ뭾K*╽:Wm3[.vF^=J)5uUR%$~UB-fpF**j-E*VV)FRKIR(7 W"Re24k(KE.!ONz-NhVK(V08j1RO:0:5$QC%$I>&7l.KI$ZϿ ic:5#iV0Tz%pup??RkH~ῄ"t9ƾ!DH?V5w´ )A˨Uǵ)q1k%q8N?'!/ݺUme"loOJ:iCNNllYMgtqCڌZbٔq[v .YntZ,kٷ{O t"ʧt^ͪ{]>_7Y= LQMf{㼍̸{xwk50cI$KǕ%:t:BN 5 NjՔ'gJCHd91/n^oMkd6BcߚD h\%%KH*V SU-EIu,R*V6z=&Jo|'zdZewH佈sc3f6-+\kY+'(ksmyɼZ-]75O#W8L)ֲ_'Y^VckmT9ۏť?c/&޾M^Ţ7XLP4%V^`jk|oS]o.+yIc?D?tGt BPjbZ%4*=C)YLo1AMpFOZlf_64cJnT}wE""'K'4$TJF ZRiZj4B05?H-хqfv+[gF~hJƓ6mW[֠~{C̥/]O{>^ʢr!mWH|76e[?l16%ʤe%ZJ͜7rҟK~?חٽ7=slS^-15[B{ωU>åldzpfղ±ͶTrgf8cy]/[4icU9G+GM6I{z>})z-'w  Ƕ]f{Ϳ^]mb ~&ܞGpJmxĄ}k1}rɼښNQ9yE?\ z{}Tok~j☬˭6V5ޏ閕^:X'oϧTmV!أfWPj͹ݿtsy[5~&cLM ؊!7+N>5cxzng>b }鬈3_LV j=fD+/Lh6ïc|p*0c:^!{/?VP[W3לyd( ӈ^FIiFf)ifIi)af?͌5r-Zyg[UiJƾwxJ^^$J,PًX2ΌJH\u=1k-뿍G .L{jɁn2"ߌmآ1ܟ^_\7yEO9?9&K[7{o>(Y/8륚 mF??M?0 7]K'@J =Ij_&[ lPH%M1cb>z(`Q0 F(`PIQripasso-0.8.0/testres/populate_password_list_repo_with_deleted_files.tar.gz000064400000000000000000000356761046102023000257100ustar 00000000000000;{]vZ!-Uyxw}ew~qk_&ٝowǞk*(*)JP -(呖"(MBHJRTcw>'u(x$fgޯ"gKKܥ+.Җ4ږ-kjp환=VNmONl״( _m-óB׾^_xenm ?:˿>>F&GlɃs.UO3V.Fwrm5[\c3Kv[_|l0߼}s gw <~MG'tI\_پwMߺcS8[oء_}m_#W*7lz3yvvw_?—&۞x+ZK_uO-ztӿ_ٯb]7;,sor?#|?~[gYߟf3Fqfgg} |=s}O/myUv~ ,/ARpt/'%X1z K#+`Q Ձ?$F/2`p~VE!^9$(G)lWY,+"YFy]Fv"64̊D$7BZUQO̔* LK DF#%PQ+GmDG4 hK`9 f-PzSL#bA /._Vb9ywp"urFe8bSk<R=uzZRK֙T82iub(-b9,\2apj=斎?V~pII峢\vܲ+|Y  1hfCvl2i,HvWĊ[ ^`w&N'dV&XXjN`wKM +l` "вG d%]:-t$? Dg×-a2; Tl3qm2vq|bc+^HL`Tӎ|x͂5,0HG-@t)3>uÑ' ^\AAvP[ Ph" @`8,P,v`vJH2ͬ9b^}sV##'m%<sQj%z'udȏ3Wn@Nn"Sӓ:!苋rD}GGNQ|x<z`*Ye ^ h<7+" JH!la% ?(O]N) \g;|$,G9h@ U:J]gf/,$3(l3>:vXݍy(V/V0e4]jEfEY0`.FuotN-una݇nb#t13n6v!NQt|W)_b"8S\U@UHPq'0]ȽVp*=?hд\qhLVS:0uDžnS" u$NũǏ|,: vqɺ]L  u Z!0l&uvJtz=abRaLB4-odU`c>`X*a6 /3}5C!ob-W\اSwe x.oJ`tA?NT?1&̸i=W׵ȷfT״U^, D)϶”衺RvdI- <{bxҀLzSS1pX&Un,dIb!򱺝΢q=*A ThP]uU*S @ > ID4#~&fs<[`lu]1k 7*]"D٭>fbBSgYitq?"p࠽4CrmD6پ2 [GxMHEs$ \J>jpn*E (sZy|=Y`JύګBCW@n%+`.T҂s S.M'`# 9E31=xіalCEw[? pJp(@?Hɤ0Fc܌'dKtr#_LB-H!"Wô?nb<-&)(g2#vXIt+QUcmlvvlQV fXjb6 ,"ZI2縠T9Y @}ٲ|x (l\fFǝ-K-. (޽[,,1d? ɼ?25h%:}Ob-M(V\L^* )FJ'CWvEêtZڊBTf4#r%DmZg_(aH/1EsزJ4=*FW=rBc+wIܙXMa[ѱê~$3DdEëDn`Rܚ Vwqmucoc6'={;.D]`C|c0ߔZ0DzHoSF(K`DqXY,cR%23|`gSbFU/W̮p9Ƣ5p@M)XJMOhl +J 'FP=7yi򛍳CEc &lE^/|9wCUOXϘ{=ht*`6<>qNywqW H 6$t]Pr,l_jܸzm?nm߱d~s=l;*x1 .%aqk~&oX:Qփ8j)"> a | }_ '>zmnipA 5CA9#UZ$;}e "5t X(q'yA.ߣZ_ f=^_SaYVB(F5{#rŬ*bC,i\A-'LOrYY.-I6 (r#աdʂ0b1n BHӹ`A׈R%1*I-T(e qb+ LJ&YC5XOjV+m;HܺU%9~By!ǘ;2 3ߖG%sB.jb Wf^Դ)[0WMW-+\w&ǧvby4qML[w{M׶ZerqNJG^"mBUhfA+P#}U!;?/b'Y3a:SLB1aNJEFvD,{FC8ǁ{1 234b 1|>e d9KsxVv]dq%(IYXi< -z}daȬJB I5:;_\{ x:R) }ơ<5CIxq:O2Uq@+LOuV13v"˞ k$k]A"yB&'T튅j2bHʙa`#GQLҴW-'kSy/CtIUOBCg~<:~<3<>X^=]8Y+Qn/(H"!#"l+,TO9ՒBH8BRLX ;9ElE>URK6Ԇ`O"-VvSl5s>` ԻP/J$6]+a`@dٰa A@nd Tܢ-~DRQ~rAB=٬QsCE xt+1Nl9IH %x #  LV݈) L_xі>Y#@!0PC1l(4!8* Xd1cXe@Ik7!ZMO}!deV$_ɕgUGYkxAZ4#B)2&%XդI* uCYhCOr?42{ CyZ-'{WDS >$ifLJm)-ER"B*,c_cGP7Y " cq{gIӖ *oWZ;s={){EYe.t΂WJ:MЬWf])y:6 Jx:D< :RLDE$9$tO7t/:RsV u⽒F٘n  %m-լAgҠ*Qq϶('Wrb(|"*)*)Zi=q\b3b@tз}1BB!+ku{(&T]v+fwU!Ip)؊@5"PG}$e *3%;n );\[: Af M% xD ܟJ-~TzTE%imBH!O M,[sE  e5,&P/T=VN)ksylfStIeݱ >tÒak$q&<.]%1i2W-*Ya5rf :v-D@ED<:*>X84ZF8= ulhxغbWeŷt_y,0?W{:U:=E?q 3CV*(y,D$r\V'$Ixȃ"Qa v!Z+ҍ,y|ʗJZܿp^c(¾N O۸U* S$BZCeqBD[-a(S \/CmͰvF܃}VnGK!%5=KW{gAV jp8!~dnE3*JI G!2Cvp,#b`O Lk E(&6--01Q}**vH ԄZ8~XMDv8cڬN9V`Luhp,3;BHZ, }'vN_(2+,'opԄq 9JC9k@!*rm0 u`x,4H*[Mr\NlrrLՄ>юBJts=aENX'tZ8]xA3J 愡Mg ,8<1$ +F-A$U 9I![fv*+n,@Tr e Vy%7*B5nrX"Ué)]Dplڅ܄C4*=8`e(lElBqsޗ0WuPET}ʎf1t_%Oz JC+f:&j<_Z&Dְ,^*k a5\ΛceEdj"* T?EJ,$ZZ xLm1Y ER4d"aX@R_U jg HY* +U? ?fS o![q T/)_>?Af(G(g0f˚=PB]O8Y^O5|a{ˏ{uG7,wu_y7]tj!c{uSgМ-n\7ŠĶS|#2W7ݻer>in͕n=bO_cgdR=_/?yf=K."&AHzhրL,mK1` JRVHWJyvY}?}'O6S6}|zOû|a}n[USBBw^5/V8way_Jqtf֜>$O-^~_Og?}mݗDZ;W7[?N Xז:mϟotZKi]۳/oak zB So AqH3)IB7i BU^Joy?k]lg<0ۘ+NywU'=OClʚRw3[eg#7ތo:y®sYf켒n|6Lx/[ƌ3ŃX\?Wwn}h5uy+7%5n=o?cڧLzDɧYs?\" )O>ȿ)7fRO[Lㄉԛ7 fBp$B#"“zg`z2Z}K(<ټ{׌#%5}5?{s.6pz^j;vmn{TuN=4*Aȿz׃ J,? 8[(h3f6,8k Eoaq$8}mR I{x/ d m#S>MDmF/5W? FHf12, cd9e4ze1=[ۃTB<)_Ycc-v6 _k마mv5ӵmSgmҕuwƷe:NIz/43Ck(6(d!_`7E+䟢qL k g8`._B_)?Pc}/V^ Em+p'gj)OR|FI(ΌXڤg4fm,hB%9[6VHU__Ŧw&O$${ٝ|WSsYncq7{l H]:kdw뼫cѷ^յuB-ZhcL~xtQ浛m+k-kۿk[$?f|ڃ,bR H #VD! y1*X$x*\h#?%-_1$)< B7W$ۭ2 \fbށ)Ϛ?τE#- -rQ "%-_t4iA_bM(*"YHe|"Y 1ʼnI/?mτGDI)qd^H s h8^s^t'1Ez( odctu*Y1fĤq dQ!P("GCrJ.ZĿ0 Q`?ք`oZ+mmdKnL xgMτ' K T$ CA+^"E,!XE xgmx3grouw&W$O[`[?/MW$OY`mcu=-mL`gBKA6OغglRxY>\0hod۾^ۅI^emqm1kfgϝ.!V7IְWU˾b؆o8\!4:8\CIA߶~8g*w9mݽxECVí$KL-&̀ Or CJ@x "$XQTSXI@"EWr"$ IM&%=:RӖ慮ղG_]kFip۫rץLaӊ~da!\:16&Z֪3+KCگ.ĵ!ƽST#P{}5Xw˓Woé/Mps'snX9-zLݽpd]R^f]תu[Oeݏ2vChgn哲~n1鶲>zuisR;/7mZpS牻NQrv|>|Nٿk>m| dgvv>]/xM#ÚdN 5=Sȇg^4m|x-sN'~?b̢0rkK[NYsz}bP)ⲯ,󽓷tݳ(usf,dxg2\ݕቁW.6o(:l6uasq9v=0Cݗ֘tޫźYI: ҙҦ!nط^D%?1n͒;|"Kv;x0>c/ʎ!/.=38-,iSY}]vkbw禌đ‚φMlU,HmSdLyKl1cME2F"8DQb$SĊHb/hB߸G^e7ȿ;-Z_0%;OMτq'$Fe$ ҤDs;y/ZĿ0#$󿚐_aseb/fr#UwKbe{Gx!a0ֱ#7ݨЙps=5(_zń7l{PsxB=>w1ϳ'$&]ZFpqrmHkTy[ܸK}]j֣=|+ǽCbXSx1%T$?-τYDgA'i@dEV(Wr"E?JU;.36Dp8{@OjL`鹨ANjyhnnRshxe쇞nssk/f8n:g ++l{sDϢnO{{Fz&{lP{d?;i;VLվ8T.ܙt6cKJ/&?*>c? / !)Ҕ D R;)JHf\]r"E?Oޚq##u?XXgq[*lLrvmν#.Xr|S+TR홝;D$߹\HaUgl-Poב)1ٽj;l9akpgdcO[7Z<弳Lğ׸Gt;cOŔg_o|&`gBs`EN*"x2<#Ȳ@6@r $hO}q&&|XmDw\R)J3{u3M_M(r?6 ~'|Y H(FdiQ/|EfE "$" ogCHI AIÑ珆ZòKoN0Z[ 0]/䍫Z/ZOۮ(q(Q,(CQu1 I"hH=axԮ$!JPWU`%9"Y9dw_M2$%DP˪27Oē$2G2$[Sak3IA8"C!JRJs<@&ihk3IQ1n-&y #ѐd$Y"jfV;@( )n@GBHB*@pb 82 `bRIp@%qA6Q~{}מEE7m+aۋU|ڽ֧vײd6Q pҗ4<+t틎wm*Jk%RzjTnp?-sɛcs\[?A>x_vkxgC{^qK5;я};ܧN}m;[>v[~~͎ѼVZ~8']^<]=?g|߽^O~?y oo͍k>7]Ow??އ7?}ֹk5g~o ~o~Χ^>֓>_|mo|ę':~h>yhx _7>m}_gSG'+]o{?w\kO}趗> /wE?\K}όڹr:n3zW&O~}sion9&WrmQ=R-V_k[o].Tr56Ԑ' %/5zC5 T=ۚRo4 ߱v=h36ib#y"ӷPP}P,qB /t j0L@DK˭U-<My)Ci qdONStݦղ}˲ePF=ixke눂tB/EaJ@W8tf`Z5g09i^\؄_L1aEXbIJ80S{ -KVF Dt&_%[#} 0 ,R" pS,Ex r8|WHXqUK8FW=Aq)\$~bҺА|uZRsK֙ A*}}xUF䘝X8J %1Fpg5a0;$%O&ψrqEf-$@e6"wv*s!M#񂿂e钴ݞ<ȸZHviRKfZIxQM_m-Pmp\ad Qk :d%]:-t$? Dg×t>Sgðo `Kj?v&P6gl#C_AE*e*~"Vkma`ddAB5<2 l"#KOA.ZF P0oC 0b( ;bJ0 ;pQ~% $f֜g>r9P6 (I5x=R o:2U_+7 'uPmEw JyRXh #TF(a>zz"٫1.IFgA˳Qzky ) G(İ 5R[]k~jmԵU R KZ,bnf}]l`j쟳03@H 4No{BBBܧ$p*Ѷ1ف<[.ֲՌ HWHLl *=@kkEF ҙ(Y q[ 4N[!$ULtwVQEAgKUzmRM S+rAٸbx(cVT٬qTQT`tKMMSuCl6a.fK(gST%,sB]Mb"8S\U@UHɸr.^=5vW APXEcҁc<BKa4]G1z@'a23nfnS9oG]H]VH2 I=p;c%: .01|0`&!FX@7c2n0i>`X*a6 /[}1C!ob-W\SB!,cY@ާcn[@mBC2O:&m̸=W׵ȷfL״{'j3-^, )R-u)ICu OKYIHj:&y(r(ZV&Tr!dPÝΏò'2;%p%%*8v& q^ x1PAz v"W BoYNq3Ko/f4&( 2&Qv HS0>R!0Fl}b1H62t9T\"et Mqd9IĶDJi/˕ %j@iPc+4ځhnWHb݁$Q2d*JǑˮ#l`^Pnt )o) (,- $GY!5ܦ'8h)6_2"l_^K2 s &4ZmqIHEs$ \J>jpn*E (s*LabfH d(Nf kȌ0llEc9'v3Bf[\FU)jk}ٱEmXA7=jf^L =Qiq'.bw B)J;wGyZPTdރd`FeߓXK`-JJQUUѰ*ѷZQc{Qn~ͨҀ %:#,0ƿHz8[V)^&G]PSYb⊔GNy @Cp1vXu7:XMa[ѱê~$3DdEëDn`Rܚ Vwqmucoc6'={;.D]hc&C|c0ߔZ0DzHoSF6(K`DqXY,cRx4NYV\u"c7և1;jJHRZmu$v' l*8*)H8AUܰ@=jKa:6LĚ(}{LjߡeBV?a=cZ3ՠu%76TFmmx86㜸 H 6$t]Pr,l_j\zm{ ݼ-6;;{dmrJr\[l;*x1Q.%aqk~&oX:Qփ8j)"> a | }_ '>zm힉TuipA 5CA9#UZ$;}e ;"t TȸK /. 4;a8?(U-]DBejz!:NlФ:k&+[fiۑG֭*ȹ|*a>ƌؖaA5b}\2D7A<̼hɦlQT_iz6j^s ޙhLԛș35 6h/w\2˘yiq(pj)N>JG^"݄ϩl߃V6G{Cv6^OfrtҔ+* * Lb sǓF#sz^\c%KdbgVj0PbV\J xN/= &!*VqCZr1{5`Ȣ,Zu{u$v)t: nfaZK$/#* 5Jܦ&e|K0~ r'j{ JRcH0;:"NX1'ũX>eBәc^LO./2cN+g.Y-E=MGH. UW'؃D5I1#>TP+ުʈ!*g |E=kX2I3"_-L݀Gй'uV%>U&Z2 uyKuxkx|zpV ZUtvkfV񃐑UxNЫ/+g"Җ]QH@39 7H4,|՟&HU*UO.H'5j`bȺ(D`ڀnՀ:IV?gUS= qEc$$H_0CS6*1t8{Jl^D;@Nd(;d~#U}h: RF:DAEKL"f:} (It 7$3Bk/ʸ+46<2  w /H{DhgMlO %X zX PSa7>D`|jtlc&a1Õf0ma@"No}dh7|.YiK] ptwffP؞\z. :Hdw&Bׯm MX CEf^9Q:srU`!m$-xFX%lSF3@QJGreeǀ^ dc2 ?vs$qs4 e~0 ):&} "dl!ВP-Pf@g/aK [ :|w\)P ~h!j/O { p>lc1pwo:CL54'+@( mˆ,2>puCV020f$ӢG`\ 4ۿ3vs zP.\MH^ ?o'5!U_w Sy_ Y(a]U\[T(,2$Hde,zq%hAzOw%p;1MF8nd`q!oh1(؞B1 fŠА}`ֆVƊc'وo``\-1 L6(33rHLKDf! ٪ }'S|P񩈬FF`36E8³~P1OԲ{ W Ð=6RFv'x.Lp&Ƌ-!^߇Cs_Ѩ\*_W_&&}Ah7d9)'xa>>mIMzx*!GO}BTiwEBOn{B T U4և]Pl47mlq\0u}wt怄oaIH+5b$Y*>qUǞ EF&lqT[Da@l^( K[}ApZ`4\b~nE3*Jq {Z= DA4#b0`O qLk ncf7Zq h,>Ae8l>?VCEh FC!0Q)̅g'`_(2+(;oՄq XJC9hh@!WG>k :0P<:E-ZDC ,?лt@,cJgť{aDt!<(K  9l0' m'K`z[!]P8DX6j :(&vZq*ȅlibVH,% e ӭ>kYߨ % DaM|i {/iTRžࠓ>lTVh F'(KAӾ"Sr< ?[B$/@s?JSHBK8 53saOf?ߐA}d 7a5l!a'%p/ ĻTw H@$?"?XjhXLO|}:2advD 3nv"&!G,Ҏ8 nM ^a?P&+Pd*w47C͚lxu-9HGJBQ^.ǿ lN]5>$DF 4ڛq;7{\cI,S?_a(|kϨl_SV\[ȠoE`GќV L$D!Vl)iiWP%BWȄ13'癶{.Q\rs{s[ǭ4e7EK> [ 4噶[OٽͶsa;oXZm:!p5/oS:t¶Ł;I؉SE'Ȟ&/9x_k=4m[lN?yI֖yPKO"2?Lr @W'%)@W?j%MuZRGKN2Ҩ4jRKuzFn@:H`٣P!) 4(qhe ܦXOeΥXg~y?87Wd/VzO4-U[vL\U;<(-y=q.10HM9m󃺨ی)6UT76e{ͷw{wXneA_>|pPoi).[_{6~cUBwWvuugud֎g2{{y :ha wkzTX4f{vej}ͭ͐Vy >xQ򝵪;kr^xIk>ݖ~gG+]sŒ+ƴnϑx&Ar1Wo|c]NكeT4[.ܾzqBɚwM4dMΩ?^'U\Npn  Du S߻4X +g&SM['bTY[pzlYr}Ke}%5Qs6W(ZgՓ%\os{⡝≗z^F={޵vSJx.|rUꮾf?weiXWP2AO$eJiDW)NU2JT/UQJJ%hD?A?yTlaTiaf6OΞ8޲??=qpR֟FxyZUnW ֿ.XltIowܽ.uI=;/&鐸2pG! Ռinٷ{}FOEqr^_n^jڔyڹn2?wmݵUSg'8ւٹzVN_{J~epykLG~nW`A\ưV7 Vy"kþ^ǰS/tXw}qIݺ1cw6o:e>v~žt`zҞ{nGL_a҃O>~3OH_-5m3CgZ__71Y6fV?3{k>,=Q/'WKex$&/B+| qUMv-i_pfK/7!|YK xjb/ASDq<KG/9&\_<7Kqxii0+bt+i|zr:adS%_Cy~y/|KՏ~~_Wŧ*=⑏z7w=Gk?x*w^8/=Ʃ9_Nu>W=Z5swRz?>{w>?~{e|όSg;?ug oJL; g^me)j785(,?˿!b1+o+rqxV &d?Y,oJ\86zү:{mdM?5>==(r\܎WxnjمrۆxJxkI׶ǿ_*J*&m|r2#ZB.f}"^Z?/~giwp"uܲ˕ʭ߫gf4PX(Ժl`Ju&J 5ή>*2kP8z #8ka"s;'+CY&N\vr]Xǘ  Z T6i,HDO-#h!]¦IZnF̪U)n= 5Wm6Ώg؎h;d,ZFqXp"j,0iyS4] =3D)slmRmTuam#4Cv?^TTLE8.,8Pi`djxTĚH1S{l,M< j%IٍBЄLaXŲlA l{ʏD$̚3+v6Gn>4j52rݦ10- GqD8>z 4AiO +2u0=mž W?@iJ}:0BޠXAE0#v;EVF Af  tRGPgLl%YZaH.b%' 'V 3Z=^i> #T~nTh _Y%3ȑ >rgx8&~㭹:ľD$s.|NISNnnE㮮 Tw QSNX|L~VȃzBVQaבAH#(S系Fιޟ=z䰭p-|GQbh0jnY7fE nxHO=dawװ\H؅xC魄ܢp)mDl0CqIiL q v'67n=!q!!|\$p:ncy.ֲU$Ӓk$&6azޞE޵"u (Y Vq[4N,]!$ULt^U.PCfbBSbYiwuq/I4OLJNq8nT A-_6=pPؽLIe%!7u#U#vo\d`p nHM% E8mŔ8Pj a\%8|6=wx4CrS|uCm}]{UU~{LhG!A͑:}\;.8P iUy,+ύ-¡@nE `.T҂s S.C'`# 9E3q3xl(I^giQ3C!Bs@BM&!yf8Qdx 'bzAZ N"}1=p̍#9b9axv!3|k @dq̦2m\U>fXN3tC0POn, c,Z;zJHRZoyu$v'q/]t=ty'=,qQG[8F'EG8C8'x5p$n +Ҷ:C̈M oMQv{&&ae m Laguˊ:Q,Dάa} ~G^SuV-Y͟MQ̌ :ncPU95{ H gOuȎK8ID.}PJCEł"!AEixZ "d|23T}edV>FKԤp F՝_\{xR+-z>ϼ%:~<ӑ]>X^89Q5w :;5;'JAȪmBVWZWDU[uD&9 *}./ `:t|wD* -.|@xd [֍`q>#`-] ;ad(r` wMz6#@eGZF;x`dt(_x"F~<_/B[xZozJ[W :yeRs\+-KxM`~fiaC ' EG ]/&[հtb4xbS3Ju7L|gb0ێߣ`$0Y+毉R%pPVĻYpmJ%= k/e5ϰ3pgSY^eKSѶ_R /hNovby9HZ0 0A31p4Fڣ}%# "|;PY6lXByP[H}X՟&h"T4\PO6iŐuQ"8 }&F?gSA-&4{Æ)I#" 㠦A&2>%Кhz !+& M.9sE8:B-*VtP]MJ;Yԧ=V rJO3 : Q@LW_.kC,J(DήA: RݙM]wkbbXcWF$VU#5F}眙-,={ߝCpw3|}U{g\c)Q֌?XW(ʅ)+]]ޤLi" r! <x!YuŠ}10YAkjݛZk-APe֠ aQ1϶kbhY fa*=ZZ⼴ls *l [=`_Ji3BJ92jn3:ڻTG.Xt"/g`bc~QCbM JJ\V5ңt|.J*e|ljX6oV ͱ4;f eWVTpx lDF<1D6$#is;6zao-1*~d {,&\G!V/^J%{ ,9*t7:xKFf`bb>K > #pǤV?`a1 ߕFǂ\5.HI{#(3V̉s.Ab6u[N{ Ȟڋc7w=>?sZ썃CGP8lUSiRp+| 5B\l% ra{W7.jrV,@])h;CpzzV΀4HcIk9J Aao8/A r T_#,ҰhRU\@`ihzP "X o c3B&P/T=2ixotۀq0uU;(apD_6aIH=q&50kc? @cݪ䢂t6S IV3[0Jl+ױ=QVJ'mf}hW*غa5E$.T"oVKdkL?tѫwH <!5{BL&Š4WXP y"a9.uDٰme< 8*̬Q@}o!`)JcR2-g8x`jCP"J:PA (3%ϡΛ*E1k0.aitu5@YOj9KrļF"4TNV`E?r@ ^ =r DArڀQg`O b* @,[k30P}* ,8)腹ZWMB~8Q399pgѹ{(l3ZԄ8Wτf@!lfHZ?wAaCaEEGÇИ0L0!5E (%+wlRBBaEоXP ZʛBp`ĐḠJqha9:(&nY*<ْyj.X w.!e([nF䕡 4^}j :pj*4a:67aϱ[T{qSFMQ*\mɢeK5G4qq&rxjL+CN|U1 Bl`3s+Y7cW>6d }5l+e 06aYU? 5K:*Q_VןGFx`-_B<*Ă]2 q*[h"eD關#4$gaY≠DE>6XY,4M ^!l=k*ͲT߹7d7{xSxMԂh>:B"1_*cT!ſ俅3ucQqRVT} q5MFkTsА1:Z?nz7~y;_JU?Vh>DkSf>6 vQ0u2=6dɐgfDbhgnG3G]_D_C!Ob-f>K[66O;MljqדS- -=ŠZygh3j\Y Q;#R*c;Fl{ uXΉؚafn6UjBCͤ /EIU~i6 mI(^m./uwS-[jڑΛRoVoP?ο|@5#p_kPÄG=FůL~KY6vG9НN>hs)>'w zOM\XxҞ/'[RRdr% s띋ߜwkVPq4QEOsw$@|k?zlۗiĎ=#NIۊ Xvpmc|(_rIzQԊg5u uulyöb v8/tjݜֺlǭv-'lQz+KRS?+OUOD<ն"I)*-bbR+R9 B$Kw|Y%|*ocq[ #I$%6s[6.37PV_.4J!J*9#igD d`eʺKHKO*B S߶ǖM=djMSimUB+2i"J&Dr ieRB)-??Y_Hi 2Wz [_kmmDԃѨ/"F?omEBAP&&$-eL(J``<@Ő>xFo2^QQ 4``Fő_~)7lЁmsL"w( 'I#?y(q:BxF_<ҷeQ Eei~'zѲĄ_$7w]1JJ^yB>̾fҙBf,msmޢ_q<ߣnp~ ~j!ݫ-7G[9ru3p]ۯloK?lpa_ܼ`ʼݜىoO>+)r.uOzb-e٢v'sLN zoF!>o?KlV3SED\\zϱ+;,H׭b]6K{{声;G v`&f0wt=fqJZ{IJ??(7Oj!*ҟ`uƙȮ#3QxBkEnumϽ\D{uhO/9h;ö;ɢ1 [m͋~DURQ* nOC"ŬۗL[/(kv񍏩[d@΋9ߐ%ȯ|doKx٧s7mJݹlϮ[.9ukCwi?KޗD\4Bܶݺ쟯WWIv.886qp飼&.h#txW͒tЅn(Gv(Nj6ꑐvF ?p\'j=kN=&!C mezܗ-wŊko&CMyTshZ նBR!eJ,ĸ BNT_ .1b^[mm?JQ`$%jۦقS+G{E2\EƸY4lӉ7ޣݰ2I;2oPhqǼ;]Gt{F-^mA@|= ہK*}Ŷ-86z䞍[杻ӳu{- wqo7[횶nN䶆瘴/nd_, jV(T~:ǂVS΄^+|Cu)ӂd$W%D?<ՙ{Y/-LjÍo2Og5;9AȪe_7yVzYu|{'u)qU?<=^{髫NW :2_ ^ֵ;>ڼpoF cwy?$grmuY9GEqojLЃc;{G;fѿΝShsөG` .4A_1z@ 9y)E Y($DsrA%)Vz[@ (8J!7 |]>r46.k/LM&nDYZ}b 잪ѳ^ƣ?<9 vȁArtt]ho7n HMFOhFt M?0`jƣۄ'K9 D/`Q0 F(`Q0 F(`Q0 F(`Q0 F(`Q?quripasso-0.8.0/testres/rename_file_absolute_path.tar.gz000064400000000000000000000276601046102023000213240ustar 00000000000000;kY)o` Ts﮴;݅=ɲζ$VD;v{wGYV$'PJ9NL8خTR8 .\"PI\虝{B!pS%L=rd[-4,[-Eߵ@-td*lW }oyr_)7v&+cxeWu|k՛΅7ڥ~kޗ>*?}7w=G~*{wG^0/=ٯ_Qߞ3?_->OtSco~o~_u5ѸpsO=x}=?̗{}ӯ_׽o'~cg_}y8vm]GO-]',_۪z᫿Ƨ|oc__|kyݷ=w=53wB?;˭w>?_~{e|όک~j:n3zW&N} 3i/[HV *J\rE''K;J\;WY ۩*MMrՂ?QW0^J\[oP2)ݱDcCWL+q^_ }h9Ŏl}%jwXN_GVuS2Qq"i@M~ͳ:\a9jz2PViAǯ YS0u.,iV1+Pw#*P08 Eۭ[ ޣcҸXH߇R,)Ϸ\Gdj=Q)]HUu>i^[ari 7 :arA~ iیZ2-ĢRHqo95Z :l[#= 0zSUG"b(KѓwhFl{Uȓ`s'2.e^׋MF)KVx{pӰz'2)H!Y1-nVk R2H̖Sk-D2Q.簐1s;Vf(2s{cv{rB9xR7߁^:ˬHV4$k*Ro2G4/+X'.*튮J[Of/B|Mⵤӌ-Uk`x-."xY>ah*G@4zfx0~~gDx&τA:ľ$s.|FHSn~fYy뭬 T\Q^)Haxbp,Zh'z<^PU{fo>qBuzz8<|쮁s>.i9lkEFjr ߖ7F[iQBƂ8gҕ=_ke"t5,;v!KPz+ᥩ g- `(9= $T.@$ځ '$.$=+\.Y'6&;g+ a-YP2-XfqDbbSWEZB]/Z]џkGYEs@ʡ-1ijw_[!:;V߿6+JCXilg\Pc6.Xb.J.BՊ!s1j8|crnt >tӔ 2!xQ50v8EW2G\j$;'\ !?U7 tUwCnمܫSn+ ҴTqhLVt``kY5 !E0ꮣpjH?.ec6j-WPd !/JEV[I31)34:RI0o'KcC?+}vt`*P :` d m [Ա;P;ZzSG(RU1P;޵|~d lO!jWM.!%.`7 E׵ShB=UFqPܑcӸM= g{ZTA\؟M0qj[WmP6GBPZpxWsVH,J@6#Vr7j 2)^ 'hi݊۝`.T҂s S.C'`# 9E313xlI^ga SC!Bs@BM&f4Qdx 'bzAZ N}1=pdn1A蓡9axv7YEf`uUW/Ɂ<ɰNeb<:>&kf'fa`Ёx0 cϰx`W9]g=ȹϩZ"H/ZL7FMBacM0*f' Tw?Wjg Qp) EʷeGsaH^GoZn $&L5 ,}Q\>BNt6$9{&rP͂a0< a&''ǑhDo?:Ι~U%QQ'uQ{ԍQ[O-Bz<>>wJ\< DF-t,We[:nlf_3x/JH3F0m9Qw$7ߝsst&x/ـ"7PJƨ,mQ< >/V. ;~i:w|p~>(-]DBej"jwt ѡI}PO ^ͪێt8nuɄGΕPiG] fŦ 9&XB;]r|P2넋6e0i\@eEK(GSo"gL0>?^tm,gϊKtϦ$FSNqQr/}PU95{ H p@uȎKID.}PJڑbAݠ4<- 2>t1{.^i ;:z t^GdBD1V tk4B\\`9 sxVv]dq(IXh< -zmdAKȬBL I5:;cx^b5Ttw1S Cfxjz',L$Բ6q @{:V 5vG=KGVI. ]W%ȃjbG } ,:kW,u3CB7U :2}a<$M~r4?}:Ļ'AY2ђWC[3m3ӥO|c~oV ؑ DQBFV߱ Ss9^y^a?qTjL r&M1 9U!ք] `:t|=wD*2-.|@xl EjͻÜOF'ΔP(`>i1G0`@Mdٰa ACncI`IT\-~DRѬ~rAB=٤QsCE xt+1N, UO(H@.ZK$ F , #Sو) L_xі>Y;EYHF*75  $t$DtVPJ7 3Bk}_Yp7Whlr9Y+y$@oɎ/}dAvPj4P#LUH}C[hO|>?ޓ5q@пw; 30I2PA0L0] 1[[*Zg=փjժZo]z]YuEk{3CB&xLf{Ǡ;8M]ox@? ,E&S`=^`c:_@K u%w]!Ю[弋KxHNDXV膘J]}"Vx'GRqBUc>/,Aay5/IW` %7;(:i:h mOxbo aZR \N{Mfd!%+_,Zϝd; g0(mOr#hhZQB`utٖ4KRg1@R C> Ӆ3D1G}>_X!ˁ-X5e4# OR|RTV`()1o8@F{..&>&BWLɺ9"u܀LK8 {xCχ*P0? w0@JB2c0wNs¨dt@ w:CE6@=A^psl>=)'X89NE͊Lݡpy> 3'$#莥ZOj=ع"с5 y-J>w5(]"]Yށvk`.ekKcNk/!Ħ_[@}Џqgb +َ)]Q:A`3 B)\^0PQl4O𨚤dnw$eG9Rc ǸSX_Ã%/$mZiYtMU~f}A2xLB7/\l߁"]3ޣz !SOr`S QJ躧ik(3/ r7z~954X]W# ͅn\8Okw%N|i \#Τ&P+BB51* ]Qnfv vXf@yqMDLM⌒s䩇`%D.9gӻNn]ċaH{oϑ$!blÒУgu sx:(5 i)0 1)se.K!e:fٰcwg|>LA1Fu8,br t+kWU/װlc_=å'6IGWՖס6}6̨>ft:֣C@FǠøZ=ه.ԨisSYOjq)Ph8 +YŃ݊vu1z~:pl4#ΡbZ* @ۺ36P*X-P ü`8|~&K!?Qs(s ISo3ؔ(~|L70553=i(7- H3$Uas;؇&A衰³C hs &{;T 7jXPPzdb/H;}A:? S8p߹z>c')!ZQG6;z6 vD{çlBHc,%Lc)@[M?Z/ҾQ?ѡDvO6ck{QKUA> l9,(j-5޺cU|?i&: V'VֲoƞM(f9wVT lZU5CW }Xa l8KJBeLJz9PބB=&&; a]xpb4KΑ4$ga=DI=6 [;T4E/f@1cWiν'׻;FmDMC+aq!eIM@ǨP^ X뗟QGR -"ް4:M5-#תcw##b'E%/0?.lN` +aQ6. h@Q;⨚0ur`6䜣ɖBbxf`G3E:%k D ? g-fo8:-qp~\cZI<!HhWԿԫ6[`e+^{:I.Qvբni\뀣k4Z&u-j&7$F<:=0A7=m&EQf(`pU;um5=yr٪tg=O'KƘz3`$#Ԣ!lV R(`%aѧMOv?.Pm_!hRT)%3JB`h)e *Eo& o~sɜP+WˉWf;"ۦnc?vju5%8$x-kd4+("]?4XЌf>eN# s cbwRb2]r/j*~~rnK|mv{|}!&*N/_zm};]upڿw8$ nsV6tJjڮm?eZ;çjBʜVt^sr̥>YHr݄^OT~\1`pK'ߓ RSJ/ݾzkFw;-]+]Q&ł$U$NZ!"pA_OO>w"ya[kSWZ\* +ܰ`VIG޿4D6/hY{ yӀunؔ7&aݮwZz4fbS" ӯf϶go,M$Ub=]_SsdԴ=!ܼmס{^Q?#r$+Kv'v߃酽eڣOov͚ٚen;;i+-mX;N3WT}?&`=i2TR&Rp RRF$R\ v3`AXd1%mEޱk@IO'~ާN?1{. ~Mo66tq̞k][mb{jӲ65j~ϲO Nlu4C#m?ꂕOlkQm_")RI 1-E .) R,@(D|Y#p{&P}RR+)$$"tD#_5oh_Y/6/HdbTJJ!J*9#i!H R!ʔ%dO6N%5cCZ?aSOW TH!A 8)d8%NrIE %ne[bSXhdJ{8e_{W]JY;ievwun h;6E o"BA .8Ii!($ \ ,R07E0xOY_۷Ѓ}|R$W^pz=poToP/6u[e*JPrBB$J$U)Kd'" MkvMq鴘ݸCGmϹH<;}s'cʛ4tf|q5}Y! ~5x"j6s-WK㩌/*]-3fO7$$av.`‹޺D YV@ 閽;o +b;?96m3d_ɋ|& 6r?x]%Ά|GCܾnMמ8Sv^ YuO]ϟRR/=H:oD_QVΝ,hqH6LMʴ ݢoO-H3m ;XXPN2\J(BPP E UB)IR!mn p (oo/9T2'x0>Θ3c?1WvVyUU F_;oh^sE~-cxkGiiաoLnܿ- .k2%)cftlA7\O={f/tiSuQ5[ٿǧHqL޳+K 56zg69{ݲt-Ȼ%eNei_1dmcOg)N5U0J'2$or'j̿.)ۘ[.1gUv6[e4Ϙ&虣Sq0r)iK7U8jt]s?,p47¾e eg-W5z'>']3Wo:Y_}kdm?mk{cҾu惒o莃^YZS HXM9z1 5&VL \)#דVgf0 7b6<~!~U|Y]eIԥL]Wr.z흧:]-$0oF|%vg{[׮hM/uLޓɵeƽ1M[C ݣ?_zE;wzL]f^?N67M@LSz,ǻ}OtDD6+8V$楤)xg^`JqHk`hjnnlO-&΢+2=vDE҈ 'F|B\D;05Ay}bf`dija`h '{F ~|<4v\Nǿ/t$?xx g Fho?M?0 t?M?0 .~`n-`bD&1=eQ0 F(`Q0 F(`Q0 F(`Q0 F(`Q0 .yripasso-0.8.0/testres/rename_file_git_index_clean.tar.gz000064400000000000000000000275011046102023000216000ustar 00000000000000]X. SI{7QAA@4%,VQguPg*j-jZZОPIf+G0.=2 VĿZ 5RZ)WHU˜LSHşX-!߄*|:?"JyA%rXyQ J_$B?L RB!`R (LR*TҔ#2T ꠥSI`E,e`r%˶xHjFb DFyCY@D&Y튭Z8B EIRJ'RIQ9F`rH0Fbmb;hm^ɑ0a#AHD 7` iRUoT> 迠{aQ@Ze) aX??8aA^PaL)GZ&qNA!jDr5Vl#XeD a،ReFEmTwd@C+ۗ4lv[fŹ[ mٷi>kG;' o|#wM9КS55_>y}~_٣d֭deib;7}6n13G􈝙 ruiǟWOX=+o#<%neCNӔ _l,t ECv:7W?Z>dzf46[Okv{o?z|D֡ M {f#:^X/0]˵:\ٰtԄ170;'YST]-FS?7f׷{_t23E?VZ͏NߺmXe RmVdʘh†kCzؘ!-Y6ꧣ1)ǡ1koN綅WRDuÅ1G:٣53YihY[~ߖ3&:߿ɭIu$er-nrI썳o96r|qV^{iES؇o@Y[=; t3G^]\TVk)7S_6kwrOR\Ѿ+ɹ}Ԯ;cs5}2DHrv8)}֣q_k燺G~?2h$ICci Z??gDKf VwO_*B cR \+ajjC۰p/Hp }Czw >kW2q.ϩi'V {ErL44gzM&M >91E󋎢ÛP)X0g/?O jL#*C EaLJ(`BJj)\T!ږep|LqUx1 B˾_x_\7qu5+m#ǧOBFa*9R\&iE_*%*T۰X9q)B^?mMo|c]Oe?OO_%VRRIUr%IL)ERAE7.T.731= R/JR)Y+SP? // %bOc|?,# &`9!RB%UU GaT>Pgoj?(,>𝆡s&FyYor-5{2KL;g' 3F} I^0 *ÿuWVlÂ#? ? fŌ_3_ v%.6ãƺ#mKl/ CI?ںm7s$~ϨqQH>QϞy+o 㯅\'E5>;Nb& ۡ=}w73qHlgM'>jnTc GꐻzG?7භqI"j¨2l~SMLT3_|٧. _l_(i u Kb[5>uݫkwlmvy"3+_~iE{iM\)gqg'_8R ߈{ȁ=S' U2{l+2Hh돮4 SW_/[qB#Wn~XZ]hnW<TS ͓oi˼>;O!OÛ>^,ԵS9Â퍽{!ugBCb7AWuJZ]y~՘qq{+nЦǏV-װ3VƳuOouuJ.3jmV> ZjP3*!\ ?^`ӆkô)xA@HNTKG'{l:s{_؈.FO揨nD1]ncΙ5Ex{Գo{ʾM n=6C[-qzN"1e F>կ-okUٽw@p0& D|tpEïۥ!,)"ur4ݸ*"MqE܅ǟZr\?btj͇-XS1,#W, Ԥ@YZp45&d5h(9[Id(ML<ղ:IOv| ߩ[XXHaԆ% ?*?/Eh@چ "t&@41Br{ܸrG[y  a~5bGw!p).?Y7T7[`_}P_ OI]6,?7?pɐ ,I$ HXR%jIlW.PWaL wPƘA\hZg4x"!=pKIP"c0j ֔FIޒ$i)HI CA1jPCi43@t-pU$B5$A)$ 5ƳCp[9X?VǤ+'zmp>U$i ZÁ% /ǿT^Jː vA-9AU%ЪA&6e~2hL51  jp-PVPLOjJ㇎I,c"@!Rgne4үȿ'E84-x=9(,(#Z<)2;[$*wS,Đ ,I/6svmE^NI++ N>g.U,V1!I* gU+phջ]"ήVD;+_ҒSY9`1DZ1_xLue{I%dڮ,#Mq^'Yi3g}e 2$sZSg :ާ7Fy2P[zR)2ϋtIZ-BĠY؞'5:WDLV]ƨ!PP*-iD1 24 zSY4H (]h.ywZ# y7#6)v)$4JnSmx2 Χ(ʬ&Hؘ^˔*T0?P'tXR tY\ڠ'UH=4-֨R+]}\YUgU\R@(i'53g2$,ٙ"d&7ݯg^^ɬ B- R(+ h%BQP_ %hQ9#dg)s9sϹR0jEڠ9DC72|ߡmZ -q fNQ |N0^$;*P-?M3~5T|*0`no ]sځѦ 1*1 .`Cպ+]]@+g39 u0-J5:=`JG^`g+TlF3KOKuJ,.TZR4V5cs-?=~0ZaEO&> 6> Jl`{-5 ,l!}OijX Ω+~ E(Y͗WeCK2* (@M%h,0G[_ui٘ 0, 3~A p"ԅL JpY Xs2ZhqK-;b]cSo/ʝ|ω?LX=ff)֯+5EeDً1[ARNMZEvzdst=Ǩ8uMGڎ'r뎍+v;2RuQ_Oa-0#Z%$Q=psgϟ^:1{t,!4OŅB!u/.6Թ3ǗO=1?43n-w͐~X+57oLƹF&Et թلa+Ǖ,hf4RO"a, ^-TJ].F6-#ғ z~Uh=fÓMiybNK1=?TߛSDE eo ל:w֖U;pׂ9&5 J0%"`b`;8֛`'ekA6ݦǢ Dn Ս!lлV' >~=n-nRAoJ.bAToYӎ4Ap֣(,,n@j(,F2 RiJlU?Oxb`򭄫=R ,y~L\)KKw ķp^@x1^trdAJ%`݇? zDKa<BxPhۮÝ8TB5:.$-?-<:f >Z>7Й8yt*7j^?wz^e?<:f4U-/WqT^|V;^\,or *82z`lj62) CJo(Im*TRkTY]YPD)e~8P}jjJUȄF v@?4<{]Q`g8]"U8c"]DhW5RQ^|[";[ 7S(u߾;viLsK=)ܕնXz_xfq{HZ1]X8x]ߒ 5Bu_.ǞP?l|GM &O}l=f|˚Iƴ*7J-'88Xp"]f AɍdVu̶iohak:AJ +m_/rkfI7X VQycqi6(aH'4! c0R-7]l~7Q^bVn1LLYܘe;IeֈnsIݸU5I(Aj@םlAp% \m࿭`26匝4!Ts$֮P@+I>B"}/˧&&H t7/L^7\P-]j,+ gi皚V|Ta鶇+ ^]NYSQ=}\IǙ sn7BA!Pr iҦu+9#GOf, ċl_mKF5 Jd˛dԡC 3KGfuXt gcp~j_=@M&g}pYma5潚ӻs@ CK^- $4=N3,5&$F$HןHٸܭ[f"`08D*b+7_J wB-WQ-N"uwGe\EqMJH`E=y&WFOGvoɹDh"vR<%h(hՉmZX GaEyDAV ^VW^kL6Qa'`O}`+↤gϋM=.JT%Kq[G"т*˰l{( &JJe@+@Y/` ۝['E`1؉QE$SOzKBCȆhX|!.e7DX9UQ4#dE" @$wP BIpF ^) ,,( Xqj߮un!K[561a$kUI9'c:GI5z5Sha`Ni 꿳Q7e̒Sd dZpW?y4 y-'Me\(/r#LchK\g6vr Ua=qb.-_-)y%Kg~7\,$ g̀XPd;Oeul&3Dck%#rPeW$$x3LbAa1ZTQƱߵ7/%|ǑJZ3󶛼}8$V%&7Y& XmY#ʦo %s0@]$^js9)5z.Fd,-M#o#ﰜ$C .pRIG7k €pbn$Rr+l~3F{Y;]f7vMm\?U bvwb=֤Go(HjҢBs!G}lpbFIb_3Ft8Dnq.EtǴ:c(l4-{QvE-y_K?{Rbϭ9I]ݮ\EX}+'al@Xu5mi8 AGL$I\Y`u;Cmo5Dx.̤KVڪW吙PLmϝ>}] ߘQ #1ʘ?2*r"Z4WjnTOx3^0EPC1$@<b5!|9DI""G=Á(rYgit:l?x77=*'㛳YHT$?nBt|&V#@dCՇWV8Q4욳L @aW ]U "Wċ 9dSs?^ПӲCme~rX'47h 3ϥ%K+v 0۲ٌ>|tU}\f I*?;C(DؤV\ډQ^ ՚$KI}\:PO @Bd W#:̝;۳'؈ҝC'r$TB8t_iUEg™ͤ`(J?'^޶KW.ÃXWuu΃iJOA3 02Ju7C!8j6穕eD9V,VoR&wq^M=kFGnyQX!3nH:zc80b{F N+$8ߩO=ܷ̔ˋaI~u'#ds ̘8|pluh0mAnH}KB X65t(k-}4w_WMZ.pGs!If=Pɵ$9q+TT `'P?Qȗ6H+ Y} aREhLu'+lD6sXڼ: Z8PaUp</ě܇ 9{^f)u萕9:7%Dl" '蕚qJ#|B]eQ^'_ʿ`'JKRQfGͣږʭE(4tz&>'ѯ'eB Ҡb*M0s'cbbN97W:Qd-, y\5[D.DU..\LſR |MLDT &3@Sn)4p0L4vin4]w1MYҫDO*{/M_˞7>Ӭ*/:״ RBPW8Kp^u?:ʣeWy[sGjkdr(s]/4wLb:no*ʠ}"|=*k#.公)7hoXѴW t0,W]n95N=\&rѲ7䠅?!ZDҳGY\s5kUL ?mӛQ&_^$n"n (u k*^/Oz}4ej5xJu~ėN-~q'o8 yڇq>/?~zW緮^;}Sϼşzw/~]KFΩ{k_o~Mǿ|=xM'5+_US}%_g~wFc}zǞ/|o*;Ws?o~>qqg>?#~sye{z{םw۾>=_x܏_~ձ>|ī׏]{;qƏ>8o}ןi?7Ǘ?Ώ_ox̓g:?q~ݿ?O_+257of{/|Yx=]pMz/7wN?+/e+3ܿS߃SOko{vv,=)ՇgUggG9H(iIKZҒ%-iIKZҒ%-iIKZҒ%-iIKZҒ%-iIKZҒ-zKUripasso-0.8.0/testres/test_add_and_commit_internal.tar.gz000064400000000000000000000223401046102023000220070ustar 00000000000000wH&͜gc" @J'2pOp6/HUHw\}v}n/|`swwsgk=w4W2##/ K]?JwM:c wfM=o@?֣~};8ys=|t߱ݨ]u{gpdwodO v4 OOF'0ĝs:ә EW0胳_Vk^Ľ"%J$i9ݟ8IߞXFS10D8,1:/pcR,k.-m1h N؎͆iaQȶ61sv#|S{ IE4TMSzmz7lhA P+mׯv- ofiU1A-W6ޒ}BWm]oO7z|pq% b^N4d^Rx(%TgA{3Av1'ʨw(^!gaᆃfLޛOͫB{3iU~=БDln^gL!2?t]& ]oaVfSftPp,]e[iIɸߥA|Aݿ9s O(6 H$VD2Yi+H0gtn/' cCsCoeo B$_H!S 48@ެ$x_s3sj9]):k^+"ݳ[*/[jK8|j/)j 7_ĖG0y1KIC|(g>96-eXln ?ZhOƽqʅP>Hvo> d[onZeU⺦ߪ7w_Zjr.x o<X&G!c#%P*&m,L /U)8w2 Čtc!S;OES y2JFN҄t~WqbFpȯB.Ɲ}H+Sg [}yxS]jO=@w n`?t}[h 7~:Es+)vqxaĔs{/}꓀KXGЬ=јXi H=}ZmWƘB`$Si$}b5>0TF)W_tt1! h_bD !B0NG<_>yn&z35 &ŭe1v}`[Y͆mYo7, Hㄤ+hЌLLs[xj! #țqQ@ݏ@j* u @x(IÁjb FeT0a0$7KpIJQ .X .B{~sqC}/f-ZEH4@@νB(g7 ] 3P(9ڔi'X1`Ԣ6p׭boe> Gj%$2I^ /~<_#dJꤛ.qC$I+BF`£Cd,>$u!7 a,ֆyGc|Ѫ A(Smr0 N|q-5f> pӡ nH#(H`s:L$-Mj@8&#`bUgx?׵.jG+$$h7z$$.frc[X.;bn- J_5w o:}n>' \ ¡?3^@lˋà+YЂ2 \O@G Ԅ7m9V霓lC PagBB;%46ͦhymӝ>ƹ`17aDcQ JDRGͲ_2bT^ ],ܭL@(EY*N. E\`iBƨG`w<3 u/,0Z7-5ؖN=(I=:!2;E )lTmDKzzm\,,xNx~;7Vs#-ĭ:3DEz6 fDʼnJmבx\CΝs2WB$`Q6)(0Qׅ*\u&g0 ڰ8¦ۥTK\wg?>~xV|K3};bݐp2^q.$ϴ{4U3J_Jし XMU_%H k@ hGH"K ^+,[ҡ_D3Y'] !7h~+aoti_W\hPB7=@+p0,p?Z 0xIHQ?lo+@=;XAWtTgG$ %=r幒"ka|@2Uh)3MZXns*gmuT-o/a50׵8qglS_@3O|/)kV&f %[/9HFyuILJ%.&ICIdm%# ,?P m~ 螛O XSgLE ^J[TE4p 6 Ȗ6n,c95R!J!js$ %B>LܸDjմut,?D YH v%+iz R Ct@y+7]R"13P,&^BʼO}ʵ5euGQ ~*HwM,lj RZ?)\cxR:ftBO=z:3K H#pe^ي6O޹{=;g-+g#ʁ `O'WSlnwoj9]Aڰb7$,0@B7Gu, ftRx7c;efl-Uݘ e9" fI1B. [+}pH<s.ɉ;u}Fa18>sfb%@غ"4d""5]6Jk;Wh[͗:7v4&>X))Ɉ1x?â8IY%Z}^`NMMfwYIk\L( sn" ̶ "{LنY :m-V\] 7Y癥K* wcJSŮ޶-  `zJwUc]UQ60|m 7ܧA~P)JSbϬ:axu_6PcVh2D6ed}MStLTh5m#z-0 (C@e 4X".%k?D,L$j K%ʫLp:X04.2}UTVL5û\Ģ/AC!qA뿔(n= 3fX %Aab=b` 'bm&t'G,̈O%|n #BL;OIl׈՚A9~۠4L2uicWk{n(52<G^kl/'clSAy(:{`Xl€4eti4\RIT>A;Bڭ[*~NWnʹ[W!ȱ{x_f/oO@j3ohHiƠif%cHr|jX6KH33c@"!ד!-2YdnO&wpZn29qmlnkwI AO@˸jLH7IGuIU7?S;B+K)O3IX LXD8CAG">}8ʓ}ɋ'TZԒ"`"Rx+\VX 3z¿v1ČCs$-5̴hHF:٥PuD"uojt kJ+gr aNH8tF4$Y9 6ck(1*HJL1;t4עPaȮa2iش)2ioxj[ "ˬimZ& 7ʌs HpsCe[W@A%LSN.I)}`m0:DЄCMڐ3\$A]GBe2NPZfDyXՓ5K &YS8eo y _@eRq\C^؄nHvԑ)+Z51> I>u㈪NfD=q'Oegա\H1\??[p# Pl ٿiϧk$Yä ^YɽxZpY)/Zέp6.W;Q* \ヲ=LBvtP:ѯY_QG7p|Z[J9c ~m7k*j׏}*tu_ O^)(O%^'f{0ZTZv#W8/P7m={zXyD|'jvwrZe'6Ц"G;>S8/_<*%#!楰> P]}>e}Y~E?szkw; ~yļe/x. }_ox&r~;zْ~fs-]W_w]sk⮯;.{v>xÉ/vtiS0?A>_.rH~s]Q|Lkœw‘xjög6q҆3ZUq;^X\ TX[o~ÎwgR6~?}ؕTCG_?}$Tx>}^B!"I!AxN@Hȹx2(>s/p#>'HQ~hSEǢG?yOs#1?Mr)}oNq[HTBW))%|O.?Ӥ)h2)1fן4A4_q߿CԴ ԍ&[#zjb# %08SqHm]VO2a]*u 1+mXjon)~ +YA/c8Kn1rΗiO_ _Ny\Bغ$])((((((((((((((((((((((((((((((((((((( ripasso-0.8.0/testres/test_move_and_commit_signed.tar.gz000064400000000000000000000250531046102023000216660ustar 00000000000000]~n/}o_?_>W]Ǐ}W?߿hOɏ?h~?r~Ms$k6//O㿗'?zv`买~/Ck8n53o43_]8/9w~kÈ%зp>KߵA߮jn0s+#r"˷ǡ=a2T^8hj}i. @ Fݪb>ЂpR=EO }*F^޷TsPbCWhbOhEЙf@82|1 ]h=$G0( ߠD:%¡ P)UМ خc)a F {N} קȅRa rey@H2Lʑ~(DHDM Q* k,jbDbL/죉(5'";h7//hrn40Pwf.b `&A*k(Ʋƒa9`)c#׳0oA4{>W. 1BU#'1( Qɱ*UV Iue[ KW? xwa= ־zپ_yV0rI/ha@9H`o~͛G޼X\k؉L(QW̼GȈ w$! ];Sb+rv_c݃x1}%M;8@pHPbjơvF{%8k(,M#Vo~h+=7Lj`y #.:h 4ƭ4$ $yaԱނ|H~C8ȇf8ui3@@@ڊ$+zY4JB=eaZ@XT.s#2MM$hܶp]-;qѐhSiJpLz-qD wu .zie޽ ?@PkHaG "uj=&ȼZ^GޘOʴm onuV.*uK#K-( =u=- PF͍g]8m9i P<=m^ $ޛ6q^>-t@G:^u1{#5N --3btڞ(䉈_Xij۶lR~O3kLJ"k;;ڕDN`(|jYAxO}pY{Y2 sDZ_.4O/9wjr.x ?X&c]%P,T*M; 8XS>d`Ɔ2TYP5B,603ҮLѵN>G;x.$4h$$.Frs'GX.[bnQLJv |^$ zʝCCYgW'hkJLWB 4rZG8?HSPlߓ}N868 / o)6~N%ɎAjbE>"redq"4Ÿ,uL0^f)6&24%LQN3eFu~ҘdIf G;_̜TXQ^bhJƜ Dv@ɘգ@t(@o=:.!E$˃@XDr+*J (, 4fmyˍ+#ʙK0a%qeq`W2Ez?- CJOȀ{,MTUU#XS: = , m),m 2RMbpma^ cK±ʕ i0;mA ]Rm̋+Q6) /!q-Yt\ fꏉfG*\IZYӰ0o#:?0CcDZ 7 SXݘ=L7-L,)%<۱do}%i08,Oa َ7ckm 꽚ln֘0kaxH5 נ}8 dfq-$=Q *f~SCR,_ is:'/X}xkE47Eus-zx~mƓ|*/bߌq,:ؿ]w!aq<9R^&U:ܲ朱KtWmByNPۏ @GWg+k/y_2p* Iąڙ{:\=A/,(>t% JAoPK!N=aP̖tKfFp\ μ?nn4>: W] |lw|/-o<0E$`+k{bK-G6S0XP4.s'ޖ=r[tMl@u)?Emi{1 ;J^B4Εdy_AɑcA>B"dj4pb) _gUD=T=jn!Й-*:E\pգcFT0 X|-5dL:O&Yߜ3 8: @(8u*`s5 i9m3`iFd۪`0Kœ'4SDlϣ(8LALqdA3P5#ŋ5 !_Oa7G2!uCL)ĂyCwӂt1x,G2x)@vcG~J8Ί/D0"Va'O$hz:L0F[R:z~SX^4 vTsV=t? .faFK lQPXd5\&af>tƙ%qb v b'kܙP/AaP6辮;:Rt3Ihq/OV@Sςe2ErLiM%Hb "$S-#8RgbX|RI_'# =R83L47u?J˫H"X^Nr^Vw_/ >+jo?رbmǭ5zԄeK4㨑(& g`M<a&o!]}OεgLV6J+ZsSEnKɋc) w5 RgfQ2Mv…[ uʜȾD qrH$y*}n n^hnX7D;r2RF;xdW}UfD7<yk pCf~ U]bF<<JVdI,F\^}$ hC1rUkv.`M0'db `]]Ƹ?|D``0n#g]+YI t)S%ጱ1%{Ipn^B%̽ ,UNjyZM3u N*TPcϖNr-|ʚHHZa6)kx2&J%wQjICId?JFX+@:F5B۽D|2Xş[Hg*T@ONHUjbF(F`؀lـ>MeD+H 9X ! BdE x(6*t6zJtӍj҇E::@kH"v,$ LC(6$H5!07H%b_= KH$&cDS[Q17ɭoVh<28HkD<%dF!; 7~ԧ= U*#ӈs0Nw dВqyg->޵GQ^D)1+ZDE$nYB(KHB^ƅM6@-VQ+z-V(Z%UD"P@EyH5m^Pcf!|?g# >ΎXrr\%sЬ2T`3G^iJrNWGeUr$t$tSɗ@终)Fu?>%`]ZRttUu#\jYX Ӻ؇.PM"۰Ɠ%äW@Ppw!ų:#C9]6c۪}QUAu;Bwy)ob/~ X?ʨW[uU~T2,حrUF%J k)QJԓrHAЇ69r%l\*{\hH „ Ꟑ F;9#Q@K^ꄦ ہ4"O'0Vmfs*o"Ss{1ƒچ9â=T X4?2:tڍ%:'UsKVVNûy$sxpqa"ZԵ k[E+Gjq:M`PuS +B]Hzܰ^LҒQ*)Z70?R?*/,+Іi6*?1bL.]XHx2pYlȩ5Gi KKɂQ 1¸$YLt?& KS̹k 6Q -d訃h:ZD~>Av7x"/mlE NR&S|bEaY]™0 Hd9ՙcsB8iʀ\R0Bhܒj4iq)&jwSPi|b݁f SpQxJi@j="h…?(O , X#,,ҰO `F 中a`A3`P G&YItz]sM(.O, *(ܩ4`@8n`hf$Tr4l@g):7,*;FkB 7NFdVeYE3?Q9B͒IC2nV]yɰnP H5C*d!EZAbhic< N`VQ33]D6Aee!7J8} GPbK倌-#2&BaPnlHF *-fPsj䚱#fdE`&<Į?0E E0'\@.'B_@`1#t)hB^ɉ*A#S@?M(ej#R76AK ?PYAQU]c\Uq䢊򩽺GTb0,EYX(,4!(nZIl?ҥ?0Q?^GrׄT{骦*nEtRG3FYd;Ì h= D_Vg):־?I^Wc(+,?F$@u5xz筓~yX5M)]umt%PY3s_[>[QSzEQTWb!(RhAQaf}ax-<_ 4%hO-(V4ϑc͕"H4$$1 =x6<&fgM/$yd!3r}Y,d|d}eeۺ:]cs%=3'1!u=f.,}@姘of{G_@YV`yFp:Nb8+':,; s#,Ly t&SA_JwJK׏H^97UQ務N릥#N6癣uy.7Kq<1<{ӈsž_98ھ-g߷OۻvsQj/Mׂ?Q@'GY :%wZKZ R7L2<ϳOjMLܜPt ^}r᭍sN4q묿"N/Vc_y}.gHq5JZ{`҈Yxw~uu) rP7d{W٫0#x ZFOj/5ۖj=s(`Sn?P-v_mSz%WF>rdYF )fmߴPZP?6_Ƃ?hBpФDs%]D;ɓ Er*:s{Dr_ׂβ ??W}_ڑٞw0io{ɑKGmsJlwśjtsm8o Fxɝ779_z6}޽ OQ~'m}~aUnhiaO`TjZCJeywל˅C;<4׷_n獻ު<.'$xO54?9vM̾%k:Ёq{o=6O'>]uXeJ>{oYnאOT2_ߎd[_ro'zOJqG s7q͝/ '\xxم׶o-rWY%ma&I Xnri̥IJ8I^jBjRK϶f4,细w3M}-/=ڷ]/kJ~?N1Roqt&G. 8KFrEe B= ?ӺׄZgybD04ٴ?}/>}yr"/OǮJ(lP]K@Q37M(6 dpO_G6DHm-GR+XK`z/. œVc+OdI&+M2]Gj8fb] pY°dpWlmlf&q^#~ 4u?ñzMHQfť5kӏ(V9F{a!SOsn5X?PlS4?tI'tI'tI'tI'. ripasso-0.8.0/testres/test_remove_and_commit.tar.gz000064400000000000000000000243671046102023000206730ustar 00000000000000wWwtvooogymmzݭWWbNIDFB|M~׵>zfdUw3sw?=~ y䇽w>QFK>V*ς4) wv{;<eorzs\b2;ݽ{%;iuF UYV+'$\FOc`U"Sy[Gj''DYQ0k0|!U͉"Ox'hQ:2C`G"~/ǾMg9%ltF(F2,w#,EhUƮK?N0| 6+VEc<ID$Ǒ>T&.,$MHw:^^4+i@"K4* t0ƐНޕKE.YCĮ@QEMfMa mc#VɮAL^0k}ƎF"haiHH>b+6y}CC~CJ}e;Zu~zBҬ;`FQWdSF]HKz(H P4s%h9n AZc55ڐXۯo X~)j[VZ!,j 2f 1Dkk-OfS|Le\裔來f&icVL`NDo"@uDCS#t`M3__`])@#ˆ|gw>К~\.y=x{-zn;.krOz;hWT+ $9BN]Lj6>%*ɚPl^ll 3w(<+OaK5{\4$%mjTMn6]So ^H! .Ђ P/m7o]i )0Z@8 R6*6cZ:/}Lۺξx`j+KX.]zɺLRx %Tg~}3Av1#ʨ,!kAᆓ>H㏝?`ׅfD&Ϋ(>n`\BKK6Fw/|T2?t& l`VfSfhPp,]e[iAxܥA|Aޜ90 Bʧb5(B&sč ed4^wV'm& ",l1B!pl([;~&h@؇ AHp4gv.M'f&7+ =sǩ䵴.կcȾnw$N~ʩa$C]+vqy ZW)m3e1LoE\> YL]MKPr`JǚYK=)s|9]IHƭ·A^{@i_[an[͟kvȳ5~\@d!α MFJlq[C0\wAzP-Ѝd yrN1ơk$XlI %f# y,j`4 W*5rꃛ%G(jz.(.a tb9u` U{gX05Xux}ްNla3\I 8w)FLqV>u: Pcƀas S$u*0x9X&s<=#ؔm ΑIH10Dg9*:L9%$xS7C\(bRh[#0=J#>mvwؽcY@B$ǂV u?4oq@k6F60DVStroV:|s@x(5IÁjb F"*T4- T87B̑kO\8>pI0|-hp/пǩw Ǯh{ʙ>KA x> E5c]r<}6+#z3#-m:(hZv8ny}ē X, @&yͽl|)u )  #md hKf6C>tZ1K$Zto~\zkU{;[%R_|A"1o)MȪ{ tNl#@3 Z7GR3YDmVC:ZĈ R)S4I{D+K5q59 81Xe{Xm"qV7Re\8`Bhb= : \[ 84W#ț.{$X fp(-]-[e(4Jy(, M)u9E5[dIš?rPaRB;46fEm'}8' '`Qbڇ>b'ۘ ДE+<:@Vbt0bI Ȓ ǰZ/jE(/"7Qаʢzr%,@o֣3kqYRɲ<NJ ^A$r|`^KZ} ˑ@Ͳ_2bT C,lnn<@(e[*N&| ֍E`iBƨn&"yf<^`Y`Ҷ07-5ؖN3(H=7Kt̀bLZVboh k i0mq]RgɋkP6% /!0dKV?ml0.jFYlܷ(UEV LU o8qIߩ䐰8)G/+nY`eseȕ<B)=7J C&[껁8?ɹL\8,>͡ ܉8\{0jҁHG&EfsQcxp^^'s%,5>cއWvvXPjǢY N,ks\_E~:yّ ڢS5_}Hj3T%I&H! I9d}s- n0y @(8u*` ̂Gش]HU nx10ɣw'] <;( }Q&9cH9,7hfxA9)H!d~Ft)c%=#7x,(0 Oǂ}.c'bhxG^iYCbCjA<IHw*^8X1Iq\ˇOk5 |Ude( EXU(<5z=2@$u3$V;[ IءqEI.A5lx0 C:T! rQR:'q2Z5~*t!>(yW޿dxk].S Jk.-G}GGV8g$HXg,HR-#8Rg1̜T)*׺ɈBs,ݏҴ>=K*Y~}cnc+izйG}Vͬ>&X_.)^2`w5y׆#:xщ ;~뽡&,[ҥOE=Y7] !Wo=]:5DGuT] <|,Fc,ad/.޶0hR7W]D^7eSEN48^돳V&oOP%$8y{`YbS<%ρ!y5+x5iYb͛Zd7ACjx1Oiʼn;Cejˮe[|iDY34Zٞ+\C,BGCd!'T؇FQlHkC$07H% WOetId@Phxn !*s &*ԔձGa:8 .>ЫE]ehkӿ󓙤@*̤M4IKՂz]+\*EwE}D]W~EYхrW]E3I$m.w}is|{q4hr Q0H#ZꪈhLM;5^ .XRea$@Ug5ό2vcFIV m=VY[ai. [%VoS3x J_ r͋"U8% !PhQS+AB]Hz0^Lx ɓFirŝu VEX112!6&0豙YIoȩ5Fi-!@Laݩ6٩.Bm>%;;Q+ưY+`bfP=W&:mtEXUѣaibp('[Lw% +H9̼ cfjN0rz-))y@ FhxwA5A]55bL;{hܠ6[Ԭ.śfV,QgSP$ > PZ:U ıF\X8K<5&bs rTYT$~<0KsU-q]Oa i_D;9 rF ,2d ? 6u6^O!^a%+\Ht08#p l&.@zV5fQ:)R1'T*^RLL dߪ#08}_3=UWd* LyfK8"@),oVУ08~tL\?x4΃0s;,ȀUnc\KhyGA =OD; [iBvCkP",YDlaC>YD7la @n֣P'Q QghhF BhOxc5;t"%HkS)K{)} M^5aCocJodv:(ւ1*]N"R!\pb"Qhnt4Ay"/ϩA$dRw֍@ag(6,xj t 15$h0Q fś`EcH>Ȳ ycz>xjr#7g(U{d_QPT'J2=L% 64p b/r ʂLFl֡v%&$~ Bhއuoq9Į>ZcsjC0(}$8 Gܭ.YyWX#+jVNX^74 xLNsQ!8"h SABâaG([˻k.YGxJRM D:+o(:6C HKP06<٘א(gTa@_s %5DAU* VE4C1x ?9 [<ZS5.Q\P_\l-m 0;GJ8%1GPĘ1Յ aBQDz.|ESNF Mؽ:wW )cDKLr_b*L-ܩx 䔗ge(NȄs* ~-QPH%cD4`(̙uKlٍ}v;蠑(w8,(>U/{p _h(" Kk?6}#s;S~X"j>"j85EppЛE 4?4_iN;UDԊ@})xLʟB(%6Lvqz  p0-=ZZʆL O\RuD #nN ;$3ajG̥[SYPAHo662F33\| |1O[QFO!?SdGyxw׆ɿ zA>iSYfW)SG+;"ecz@1fYUDO_f/\qgˈـ?TA(*g7Y{s\{?u>WMtu53zع2"jaim f.Ts*"yбVM z3F^zz鄁ĈU6Ϧ!2zRk??Ivo3Q?ef4:_T^ ?uK+ut7L{C?tY16gz7y79?n]op F=Wlz;w*HRSŲֵEGIi=u!@rܬm?x~ۛvl]Ȓ3Cn*Ml+k٫=%!F_}@1h̚G $0cswou+˚ k5h[ÉO|>u7 'kG<Օ<My:OӶ]n}{Z^<3잝.*}a`adVnOW{vgod37~3򾂹G 1w^ϼP.sƇO^~#-"oU_3 j J%fy efLI,P"Βk)## {~M+Xg~Ϳe#*0ly쉱4]tW`͗t]%iwozp0%+5[{r[{%[{cOU^Jv<yT:hókb'l)jӫ-jntmsw˙zb +h?UITQ_4K$C,)l6" P&hN覌H!0j5.nHmqGh=c^OL]/oݪo|E3xy?cβT*- w 9|dVS\||ʖs"64)gSS7 wp uK/fZz?wȟ퇍~k{ Hߩ.U%t^fi5PQʟ,,lq ha$" GUAkܤ];KINÒ4Ts3󚩉&|3(:79- ctdwNCiSI /Z/u@F D$R+I4{KIw?;ca>0#iyő@(")Lp;pԌQfamQ.Fg]uS"0ڈ[14<F@tǹaz[FJP?MSwT?B,SGITQʿlb5'RYǿ8fO_ t#ePmX4hРA 4hРA W! Xripasso-0.8.0/testres/test_should_sign_false.tar.gz000064400000000000000000000177061046102023000206730ustar 00000000000000ѷ 7_9?83yzZΓP]{?vv3?q_sAOT:NYxݟ4 _OFm9Φx AA,Ai*3%^Y%zOl{Aܷo3x( щRLy`p"u&dyOP鞳 ~\-5T:#4UQ.V@RgGLD!8,Þ:#5),3Me>׈L6 2 -d&fT:<Vsʹ` e{jq& ݟ "JZ%q/~2ԉ8q@H ɹy(@\B]Qj"k8S^a!Dl,oh5 ':?b_|DE9 )s?vd_m QÙNb, ) 'KQ<>8~"y)gUNnM كqEp߃( Qd*sI#;b7y͎- EE9BFLca%>ܹ[M\`;N w>xa읜w7'i9;G"K u |I}H E@Ħ1@IMf m="VALQM gx l;;h5DD- Ѐg/~2 {4AF~CY"0w@$4 ,q^h4,u)4f u -}1@Z2iT*Aq =tjWHRhjڭz|L6OvDy7Fq7El@[Lh#JK~b{ɸU3>J)Znޠ5$03M j|W~ds JMBzIA tK#H`^)K*OE,ӜU~\^<^\p]ls >&dhnL0L^ˀID Jtİ-;z.5Er8"%D=}o6^ECRӦ a(=Z7W.R}͏+Wq_R1\.vX- gi1AV-0W6ފ}BWfl]Oz$HeA6Ʌ6JRbv MiJo1t$ p3 Pp$S 1* 7lcIq!A53HpGb.$!f&7+ 78 \\zFNrZWy}IdrnYe+m '?0Oá<櫖ز74zGaxY5i/h|ìY?:Ѯ  pjV{3 +{ s_]ruunMuEkg]nmR,WTճ˹Ba)<ǜ@P~OC0\wtAzPo\!'I yN0ơkgXlY%fc yb,`4̓ W* r}ic]!^j ŦwCn<!ƮXN+Q~ k8ǁX! ?v®/~ol WR04LjIs{/C꓈KXG0a>'&2U&bu0:8xD/G@ȗ)ITa2 sTM콞!0+ x!ΐb`N1&$zKUu09$$rJxSh7C\4bR9#6!l`pq!NHf  衹S 2 - E A@w{ Oՙ"i8[CBlbh U< vNgB%r2I71.ob@ 0BZKD oq(%q]"f8z ҥ8QC9>zC:to]U:U]&〫o뿃{;_JEF~&?^,ԲA_R'tُBWbL'i6Z|M:!7 a. y`|Ѻ"@qCduYigtĈ2 R)qS4IkD+Sq59 8w,}Xm"q/Lnz˸p< z4eq5Gs-,l=|1U%ph/;Gם>VI_E zp(.̌#[E(0Jy(, L)ct9E5{lIáXxOp(LRB]{4z.ͶhEНƅ0`h 07aDsY *ݶIe /"[* ],ܽ{W!aqR^T:\朱+y( /W^WbX~O]YݩRmB©$$Ѫ;Ah s:,|7(5ߋJ^-g ˱tt:Dv+5m1aW{'ցCB5moo뿻{_s\'42b Ls{8?ʹBR8b/ha ܈Y.\-whC'bZ"3P#(< l/VtsD\ `'5}Q[:y -cPA0|t<9R 1bdh bXϥIG{cCx < C?t` 6Dr?NrSPQaLu#=fICX>t At?58RXEoYB.[:".˝P#D2S?Ebr^3δ( %\sWFCHtL ly]#wu)cp<˟B#N[t/5Ke dKiCHoi ybGaI ;Ly2b8+BR@7Si)Y~5xmO5k[оGWξ&Z.^2`7W_ۂls!?: ?5²% \ 5xҥ7^1o-.ksQ =*heo,7FX(;+]a6$fY?jo+@=~vxLNS9Q\^돋N&ZzOOP%$8xa{8x7kC;+dbVLںDzTմ0ut,?D YHJP5 RuA3%:}(PJKF$&cDK[QYP17ũoVh<*dˤQc hVl`9dSSI/ÓfБç6n{?#ԣo"=w[ =Xݱ,a- hc3NmG{DVB]-$Py.{ewNZoBmgͬjޟtͱ䔺߄1:+ACiYK%,-傘M1y!(<#D ̒ De38B0,#s5" c|pg um7@ԐD)*2/1P{3~W|Wb"?KG7n<čY #BhRk!φ34Xz+lB;X!D:U; Nޗ c&KFhwlm)‰ϖ QgkapFx3wFq'Lt _8ݪY0LCuuť%utު# wRU;n\ Av"HDLaIAVU fgGdvC,LqɏJ{eyG P,ϖ>ʖjCت/P_AhBE}bA Ѡ8L1#1-L+"hah=Zs#>%Q I{Z(3L/ &H#Ԛ݀N#f!(#ҔqL=>|xэ>ƃ\#6SwtD_j=+h=\=[Z363RD(L8"yFOEvĿe+WֽZbj?lKg?/zOžYgjxn O' ~`ƈyBAZ8EbA9;d!}ߖ%%{:~qVWS{ieg*1c}3  U>X~x 4;4CHTW\\ONNoR1X߅?B34Qq-(h*xX46 o.FHgL"uo*u EG]|V8tי}P0+9?'By i (c[e0LFg/\X6NDСFS+7?3?Fdθy/mX"w-7L_z0 H\.`˗7Sd>ܹNt!dz$o%hpP_цT!HXI]­G2ɶ3E]X?ԔӲs"TB W_rIɡ"t@i,ias—Oq&Ek(qh߇MnPyt_5io+D kѨȗE //_u}k"߉MPoB1&gY͙9+w29r-%I%LjXIcXFiGR2@"/Qt tܼ[:vBc#hi,jg/~g&[#XMӌ 'bjE6H;ȑ"1~L[\>:ۯiD[T]Rg7YMõ&ʩ.zMAm.EW]3*Oc noM٢bŀZOyykt?[Ł7 ? [1'Tu'a`'HcDli3,UھKĖ?]mڴiӦqd(ripasso-0.8.0/testres/test_should_sign_true.tar.gz000064400000000000000000000177041046102023000205560ustar 00000000000000b' ќ\fMugIiB,Fd!ahl&31J.AAڽ'lHU(S43O,NO,U0*Hv_;<}N,T$! BBu`HDr*]:RY$ŸT !*vcF/Da8 1fG?_$Z/"/ρW8N #nc ,50"ΔwBcLQd8DX%Γ$Nd$?<Ԟ%[!cyTLH`{_RpwYxQ%QAM(RBc6Px((CFtw _y@V{{u$v Oc"#̻n7 -mge'bBWfnWZd#xMeL&dQJrk!hZU("#4Ӏ(1U n"pH@=t &\F `H^"Ty*`q}Bf"&[ }b0c@X1'3(\GHt`*g:EN]Lj6>%wUuĠ#BaZCXT.w#r]M$h}f{\4$5mPң5{Cx"B zg .Z~ ?@RkHazPQA,dq |e+nc'te[b^*ul e_N, GQB`{F{3d "{WD269hUS}ۼ.(7#j6q^)q_w ГsYx@ Ƙ[@yMP#Dٽ`]{wۘU4x1]K%K:gYsyRQ2wǼF5_cPt moөMO_ k\h$<.fw1 RHلIWA" 8e WLB9Pʟ"`M8hG$~Q3wAqd/NLbfy ;}%ϥgt) uoחD-u^Җ qSO= #4^S |6Yڞ\/tS|(o1= 1-UXeF?7ZjOQʹP!Hvo><b>w/-_W!x&ݸ[^W;ZwxoYͯgs8R&x499+¡`6`޸BN C76R`Cy<ΰ,2KH >2.X45h'T %ǨJcC5C~M'1xB]:W0=n-S,PlxBc08Ru]_m50 /Ɲa0#i. ^<'`XG|N#Ld L$aupX_6/SL!044d`{=[ o૒/lli|L:C%:Ř/W)x*搐)!fOݗO qMxIq8r< f:ۃ5/8! )4s3:~L+.F60DfQt#*9'#g:ܙ%j DQ:O1W2yCw˂pw1x,R|ѣ=O!~c<ʀɊ:D0"V]9Ϗq'h )L0FYQ:Ǒh3$!Jb,:@ :t~)"7ܬ@m!\rJNfӟ"zl LMygZĉ \.۹+?h: aP6輮;:1v8ު˟B#N ךU2E!EJ$yطuisVLuւ B1#L$ٝJ&E<1Y!`)K4mvE,?޵-hߣ٫fg_  iˆ|YTP/sA֫A?dwmAG6Z?: ?kzeK:4h k0Kobȍ!o-ksN9ѨMS7uF`#|,aΝ֮l6$fY?jo+@=~vxLNS9Q\^돋N&ZzOOP%$8xa{8x7kC;+dbVHaA*$| 4 5DzTմ0ut,?D YHJP5 RuA3%:}(PJKF$&cDK[QYP17ũoVh<*dˤQc hVl`9dSSI/ÓfБç6n{?#ԣo"=w[ =X.8UrX oTS1?K'#/9Ǐi z*{Y99"W˶^υ|3LV洂ޯgx0Ad@ uR]f3eu91QR~焼e$ ~QbE81mښ63a Ɍ<:Nd=}#h*F8im~{eW .Ҵ{BʣaXp$Z6H>d^-]TlIEq=S7k3#- Fޗ c&KFhWlm)‰V QgkapFx3wF>}&:`unU ꡌ:ywys~wq>B:Z6Ârm;}Tz`-hg.ftGYd " P+)#rۭ`aGeŢ#vY*avvE_Bۏg^oj/[ꫣ?M\,fC!)f"氅iX7v}g;?FVO(|IB|F޲+* c~!".cIjn@{@n8Ew]^i=FIGRY!S'WǑgs vxfTQT/fD8tS_+6o*fu#}[B0f?Ӱg[Ic@B1a2SV,cѬҰ ̛:_soODofMDcO? [P =;YiOŒʟQޱFT,i ߱FukӦM6mf ripasso-0.8.0/testres/test_verify_git_signature.tar.gz000064400000000000000000000262611046102023000214270ustar 00000000000000=l+c3_}|>~qc7|?_~N-w/~7o|7RاoK7?xI濿/o^};/GCrydcpqwM~gR_9rg|ƻK|_U~&I)Z7UلYV l-ذzFۚw{ )'*;yF([lv]KQ3wMßeݵM_VGzܮL+(;`3~$0p͞|ۮsv\ tv|UŖn @ tds N-qdA5 086_fd]iLho4eH\La :LfY#p]nVUr=7SYfqH#56v!74#2I%b~GYG_9B3m;uA0L3[o.a;vgNޗw] Ct${5;4s=$0n !0wD'RGJRYc8vl "6cp|fY6-9gi|]N+ϱ}Dm8. o~G;LMXt Jf;X$ tQCPV㼒jl/*V/2!2MI1xC ȣ(*i ih}ue =ME?nϱ—t vt9{#cL+ T0w 0$zѣk|.̜7,X\ɳ{OҴE,FIih6a$!ީuE.YBĮ@E:yΨ?oAX&6>#ګRg ,[v$>{4 9h$DԅWt)f1tHC&P s* ZsHMzf320$Ҙ&@Z5VBZ2+Aaqa Rir蔍pϭ^#IaV#V {fhN,p S ~?SR(3h Z*K!׶%@VL 9qipYf<{dQJr{ b_CvcVEeiN&DV(g=:#TA`H n}M~- V4#܋ njz*v.%LIٶ{{5R1wai.4 0>%jY)92PaZ\HU6 ؾ}jNdH4UJR7.We'TzR|NЂ PP/iԓ' Rj5H]5TTic'҉R^<>+8z8LD﫬em 5ߒg &4x;H n̆f5fhRNrβz>՜@g-/KD3z@F)9T"..a[+ˑJ2)4Lta.o/vF$;ncj>.7%z<|P\(]YaI?O[= V0x=[BY^g[U٤_(*;j^grLhzX3 )*Eez 20tc݃Pu|,6AfiBt8Ms%*9~=uv :(jz,(.r6rj* K!gX05Xr4x68h:6ǡz6 a";Hp7y;n;ɋ ^Rۢu: ]a\b a2E<"ED@`B`$)SiF}"syk&|:z~HEK(E,#b`0&$zc+f]HH6!#UYs6&9MQ55]Vॢ*Zi -\dRhZ &9E+|U' # qXM@j!|*)t&@Ca/Cd' ^R`+I\,Ւ`P%<ܢ@| Fc4mz(c\ ANc@ /X0/+@Z)T GM!q KNx< E:b]r<}*+gTh6/jm3lr|x|4+ LWFRkG` LIdA?.c N,{`{6)< hKZGowGtX TpS _)Mp}~8@Aڽ[a縱Q_ۖ-H0TJcC_Lx ڻ lH |#XYc.A D6 j#B>Uгt2^ cDXl6`T@)=UK jCsq@8xx">j-w/ku "9 &ԏv(F㪐(͑qv8wtEe_-ĤMFkV/zkI&P՗+^ 'hJيC AW*V+$Xl`ʥH D"E5cj8H ?)Vn`$ю@CS1n,;9<EgrB f}XL.)mLeHJbt[Y4YaQKg' 5u MNY%[SEu3] [ŖFGsHQ# lr!mx "3{2峙DhC84&mz+#ʩ{) {aGŰ$•ttONz?(i{ 8Q" iBƨF4#S23, 4ܚRȔ?#ԋ:D Dl-H$Cˇ`"v ڼ"➼X\. xN<’˷J\-U xbC15ÝazIYNB>T M̐?͠P9=r +Y8@` A$)"%ܔsz+K`DqI,#eoza7@v2<\ed!X ikpWm`SVu`aBDm0u؀aX!kH"V9۵WX]xkk)ӪqbCPj{گ͚x'wB~0N$ ovmrHX,Ǡ9E yCmܹ =[Ķ@'VN:<ײU,JB ]ݴB=QF/׬Q]{x%kzܠRE,86\ͩqa181}|G]y[7 'f+?'wzD~`O0+>2G(j, Y"|4X YsyQi[;V4#-ģ23EE36?gB̈m 󀯡u =$ Y&,oN 4,  Lio5UAPEKd(fAEDشHU)~XQ`N diĦGDlӡ(AϏnBLqdA3P5#E !?O`7G!<]!4nyCwt1x,л񤓇ȃ"ǎ{?1 <,W` 6Dx.Iz#@{LN5^:i,7ž*]l3 "C/<μ5CbElSh)m.V3ΈḈkhuAE忴0(ҁt@셇tC4+kv_5<퓉+kX9tNW[9J0a!v9ZJ0.e$1<xw%iĊRpDnBG{mtV/Ja]@6U dVt- \ʚQ_ZLaR(bwXē6 V*A4 !IC= xUHi^iML&i ERlC e`ڔ$ ^ ^( ^D(WEdQ?𪫢B޽"* 5I2mez;I99 ZF\0#+ 'EaTQC'b~n؅\@ Y*@LD%< l*"~0l F3!M$Eԇ1  :YxAG }\4Z{ eT#> bG=ST \"فf!*(Ay(D06Iȋ )DK ,HMynBYߘdeH#bt g 1mDx wd4#(!f5KA(4[6}&)lzB4=gxB-IƼ]k"/-:^ixÅ9]IP҂V]Rz}|tNkD0+EtCi"u}1 S` OBmP7@< ՑF07E$H"W˒z 7­eȊ(u|_9TnP+: \ t]C#uM@2 &ľ#Ca9=ENq2 mƲ >LP/=m; l&h+o3vfUDf[ň!Z2,X M+HG&2\tCRRF@rIiP79R%ƓCٲ ^10__SyA.Cm =,Fp@oHA(K^)r H UF<7 ]s b\9â5 kJ(2:Hw:!%j3\s'@vN|s82YSR+,_ mAaH^uh&ǐ (zh1+B]>H^LpP*flQqAN*#UQVU+ sTV1yNV擆Jze9N-f3,%bYHabn7c* 2Pq^HI (=h K!\5(P#6 ߣwt2*`ME93ɡXFe^QGoNqʂ1 'PO,Ӫ TK MU34xõbB`&O , xɷ``᱖EM{8hϱk P ܩ1ћA4e/ۣtV Licba!uj@'bc iilP H+S@a%}UsU(QUZ){I=9>| Up ]2 T N`pErhP0 ECZbsQ)9wbG:Xp b +5=CeR\/;Khp"E ?]*y4MYƘ t [EĈF't"-DNҍa?jo= _BTEQŒ"p|!L;mՈD՚p HöulV`ܪ².aIӬ5"GjŬa=}ɻEbcよZб%6J'E&F' *pY%8[Mf0EL3tsyCz]!g{thTsvr|QDo.$FtYVLfŇ"qD", (w6 0EǀP_-3L' oWrknE~QK\H? =rl iIpyRԺ0; 4#E8&jGĸ[p?e@e;h_ I~4@Wd*/HtZ4~#障0VH7%S8T:+;O꾢PQ@ 0:-66#kГ Ё Xa>{IQQ‚ԀHCop >`- -·*ί-.ɇ7m)ij: . 9. `Q `Al*7=~ L":a|v/ ]ÆdA"bIt=P&W8Ek6O \SpYKℑpPBWrhHG5@ap(A1Q.M&ػ goiT:`$nsF ?u9lE8G >\U b"/%@<P:Z*[)b[4O9_#k~S5>u5e粆?tu!W՞ (Ԅ<Ί?cc?Ux& &s:lqEh^JR-!9'.:7$o˰#GSYШ0i* ٠qyO ?ה(Cn1?mz3ժQᕳqF&,#>FMƬ1.LC$`LQeIō]#gBlv5 `/ Fcĕlt _ ,@tRO3F^d'<@ h{} Q~@M'JM#8&Jѿ*0<]^jde,%tOu[ ~>_L;g]3yҬ/nfW7kjM\PsNmwQl1?bcqN^OSחc;WsTϮps}7] 54pР ӱXa,5mdi_^59[ʘ=q_-P?`Gd=w25@!nlNCag sY,o;D!$&8n /ڭ4+1vGVL xs> Ձt#8>'dLx].s7JW.Nտu'Wyz.=8Pys+MLl|Ϲ]ק6;9>mDsi#7O#?ܚp/oe[ϼ4YW9_ım0qtC߭S>1,8p:}Xž^ :16^RoWG?r8,9.mu$89NtbO3hs4%dƕTm] jb7o)hwٓEy1js"uwV6o ]Ո+ȴJ/f+7}#O<~ wҥ^;|dOߟ*w+ڪUXchFbV.vNv3J3v5Fښ -{T~s*V,T۹?1mCۜU%nrgfv>9pՖ.9}ӫWݱ0±Io9c)s^r_Τ}?HC~uR-c?zî>}H'ߐ|肺ieyqMPHM-/OOPeiYx+'Xh7 ~NEk1Gs{ aK}OLgO]3aSz{cu>}loO5:O~_wp֊77򒚮8yoےp­^?'I:rEC/~^vfruG/vtyU}zL`\=ggeɷ{\{O;qG?vf 7{߾w5-̩k_C6,۷p){=wsT]LE9Pnܱ]5]o9:_])~7u~gyWYmq*qebz猧wزoGFgk|kGe3[~hW(y^7kʯ{i_P:8fBDiu16cg,6yĺ?9ցN.^a^r(˯~:cm2U%o.y.qW,_+ޢ_v;zz/pPH+Y6&J,!N;,'mu$$Ѵ$DY\O??g5 mJJ@Of1vƮW?z=hxs?n^vKU5k&e*YyKzyѻ,.sǘ~xNx