diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 76052f66..f7b2cc74 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -51,7 +51,7 @@ jobs: tool: 'cargo' output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: ${{ github.event_name == 'push' && github.repository == 'ucan-wg/rs-ucan' && github.ref == 'refs/heads/main' }} + auto-push: ${{ github.event_name == 'push' && github.repository == 'ucan-wg/ucan' && github.ref == 'refs/heads/main' }} alert-threshold: '200%' comment-on-alert: true fail-on-alert: true diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bbcf0c75..db763fcb 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,7 +33,7 @@ jobs: - name: Generate Code coverage env: CARGO_INCREMENTAL: '0' - LLVM_PROFILE_FILE: "rs-ucan-%p-%m.profraw" + LLVM_PROFILE_FILE: "ucan-%p-%m.profraw" RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' run: cargo test --all-features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c6d8dcf..397b0e3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to rs-ucan +# Contributing to ucan We welcome everyone to contribute what and where they can. Whether you are brand new, just want to contribute a little bit, or want to contribute a lot there is @@ -84,7 +84,7 @@ need to be the best programmer to contribute. Our discord is open for questions. - You can learn more about cloning repositories [here][git-clone]. 6. **Build** the project - - For a detailed look on how to build rs-ucan look at our + - For a detailed look on how to build ucan look at our [README file](./README.md). 7. **Start writing** your code diff --git a/Cargo.toml b/Cargo.toml index 450ae834..cb455efc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,20 @@ [package] -name = "rs-ucan" -version = "0.1.0" +name = "ucan" +version = "0.2.0" description = "Rust implementation of UCAN" -keywords = [] +keywords = ["capabilities", "authorization", "ucan"] categories = [] include = ["/src", "/examples", "/benches", "README.md", "LICENSE"] license = "Apache-2.0" readme = "README.md" edition = "2021" -rust-version = "1.67" -documentation = "https://docs.rs/rs-ucan" +rust-version = "1.77" +documentation = "https://docs.rs/ucan" repository = "https://github.com/ucan-wg/rs-ucan" -authors = ["Quinn Wilton "] +authors = [ + "Quinn Wilton ", + "Brooklyn Zelenka " +] [lib] crate-type = ["cdylib", "rlib"] @@ -19,7 +22,7 @@ path = "src/lib.rs" bench = false [[bench]] -name = "a_benchmark" +name = "a_benchmark" # FIXME rename harness = false required-features = ["test_utils"] @@ -28,53 +31,68 @@ name = "counterparts" path = "examples/counterparts.rs" [dependencies] -anyhow = "1.0.75" -async-signature = "0.4.0" -async-trait = "0.1.73" + +# Docs +aquamarine = { version = "0.5", optional = true } + +# Encoding +multibase = "0.9" +base64 = "0.21" +nom = "7.1" +nom-unicode = "0.3" + +# Crypto blst = { version = "0.3.11", optional = true, default-features = false } -cfg-if = "0.1" -cid = "0.10.1" -downcast-rs = "1.2.0" -dyn-clone = "1.0.14" -ecdsa = { version = "0.16.8", optional = true, default-features = false } -ed25519 = { version = "2.2.2", optional = true, default-features = false } + +# Web Stack +did_url = "0.1" +ecdsa = { version = "0.16.8", features = ["alloc"], optional = true, default-features = false } ed25519-dalek = { version = "2.0.0", features = ["rand_core"], optional = true } -erased-serde = "0.3.31" -jose-b64 = { version = "0.1.2", features = ["serde", "json"] } + +# Code Convenience +derive_builder = "0.20" +enum-as-inner = "0.6" +getrandom = { version = "0.2", features = ["js", "rdrand"] } k256 = { version = "0.13.1", features = ["ecdsa"], optional = true, default-features = false } -lazy_static = "1.4.0" -libipld-core = "0.16.0" -multibase = "0.9.1" -p256 = { version = "0.13.2", features = ["ecdsa"], optional = true, default-features = false } -p384 = { version = "0.13.0", features = ["ecdsa"], optional = true, default-features = false } -p521 = { version = "0.13.0", optional = true, default-features = false } + +# Interplanetary Stack +libipld = { version = "0.16", optional = true } +libipld-cbor = "0.16" +libipld-core = { version = "0.16", features = ["serde-codec"] } +multihash = { version = "0.18" } +nonempty = { version = "0.9" } +p256 = { version = "0.13.2", features = ["alloc", "ecdsa"], optional = true, default-features = false } +p384 = { version = "0.13.0", features = ["alloc", "ecdsa"], optional = true, default-features = false } +p521 = { version = "0.13.3", features = ["alloc", "ecdsa", "getrandom"], optional = true, default-features = false } proptest = { version = "1.1", optional = true } -rsa = { version = "0.9.2", features = ["sha2"], optional = true, default-features = false } -semver = "1.0.19" +proptest-derive = { version = "0.4", optional = true } +rsa = { version = "0.9.6", features = ["sha2", "std"], optional = true, default-features = false } serde = { version = "1.0.188", features = ["derive"] } -serde_json = "1.0.107" +serde_derive = "1.0" signature = { version = "2.1.0", features = ["alloc"] } thiserror = "1.0" -tracing = "0.1.40" unsigned-varint = "0.7.2" -url = "2.4.1" +url = { version = "2.5", features = ["serde"] } web-time = "0.2.3" +# FIXME actually use? async-signature = "0.4.0" -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -# `linkme` relies on linker features that aren't available in wasm32 -linkme = "0.3.15" - +# FIXME also have a wasi target [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = { version = "0.1" } -getrandom = { version = "*", features = ["js"] } js-sys = { version = "0.3" } -serde-wasm-bindgen = "0.6.1" -wasm-bindgen = "0.2.87" -wasm-bindgen-futures = { version = "0.4" } +serde-wasm-bindgen = "0.6" +wasm-bindgen = "0.2" +wasm-bindgen-derive = "0.2" +# wasm-bindgen-futures = { version = "0.4" } web-sys = { version = "0.3", features = ["Crypto", "CryptoKey", "CryptoKeyPair", "SubtleCrypto"] } [dev-dependencies] -multihash = "0.18.0" +assert_matches = "1.5" +libipld = "0.16" +pretty_assertions = "1.4" +rand = "0.8" +testresult = "0.3" +test-log = "0.2" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] criterion = "0.4" @@ -86,47 +104,48 @@ proptest = { version = "*", default-features = false, features = ["std"] } wasm-bindgen-test = "0.2" [features] -default = ["did-key", "eddsa-verifier", "es256-verifier", "es256k-verifier", "es384-verifier", "ps256-verifier", "rs256-verifier"] -test_utils = ["proptest"] -did-key = [] -eddsa = ["dep:ed25519", "dep:ed25519-dalek"] +default = [ + "es256", + "es256k", + "es384", + "es512", + "rs256", + "rs512", + "eddsa", + "bls", + + "ability-preset", + + # FIXME temp while developing + # "test_utils", +] + +test_utils = ["dep:proptest", "dep:proptest-derive", "dep:libipld"] + +eddsa = ["dep:ed25519-dalek"] es256 = ["dep:p256"] es256k = ["dep:k256"] es384 = ["dep:p384"] es512 = ["dep:ecdsa", "dep:p521"] ps256 = ["dep:rsa"] rs256 = ["dep:rsa"] +rs512 = ["dep:rsa"] bls = ["dep:blst"] -eddsa-verifier = ["eddsa"] -es256-verifier = ["es256"] -es256k-verifier = ["es256k"] -es384-verifier = ["es384"] -es512-verifier = ["es512"] -ps256-verifier = ["ps256"] -rs256-verifier = ["rs256"] -bls-verifier = ["bls"] - -[metadata.docs.rs] + +ability-preset = ["ability-crud", "ability-msg", "ability-wasm"] +ability-crud = [] +ability-msg = [] +ability-wasm = [] + +mermaid_docs = ["aquamarine"] + +[package.metadata.docs.rs] all-features = true +# # defines the configuration attribute `docsrs` rustdoc-args = ["--cfg", "docsrs"] +cargo-args = ["--features='mermaid_docs'"] # -# See https://doc.rust-lang.org/cargo/reference/profiles.html for more info. -# [profile.release] -# Do not perform backtrace for panic on release builds. -## panic = 'abort' -# Perform optimizations on all codegen units. -## codegen-units = 1 -# Tell `rustc` to optimize for small code size. -## opt-level = "s" # or 'z' to optimize "aggressively" for size -# Enable link time optimization. -## lto = true -# Amount of debug information. -# 0/false: no debug info at all; 1: line tables only; 2/true: full debug info -## debug = false -# Strip debug symbols -## strip = "symbols" - # Speedup build on macOS # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information [profile.dev] diff --git a/README.md b/README.md index c36d8842..842d3fe6 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@
- - rs-ucan Logo + + ucan Logo -

rs-ucan

+

ucan

- - Crate + + Crate - - Code Coverage + + Code Coverage - - Build Status + + Build Status - + License - + Docs @@ -29,63 +29,55 @@

:warning: Work in progress :warning:
-## - -## Outline - -- [Usage](#usage) -- [Testing the Project](#testing-the-project) -- [Benchmarking the Project](#benchmarking-the-project) -- [Contributing](#contributing) -- [Getting Help](#getting-help) -- [External Resources](#external-resources) -- [License](#license) - ## Usage Add the following to the `[dependencies]` section of your `Cargo.toml` file: ```toml -rs-ucan = "0.1.0" +ucan = "1.0.0-rc.1" ``` ## Testing the Project -- Run tests +Run tests - ```console - cargo test - ``` +| Nix | Cargo | +|------------|--------------| +| `test:all` | `cargo test` | ## Benchmarking the Project For benchmarking and measuring performance, this project leverages -[criterion][criterion] and a `test_utils` feature flag -for integrating [proptest][proptest] within the the suite for working with -[strategies][strategies] and sampling from randomly generated values. +[Criterion] and a `test_utils` feature flag +for integrating [proptest] within the the suite for working with +[strategies] and sampling from randomly generated values. -- Run benchmarks +## Benchmarks - ```console - cargo bench --features test_utils - ``` +| Nix | Cargo | +|---------|-------------------------------------| +| `bench` | `cargo bench --features test_utils` | ## Contributing :balloon: We're thankful for any feedback and help in improving our project! -We have a [contributing guide](./CONTRIBUTING.md) to help you get involved. We -also adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md). +We have a [contributing guide][CONTRIBUTING] to help you get involved. We +also adhere to our [Code of Conduct]. ### Nix -This repository contains a [Nix flake][nix-flake] that initiates both the Rust -toolchain set in [rust-toolchain.toml](./rust-toolchain.toml) and a -[pre-commit hook](#pre-commit-hook). It also installs helpful cargo binaries for -development. Please install [nix][nix] and [direnv][direnv] to get started. +This repository contains a [Nix flake] that initiates both the Rust +toolchain set in [`rust-toolchain.toml`] and a [pre-commit hook]. It also +installs helpful cargo binaries for development. + +Please install [Nix] to get started. We also recommend installing [direnv]. Run `nix develop` or `direnv allow` to load the `devShell` flake output, according to your preference. +The Nix shell also includes several helpful shortcut commands. +You can see a complete list of commands via the `menu` command. + ### Formatting For formatting Rust in particular, we automatically format on `nightly`, as it @@ -93,7 +85,7 @@ uses specific nightly features we recommend by default. ### Pre-commit Hook -This project recommends using [pre-commit][pre-commit] for running pre-commit +This project recommends using [pre-commit] for running pre-commit hooks. Please run this before every commit and/or push. - If you are doing interim commits locally, and for some reason if you _don't_ @@ -103,7 +95,7 @@ hooks. Please run this before every commit and/or push. ### Recommended Development Flow - We recommend leveraging [cargo-watch][cargo-watch], - [cargo-expand][cargo-expand] and [irust][irust] for Rust development. + [`cargo-expand`] and [IRust] for Rust development. - We recommend using [cargo-udeps][cargo-udeps] for removing unused dependencies before commits and pull-requests. @@ -125,9 +117,9 @@ a type of `fix`, `feat`, `docs`, `ci`, `refactor`, etc..., structured like so: ## Getting Help -For usage questions, usecases, or issues reach out to us in our [Discord channel](https://discord.gg/4UdeQhw7fv). +For usage questions, usecases, or issues reach out to us in the [UCAN Discord]. -We would be happy to try to answer your question or try opening a new issue on Github. +We would be happy to try to answer your question or try opening a new issue on GitHub. ## External Resources @@ -135,21 +127,41 @@ These are references to specifications, talks and presentations, etc. ## License -This project is licensed under the [Apache License 2.0](./LICENSE), or -[http://www.apache.org/licenses/LICENSE-2.0][apache]. +This project is [licensed under the Apache License 2.0][LICENSE], or +[http://www.apache.org/licenses/LICENSE-2.0][Apache]. + + + +[Benchmarking the Project]: #benchmarking-the-project +[Contributing]: #contributing +[External Resources]: #external-resources +[Getting Help]: #getting-help +[License]: #license +[Testing the Project]: #testing-the-project +[Usage]: #usage +[pre-commit hook]: #pre-commit-hook + + + +[CONTRIBUTING]: ./CONTRIBUTING.md +[LICENSE]: ./LICENSE +[Code of Conduct]: ./CODE_OF_CONDUCT.md +[`rust-toolchain.toml`]: ./rust-toolchain.toml + -[apache]: https://www.apache.org/licenses/LICENSE-2.0 -[cargo-expand]: https://github.com/dtolnay/cargo-expand -[cargo-udeps]: https://github.com/est31/cargo-udeps -[cargo-watch]: https://github.com/watchexec/cargo-watch +[Apache]: https://www.apache.org/licenses/LICENSE-2.0 +[`cargo-expand`]: https://github.com/dtolnay/cargo-expand +[`cargo-udeps`]: https://github.com/est31/cargo-udeps +[`cargo-watch`]: https://github.com/watchexec/cargo-watch [commit-spec]: https://www.conventionalcommits.org/en/v1.0.0/#specification [commit-spec-site]: https://www.conventionalcommits.org/ -[criterion]: https://github.com/bheisler/criterion.rs +[Criterion]: https://github.com/bheisler/criterion.rs [direnv]:https://direnv.net/ -[irust]: https://github.com/sigmaSd/IRust -[nix]:https://nixos.org/download.html -[nix-flake]: https://nixos.wiki/wiki/Flakes +[IRust]: https://github.com/sigmaSd/IRust +[Nix]:https://nixos.org/download.html +[Nix flake]: https://nixos.wiki/wiki/Flakes [pre-commit]: https://pre-commit.com/ [proptest]: https://github.com/proptest-rs/proptest [strategies]: https://docs.rs/proptest/latest/proptest/strategy/trait.Strategy.html +[UCAN Discord]: https://discord.gg/4UdeQhw7fv diff --git a/SECURITY.md b/SECURITY.md index 777f0214..a7a3276b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ ## Report a security issue or vulnerability -The rs-ucan team welcomes security reports and is committed to +The `ucan` team welcomes security reports and is committed to providing prompt attention to security issues. Security issues should be reported privately via [quinn@fission.codes][support-email]. Security issues should not be reported via the public GitHub Issue tracker. @@ -8,10 +8,10 @@ not be reported via the public GitHub Issue tracker. ## Security advisories The project team is committed to transparency in the security issue disclosure -process. The rs-ucan team announces security advisories through our +process. The ucan team announces security advisories through our Github respository's [security portal][sec-advisories] and and the [RustSec advisory database][rustsec-db]. [rustsec-db]: https://github.com/RustSec/advisory-db -[sec-advisories]: https://github.com/ucan-wg/rs-ucan/security/advisories +[sec-advisories]: https://github.com/ucan-wg/ucan/security/advisories [support-email]: mailto:quinn@fission.codes diff --git a/benches/a_benchmark.rs b/benches/a_benchmark.rs index 6169ded5..fccc419e 100644 --- a/benches/a_benchmark.rs +++ b/benches/a_benchmark.rs @@ -1,13 +1,13 @@ use criterion::{criterion_group, criterion_main, Criterion}; pub fn add_benchmark(c: &mut Criterion) { - let mut rvg = rs_ucan::test_utils::Rvg::deterministic(); + let mut rvg = ucan::test_utils::Rvg::deterministic(); let int_val_1 = rvg.sample(&(0..100i32)); let int_val_2 = rvg.sample(&(0..100i32)); c.bench_function("add", |b| { b.iter(|| { - rs_ucan::add(int_val_1, int_val_2); + ucan::add(int_val_1, int_val_2); }) }); } diff --git a/examples/counterparts.rs b/examples/counterparts.rs index 76225502..6ad86f4d 100644 --- a/examples/counterparts.rs +++ b/examples/counterparts.rs @@ -1,5 +1,6 @@ use std::error::Error; +// FIXME use? pub fn main() -> Result<(), Box> { println!("Alien Shore!"); Ok(()) diff --git a/flake.lock b/flake.lock index 29249c58..c13ba4a7 100644 --- a/flake.lock +++ b/flake.lock @@ -1,18 +1,40 @@ { "nodes": { - "flake-compat": { - "flake": false, + "command-utils": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "lastModified": 1709702368, + "narHash": "sha256-1YOPubkJ5M6HigdfN0gn0AZ3kx6MHboG9UbWpYpk3gM=", + "owner": "expede", + "repo": "nix-command-utils", + "rev": "8f7179876383495b1f98311e53ebb41649ca270a", "type": "github" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", + "owner": "expede", + "repo": "nix-command-utils", + "type": "github" + } + }, + "devshell": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1711099426, + "narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=", + "owner": "numtide", + "repo": "devshell", + "rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", "type": "github" } }, @@ -21,26 +43,91 @@ "systems": "systems" }, "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixos-unstable": { + "locked": { + "lastModified": 1711616573, + "narHash": "sha256-FvZiEl6D4iLXqSQ3oGjQ/qehhPZ5E7iTHr/YA1Rw8kY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c99b66784962e8984444b4d9e72d00d3549afdb2", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable-small", + "type": "indirect" } }, "nixpkgs": { "locked": { - "lastModified": 1698266953, - "narHash": "sha256-jf72t7pC8+8h8fUslUYbWTX5rKsRwOzRMX8jJsGqDXA=", + "lastModified": 1709569716, + "narHash": "sha256-iOR44RU4jQ+YPGrn+uQeYAp7Xo7Z/+gT+wXJoGxxLTY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "75a52265bda7fd25e06e3a67dee3f0354e73243c", + "rev": "617579a787259b9a6419492eaac670a5f7663917", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", "type": "github" }, "original": { @@ -50,11 +137,28 @@ "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1711460390, + "narHash": "sha256-akSgjDZL6pVHEfSE6sz1DNSXuYX6hq+P/1Z5IoYWs7E=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "44733514b72e732bd49f5511bd0203dea9b9a434", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, "root": { "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", + "command-utils": "command-utils", + "devshell": "devshell", + "flake-utils": "flake-utils_3", + "nixos-unstable": "nixos-unstable", + "nixpkgs": "nixpkgs_3", "rust-overlay": "rust-overlay" } }, @@ -68,11 +172,11 @@ ] }, "locked": { - "lastModified": 1698199907, - "narHash": "sha256-n8RtHBIb0rLuYs4RDehW6mj6r6Yam/ODY1af/VCcurw=", + "lastModified": 1711592024, + "narHash": "sha256-oD4OJ3TRmVrbAuKZWxElRCyCagNCDuhfw2exBmNOy48=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "22b8d29fd22cfaa2c311e0d6fd8a0ed9c2a1152b", + "rev": "aa858717377db2ed8ffd2d44147d907baee656e5", "type": "github" }, "original": { @@ -95,6 +199,36 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 71d7c6cd..925581d7 100644 --- a/flake.nix +++ b/flake.nix @@ -1,14 +1,14 @@ { - description = "rs-ucan"; + description = "ucan"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "nixpkgs/nixos-23.11"; + nixos-unstable.url = "nixpkgs/nixos-unstable-small"; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; + command-utils.url = "github:expede/nix-command-utils"; + + flake-utils.url = "github:numtide/flake-utils"; + devshell.url = "github:numtide/devshell"; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -19,29 +19,64 @@ outputs = { self, - nixpkgs, - flake-compat, + devshell, flake-utils, + nixos-unstable, + nixpkgs, rust-overlay, + command-utils } @ inputs: flake-utils.lib.eachDefaultSystem ( system: let - overlays = [(import rust-overlay)]; - pkgs = import nixpkgs {inherit system overlays;}; + overlays = [ + devshell.overlays.default + (import rust-overlay) + ]; - rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { - extensions = ["cargo" "clippy" "rustfmt" "rust-src" "rust-std"]; - targets = ["wasm32-unknown-unknown" "wasm32-wasi"]; + pkgs = import nixpkgs { + inherit system overlays; + }; + + unstable = import nixos-unstable { + inherit system overlays; }; - nightly-rustfmt = pkgs.rust-bin.nightly.latest.rustfmt; + rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "cargo" + "clippy" + "llvm-tools-preview" + "rust-src" + "rust-std" + "rustfmt" + ]; + + targets = [ + "aarch64-apple-darwin" + "x86_64-apple-darwin" + + "x86_64-unknown-linux-musl" + "aarch64-unknown-linux-musl" + + "wasm32-unknown-unknown" + "wasm32-wasi" + ]; + }; format-pkgs = with pkgs; [ nixpkgs-fmt alejandra + taplo + ]; + + darwin-installs = with pkgs.darwin.apple_sdk.frameworks; [ + Security + CoreFoundation + Foundation ]; cargo-installs = with pkgs; [ + cargo-criterion cargo-deny cargo-expand cargo-nextest @@ -49,59 +84,183 @@ cargo-sort cargo-udeps cargo-watch - binaryen + # llvmPackages.bintools + twiggy + unstable.cargo-component wasm-bindgen-cli + wasm-tools ]; - in rec - { + + cargo = "${pkgs.cargo}/bin/cargo"; + node = "${unstable.nodejs_20}/bin/node"; + wasm-pack = "${pkgs.wasm-pack}/bin/wasm-pack"; + wasm-opt = "${pkgs.binaryen}/bin/wasm-opt"; + + cmd = command-utils.cmd.${system}; + + release = { + "release:host" = cmd "Build release for ${system}" + "${cargo} build --release"; + + "release:wasm:web" = cmd "Build release for wasm32-unknown-unknown with web bindings" + "${wasm-pack} build --release --target=web"; + + "release:wasm:nodejs" = cmd "Build release for wasm32-unknown-unknown with Node.js bindgings" + "${wasm-pack} build --release --target=nodejs"; + }; + + build = { + "build:host" = cmd "Build for ${system}" + "${cargo} build"; + + "build:wasm:web" = cmd "Build for wasm32-unknown-unknown with web bindings" + "${wasm-pack} build --dev --target=web"; + + "build:wasm:nodejs" = cmd "Build for wasm32-unknown-unknown with Node.js bindgings" + "${wasm-pack} build --dev --target=nodejs"; + + "build:node" = cmd "Build JS-wrapped Wasm library" + "${pkgs.nodePackages.pnpm}/bin/pnpm install && ${node} run build"; + + "build:wasi" = cmd "Build for Wasm32-WASI" + "${cargo} build --target wasm32-wasi"; + }; + + bench = { + "bench" = cmd "Run benchmarks, including test utils" + "${cargo} bench --features test_utils"; + + # FIXME align with `bench`? + "bench:host" = cmd "Run host Criterion benchmarks" + "${cargo} criterion"; + + "bench:host:open" = cmd "Open host Criterion benchmarks in browser" + "${pkgs.xdg-utils}/bin/xdg-open ./target/criterion/report/index.html"; + }; + + lint = { + "lint" = cmd "Run Clippy" + "${cargo} clippy"; + + "lint:pedantic" = cmd "Run Clippy pedantically" + "${cargo} clippy -- -W clippy::pedantic"; + + "lint:fix" = cmd "Apply non-pendantic Clippy suggestions" + "${cargo} clippy --fix"; + }; + + watch = { + "watch:build:host" = cmd "Rebuild host target on save" + "${cargo} watch --clear"; + + "watch:build:wasm" = cmd "Rebuild Wasm target on save" + "${cargo} watch --clear --features=serde -- cargo build --target=wasm32-unknown-unknown"; + + "watch:lint" = cmd "Lint on save" + "${cargo} watch --clear --exec clippy"; + + "watch:lint:pedantic" = cmd "Pedantic lint on save" + "${cargo} watch --clear --exec 'clippy -- -W clippy::pedantic'"; + + "watch:test:host" = cmd "Run all host tests on save" + "${cargo} watch --clear --exec 'test --features=mermaid_docs,test_utils'"; + + "watch:test:wasm" = cmd "Run all Wasm tests on save" + "${cargo} watch --clear --exec 'test --target=wasm32-unknown-unknown'"; + }; + + test = { + "test:all" = cmd "Run Cargo tests" + "test:host && test:docs && test:wasm"; + + "test:host" = cmd "Run Cargo tests for host target" + "${cargo} test --features=test_utils"; + + "test:wasm" = cmd "Run wasm-pack tests on all targets" + "test:wasm:node && test:wasm:chrome"; + + "test:wasm:node" = cmd "Run wasm-pack tests in Node.js" + "${wasm-pack} test --node"; + + "test:wasm:chrome" = cmd "Run wasm-pack tests in headless Chrome" + "${wasm-pack} test --headless --chrome"; + + "test:docs" = cmd "Run Cargo doctests" + "${cargo} test --doc --features=mermaid_docs,test_utils"; + }; + + docs = { + "docs:build:host" = cmd "Refresh the docs" + "${cargo} doc --features=mermaid_docs"; + + "docs:build:wasm" = cmd "Refresh the docs with the wasm32-unknown-unknown target" + "${cargo} doc --features=mermaid_docs --target=wasm32-unknown-unknown"; + + "docs:open:host" = cmd "Open refreshed docs" + "${cargo} doc --features=mermaid_docs --open"; + + "docs:open:wasm" = cmd "Open refreshed docs" + "${cargo} doc --features=mermaid_docs --open --target=wasm32-unknown-unknown"; + }; + + command_menu = command-utils.commands.${system} + (release // build // bench // lint // watch // test // docs); + + in rec { devShells.default = pkgs.mkShell { - name = "rs-ucan"; + name = "ucan"; - # blst requires --target=wasm32 support in Clang, which MacOS system clang doesn't provide + # NOTE: blst requires --target=wasm32 support in Clang, which MacOS system clang doesn't provide stdenv = pkgs.clangStdenv; nativeBuildInputs = with pkgs; [ - # The ordering of these two items is important. For nightly rustfmt to be used instead of - # the rustfmt provided by `rust-toolchain`, it must appear first in the list. This is - # because native build inputs are added to $PATH in the order they're listed here. - nightly-rustfmt + direnv rust-toolchain + self.packages.${system}.irust + (pkgs.hiPrio pkgs.rust-bin.nightly.latest.rustfmt) + pre-commit + pkgs.wasm-pack + chromedriver protobuf - direnv - self.packages.${system}.irust - nodejs - nodePackages.pnpm + unstable.nodejs_20 + unstable.nodePackages.pnpm + + command_menu ] ++ format-pkgs ++ cargo-installs - ++ lib.optionals stdenv.isDarwin [ - darwin.apple_sdk.frameworks.Security - darwin.apple_sdk.frameworks.CoreFoundation - darwin.apple_sdk.frameworks.Foundation - ]; + ++ lib.optionals stdenv.isDarwin darwin-installs; shellHook = '' [ -e .git/hooks/pre-commit ] || pre-commit install --install-hooks && pre-commit install --hook-type commit-msg + + export RUSTC_WRAPPER="${pkgs.sccache}/bin/sccache" + unset SOURCE_DATE_EPOCH + '' + + pkgs.lib.strings.optionalString pkgs.stdenv.isDarwin '' + # See https://github.com/nextest-rs/nextest/issues/267 + export DYLD_FALLBACK_LIBRARY_PATH="$(rustc --print sysroot)/lib" + export NIX_LDFLAGS="-F${pkgs.darwin.apple_sdk.frameworks.CoreFoundation}/Library/Frameworks -framework CoreFoundation $NIX_LDFLAGS"; ''; }; + formatter = pkgs.alejandra; + packages.irust = pkgs.rustPlatform.buildRustPackage rec { pname = "irust"; - version = "1.70.0"; + version = "1.71.19"; src = pkgs.fetchFromGitHub { owner = "sigmaSd"; repo = "IRust"; - rev = "v${version}"; - sha256 = "sha256-chZKesbmvGHXwhnJRZbXyX7B8OwJL9dJh0O1Axz/n2E="; + rev = "irust@${version}"; + sha256 = "sha256-R3EAovCI5xDCQ5R69nMeE6v0cGVcY00O3kV8qHf0akc="; }; doCheck = false; - cargoSha256 = "sha256-FmsD3ajMqpPrTkXCX2anC+cmm0a2xuP+3FHqzj56Ma4="; + cargoSha256 = "sha256-2aVCNz/Lw7364B5dgGaloVPcQHm2E+b/BOxF6Qlc8Hs="; }; - - formatter = pkgs.alejandra; } ); } diff --git a/package.json b/package.json index 26486d77..93317fbb 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "ucan", "version": "0.1.0", - "description": "A UCAN library built from rs-ucan", + "description": "A UCAN library built from ucan", "repository": { "type": "git", - "url": "git+https://github.com/fission-codes/rs-ucan.git" + "url": "git+https://github.com/fission-codes/ucan.git" }, "keywords": [ "authorization" @@ -12,30 +12,30 @@ "author": "", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/fission-codes/rs-ucan/issues" + "url": "https://github.com/fission-codes/ucan/issues" }, - "homepage": "https://github.com/fission-codes/rs-ucan#readme", - "module": "dist/bundler/rs_ucan.js", - "types": "dist/nodejs/rs_ucan.d.ts", + "homepage": "https://github.com/fission-codes/ucan#readme", + "module": "dist/bundler/ucan.js", + "types": "dist/nodejs/ucan.d.ts", "exports": { ".": { "workerd": "./dist/web/workerd.js", - "browser": "./dist/bundler/rs_ucan.js", - "node": "./dist/nodejs/rs_ucan.cjs", - "default": "./dist/bundler/rs_ucan.js", - "types": "./dist/nodejs/rs_ucan.d.ts" + "browser": "./dist/bundler/ucan.js", + "node": "./dist/nodejs/ucan.cjs", + "default": "./dist/bundler/ucan.js", + "types": "./dist/nodejs/ucan.d.ts" }, "./nodejs": { - "default": "./dist/nodejs/rs_ucan.cjs", - "types": "./dist/nodejs/rs_ucan.d.ts" + "default": "./dist/nodejs/ucan.cjs", + "types": "./dist/nodejs/ucan.d.ts" }, "./web": { - "default": "./dist/web/rs_ucan.js", - "types": "./dist/web/rs_ucan.d.ts" + "default": "./dist/web/ucan.js", + "types": "./dist/web/ucan.d.ts" }, "./workerd": { "default": "./dist/web/workerd.js", - "types": "./dist/web/rs_ucan.d.ts" + "types": "./dist/web/ucan.d.ts" } }, "files": [ @@ -59,7 +59,7 @@ } }, "opt": { - "command": "wasm-opt -O1 target/wasm32-unknown-unknown/$TARGET_DIR/rs_ucan.wasm -o target/wasm32-unknown-unknown/$TARGET_DIR/rs_ucan.wasm", + "command": "wasm-opt -O1 target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm -o target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm", "env": { "TARGET_DIR": { "external": true @@ -70,7 +70,7 @@ ] }, "bindgen:bundler": { - "command": "wasm-bindgen --weak-refs --target bundler --out-dir dist/bundler target/wasm32-unknown-unknown/$TARGET_DIR/rs_ucan.wasm", + "command": "wasm-bindgen --weak-refs --target bundler --out-dir dist/bundler target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm", "env": { "TARGET_DIR": { "external": true @@ -84,7 +84,7 @@ ] }, "bindgen:nodejs": { - "command": "wasm-bindgen --weak-refs --target nodejs --out-dir dist/nodejs target/wasm32-unknown-unknown/$TARGET_DIR/rs_ucan.wasm && move-file dist/nodejs/rs_ucan.js dist/nodejs/rs_ucan.cjs", + "command": "wasm-bindgen --weak-refs --target nodejs --out-dir dist/nodejs target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm && move-file dist/nodejs/ucan.js dist/nodejs/ucan.cjs", "env": { "TARGET_DIR": { "external": true @@ -98,7 +98,7 @@ ] }, "bindgen:web": { - "command": "wasm-bindgen --weak-refs --target web --out-dir dist/web target/wasm32-unknown-unknown/$TARGET_DIR/rs_ucan.wasm && cpy --flat src/workerd.js dist/web", + "command": "wasm-bindgen --weak-refs --target web --out-dir dist/web target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm && cpy --flat src/workerd.js dist/web", "env": { "TARGET_DIR": { "external": true @@ -112,7 +112,7 @@ ] }, "bindgen:deno": { - "command": "wasm-bindgen --weak-refs --target deno --out-dir dist/deno target/wasm32-unknown-unknown/$TARGET_DIR/rs_ucan.wasm", + "command": "wasm-bindgen --weak-refs --target deno --out-dir dist/deno target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm", "env": { "TARGET_DIR": { "external": true @@ -143,7 +143,7 @@ ] }, "test:chromium": { - "command": "pw-test tests/rs_ucan.test.js -r mocha --reporter json --cov > tests/report/chromium.json", + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --cov > tests/report/chromium.json", "dependencies": [ "build", "test:prepare" @@ -153,7 +153,7 @@ ] }, "test:firefox": { - "command": "pw-test tests/rs_ucan.test.js -r mocha --reporter json --browser firefox > tests/report/firefox.json", + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --browser firefox > tests/report/firefox.json", "dependencies": [ "build", "test:prepare" @@ -163,7 +163,7 @@ ] }, "test:webkit": { - "command": "pw-test tests/rs_ucan.test.js -r mocha --reporter json --browser webkit > tests/report/webkit.json", + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --browser webkit > tests/report/webkit.json", "dependencies": [ "build", "test:prepare" @@ -180,7 +180,7 @@ ] }, "test:node": { - "command": "pw-test tests/rs_ucan.test.js -r mocha --reporter json --mode node > tests/report/node.json", + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --mode node > tests/report/node.json", "dependencies": [ "build", "test:prepare" diff --git a/proptest-regressions/delegation/payload.txt b/proptest-regressions/delegation/payload.txt new file mode 100644 index 00000000..03657047 --- /dev/null +++ b/proptest-regressions/delegation/payload.txt @@ -0,0 +1,8 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 2219b6f1cfd2b9c29cdae1174fd8633335cb25947b15ef61c3df44f2664175c3 # shrinks to payload = Payload { subject: None, issuer: Key(EdDsa(VerifyingKey(CompressedEdwardsY: [46, 111, 204, 227, 103, 1, 220, 121, 20, 136, 224, 208, 177, 116, 92, 193, 227, 58, 76, 28, 159, 204, 65, 198, 59, 211, 67, 219, 190, 9, 112, 230]), EdwardsPoint{ X: FieldElement51([1689611602193863, 490607132032821, 1343312146746774, 1090732682789050, 1815270510391065]), Y: FieldElement51([1127445621927726, 1752742079139643, 335263251657170, 455073811812238, 1802102173971517]), Z: FieldElement51([1, 0, 0, 0, 0]), T: FieldElement51([396516250482892, 1271770325328148, 2066179188049959, 970219954360817, 1259266248234093]) }))), audience: Key(EdDsa(VerifyingKey(CompressedEdwardsY: [46, 111, 204, 227, 103, 1, 220, 121, 20, 136, 224, 208, 177, 116, 92, 193, 227, 58, 76, 28, 159, 204, 65, 198, 59, 211, 67, 219, 190, 9, 112, 230]), EdwardsPoint{ X: FieldElement51([1689611602193863, 490607132032821, 1343312146746774, 1090732682789050, 1815270510391065]), Y: FieldElement51([1127445621927726, 1752742079139643, 335263251657170, 455073811812238, 1802102173971517]), Z: FieldElement51([1, 0, 0, 0, 0]), T: FieldElement51([396516250482892, 1271770325328148, 2066179188049959, 970219954360817, 1259266248234093]) }))), via: Some(Key(EdDsa(VerifyingKey(CompressedEdwardsY: [46, 111, 204, 227, 103, 1, 220, 121, 20, 136, 224, 208, 177, 116, 92, 193, 227, 58, 76, 28, 159, 204, 65, 198, 59, 211, 67, 219, 190, 9, 112, 230]), EdwardsPoint{ X: FieldElement51([1689611602193863, 490607132032821, 1343312146746774, 1090732682789050, 1815270510391065]), Y: FieldElement51([1127445621927726, 1752742079139643, 335263251657170, 455073811812238, 1802102173971517]), Z: FieldElement51([1, 0, 0, 0, 0]), T: FieldElement51([396516250482892, 1271770325328148, 2066179188049959, 970219954360817, 1259266248234093]) })))), command: "eâ'Ⱥ{Ⱥ𞹒/𖿠¦꧊=:Z꧉&ceG4ᅲQ&]%", policy: [And(LessThan(Select([Values]), Integer(-17977310508110653107821168443657725762)), GreaterThan(Select([ArrayIndex(-2063331637), Values, ArrayIndex(1729047197)]), Integer(-71863900340009931549720359637876494783))), LessThanOrEqual(Select([ArrayIndex(1500241979), ArrayIndex(-659046873), Field("`Ⴧ*ᧂ:𑛄&"), Values, Values, ArrayIndex(1750800327), Field("Ⱥd"), ArrayIndex(-1789308745)]), Integer(70158225305663682194208457939428168215)), LessThanOrEqual(Select([Values, Field("*🫁𝕄L𞸧/ຄ𑻤$u\"*𝼥m':ᨼ𑾰/\\"), ArrayIndex(-554915756)]), Float(9.08306675974206e-234))), Or(LessThanOrEqual(Select([Values]), Float(1.003428263794859e-307)), And(Equal(Select([ArrayIndex(-233587294), Field("𝒫𞻰& キ<&ஞ臨2m𞺣'𞋿&{\u{ac3}{D8𒑀𑰈5\u{1ac0}\""), Values, Values, Field("G.吸aj𑿤q^a9%.<\u{84}%.𝝹𔓶": true, "*E🕴5/.`\\x=\u{7a853}&\u{5fd16}𩟨\u{b}\u{b3012}\u{9e345}/A\u{1b}Dts&E{w}\u{d8473}": 51, ".Z\u{795c2}?*\u{bffa7}B¥-/\tb阂?": 50, ".\u{7f}": baguqegzax2jx7mre4zpv63vto6u3mtjy4ivcrpnsnrgfcb6bymbpbwz5ky2q, ".\u{80}%\u{b}$\u{c6cf9}=&&\u{4}↜$'N\u{7f}\u{b}2\u{2}?\u{65b02}Uð𫻓\u{b}\u{feff}": -1.3122304768215823e-86, ".𗨥Ø\u{7f}\u{fe136}g\u{880c8}0O\\è": false, "1={\u{4fc0c}/\u{37be6}+\u{73284}.{\r�腄/_%�?\r\u{b022e}H": false, "2𱟴.": null, "4'\u{70fad}\\�o{\u{ce77b}\u{7f}~\u{d4dac}\u{1b}V\u{b}𩱋�\t&\\\u{aee01}`🕴\u{ee7dd}": null, "5$w\u{df9bd}\\\u{6}\tѨ\u{1b}M\u{cd289}:U%\u{202e}I\u{ea2ae}\r\u{d0993}D\u{feff}Ñ": null, "5\u{db9ee}\u{feff}\"�Ⱥ🕴z\0v<<\"5": 0.0, ":": bafy2bzacecp4g47wlyry7zukgmgp6xoxsh5aqlhn2vtxbpk2j77pzmx5w5xci, "<\0\u{3fe95}\t=`Z𰲨pdËf\u{5}�\u{1b}30\u{a2f77}\t:|%\u{a65a3}": 25, "<\u{2}¥?\u{9ae9b}'?q\u{7f}\u{84177}/?d🕴\u{d0dee}_x\u{a73f9}=`/\"Ѩ¥+\u{1}\u{202e}+Ⱥ/": "%){\u{f34d0}\u{91bf2}{]¥?<\u{7e5a3}\\\u{7f}¥", "<\"k&🕴Q\u{a0cc1}\u{193c1}\u{6fef1}\u{57765}=\u{b04a9}\u{1ee38}*$i�趼\r:^�\t\u{2}/": null, "<'\u{202e}": [246, 133, 18, 125, 51, 172], "<*\r\u{feff}'\u{b}\u{feff},\t\u{202e}\u{b}": 1.9096030614441494e34, "\0#.\u{e50a7}`\u{6b781}\u{72c56}IѨTȺ:\u{202e}\u{c1d84}\u{d1ebf}F\u{bc6f1}P�\u{1b}}?d\u{feff}Ⱥ\u{96517}\u{7}": -21, ">\u{7f}h)\t&'\u{b}": "p$\u{ce2d9}T\t\u{6486c}\"\u{af416}", "?*=\u{eea8d}\u{a8f3d}\u{479bf}\\\u{577da}<Ѩ&\u{202e}\u{ceb29}\u{8}/\u{7f}\u{7f}𘂤*\u{91}\u{96d48}\u{202e}\u{60a35}6\u{7cd5b}\u{b}~7\u{feff}\u{fc2b3}\u{6ca08}": bafykbzacedzd5rmt4wtpaenao7lyjfstkk3babvem5zvxsx66esxsdwdmnuok, "?\u{f5061}\u{6}\u{adb94}\tz<\u{8e6f5}\t]\"\u{1b}\u{50c21}?=\u{feff}\"'\u{cdff0}{�\u{11b58}\u{7f}ñ¥'\"I\u{6efe5}\0": "M\u{d7240}\u{5fa2f}\"\t\\Ⱥ\u{8}\r𓂂E\u{615a1}$ :\0\u{4b6de}2�&::", "D\u{834c0}": bafybwictq6cimsvk5g36pcopei5vzwuqjgz6zab4qnlw6h6pexvenpnuz4, "G{\0P\u{202e}z?<*\u{e4d94}\u{caeb6}c\u{b}\"\u{d4fa0}\0\u{3d0e1}\u{c315a}": true, "O:{.:6\u{1}𒋵=": [215, 19, 250, 35, 145, 227, 89, 97, 226, 22, 100, 90, 212, 248, 231, 66, 1, 74, 222, 19, 252, 253, 50, 43, 95, 28, 217, 195, 43, 193, 37, 211, 223, 168, 162, 177, 117, 244, 235, 50, 129, 137, 255, 246], "P\u{7f}m[*^\u{d4b73}\u{a4603}\u{cae36}\u{202e}C\u{2}\u{99a81}/<": "\t=/\u{202e}{?=z𣿸&Ѩ\t\u{10fefb}:`\\\u{88684}ᇵ&<'IB9Ѩ", "Q=/\u{2}[W$🕴Ѩi`\u{6b1f0}g\0\u{202e}&äq\0\\\u{e7b6e}\u{7}2": 0.0, "S\u{feff}¥\u{363f5}\\�\"|/$\u{1b}🕴\u{d61d7}\u{9c5fc}\u{5}%": true, "\\": 1797410591.2173085, "\\'\\\u{72082}9\r\u{fd777}&'\u{8}-\t$Ѩ\u{85}&'\\$\u{202e}\u{4}\u{d59a8}𐋪": null, "\\*\t&>ó": [197, 70, 103, 161, 242, 103, 93, 85, 220, 20, 175, 146, 70, 248, 167, 201, 81, 105, 89, 137, 113, 31, 52, 197, 100, 44, 200, 169, 146, 8, 75, 185, 88, 27, 164, 65, 95, 19, 82, 178, 129, 49], "\\¥𧷋%\u{7f}/%": 5.3388979855107657e-222, "]\u{2},n{\t": 4.726007655014673e-268, "]\u{e5bc2}:\u{67d79}": [192, 72, 37, 153, 92, 243, 115, 45, 85, 213, 125, 24, 161, 32, 231, 155, 146, 251, 181, 211, 205, 114], "^": false, "`\"\rBbIU\u{b38e7}~{\u{1b}<": [166, 52, 57, 140, 164, 87, 17, 112, 52, 240, 111, 77, 237, 27, 236, 44, 43, 106, 52, 249, 5, 150, 136, 195, 238, 123], "`=": 37, "`=\0\u{ae1db}$'\u{b}<{\u{1}\u{2};": true, "`~HѨ\r=": null, "`\u{91304}8?=\u{456a5}?\u{ee878}`": [72, 35, 50, 245, 247, 42, 83, 78, 154, 184, 6, 235, 171, 159, 23, 112, 160, 167, 206, 73, 56, 6, 234, 198, 253, 149, 213, 217, 121, 184, 227, 205, 182, 89, 67, 116, 130, 153, 30, 111, 90, 28, 205, 170, 195, 105, 252, 13], "`\u{b62b4}l\rȺ\0䱮Ѩ\u{325fa}://%.H*\r4\u{67fc7}f\u{feff}\u{97c83}d_z𒋤𩞬<": "{\u{7}\u{1b}\u{1}\t\"Ѩr\u{5}", "`\u{db05f}\u{82068}%\u{e3259}\u{202e}&\u{8916b}r\u{b}\u{eda56}�7\u{7f}\u{800bc}": null, "a\u{7f}": -4.342818332244186e254, "e;='\u{66fb7}¦_.R\u{6}S\u{99a3c}1*'d�\u{aecf1}?¥:\u{cd4f6}🕴¥{\u{202e}V'": null, "i\u{7acc3}\0*\u{3a0a4};\"G\0x\u{202e}\u{202e}\u{a6ef1}\u{7}y\u{c4c0c}*=\u{202e}Ñ|]\u{b}`S�\u{63b03}&¦🕴|": "\u{9b}\u{feff}/\u{2}\u{32ba0}$5N\u{c18b1}\u{6e305}", "i\u{b88de}:%<:\u{b}i¥\\%&\u{e9081}\u{36b96}\\wa\u{53cd7}\u{7f}\u{441e2}": [254, 111, 111, 15, 235, 43, 5, 115, 184, 222, 104, 79, 29, 45, 167, 151, 205, 114, 64, 194, 79, 2, 66, 154, 153, 108, 151, 126, 250, 233, 134], "z": true, "{\u{7f}Ⱥ&��0a]\u{7f}\u{b3046}\u{2fa93}\u{7f}<`a\u{10b100}b\u{e36be}-:\u{b}}I\\\u{c99dc}(�\u{d4177}<": 5.69430214693307e-309, "{\u{90}�\r¥%`": null, "\u{7f}=n\r\u{b904f}m\\`&\u{8d}&\u{8}q\u{10fb4a}\u{202e}\u{feff}": 30, "\u{7f}\u{6677f}\"\t\t𥡏&\u{929fa}%\u{1b}": false, "¥\u{4b398}$\u{b}.\u{10edc3}.v\u{4}\u{c8f3b}`\u{1b}o\u{b}B�.\u{8a4e7}": true, "¦\u{567e2}\u{789fb}.\u{1}\u{bac03}FeLѨ": [46, 154, 121, 10, 113, 153, 177, 226, 108, 199, 94, 194, 182, 56, 189, 253, 144, 68, 220, 178, 227, 39, 5, 27, 46, 171, 2, 171, 100, 125, 136, 48, 185, 143, 175, 224, 38, 180, 89, 73, 253, 97, 152, 89, 126, 226, 69, 159, 39, 4, 5, 151, 166, 157, 129, 18, 74, 141, 30, 245, 18, 68, 59, 4, 84, 71, 127, 83, 157, 160, 70, 180, 19, 67, 237, 156, 119, 247, 69, 10, 220, 32, 51, 35], "Ê`/\u{e98d1}`8\u{5a946}¥\tѨ\\\u{71794}\u{1b}\u{5d467}\u{5f80a}\u{2}o\\\u{78e03}\u{1b}$V\0,Ⱥu<\t\u{4}": "l\u{80}/H", "Û\u{a246c}\0\u{cc1d1}\u{1b}": -2.3446004632097955e-204, "Ⱥ\u{48608}�\u{1}Ѩ\t\"\\\rRx\u{feff}𱋭🕴.>.\r?/Ѩe": bafybmibqonatpssw5xwj3ckqwghe3uv52fbojbwo6wevvgyxhhi6is34iy, "ЏѨ$N==jX\u{ef98d}?{Ⱥk\u{4c9b3}\u{a5c43}'\rx": true, "Ѩ?=Ⱥ\u{7f}\0🕴$�\u{39466}\u{1b}�\"`\0.\u{feff}*.x-\u{7f}EѨÔ\u{7}\u{10f98d}$P": false, "Ѩ뀳🕴\t\u{202e}<{\u{950c8}\0\u{6abe2}2¥G\0": bafkrmicucdpcfbvzkhxwm7wgcnbx7wbbsx7yf4xs3xnebcwqv4fzfnyeui, "\u{202e}\u{5}\u{f93a8}�\t\u{93751}DX.4\u{b}\u{47079}\u{cb546}Ê\u{3c9c6}('Q\u{df412}^(?%\u{1b}\u{d4087}\u{87657}\u{69291}": bafyr4igptyd3pys2jr5l7f6wly2ixknaqjhxa2goliyvnoxef4q5npn2wq, "\u{202e}//)H\u{489d0}\u{b}\"\u{7f}7\u{fd8c1}\u{52967}": -49, "퀹\u{1e584}Ⱥ\u{b5e48}?\u{d70a5}\u{202e}\u{feff}o&Ⱥ\t2&$TѨ\u{c7d9d}¥z9�#B,\u{7d2c6}\u{a9082}": -39, "�\u{b}\u{9b}:\u{afa9e}\u{8b}\"G%=\u{6}\u{b1e04}𫃏\u{202e}{:Ⱥ'\0[;\u{cbe4b}Ѩ¥V\r:e\r�\"\u{36274}{\"\u{feff}\t\u{1b},\\�鴇\\µ": "*\u{b2484}(j'0\\�\t\u{feff}~I*\u{1abf0}鲫!¥í\u{7f}\t\t\u{feff}", "\u{2}¥@@x\u{7}o<>\u{b}>�\"=\u{4a714}H\u{356a6}Y\u{feff}¥\0Ѩ`\u{8586b}�@\u{86433}8\u{b5eb0}U\t\u{928a7}?:3V\r\0\u{e963}\u{91606}\u{f5d9a}`&&\t\\{$&`": [203, 219, 129, 233, 167, 86, 133, 68, 30, 123, 72, 152, 245, 36, 18, 51, 184, 218, 78, 248, 74, 234, 85, 239, 225, 163, 68, 62, 10, 51, 41, 233, 241, 66, 225, 241, 213, 58, 199, 207, 196, 20, 9, 130, 162, 125, 157, 172, 66, 51, 176, 230, 83, 217, 243, 126, 106, 4, 227, 168, 130], "?\t#?\u{c8453}l<\u{ebb70}<\u{202e}\u{1}\u{4a0a6}\u{bb34d}𧆽{\"\u{202e}\u{a6e5e}<\u{2}$\u{1b}{\u{1b}\u{a7345}\u{bcba2}=\u{55f80}𠘓`": false, "@\0 \u{e2e8b}\u{feff}&{¸\u{1b}`Ã`<\u{c80a3}\"h%V\\:\r:{%\u{b}\u{91b96}\0": 16, "A\0\0%Ⱥ\u{7f}o6Ѩ\ra\t\u{fa6d1}": 0.0, "E5\u{3ef04}\\iѨ4==\u{ad632}\u{103a8b}\0\u{882ea}\u{3fb8f}\u{80a35}{\r\"\u{9afa0}\\<3*\u{e368f}\"{Ѩ\"\u{85b40}/": [62, 206, 252, 139, 165, 123, 83, 85], "K?\u{15ea9}%/Ⱥ\u{39c0e}Ѩ\tM/\u{806b5}𣎂+": "\u{7b58c}\u{eb27}", "Py/\u{eeae7}\t:z/u&\u{fdecd}\\`<": 2, "P🕴EU\t$\0V\"*": true, "S\u{6}?'{¦~:¥\"\u{797af}:Ⱥ": false, "W/\u{7d22d}\u{7f}k\u{6f5bc}\"/\u{7ac51}\u{feff}\r\u{1b}\u{b}`\r\u{16d8a}\"\u{feff}\u{4e68c}": null}, 3.7320199453131945e18, true, "\td", -2.1302548429054803e-66, [9, 130, 254, 29, 224, 58, 3, 226, 45, 154, 180, 58, 156, 107, 43, 160], [184, 58, 122, 21, 191, 153, 91, 193, 125, 41, 253, 9, 18, 17, 100, 75, 60, 215, 91, 98, 131, 82, 225, 220, 226, 154, 246, 54, 253, 108, 18, 104, 142, 238, 92, 249, 74, 194, 85, 60, 101, 114, 120, 130, 76, 198, 254, 54, 43, 192, 5], -3.035213207805024e-151, true, null, true, [144, 5, 247, 45, 158, 101, 252, 143, 38, 225, 226, 145, 14, 34, 204, 157, 201, 182, 152, 54, 29, 245, 248, 233, 203, 103, 50], 7.126162207025803e98, 0.0, 38, 44, false, 1.442945330979412e-308, -3.2821103465088214e185, false, -0.0])), GreaterThanOrEqual(Select([Field("\u{dca}?i%ѨѨ<\\{Ѩৡ=ᨁr\"T\u{1a7f}🛞{༴Ⱥ:ᛧ"), Values, Values, ArrayIndex(-206845918), ArrayIndex(284566073)]), Integer(-130442174005234510445148006169593931048)))), Every(Select([Values, ArrayIndex(-1235287252), ArrayIndex(1235974543), Field(":�𐔆Ѩ1ꬓ*%𝔹⑩%By�ࡤ{6&🃅%ᒜ!*t\u{cd5}ܔÈ🃌K-:f"), ArrayIndex(-74267187), Values, Field("𐝧¥ѨK𛄲ﹰ~/𞺀Ⱥ𐁑{A℈ꧻxኁ$X")]), Every(Select([ArrayIndex(-835767092), ArrayIndex(-90842294), Values, Field("&Z𛱺\u{1a6b}¥¡\u{2002}O�Eࡪ(/ѨѨ\u{fb3}ö2<=u,ⶣb"), Field("হ\u{1d242}]𐺱'<×\".᪗\u{cc6}%\u{9e2}/$`+ꚠ𞹲ѨRr'7\"\u{1921}"), ArrayIndex(-1657524013), ArrayIndex(-1358049927)]), Equal(Select([ArrayIndex(-2133215332), Field("�ѨA$=%0𑒌$𞴻{ῚL𒿎<*Hÿ)HಯmJ᧗ૉvকj¥𑶄ዅ"), ArrayIndex(1372867808), Field("9U𒐽ᅥטּ:^�ཬ{L\u{1a7f}🛞e{𝔊sை8%$-SޥJ=\u{1e136}&"), ArrayIndex(-1515605936), Values]), Newtype([[220, 112, 193, 144, 130, 2, 201, 3, 173, 97, 18, 34, 79, 189, 93, 185, 99, 8, 146, 72, 228, 21, 109, 226, 107, 211, 58, 9, 68, 90, 109, 0, 69, 37, 94, 48, 179, 143, 33, 56, 70, 63, 33, 221, 197, 203, 25, 245, 187, 130, 222, 127, 221, 178, 88, 248, 218, 158, 87, 24, 127, 138, 100, 104], bafkr4ighuk7gjom3gactib6iatiimrm4ytvtdk7y5k4pzutsg56i77lanm, -8.793256611809324e-208, [49, 12, 111, 228, 6, 196, 193, 26, 82, 212, 21, 13, 44, 254, 161, 77, 168, 161, 151, 85, 22, 53, 168, 74, 48, 15, 119, 171, 245], 3.675952639990706e158, false, null, null, null, null, -156.1567975459344, "\u{1070c5}\u{202e}m\u{7f}'#\t\":\u{dad72}ü4\u{202e}\u{7f}'涛\0$E{-/\u{202e}\u{7f}b", "\u{13e62}$e<\0&<\u{c5acc}ny\u{9a704}\"\u{202e}𔖚D", bafy6bzaceagsds7mbaud2m6qhdohvpgwuhfeedqwrntvioyma3kuo5v6vtyt6, null, null, "\r\tg:\u{46cc2}", [142, 185, 121, 120, 78, 34, 236, 132, 123, 49, 125, 116, 146, 139, 230, 17, 174, 47, 83, 146, 252, 30, 220, 161, 163, 207, 136, 121, 230, 68, 139, 61, 190, 89, 223, 74, 72, 219], [28, 249, 124, 123, 205, 232, 92, 8, 106, 47, 104, 13, 104, 201, 76, 228, 38, 128, 160, 168, 2, 34, 83, 71, 248, 91, 111, 250, 225, 49, 225, 194, 169, 133, 253, 173, 44, 47, 27, 4, 181, 140, 138, 211, 3, 136, 21, 139, 138, 196, 191, 191, 76, 163], true, [87, 3, 80, 153, 162, 125, 138, 138, 127, 8, 103, 89, 128, 18, 40, 41, 190, 156, 216, 106, 118, 162, 241, 107, 134, 144, 107, 52, 51, 165, 85, 124, 197, 155], "\u{2f39d}{¥\u{40daf}ѨS\u{a44e7}{\u{a7dc1}�\u{cee1b}'", [235, 139], false, "", "\u{a1d4d}\u{feff}\u{4}\u{feff}3\u{1494d}\u{e1172}.\u{1b}`\u{d2482}{\u{9e}\u{e1d5d}\"3/{\u{5}2].Ⱥ¤<\u{96fa3}Ⱥ\u{49ea4}*\"p\u{b83db}\u{6}\u{6}": null, "?/\u{bdd3c}\u{370b4}\0*\u{102d37}\u{cbc47}.\\Zâ\u{1ced4}KѨ🕴::\u{7f}/": 44, "?8?A:/'H\u{b}\tr\"¥=qu\u{b}¢\u{fde59}\"\u{2}ye'?\t\u{ea1b7}K`\t": -3.9299672377331247e-146, "?{\u{7f}%\u{7f}\r%\u{d3348}\u{b}\u{eb7b2}%¥\u{9406d}\rѨѨ],\u{202e}'\u{105dc6}<^úr\u{8015f}": "\u{9d}\u{891c2}𫩼\u{606aa}\u{45288}p!/&G:㝸ã\u{97}Ⱥ\u{b}", "?Ѩᆬq7/%\\\t\u{feff}\u{da5c3}\"\u{8509f}\"H`\u{1b}%\u{4d464}\u{479f4}\"\u{c147b}\u{1b}/𧳓*,|": "Y\\\u{7f}\u{b1fcf}\u{e99b9}7\u{9e18b}:?\u{ede3}$W", "?\u{5d644}\u{5a09a}\"�l\u{1b}/\06Ѩ.&{\u{10a396}": bafyrmihqvy27erjh5esd3x6wdgnr6z6lru7wfcmkuuvzp7h7am6z63mbia, "A\u{763e5}& \u{d7169}\u{e8031}{\u{b}*\"\t𰩧\u{548b4}?u`/\r🕴\u{7c626}$å🕴\u{10dd6}:🕴": null, "DѨ\u{6ebc8}\u{7f}<\\[?\u{7f}&ኵ:f=𫗿\0�\u{202e}": "s\\/;*\\\u{4150f}E", "F\u{1b}\u{10ea63}\r": -3.881911088191245e-265, "H.¥$": -21, "K\u{cbff5}\u{202e}\u{1260e}\u{feff}&:%\t$\"|&\u{342e6}*:\"&ju/c": true, "N\u{b}&\u{feff}\u{691d4}\u{6}\u{b}:&*Ѩ\u{6f0dd}w\u{e581c}¥z�\u{9cce5}\u{16d5d}\u{4f30a}**\u{93d8c}\u{202e}\u{7}{\u{1b}{±'{K": "%\u{feff}¥", "[𪲁@\u{1387b}\0]\u{6fca2}Bi.\u{89a95}\u{6a578}\0\u{4a64d}\u{b}\t\u{647d9}n\0^\u{8ff74}Yk\u{9c}\u{9c}(": [116, 173, 176, 186, 52, 23, 252, 79, 37, 2, 204, 64, 93, 133, 149, 198, 164, 255, 254, 251, 238, 69, 160, 225, 130, 239, 127, 62, 193, 179, 31, 95, 161, 114], "\\\u{6d15d}\u{b}&\\Ѩã\u{e6f74}<\u{a15a9}\tѨ,\u{b}\u{819b6}\u{3ef57}/\u{4ddf0}Y(\u{5}\u{100a6f}\u{b}/.:": -37, "]\u{3}\t\u{e5fb9}?{\u{202e}z\u{44ab3}\u{39431}\u{e8c44}¥\u{79b81}'\u{777e6}�:{%\u{3900c}{%\u{108b7b}<{`\u{7f}Ó\t🕴T.": 29, "`\u{6}.:🕴{\u{9c4a2}\\<*:": bafy6bzaced7s7j4l6dybvc55yypxhw5em5sg2ciws33rjhgce6hmfmpgf4fdu, "`o=\u{6}3\u{e81b2}\u{1b}G&\0tw\u{3}/\u{3d977}`㊈)=Y\u{2}\0": false, "`\u{44886}/\u{a1f9b}\u{5f978}:\r\u{95be2}/🕴": 0.0, "c\u{5}\u{4bbc4}b\u{c96f8}+%$\u{202e}'<\u{e078c}\u{ac7cc}x\"Ѩ": 53, "n": "\0?\u{efd30}\u{686e5}\"\r\u{5fe32}\u{b5b0c}\u{51f54}\t\u{89}=\u{33916}ZM¥\u{4}\u{7}m�}\u{91c01}\u{6839a}*\u{c59e9}", "oѨ`": null, "w\t\\`\u{da1d2}*&.$E;\u{62ec7})\"Y\r\"5\u{bd563}\u{326cb}{å?/\u{b}\u{98e3b}": "D::\u{202e}F\u{d9e2c}J'=\u{ec5db}\0'\"Ѩ\u{1b}\u{419e5}𮞿?E*mb\u{feff}_Z\u{ab92a}=\u{89c4e}逋\0\u{7f}J", "y6\t<": [179, 71, 5, 85, 100, 29, 222, 53, 97, 142, 194, 243, 96, 220, 13, 106, 64, 105, 167, 218, 123, 136, 220, 228, 82, 153, 8, 92, 185, 11, 112, 146, 197, 109, 163, 11, 117, 83, 66, 85, 178, 149, 17, 95, 212, 87, 96, 62, 216, 80, 38, 36, 236, 156, 22, 40, 44, 133, 95, 15], "{Ⱥ/&\u{7f}\u{5795a}_*\0*\u{a13ce}f?`/\u{1bd42}<𮧏<\\U(n\u{2ffdf}\u{bad0b}&]\u{feff}U🕴$": 9.93350022419413e112, "{\u{39ee3}TI\u{ea081}%Pï>\"Ⱥ\u{feff}{\\j": {"": "\u{feff}\u{1}\u{1}&=¥\u{68854}&/ö\\\u{33a28}\u{d8908}f4]Y\u{39e57}**Ѩ\\*Ѩ\u{5270a}{B¥+x\u{feff}", "\0${\u{8f006}:f<\u{3}\u{ae249}\u{a3dc8}\u{5eb29}\u{b67f9}%:\u{37105}\r\u{1b}'$\u{2faa8}$/A\tW'\u{909b7}\0\u{2}": 2.914324980421285e-195, "\0$\u{ac00f}\u{1b}9\r\u{3}\u{2fd7f}=:R.\u{e3705}\u{c4450}NG\u{73b7c}&$\u{202e}\u{b527c}GL": "\u{a3b69}6a*==?\u{feff}\u{1a1cf}\u{a82da}`/==�🕴\u{6b826}\rD?A\u{feff}\u{1}", "\0*{\u{202e}\t�\u{1b}/|Ѩ%©%": baguqefragwjjb3m3ljrschzrusb2unylas52kabmwfr34wqmvo6rkg2avvuq, "\u{3}\t\0\u{6695e}\"{»1\u{202e}\u{68a4a}N$": [233, 212, 54, 104, 52, 88, 23, 191, 247, 21, 223, 50, 174, 43, 59, 131, 37, 131, 62, 190, 142, 176, 67, 43, 184, 235, 120, 202, 175, 190, 189, 145, 211, 136, 59, 252, 222, 235, 131, 213, 187, 34, 118, 61, 42, 93, 166, 43, 180, 114, 34, 166, 57, 195, 172, 167, 175, 177, 81, 106, 26, 118, 209, 95, 105, 177, 234, 126, 58], "\t\u{1b}7🕴¥qb\u{8}\r\t\u{1062f0}%'\u{b}.g\u{411f2}𭕨B{Ⱥ\u{77264}/\u{b}D\u{3d766}<": null, "\u{b}/<\u{34eed}\0S&>�텐$$": ".\u{7ebf4}Z\u{d3912}\u{feff}?*H\"\u{202e}𦊇\u{91}Ⱥ\u{7f}\r?{)<\"\u{42b46}?T\\\rg\u{ec834}\\\u{7f}\u{7f}¥", "\u{b}?\t\u{2}�4¥𞸩*\"\u{578ba}\0G%��\\r%«=🕴=\\&*=": "\u{8237a}\u{8a}<\u{8ab59}.?<.tàô\u{945d6}\u{feff}`\\#\u{ce61d}c:)?.`a<\u{9d398}\u{962eb}\"&", "\u{b}ÞѨ¾y狦\0🕴\t\u{19d5e}\u{f0148}": bafykbzacebxelvnoczrdap55pukvsxpc6gicbmnqzgsouqceppvlhsicwet6e, "\u{b}ãx\"\\\u{4}`\r:\u{feff}ᦇ\r": 22, "\u{b}�`+\".?hC\u{88713}:\t\0N¥/Ⱥ\u{1d2b9}1&`Ѩ.": 0.0, "\rAf7\06`\u{ff186}\u{9f715}\t": "/$\u{eaf96}\rW.{{.l흄*\u{1b}\"🕴!?\u{a64ef}\\\u{7f}/\u{109cc5}\u{5}'\u{b}\u{fc0c5}𱇴\u{d1388}", "\rUKG/\u{3}\u{2}ꕻ\u{b}\t": -1.3995651248504633e262, "\r\u{72228}\u{3}\t'\t\u{c5bb7}\tv": [207, 209, 38, 232, 225, 181, 157, 174, 248, 85, 75, 104, 14, 234, 9, 86, 149, 189, 217, 176, 122, 32, 78, 186, 174, 19, 241, 185, 202, 135, 32, 184, 35, 47, 78, 189, 35, 153, 142, 128, 46, 7, 65, 55, 37, 215, 0, 109, 138, 244, 78, 202, 124, 93, 146, 143, 199, 70, 40, 153, 254, 139, 213, 103, 34, 98, 157, 146, 23, 126, 115, 247, 59, 186, 119, 63, 16, 179, 123], "\"%3\r\0\t*\u{74f05}\u{10ecdf}%\u{56b43}\u{e2041}\u{b}Ѩ🕴%\u{47ac1}\u{b4501}\u{10314f}\u{b}": 8.190682906409504e77, "\"üE8À%": -5.01070380812316e-309, "\"🕴¿%z4q\u{f5f81}\"\u{202e}\\": [99, 154, 31, 116, 21, 233, 127, 12, 159, 139, 166, 170, 113, 31, 35], "$&\u{81e34}�\u{feff}\u{3}🕴\u{b}\u{3e109}\u{8e8b1}A\u{1b}Ѩ)\r¥\u{c68f3}\u{10081f}\u{10dd8f}": bafyrwibh3uv7biwsbpe37hkwovhrpkm3l7s5icdgji6imq5fjykcl7p24e, "$'Ⱥ':\u{f6dca}{&\u{7d81d}GQѨ𧗿~.Ѩ\u{3f8b4}\u{feff}\u{2}\u{2}`\u{b}\u{202e}\u{7f}\u{8}\u{b45ad}": bafyrwiczadd6c3wkaraxq3lg5clhqyngn6fprrjscfzxfe2mp2kg4pvxba, "$}\u{4998f}}\u{7afe3}\\\0n?\"V\u{1}\"{!*y%\u{1b}\rꢢѨI\u{f13fb}X\u{1061f7}\u{7f}%'l\0=\0^)\u{5ac7e}<": [60, 118, 165, 67, 27, 119, 21, 252, 153, 133, 75, 209, 126, 27, 107, 9, 142, 133, 201, 242, 96, 56, 139, 85, 39, 185, 191, 172, 121, 16, 61, 101, 100, 93, 23, 114, 153, 201, 213, 220, 14, 1, 73, 194, 75, 71, 226, 125, 205, 187, 97, 24, 229, 247, 75, 87, 172, 90], ".`\u{bec17}Ç'\0/\u{d491b}𘅣🕴.\r": 1.2131909958473984e-266, "/\"": bafkr4ifg4p3nsgepq6lvarhhrt4lxydl5arndmqieaknvnqhyvw4l62eau, "/,/\u{b}\u{a9092}\u{3}.\u{7f}ñu?Ⱥ'&\u{e2082}\u{3d612}u": baguqfiheaiqibdayxvhi3ktwv4rfqhx7adreiuo2muqdzc72zj7wydxi2ircenq, "/.\t¬79\u{202e}<%Z\0\u{7}\u{b}&\\🕴\u{c07fd}j=(1\0𮟻mJ�.\u{fe739}ȺXB\u{feff}": [35, 209, 94, 4, 132, 12, 218, 13, 76, 94, 110, 176, 233, 104, 74, 8, 231, 103, 204, 48, 35, 1, 208, 81, 103, 39, 15, 209, 174, 232, 89, 234, 13, 222, 88, 45, 83, 226, 52, 81, 83, 59, 125, 241, 159, 38, 79, 5, 21, 197, 38, 169, 183, 154, 154, 183, 251, 209, 151, 212, 196, 132, 155, 189, 109, 45, 206, 253, 141, 141, 226, 210, 69, 80, 142, 13, 168, 40, 84, 52, 228, 159, 231, 76, 229, 248, 47, 21, 115, 15, 247, 164], "/.\u{b}\0gMg%\0\"n\u{1b}�\u{dc8ef}": null, "0\\\u{b2c71}:.\u{7f}.q\u{ee1a4}\u{3}.`:=\u{7f}shÕ\0'\u{ff5c2}\0<\u{48d2d}": null, "6\u{7f}z\u{fb43d}\0\u{e71c2}n\t\u{1b492}\u{be144}%m\0O£{\u{4}ß`S\u{cc20a}\t\r@\u{1b}\u{2f392}T\0�:4i": [184, 132, 8, 217, 132, 246, 183, 246, 210, 254, 92, 125, 142, 179, 49, 205, 173, 48, 36, 66, 57, 14, 184, 195, 88, 109, 101, 153, 91, 53, 120, 69, 198, 5, 81, 144, 203, 189, 119, 182, 143, 191, 14, 61, 34, 36], ":(`T\0\u{1b}\u{1b}\t$¥z\u{36613}\u{a5f2d}Ѩ\u{d1ec0}Ⱥ\u{7f}\"'u\u{b7fb7}ë` \u{feff}\u{5d6e0}f\rê🕴": [191, 71, 93, 227, 249, 253, 186, 190, 214, 13, 71, 250, 111, 182, 66, 219, 22, 139, 183, 203, 250, 58, 184, 20, 20, 213, 80, 32, 66, 40, 214, 69, 53, 203, 170, 113, 150, 95, 7, 238, 77, 250, 5, 90, 119, 215, 135, 80, 225, 91, 49, 36, 143, 153, 196, 238, 205, 224, 18, 127, 173, 184, 246, 90, 134, 17, 119], "=@\u{8f9f3}7?\\𫥂\u{1}": "i|*\u{c09ed}'\u{b871f}\u{4}*\u{202e}&\\\u{105cbd}bѨ\\*?{F\u{feff}uȺz\0`\u{feff}{+", "={": null, "=\u{a02a9}\\\":¥🕴\"\u{202e}P\u{7f}\u{b7f71}\0\u{3a7e9}Y<î\u{157d9}Å": [19, 138, 165, 245, 200, 2, 67, 217, 45, 240, 188, 107, 149, 30, 61, 57, 176, 165, 123, 163, 219, 5, 81, 31, 155, 252, 139, 204, 155, 201, 120, 3, 32, 18, 218, 98, 147, 233, 22, 106, 60, 86, 204, 202, 147, 32, 223, 159, 66, 107, 250, 251, 19, 167, 246, 252, 90, 205, 212, 54, 251, 5, 241, 43, 213, 120, 23, 1, 29, 163, 69, 234, 99], "?&🕴.*.\u{605a3}¥&j/=a\\\u{b}*Ѩ*'¾�ธ#Ⱥ\t": "\u{7f}\u{8}Y{5\u{60a62}jN=f\0'𩧦\u{feff}\u{7}//a¥kK\u{48989}&\u{f203b}\"\"`", "@Ѩ": "/*\u{95147}\u{b}{`\u{4e71c}", "C\u{3}<Ò?B¥@&": [130, 96, 105, 54, 223, 53, 178, 56, 46, 187, 18, 221, 159, 48, 247, 89, 117, 41, 108, 127, 91, 200, 247, 120, 240, 237, 155, 170, 65, 55, 58, 141, 247, 254, 60, 102, 29, 148, 211, 55, 4, 14, 68, 42, 90, 173, 222, 142, 20, 22, 68, 185, 132, 255, 132, 185, 189, 197], "D\u{a65a4}$\u{df148}\\F\rntѨK\u{1b}jd\u{8ea98}\u{b106e}�": 1.3784978135410346e-170, "Eò<.z\u{781d5}\u{2}\"�^": null, "H\r\u{361f5}{\"�\"\u{41b3d}\\'𦀳¥\u{80}Ⱥ\u{c26fd}�6-`N\u{dca3b}G\u{1de7a}=\\": true, "L\u{7f}:¥=t*{_AP\u{366cc}?wȺ\u{eb31}o.\u{881ae}\u{77df7}t\u{b}\0{\tѨ\u{d6c3a}": [97, 251, 86, 194, 220, 214, 153, 209, 190, 118, 25, 200, 75, 133, 240, 162, 84, 193, 128, 3, 238, 222, 6, 149, 222, 157, 239, 202, 16, 102, 50], "S=U\u{bff90}/?\u{da120}.\u{633c3}a|\u{1c959}\u{3be11}\u{feff}X\u{a9622}?\t.Å.🕴=\u{73c63}𰰅\u{61725}¥\u{a6664}<\u{895f4}i": [140, 113, 176, 106, 69, 9, 192, 221, 52, 44, 56, 187, 134, 114, 29, 65, 208, 39, 0, 95, 237, 224, 76, 195, 8, 225, 21, 98, 228, 60, 95, 240, 189, 156, 136, 235, 72, 132, 236, 170, 1, 250, 184, 134, 77, 48, 249, 199, 172, 3, 66, 201, 75, 15, 29, 254, 104, 111, 158, 53, 80, 85, 74, 36, 20, 15, 150, 52, 31, 50, 233, 6, 228, 244, 185, 202, 142, 56, 126, 47, 69, 35, 225, 162, 147, 42, 172, 213, 71], "\\\u{1b}Ç'(\u{76ac1}\r\u{202e}NÙ\u{87e35}`{\u{52666}Ѩ\u{7f}\u{e28bd}?窣\0\u{e4901}🕴\\": 1.307777027892035e-308, "\\V$\u{71773}\0.H𘑞\u{202e}`&)'\u{cc7d7}4_�📲j.\u{c5777}:\u{b}{\t?\u{abcf6}\u{2}\u{f114a}": null, "`\u{1}7\u{dc7f6}è\u{1b}\u{e6461}", "bF罩`/?{\u{6}m�🕴\u{a0dcf}\u{1b}\u{85683}\r*z\u{3}`\u{202e}\t\0¥\0ó�\u{fa710}": null, "f\u{71088}\u{eeba2}/\u{1b}\u{b}>\r:$H\"ï\\\u{7f}\0¥{+?": null, "k:\t<鏁\t^:u碗\tñr:": bafkrwidekl2tflko33qlm47p7adikj4q6bdju2kq5elnuipzykk565mhby, "s<Ⱥ*[L*<ѨÉ\u{88d19}": [88, 175, 1, 80, 241, 221], "u\r": -2.6904580729605092e122, "u¥\u{feff}\u{1b}": [1, 36, 101, 136, 222, 223], "x`\u{6d787}\u{e5350}\u{df1fb}(>qP🕴\u{ba6ac}𮠰🕴\\\u{4b5e0}J<": false, "{\0\u{3b4bd}=g¥\u{107402}\u{5c069}Ѩ=5k*$`<%p": [42, 216, 219, 136, 90, 214, 91, 194, 12, 10, 62, 81, 119, 54, 126, 138, 227, 206, 148, 138, 95, 191, 247, 89, 212, 142, 18, 121, 148, 163, 152, 177, 37, 69, 222, 22, 226, 117, 153, 5, 89, 234, 92, 155, 223, 10, 158, 200, 18, 37, 110, 103, 181, 109, 202, 35, 39, 117, 93, 5, 90], "\u{7f}\u{7e6e9}\u{8b}𥚝": baguqehrahkajpop4geeiwsi7gu2wdsuybkf6lbq6rfen4f7v3vrtqbyazd3a, "\u{7f}\u{b601e}'\rK/\t\u{feff}¤?\rp\u{af66a}": [73, 33, 204, 97, 67, 215, 167, 191, 121, 17, 85, 93, 75, 141, 150, 250, 223, 157, 229, 29, 48, 217, 58, 27, 191, 36, 145, 93, 14, 17, 12, 32, 61, 36, 83, 58, 181, 136, 104, 35, 237, 219, 146, 240, 223, 170, 203, 45, 27, 109, 190, 5, 96, 122, 180, 241, 211, 211, 147, 12, 136, 219, 25, 19, 108, 191, 73, 165, 95, 129, 7, 16, 40, 44, 123, 182, 100, 246, 148, 80, 99, 191, 137, 144, 177, 240, 82, 242, 163, 30, 210, 13, 45, 140, 6, 196, 122], "¥O\u{7f}Ⱥ𗢎e/\u{10697e}\u{202e}\u{7f}0�Ⱥ𤂹\u{e098b}\u{8383e}`G<<\u{b94d8}Q\u{e6032}\u{cf7bc}\0:\"?\u{1}\u{1a8a3},": null, "¥\u{202e}'\u{8d073}\u{78d6b}\u{6e610}\":E?\u{9bb82}Ѩ<�>\u{4c02a}//\u{a27e4}=\u{f456f}<3𧉟/\u{98}\u{f3520}": "x=`ᒼ\u{12af8}\"=", "Ò\u{51a90}�\u{1b}\"䩖𦄱\u{b3b7f}\u{aee50}<🕴{\\A\\\"X\u{f32ab}::\u{4}\u{c7c01}`\u{1b}\u{4}&\u{ff44f}": 0.0, "\u{86b18}Ⱥ<": null, "\u{a0feb}&¥\u{106aba}{B\u{be082}1\u{62718}\tS\u{aa928}\u{2}\u{3d1dc}\u{f5e89}": [16, 76, 209, 135, 206, 142, 16], "\u{afe7a}A`>\u{dafee}𡪩b\u{77678}%\r劗?\u{10819a}^.S\u{7f}\u{94083}$�\rA": 5, "\u{b2ccd}ë9('*\u{b}2\u{8}\u{c9f32}\u{5a462}\"?2\u{b}\0W�\u{3e6e8}th\\=\"F": null, "\u{e6ebb}r\u{202e}\u{80bd8}:🁚\t�\u{fd427}\u{b2fb5}\u{4badf}w/\"0\u{202e}<\u{15010}^v\u{53eb1}\0='": null, "\u{f31e7}41\r\u{68a9f}\u{202e}\t.p\u{4ea54}\u{860c8}\u{19987}l": -0.0, "\u{faf52}\u{6}¥\r\u{202e}䲖\u{202e}R&\r\u{973bd}\u{feff}l\\\"$\u{e5961}¥\u{d805a}\0+OB*tu\u{eb5a9}\u{109146}Ⱥ\u{759f5}`\0": null, "\u{fc398}\u{2}\\.\u{ee493}\u{d0de6};\u{ed8fa}🕴<": null, "\u{10ed00}?\u{7f}�`\u{b}L*\0®": -0.0}, "\u{7f}':e/Ѩ\u{36fde}`/\u{202e}\u{7f}=\u{feff}🕴\u{f28b6}\r": null, "¥": -0.0, "¥ :?\r\u{70234}": "�¥", "¥i🕴\0\u{93569}\u{71ace}\t:\u{75563}*+": false, "¥\u{be29b}\u{202e}\u{7ef68}5\u{1b}`\u{202e}\"\0\u{feff}\u{10e7da}�\u{7f123}|\u{8c8c5}n": 11, "±:𢽭🕴t�%r\u{8}?`^'r": false, "Ⱥ/\u{882e4}:\u{e0f71}$%/{\u{36b45}:,z\u{8}[1\u{7f})%\t\u{b}v\r\u{aaa37}\u{8}𫷝\u{7f}\u{46db0}¥\u{ad5a1}ke": -12, "🕴\u{feff}\\x\t\u{6}\t~\u{39f68}\u{202e};�\u{1b}6:/3\t\u{7f}": bafkrmiepyixjho2atqqcqgtd575iqsam6klfzdtkx5g4wummups24pdqcu, "🕴\u{5de4c}\\�𐝣\u{feff}𱴜\u{feff}": "\u{6}\r:\u{190a7}�%Ѩ𲇭:\u{6}_🕴Ò%\"'\u{e1b6d}F'\u{f7ad8}\u{57ad7}`\u{9c70b}$`\u{202e}\u{4b5bb}UK\u{90}", "𣉓��e\"$0R\u{a837d}\0<🕴?@,": true, "\u{2fa70}\\𪚅¯\u{108509}\u{4ab0e}\u{4ecc6}\u{bebaa}B\u{1}\\\u{f24f8}'%ð\u{202e}¥\u{3a476}/%Ⱥ\t$vyC]>+\u{87304}": [94, 245, 167, 7, 68, 250, 234, 108, 122, 64, 21, 238, 150, 215, 116, 48, 232, 184, 232, 43, 30, 213, 149, 183, 215, 1, 242, 95, 189, 228, 190, 9, 43, 222, 14, 254, 203, 238, 30, 51, 216, 40, 125, 87, 240, 71, 185, 206, 114, 87, 214, 85, 33, 163], "\u{373f7}": true, "\u{4f31f}*A?\\${\u{91276}\u{52eb1}#\u{b}\u{1b}": -47, "\u{5e975}🕴z${'\u{5d19a}b\u{aa904}'J}`Ѩ9": "\t=\u{1}±\u{84533}T*\u{4d0eb}\"A\u{8}<", "\u{69742}o\u{81}n<@\u{feff}L:\u{1041f7}🕴\u{8}𬾧\u{7f}\u{108007}?[\u{feff}\r": [31, 200, 160, 81, 110, 124, 249, 10, 85, 215, 192, 121, 17, 28, 185, 121], "\u{6cfa0}\u{6}𡽫{\u{feff}bJ&\u{5}`x\u{d2999}d&\u{badf8}\u{1b}\u{365dc}'�\u{5a37e}": null, "\u{a202e}~QE?\u{b}\u{7}/Ⱥ/\\&\u{fb894}\u{e0565}\u{2}�^?Ѩ'": [146, 41, 216, 116, 157, 252, 155, 192, 137, 113, 110, 231, 49, 142, 206, 2, 93, 201, 68, 48, 93, 58, 173, 197, 93, 40, 41, 112, 193, 244, 79, 228, 221, 71, 177, 129, 102, 97, 55, 137, 3, 148, 166, 101, 253, 166, 170, 139, 23, 120, 178, 140, 132, 104, 89, 46, 120, 30, 154, 194, 138, 39, 103, 152, 6, 136, 237, 241, 138, 193, 248, 33, 185, 56, 220, 118, 23, 17, 132, 220, 239, 90, 207, 237, 94, 75, 90, 222, 46, 180], "\u{b2ebe}*\u{84}.\u{a909e}7ѨjV𠷃Ⱥ?ü\u{6}u𝣒L\u{34303}:\u{bc294}?DZ\0\u{7f}/\u{103c3f}\t\\": bafkr4ihqz56mmzigglx6apxygvxcnreb7hjve6eftqfcooxtdzyzahyil4, "\u{c3588}*\"\u{8}\u{51cd1}躅\u{8f2d6}u🕴*\r\u{1}7🕴\u{71aa7}{\u{77c1d}%\u{cf015}\u{7a969}\u{8}\u{3cbf9}\u{92}\u{b}*\t\u{15db3}": "/{ªN?\u{8711e}=\0g{\u{202e}\u{202e}¥.\u{1b}H%\u{95}\0r%:", "\u{d7ffe}\u{44f52}\u{1b}\"%ỵ*m\u{5}$\r¥Ⱥ\u{1b}Uo':\r/%'<Ⱥ": -7, "\u{e0680}={\u{fd1e9}j<'CѨ�3\u{ce0a4}Ѩ\u{4b081}C\"`%9GGv\t\u{10322d};Ⱥ\\\u{e1a9c}\u{202e}\\": true, "\u{e5e60}\u{b}{\u{e3551}=\u{b5edf}<Ⱥ\"#?\u{7}\u{80}\\'5f=\":𪟤=\t\u{dc111}*<\u{346be}": true, "\u{ebf8a}Y\u{8}\u{f98c9}\\\u{7f}9🕴\u{4e9df}=\"t": [222, 226, 183, 11, 204, 93, 208, 118, 17, 243, 19, 124, 63, 137, 157, 195, 178, 3, 70, 223, 216, 164, 164, 66, 246, 151, 76, 100, 93, 11, 7, 234, 146, 38, 18, 169, 97, 221, 8, 133, 14, 197, 108, 213, 201, 214, 93, 202, 188, 165, 171, 86, 195, 223, 164, 6, 51, 234, 214, 26, 158, 32, 12, 35, 10, 245, 171], "\u{ffc11}#\u{b}¾": null, "\u{101fde}\u{b6522}'.&$": {"": 1.5658615101276272e160, "\u{2}=\u{5bd45}?o뗡p\u{2}\u{b}\\Ѩ¥\u{8b3a4}ib=": null, "\u{3}𤥺": null, "\u{6}¥\u{feff}%\u{202e}�Ѩ%\u{10df7a}$%\u{2}\u{b}\u{59972}𝝊\u{5107d}\u{202e}\0.k%V\u{55f7d}±": "`\u{7f}\u{4e0cf}\u{f7dab}\u{7f}zw[Ѩ.\u{b}Ⱥ\u{202e}\u{1b}#�\u{61e5a}\u{569c2}:\u{51a0f}\u{1b}*R\u{bb37d}:🕴{+Rf\u{ea9f}", "\u{7}Ѩ\u{1}\u{dca54}t": 17, "\t`": [25, 228, 9, 39, 85, 50, 121], "\t\u{2ef3e}\0": bafkrwibffxly7lxjvxuvp2ic3ao6f2icoflqqiadi5dvz4yvpdsyd27jne, "\r*\u{1b}\u{4}{`_\u{feff}\u{b}¥.K\t<\u{3}&\u{7}$🕴\u{7f}": -40, "\r\u{587e8}Ⱥ": [207, 53, 166, 39, 184, 202, 32, 200, 7, 155, 18, 5, 102, 226, 211, 93, 116, 183, 163, 131, 161, 249, 63, 112, 198, 231, 166, 86, 176, 31, 56, 43, 28, 142, 150, 56, 244, 64, 75, 60, 122, 214, 109, 138, 103, 217, 188, 127, 42, 7, 30, 47, 190, 51, 7, 110, 190, 106, 215, 102, 86, 94, 41, 76, 159, 221, 62, 236, 96, 83, 215, 26, 102, 233, 126, 117, 233, 43], "\"z=%`\r�ñ%:Ⱥ\u{ee132}.}Ue'\u{8}\u{a6c62}": bafyrwiefif6srseryrwhg7lx5ddm3cuzdwepr4rmvorftfhq2vj57d2gsq, "#\u{75c49}!": "\u{7f}\u{5a95d}L\u{32a81}\"{\u{7}¥²�\u{e3087}O'!\u{ab00c}\r./ \u{202e}h🕴'", "$\0\"\u{ca931}\u{2}\u{1}:\r\u{3}\u{63225}g¥Ñ": true, "$Ѩ\u{bd470}û54Ⱥ%¥\u{feff}": null, "$�0\u{a8b97}𱗶\\0\t$": 6.488444060884696e306, "&'\\w:<<%\u{feff}\u{5}\u{1b}`¥/?n𡚋&,4\u{5e1f3}\u{4b359}\u{fbe37}i\r\\\t:": true, "&?\u{6}r\u{c3665}\u{7f}i%": baguqefranz7i2sd2lryjkxvq7st4ujh3xjdttm6fldqgbci5rlbzwbet2dma, "&?%": bafykbzacea3napu4rwrzbqevwhiaptytsgo3gscof2glvjfje6h75nyc5npb6, "&\u{7f}\r}>`\"%\rY\")>\0\u{1b}X\u{b65b4}": [93, 89, 232, 156, 166, 193, 205, 122, 207, 235, 2, 186, 202, 177, 228, 245, 238, 222, 129, 191, 32, 176, 162, 238, 204, 162, 152, 39, 105, 236, 197, 76, 201, 44, 148, 170, 224, 150, 87, 94, 134, 189, 238, 51, 118, 124, 144, 5, 252, 27, 17, 111, 250, 236, 173, 159, 46, 45, 170, 32, 133, 60, 134, 50, 28, 47, 178, 197, 160, 126, 143, 31, 160, 25, 175], "'d'\u{4e27c}\0\u{b4b7c}`{?\u{3cff7}Â<\0": "h\u{feff}\u{15730}\u{c364f}]WV.TH\u{feff}.Z\0.\u{9d3ae}$\u{e5559}?\u{d403d}\u{7f}%(k𠢘", "'i🕴\tb{\u{7}\u{feff}&\u{49079}'S/\\\u{10600a}\u{af8aa}\u{3a6a6}\r": false, "'ѨH8'Ѩ{\u{f5f05}ѨQ\u{d5ba2}\u{3773e}::\u{eb168}:)CȺѨѨ": baguqehra3ml5lg6x5oaboqvr65pulvfntbvdrj6v5zyvuaf2j7bpx7wkhbpa, "*I¥w\u{b}\u{7b9a4}\u{3e747}\u{b2e81}/y<5'*S?$$O\"\u{7}\u{9f10a}\u{8}S\u{7a8bd}I\u{7f}U%": null, "*\u{202e}": [61, 242, 249, 132, 34, 222, 153], "*\u{861a1}//\u{10ca95}`'𓀒\\'=Q;\u{1073cb}M\u{5dbd9}": [34, 41, 251, 216, 124, 171, 190, 175, 12, 20, 65, 118, 149, 193, 33, 184, 96, 7, 181, 39, 92, 97, 36, 84, 72, 99, 69, 241, 55, 2, 31, 74, 55, 172, 146, 32, 230, 228, 234, 148, 164, 27, 55, 71, 222, 203, 70, 239, 15, 85, 157], ". \u{202e}v'*<\u{3c008}\u{ea8b}c¥?g:5$Y¥\u{5a3ec}": [60, 209, 45, 129, 120, 83, 199, 46, 198, 62, 131, 13, 210, 117, 41, 136, 9, 59, 142, 242, 198, 194, 181, 206, 19, 221, 114, 94, 216, 25, 58, 191, 30, 252, 131, 130, 133, 109, 116, 32, 231, 108, 160, 173, 195, 103, 78, 179, 165, 157, 227, 152, 245, 222, 164, 242, 208, 136, 66, 6, 54, 146], "/'%\u{3b4df}`𫨁e*\u{71a70}=\u{c5203}\rȺ궖JȺ/\u{4bb7c}\u{3}*#\u{d3bde}p\0qÙ\u{b}🕴\u{202e}\u{202e}r.": false, "/{H\u{10c9ea}\u{79be5}?\u{ca12d}": 8.6395723193706e-309, "4\u{54c08}'`?": -8.973748901720123e-29, "6\u{883eb}Ⱥ$\u{1b}": -1.0199828187670042e-159, ":<\u{1a04a}U�": -1.8179682492493788e-167, ":n\u{8a}\u{8}?𡜬\u{7f}F'ýr0\u{1edbd}%\r8oѨ\u{1b}": "", "<=🕴\u{dd6ce}\u{3}\u{65218}�\u{b3002}\u{81}<\\//z=\u{6106d}*Ѩ\u{8090a}hC\u{88ce1}\u{1b}\0🕴\u{ac262}\r/": null, "?*\u{a6066}aѨ\u{7f}\u{3}\u{1b}`'\u{aa717}\u{14b1c}o&\u{8d098}É?Sv\u{7f}¥`\0\0<]\u{15ce3}\u{1}=:$=": [66, 253, 182, 100, 225, 215, 132, 63, 183, 229, 8, 172, 23, 12, 49, 232, 47, 74, 175, 130, 83, 252, 88, 185, 110, 27, 188, 151, 251, 208, 54, 250, 230, 62, 210, 35, 23, 251, 21, 206, 161, 29, 185, 225, 190, 48, 125, 83, 49, 116, 98, 148, 46, 18, 204, 43, 23, 115, 8, 245, 113, 58, 152, 205, 222, 26, 169, 223, 244, 99, 43, 185, 3, 45, 50, 190, 66, 69, 188, 185, 150, 232, 128, 102, 195, 99, 78, 226, 251, 253, 84, 106, 110, 178, 192, 203], "?\u{7f}\u{b}\u{9f2c9}i\u{10bb8b}:{&8\r𠒫*t�\u{1}H\u{98f18}\" \u{c92b6}\u{8})\u{7d0c3}<Ï": false, "?\u{69bad}I&¥@🕴n\0??C\u{607cf}\u{7f}/&\u{b}🕴..qѨ\u{1b}": "𘉢`\0=\u{cc51f}\u{a4fb8}\0\u{47e89}/\u{b}\u{a0a22}±\\\u{feff}\u{feff}/.ck.\t\u{e3434}j", "A\u{e0fe6}'\u{202e}`%\u{202e}\0<\u{c18d9}/:\u{10b6bc}b\r*R:\u{feff}\u{3}\u{10ba3c}\u{84727}\u{c8ebd}^": -6, "C\u{c5e80}*g🖻\u{66e08}\u{9d567}{'Ѩ\u{58cb7}\u{9a1ca}¥Ⱥ\u{19181}\u{b1066}\u{5b8ee}\u{b9fcf}= =\u{dfbde}\u{18d8b}": "\u{5}\u{4d180}M\u{b}\\.Ⱥ*è=.e", "I*\u{96}\t=$\u{4747f}\u{5}\u{1b}n=a&rȺȺ\u{6cd13}": [230, 44], "R´\t\u{1}&\\\u{1b}kN\u{1}{´<\0\u{95a25}d": "k*`$U,\\\u{feff}\t\u{916a3}\u{849e2}<䨑\u{a2dd6}\u{9126c}", "Yq🕴\u{c5293}y:": 21, "Y\u{101e9f}JE{\t\u{7f}\0\u{4a162}aѨ.\u{7b388}댦\u{81d62}Ѩ\u{a6fb0}\\\u{dec90}\u{1}\r\u{b}/": null, "\\": -7.477551823968463e-129, "\\<\t\u{e9e4c}\u{b}:\u{a258b}¦\u{53415}5G\u{7}": true, "\\O\u{9e0a1}K.𠓎\u{fb34d}\u{7469e}\u{4c980}.:\u{bbb4f}8\"J:/\u{829d7}:\u{1}𣂇Ⱥ*J?\t\\\0\u{b}Ⱥ\u{ea85b}`": bafyrwihiw4vja2le5bbo63lbzgzdc7npl6b6ixw6nlheeirllwo5zpuzuy, "_]:\u{e11f7}¥On/<:\t=\u{1b}": "G\u{b}\u{71804}'🕴Ѩc\u{feff}<%Ѩo\u{1b}¥", "e\u{70c00}\0¥\u{202e}": [207, 96, 93, 148, 103, 198, 140, 36, 26, 221, 215, 6, 202, 231, 233, 247, 107, 178, 240, 144, 50, 206, 143, 13, 95, 15, 171, 239, 158, 75, 248, 192], "f\08<\u{9c37d}?\u{10f8f1}=`\u{95}Ⱥ{.\u{1b}\u{15cdd}\u{202e}Ⱥy<$\u{8}$\u{6c5d0}K\u{c4f04}\u{19f0a}": bafykbzacede6nne55uave2wj6epfrbud4xcozirijfysx2p3a4isvzez7oiza, "gÔ\u{b}繎.U": null, "i\u{889f5}r\u{e98e9}<Å\u{ad256}\u{b}R\r_%\u{d8e2e}/%r\u{b908c}%\rz~'\u{e04db}.\0\u{d9659}$\u{7cda4}`)": null, "u\u{a1658}0\u{f2355}}🕴\r": -7.035669443392254e-42, "v\u{10b659}�\r!o=\u{a24cd}]XÂf(\u{859d0}w\0.:": bafkrmihatl6qchcaitd2pcbxodwzlgbynl4rpzu6fnfcy3wgudnxpnabpm, "{%\u{61d33}=/EѨ/\"?�\u{5ca71}0\r": null, "{/:/JK\u{62214}Ⱥ¢\u{b}1o\u{5d859}\\\\\u{b65b6}*¥Ⱥ\u{1b}ѨѨ": bafk6bzacebf3fnemqzl52apgxmcok4gweclupgjrxwtee6ci75qsawcnyjuw6, "{:\u{f901d}\u{da30b}*:𱪙\rF\t\u{405de}\u{feff}%~\u{7f}*=\u{46e3e}\u{b}\u{202e}": true, "{f\\\u{b}\u{1}\u{7f}\tD\u{b}&Ⱥ𣡼\u{e84fe}J\u{d2ef7}E\u{1}'\0f\r\u{6}\u{4a79f}Dx\u{2}": -13, "{\u{d9a04}&ö%\u{7f}\u{d686e}&´": 7.588162654188097e-308, "\u{7f}\t\tj\tw\u{feff}Ѩ�練\u{3}:𧸨\u{a3acd}\u{101251}'\u{42a8f}E\u{3e47c}\u{7f}\u{7}&¥%<𩌯c": bafyobzaceco6clzsn57sjq2u5patyvukq3ksi4wyrztinj5srwspsi63qyfqc, "\u{7f}c\0\u{ffc41}<\r^Æ\u{9f}Ñ\u{b6f14}U\\\u{c45cd}\\\u{1b}g\u{ed9b9}🕴{\"R\u{60150}\u{b54c0}I:\u{10a33a}": [126, 247, 86, 174, 51, 26, 21, 148, 201, 8, 130, 49, 162, 197, 14, 242, 15, 142, 249, 226, 194, 131, 116, 158, 109, 70, 138, 10, 113, 85, 166, 21, 217, 49, 112], "\u{7f}\u{39cc5}'🕴\"\u{71cc2}&\u{7f}'": -4.297679451122769e-29, "Â\u{4}\u{feff}<'\u{c2f64}ѨȺ\\2\\\u{41534}*{<�\u{4a196}'8$\u{f88db}\u{feff}\u{feff}\u{9e1a6}*\u{cc465}\u{101f58}\u{dc98c}\u{b}ȺP\u{87281}", -29, bafkrmigi5ucblo6m6dbnw2y7po27ahssm6r6gwfog5w2eaxd7ze64u3vki, "(", -50, true, 4, null, null, false, bafyr4id3ardj34fsaufowtepdegrlkvrkv4jmyiva54gz4afht26prarau, [206, 72, 170, 13, 12, 162, 106, 248, 83, 67, 95, 57, 191, 163, 90, 243, 125, 164, 14, 6, 150, 168, 71, 206, 239, 41, 111, 27, 246, 79, 53, 193, 98, 168, 210, 107, 64, 164], -3.33502927060181e-309, -49, null, 3.5445664834722476e19, -1.563337693800014e-308, bafy2bzacedec3zioqwt4bdhiel5jmdrot2fhkwtx4u3fqug5x5djkkgop6rbm, -7, -18, [216, 49, 164, 168, 254, 77, 204, 85, 63, 36], baguqfyheaiqipcrhl2ltrwj5ykc3gnwp2i4i6m3wqqrmfi4ap7uchecatjrmp7y, -0.0, [64, 148, 93, 100, 165, 243, 24, 181, 67, 141, 104, 128, 194, 110, 216, 253, 234, 40, 83, 43, 194, 82, 152, 207, 225, 141, 174, 148, 236, 77, 110, 231, 110, 107, 120, 161, 188, 242, 70, 187, 10, 8], null, null, bafyrmigihlvyxai27ay3ws4ytyoickvlnpli3rxing2zlvcrwklqgvhp6m, null, 8.12819559637677e-221, 14, -2.3233856055803314e-132, true, [33, 203, 100, 207, 226, 57, 37, 141, 9, 44, 127, 89, 88, 208, 26, 54, 107, 192, 81, 78, 170, 47, 90, 250, 222, 53, 73, 136, 60, 141, 50, 179, 193, 1, 240, 61, 212, 102, 158, 81, 130, 103, 57, 0, 144, 186, 253, 79, 200, 167, 150, 245, 107, 237, 87, 24, 222, 200, 67, 180, 136, 164, 87, 137, 92, 199, 194, 10, 174, 65, 1, 168, 43, 25, 36, 186, 55, 153, 217, 27, 78, 197, 170, 47, 74, 87, 78, 123], "&t\u{51b31}\u{10b5b7}·Ѩ$\u{491b7} \u{e82db}S`\u{13ba2}$\u{feff}\u{3}🕴V¥\t{*\u{79d01}", true, 32, true, 9.168173353619497e259, null, 0, 41, "🕴\u{6ad7f}/V\u{8d90e}\u{7}\0$\\\u{1de07}ѨDw'\u{5f7ad}�$$🕴qa", [183, 153, 172, 129, 204, 68, 233, 142, 34, 132, 183, 94, 210, 53, 181, 118, 12, 36, 147, 178, 163, 147, 226, 164, 22, 84, 47, 52, 68, 11, 3, 202], true, -47, "'\u{7f}\u{9f}{\u{4bd1b}${{\u{9f533}\u{4bd4c}$\u{49671}`\u{5}🕴|�k\u{c46c0}\u{1}🕴*\u{feff}7\u{c7f8f}\ra:", 24, [124, 23, 6, 17, 136, 174, 8, 255, 36, 94, 92, 161, 178, 30, 64, 18, 140, 103, 208, 64], "\u{41adf}\u{b}\"\u{bb2f8}\u{7f}Ⱥl**", bafybmic7lxxicy772aqqwgxzod5qnr2q42gvznf7ybo3focmp5garzkofe, false, null, -1.453409372917667e275, -4, 37, -6, false, "H\0O\u{de3c0}I 욽;\u{e55dc}/qY*\u{9b}$\tHô^\u{c1add}.\rȺ:&_\0�\t\u{f05bb}\u{a5ba9}\u{fac7d}", null, true, "U\rȺ\rÖU\u{3e4ac}:\u{a58ed}/|\u{b}\u{74e95}$\\[x\u{b652f}'/", -48, -3.21819364036035e-56, 9.213513078060828e-91, "'\u{890b8}=\tx\u{d5bf7}?\u{a4dd2}\u{9811e}\u{a5c67}": 6.664526628123302e214, "&\u{54d6c}//\u{d211d}\u{7f}�": "¨\u{202e}\u{413db}T\u{1027f3}\u{b}O`#\\\u{a494a}\u{3b0c6}s.\t\u{103dc}:u/\u{a5e48}A\u{202e}\u{77c5c}🕴ȺmM'\u{6a3fe}K", "'\u{1}": "\t🕴\u{eab5e}\\\u{bab9b}\u{ee77b}\u{52d9f}\u{ab141}{\u{6}", "'\"9%?": null, "'#\u{6}\u{bdef5}¥\u{ca946}\u{7f}\u{202e}": 1.7292683522733168e-61, "'M\u{d3d34}\u{202e}$\u{3}\u{42800}𗸴\u{7f}\u{62be1}<\0¥<\u{202e}\u{feff}/Ñ4\u{f92f7}{´": 9.820150129594855e-167, ")C\u{ddeef}\"\u{c2e4f}": [219, 75, 110, 213, 246, 194, 60, 45, 146, 98, 88, 191, 123, 141, 55, 76, 57, 2, 29, 8, 227, 206, 96, 136, 115, 139, 137, 243, 222, 156, 251, 57, 28, 97, 118, 49, 218, 195, 184, 189, 188, 165, 205, 70, 190, 145, 58, 42, 104, 26, 29, 3, 218, 118, 2, 133, 228, 7, 235, 110, 91, 232, 215, 78, 99, 2, 64, 17, 72, 56, 70, 160, 255, 37, 168, 19, 29, 27], "*´\\yn<%\u{3cb12}\u{99}&\u{5}Ⱥ/\u{7}𭆼\u{4263d}\u{88dca}Q<\u{efab0}\u{f6ec2}": -1.1059496664576365e153, ".\"\\\u{3}{\r\r:�\u{81c10}\0Ⱥ\t>{\\:\u{e877b}\u{5e93f}": [65, 186, 167, 66, 51, 133, 164], ".e$Ⱥ6\u{63ca7}Ѩ`u\u{33bd2}'n\\\u{48590}\u{e646}Q\u{a08f7}.Uf": null, ".\u{9d849}\u{f94cd}=𖩡\u{bc869}¥+$5?\u{1016e0}`䎟\u{1043c3}\u{6c91d}\u{667af}🕴": false, "/\0\rѨ^\u{1a826}\u{202e}\u{7f}\u{fb3dc}\r_�\u{3}\r\u{7f}&ï\u{3}:8\u{9ed75}q𪥇\u{1b}\u{e8fa3}\u{4}𧽨\u{4efe1}\u{977d9}\u{3c6dd}\u{cfeed}X": [17, 67, 94, 98, 195, 0, 112, 199, 95, 233, 81, 197, 28, 69, 112, 207, 197, 72, 18, 50, 114, 108, 53, 31, 197, 3, 225, 75, 134, 216, 253, 203, 70, 71, 48, 33, 235, 32, 133, 245, 9, 224, 57, 91, 77, 233, 171, 9, 39, 186, 240, 49], "8:\u{c9e98}\"%1🕴<🕴\u{977c5}띦b4'": true, "9Ⱥ\u{84761}a\t\0": "î+%\u{202e}\"\u{c9bb9}*\u{2}\u{eb474}\u{4c05d}R{=\u{feff}\u{e7f1b}\u{1}\u{2}<Ⱥ-=", ":<'": bafy2bzacebdndpakl7grp6t2qzddcmrtpw4klywtvk77qimmd5fya7havzbyk, ";\u{8}�\u{1b}P'Ⱥ": null, "=$ȺN\u{e377a}\u{5a12b}`fW\u{7f}\u{2}:\".": -1.284865551557179e224, ">:O\u{3a925}ÿ\u{5}�{": -20, "A\u{7f}$'\u{b}¥\u{7}\"\0\\$=\u{b}\u{1b}�\u{feff}\u{b}\u{7e7a2}\u{b5184}\":\u{feff} D\0=\u{7f}耋Ⱥð`ࢁ": -41, "J\u{1008ed}\"\rLq\u{5}7": true, "[啡🕴mѨ®\"\u{100589}{\u{8}u\r'\u{b}\u{47a49}}\u{7}\u{1b}R\u{b4ad6}\u{b45f5}\\Ⱥ`'?": [234, 239, 156, 159], "].`'&\u{7f}^&.\u{202e}\0?%?¥\u{9929e}\u{7dd49}?a": 6.8070914168451965e-68, "_¦\u{ccda8}O\u{10bdee}fk$\u{feff}`픰>\r'�f➦\r|{\u{feff}laA=D\u{dc4db}\u{5f977}z": [142, 251, 14, 32, 228, 8, 15, 49, 217, 216, 50, 236, 66, 67, 25, 55, 193, 159, 240, 75, 84, 154, 198, 182, 176, 139, 245, 12, 189, 16, 217, 208, 9, 19, 164], "f\u{3f22c}\u{7f}\0.Y?'.*\rѨ)Q¥=�\u{6b46c}\u{42bbe}\u{ee66}": bafkr4idibino2zrwzvmr7jw2oqbramjdedkkhhexizf2kgnxongndjyvvm, "k\u{e0bf5}Ã\u{202e}<\u{202e}**\t\u{656ee}<&𘞄\0\u{b}*.b\u{1b}\u{b}\u{8}\u{c9220}&\u{36fa6}.?]G<ȺG": bafk2bzacec3uxd2irqv3kuoh6433iq7cdb2rkr4jes4ibxqlagobh7kcbz7b4, "pmOAB\u{b}\u{1094f0}\u{c3774}?\u{10918f}¥": true, "v\\\u{109f70}7`@\u{4}c'%!\u{7d32d}Ѩ\u{1f298}i\u{4d1e4}'�\"]�.\t𝦈": null, "v`\u{1b}\u{58c73}\0\u{101d7f}\0\u{d946b}\u{7f}\t\u{f2bf}\"\u{831aa}\u{60049}\u{51864}&?祕u\u{10403a}D]�\u{578ee}\u{ab4b8}:/ 𦁋R7Ѩ": -24, "{\"C썶\t\u{eec8b}<\u{ee555}\u{7574e}Ѩ\u{7f588}\rѨ*'\u{5eccf}%w{\r\r\u{b67d7}\0^kᗩ\u{1c3dd}G\u{8}`": 18, "{%🕴\0Ѩl%𤍭\u{feff}🕴\u{96}I\r=": "¥\u{93d84}y:w={", "¥~\u{566dc}9\u{ec076} \u{bd2c1};": false, "¥ȺD\u{1}\u{6b992}\u{434c4}\u{3}\t\u{1b}?\u{2}𥹩\u{9cb8c}\u{7f}𢲦N\u{e11b5}Q^{Ѩ\"w\u{505da}{": null, "Á-`^�\"\t\"`\"/<\u{1}=%�=\u{f8043}\t\u{3cb4c}\u{af74a}^\u{356ed}.:d?\u{65101}\t;\u{d0319}\u{b6b3a}$q\u{b}": [163, 128, 176, 15, 181, 109, 115, 80, 122, 244, 94, 32, 58, 251, 121, 38, 145, 131, 101, 44, 148, 129, 190, 123, 55, 19, 64, 213, 134, 70, 238, 108, 62, 157, 43, 176, 76, 240, 174, 14, 182, 98, 16, 190, 79, 42, 142, 212, 204, 192, 112, 130, 89, 121, 0], "�*??*e": [172, 83, 171, 22, 2], "�\u{9b09a}\u{15e59}\u{7}/\0E\u{6}\u{3cb2b}\u{4645e}\u{7f}\u{2}": true, "🕴": 41, "🕴\t\u{f0635}`Ⱥ\u{c865b}h\u{f39b}\u{fd001}\u{f197b}¥F/?\0\u{52376}%Ѩ/\t\u{b}\u{9a1af}\u{9b1fd}\u{feff}�\u{8f}/bt`k": [235, 24, 242, 136, 3, 90, 188, 21, 23, 10, 85, 3, 90, 163, 176, 209, 186, 96, 160, 240, 221, 232, 34, 228, 54, 35, 11, 200, 6, 93, 92, 140, 92, 36, 62, 178, 157, 50, 12, 66, 22, 208, 28, 217, 177, 9, 79, 248, 77, 198, 73, 78, 120, 28, 102, 200, 197, 67, 232, 133, 41, 38], "𥇖`": -3.1043921612514823e111, "\u{fdac9}\u{2}.𘠺🕴:'\r\t\u{3}嬊": bafk2bzacedwvgub3e2lthnyghripac4wx5ld6z3tavnb3iws5vdjukng5fwsk}, "$\u{890b7}&🕴=Ѩ\u{e0cc8}\u{7f}\u{8}\u{b}\\\u{ed02b}🕴°d\u{7}\u{202e}L\u{6d2ae}<\ta{*": {"\0&/\0&\u{8999c}.`%\u{feff}": baguqehraxkytankub52h3iajclairokepzb3skwifyzrg5qnfmbz4g4oodza, "\0🕴\r\u{2}`%O$%\u{de829}": bafkrmiht3e7seikbsbinibsxaqh4je5pzdcd4urxrwswdctilymrairlqy, "$\u{1e406}\u{1ea29}\u{7f}\u{c9cd8}𮖲X": [168, 224, 17, 123, 200, 96, 201, 98, 223, 37, 175, 231, 138, 28, 166, 190, 55, 123, 102, 20, 44, 208, 18, 184, 161, 233, 19, 136, 101, 164, 157, 248, 80, 214, 88, 79, 112, 146, 242], "=`C": -23, "\\8\u{d9453}($�[\u{539d4}\u{90f0d}\\u�\"$\u{bf277}\"\u{749e6}": 9, "wѨ&\u{f18dc}\u{8f209}\u{8}/\t\\": bafkr4ibmkkkbmtivel5pdx3jrmhp4wb5bd7rqzvoovp2j4b5hn2rtovype, "Ⱥ\u{b}\u{10433a}\\\u{1b}{`!\u{cd71b}¥<`ȺDȺ\u{d310e}\u{fee4b}𭨠0\u{b6c98}": true, "Ѩu鉏<}\u{8}Q\u{5ef3a}:Cm&\\\u{3}Q':@\u{f9a57}'\u{3f836}c<🕴P": false, "\u{202e}\u{6899a}<\u{b}/Ѩ&\u{2edba}\u{103edb}\u{9e328}`\u{4979f};\u{a6f16}!`\u{feff}w`": -32, "�Ѩ\u{8}�\"\u{fc9b3}\t": null}, "L_Ⱥ=,\u{4}\u{71158}'�\u{d01e8}/$\u{202e}\u{3b573}:`{w'": [-48, [93, 196, 141, 154, 134, 176, 139, 189, 180, 134, 124, 223, 175, 198, 167, 42, 110, 24, 222, 67, 196, 65, 202, 107, 120, 13, 17, 105, 196, 172, 101, 89, 218, 26, 140, 88, 96, 162, 89, 106, 160, 211, 109, 26, 157, 200, 132, 194, 38, 44, 145, 80, 249, 47, 202, 234, 243, 149, 231, 133, 231, 1, 121, 28, 157, 175, 187, 89, 100, 158, 213, 243, 162, 111, 87, 174], bafk2bzacecsuznauruwgtuwqc6nidbjw7qp5usnb5vr3w7lxuvebwffbqe3t4, -1.8324536891683285e-30, bafk2bzaceadjwnpm762btxzz5nfl24lfog3wm5ldrl7q7namae7ysv5647h76, -6.370155741536763e-309, null, null, 23, false, true, -41, 48, "`\t\u{cd15e}<{\u{ad7da}%W*腁=\t¯\u{7}-?,\\", 5, [238, 157, 140, 174, 18, 122, 121, 22, 84, 53, 144, 119, 53, 216, 130, 89, 6, 121, 229, 24, 153, 6, 11, 186, 92, 6, 52, 44, 74, 132, 139, 202, 28, 213, 100, 205, 127, 222, 130, 104, 210, 158, 235, 223, 136, 77, 58, 114, 135, 100, 151, 103, 91, 82, 150, 91, 149, 124, 36, 26, 78, 105, 207, 54, 253, 9, 201], [195, 193], -2.0736431262565832e-7, null, -7.471855593429185e234, [212, 22, 11, 89, 86, 227, 1, 226, 174, 110, 146, 235, 217, 246, 70, 133, 107, 165, 157, 151, 202, 116, 160, 243, 169, 65, 185, 193, 131, 184, 185, 157, 65, 108, 39]], "y#\u{49798}\u{202e}G<%r\u{1b}\u{c2763}\u{1b}\u{d7645}$M🕴\u{b}:": baguqegza3fmjvuxyw67cjennqkgi6ipcsxboanp4xjz4prjmp7h4r5muxaja, "¥\u{6}xS%\u{a21c0}Ѩ\0\u{b}\u{e52b5}\u{8cd0b}:\u{feff}\tS¨\u{dc97a}%$\u{8a51a}\u{8945b}\u{3c40a}🕴\u{feff}\u{feff}": "\u{b}\u{fbd6c}*", "🕴$�\u{1c50d}\t\u{2}": {"": 14, "\0�&\u{feff}\u{685bb}YVE9c\"|\"<[�K𤅰\u{cd669}}\"": bafy6bzaceak3lys2lo5m7woe2mfjyxxl3ore3aixosi5uajethhkjyze72xpm, "\u{1}\u{d0b74}\u{b6c77}y?[\u{202e}¥~{T\u{1b}\u{feff}\u{f8717}\u{a0f04}:\u{10f8b6}Ⱥ🕴\"Q\u{df178}\"\u{fe2ee}u": -1.5028963084100644e18, "\u{4}`\u{100473}\u{f434d}=�*/🦲Y\u{a0}W\u{a0e46}\u{5}": false, "\u{5}4\u{33c5d}\u{4}": 6, "\u{5}ZѨh\r&": true, "\t(/=:<\u{7}🕴.'Z": bafyb4ig3qyn3653hzem53fsfvx27lsgdkfmln4vgzjnu7bhb22rr6h5nvq, "\u{b}\u{5}\"\r𐴅\u{a3132}z$8\u{74e26}\0F\0\"*�J\u{7d9f5}\u{ad7c2}\u{f3faf}?\u{f35ed}": [158, 45, 189, 198, 136, 12, 91, 193, 206, 13, 142, 218, 17, 126, 95, 11, 139, 29, 191, 233, 236, 221, 187, 109, 193, 176, 213, 129, 96, 120, 253, 24, 3, 65, 228, 116, 138, 102, 127, 19, 54, 235, 37, 201, 238, 25, 41, 26, 248, 239, 191], "\u{b}Á\u{4}\0$\u{baa23}N\u{105c4d}\u{feff}g": 6.651792988712289e-76, "\r&\u{7f}𬛩\r*m\\𭘺\t": [7, 86, 209, 90, 4, 82, 142, 122, 123, 191, 41, 84, 37, 155, 147, 150], "\u{1b}=\u{ae8cb}🕴🕴YѨ": [15, 58, 171, 250, 163, 49, 118, 146, 209, 32, 161, 202, 1, 216, 170, 61, 6, 250, 37, 28, 65, 69, 84, 230, 70, 124, 99, 121, 212, 211, 35, 15, 202, 210, 87, 237, 13, 14, 12, 203], " n𦜹": false, "\"I¥": null, "'\u{202e}\u{1bfda}¥\r\u{e18c}{\u{4a199}Ⱥ\u{98ba2}{\u{202e}🕴f\\": false, "'🕴": [0, 125, 38, 17, 120, 195, 201, 150, 64, 36, 204, 245, 82, 31, 21, 85, 169, 69, 249, 10, 19, 33], ")�\r\"\r¥\u{202e}\u{cbc20}\u{5a381}\u{202e}𱙤{'": false, "*\rȺ\u{9af1d}g�.*🕴\u{1b}\u{feff}\u{6ecf1}`\rY{\u{88}\\*.\u{f8037}($\\¥.\r&ÑD`": null, "*'\u{fe9ce}\t𪡏h'Y\\{\0IWë\rU?$\u{7}IQi!%": bafkrwif2qr2egu3en3ttpfc277nx7wbogbs3exytgbmipe2ev4itu4abxi, "*{U\u{7f}3\u{feff}🕴%*``/ꁏw\u{88393}\u{1b}>": 16, "-&\u{588d4}J": -136947584516.8358, "/": bafybmigzjzmiin3bgn6qms6vavgzn6iyqjuo4onptcetzu657kebbwt5hu, "2\r\u{8c375}i\u{99}&🕴0J\t$Á\u{66ed4}/": "¾\t\t🕴$A\u{136fd}¥=l\u{8937a}\"\u{db049}`Ⱥ>@\u{b}\t\u{feff}%.\t\u{8ec09}\u{c5f5f}/$", "<\u{1b}/\\?z9\u{dbde9}\t?𦤐\u{1b}&H\u{1b}\u{125ec}\u{cf4c4}\t\u{b}\0.\t§\u{4a3f5}\\�": bafkr4id2n26zagqdmec7pq53l34j6st3x3xzqualxr3kl57litjvhiqjku, "=]\u{1b}\u{f411d}\t\"`\u{a4a2c}\u{6}": [238, 35, 78, 136, 226, 234, 214, 233, 77, 30, 183, 34, 221, 130, 60, 71, 179, 70, 33, 252, 49, 64, 114, 217, 11, 226, 152, 252, 143, 174, 237, 102, 78, 100, 138, 217, 6, 74, 250, 0, 234, 244, 152, 245, 69, 148, 151, 8, 51, 18, 105, 167, 200, 213, 240, 64, 138, 104], "=\u{8bf06}?x\u{1}𡊽@\u{6}.<%": "L\u{79877}\u{eccd7}*", ">=\u{feff}\u{8}à\u{1b}vL0\t\u{7f}I\u{feff}\u{7aba3}b$$Sv~i\t錦🕴&\u{709ee}.": null, ">`:𦎼\u{7f}º\u{be352}n$\u{eafbd}\u{cddd8}jw\u{1b}\u{feff}YT": true, "I韸\u{53960}\u{feff}?\u{7f}.\u{cf92c}Q\u{4}.\u{b}\u{8}🕴Ѩ%": -26, "M\u{2}𬗃\u{9082e}\u{e23ca}\u{4}\t\u{feff}\u{5bde3}*\u{e514b}x,\u{48624}\u{1b}\u{b}+\u{f8546}\u{8a2a6}\u{d4710}'🕴f;\u{46c31}\u{1004de}y{b:": true, "M&\u{e303}?&�\u{7f}j;\"\u{202e}픢\u{b}🕴": null, "W*/X\u{7f}^\u{7dd38}Þ7%.*3\t\u{ce607}:\u{d3d17}c\t\r\"\u{84fa3}\u{202e}=\u{92}": null, "Y*\u{c2e6a}?<~\u{feff}P'¥e¥\u{43a8e}\u{fa00a}`'": baguqehra4nft63u5d2aawrcgvhart2zja5ogq6c624azqqxgbgtoxekvlnpa, "Y\u{202e}'?": "\u{3}\u{cc1ea}\u{c7649}\u{a973e}\u{8}0\r^u\u{8eff2}l\r<]\u{5e264}&\u{f3e11}\t\r\06S", "\\\u{5}.\u{7}\u{b69af}*\u{19abc}]¥\u{202e}{🕴'\u{e46c7}`.xs\u{b23ec}\u{10beed}": -21, "\\*\r¢峭ìE\u{d96e1}\u{f8ff9}^\u{bc6ac}%:\u{50965}$6\u{8119f}¥\u{f67d0}y~\u{6c741}\u{3beb4}🕴": [126, 126, 7, 32, 17, 6, 234, 156, 163, 166, 193, 46, 81, 206, 32, 125, 166, 57, 195, 140, 39, 47, 171, 70, 117, 175], "\\\\/&Ѩ\\<": [92, 59, 122, 162, 91, 48, 33, 29, 29, 249, 254, 64, 21, 67, 100, 54, 206, 184, 116, 163, 175, 47, 42, 243, 205, 18, 136, 16, 249, 116, 141, 232, 122, 115, 123, 202, 72, 56, 116, 229, 177, 60, 49, 83, 199, 52, 23, 85, 114, 147, 155, 50, 31], "`¥?쉲G": "\t\u{4095b}\u{6c6a4}𮭀\u{2}\\\u{da8f7}8\u{90a53}.痗\\\u{202e}\u{56f97}{E]\u{492cd}&f\u{7f}¥D\\]Ⱥ?🕴", "`嬥\u{db443}.\u{ce5}\u{12eae}\u{108af8}\u{1b}\u{7f}\r\u{60df2}¥%&\u{5b0fe}-.&\u{3}\u{e2c1d}DR&:�\u{10c17b}Èc\u{8}U%": [49, 169, 171, 64, 201, 141, 228, 46, 7, 206, 13, 162, 170, 144, 168, 169, 198, 15, 58, 71, 112, 228, 149, 218, 144, 84, 70, 253, 39, 168, 223, 3, 232, 33, 162, 234, 136, 215], "`�`\u{7f}{\u{5c37e}\u{2}\u{202e}?": "/\"\u{3}𪜥N\u{57f77}h$\u{d1d92}𐊘�\u{7f}*¥<.\u{9e397}<\u{1}g\u{d0ad6}/\u{11ede}\u{7f}\u{ae007}\0$\u{3afa0}🕴", "c��W|`�*@.aѨN\u{feff}\u{f92a0}t$\u{f53cc}1\u{c8fbe}\r\u{19667}\t\r\r-\u{7c67c}": 2, "o\u{1}\u{1}\u{1}=\u{e8927}I\u{f584}&\u{a0}:9\0a𠿭/": true, "q$Ѩ\08{\"鉱[?ÀzY\u{9b8c9}%\u{3}{%\u{5}\0.\\\u{6c540}\u{7f}s\u{c05e8}`\u{47f61}=)🕴": -12, "s*¥\u{9a}\u{feff}": -1.6309188456998563e185, "v=<¥'<\\`\u{cc9eb}\u{798aa}\u{64542}:®3\u{202e}\tD&𰓏%�\u{7f}LF𒉠\t�\u{ee420}/'D": true, "v陈'\u{d832c}R??\u{7f}/\u{6}?\u{7d4d1}\u{dca1e}~🕴\u{aca77}:ç\t\rѨt?\u{10f5f3}\u{9db4d}": [35, 3, 217, 123, 77, 104, 154, 141, 118, 202, 130, 101, 62, 0, 182, 199, 130, 209, 149, 102, 212, 81, 90, 149, 217, 96, 127, 42, 251, 151, 46, 49, 33, 84, 28, 55, 109, 95, 208, 174, 193, 163, 28, 103, 91, 127, 159, 204, 177, 182, 31, 112, 61, 243, 10, 202, 83, 234, 176, 174, 61, 22], "v\u{43b04}6\u{f16c9}\u{2}\u{ad277}{Ã={/\u{202e}": [130, 234, 7, 201, 230, 244, 129, 23, 205, 123, 90, 239, 235, 197, 10, 88, 224, 76], "y/\u{1b}Ѩ\u{1b}": "?;*\u{156a5}🕴\u{1ad1f}:-\u{a1bea}r&\u{eb06}\u{a5275},:R\u{7f}&<\"®\"w\u{202e}:Ó.:\u{202e}\t.", "{B\u{7f}𲌜\u{9a6bf}.\0\u{4a3ca}🕴\u{7}\u{35765}/": -29, "{`=\t;\u{b}*EX\u{1b}鋪\u{61ce8}\u{b742a}/s🕴\u{5}\u{db6e7}'\u{10aa16}q?\u{8}\u{8d}\u{518fe}": "?\u{8d694}🕴", "{b\u{fe666}䀾\u{feff}]y\u{1b}[$\u{202e}\u{202e}🕴㞔\u{c132a}": true, "\u{7f}\"P=\u{555f9}\r\u{f034e}j\u{67c81}\t)Ø\t\u{a2116}\u{bcb49}<\u{108172}\u{489d6}3": [11, 195, 207, 131, 8, 71, 197, 12, 152, 172, 119, 96, 131, 191, 170, 2, 143, 112, 247, 189, 191, 156, 180, 76, 57, 86, 59, 6, 237, 106, 34, 175, 98, 78, 217, 192, 190, 63, 247, 202, 245, 190, 185, 58, 157, 147, 177, 129, 101, 197, 232, 22, 220, 131, 53, 79, 127, 137, 106, 0, 139, 8, 238, 179, 248, 40, 242, 178, 191, 193, 47, 56, 113], "\u{7f}_%\u{e1aa4}\u{c1f6f}/$s:\u{1010bd}*j\u{b}nêeȺ$\u{7f}\u{202e}\u{feff}\u{1b}\u{feff}%\u{9f08d}w\u{e8557}<$<\u{3e78b}": "Ⱥ\u{feff}$", "\u{7f}\u{9a}\0�\u{9c}'$=\u{4}&\u{94281}\t?\"Ѩ\u{654e1}": false, "\u{99}.\u{9d66b}z\u{db26c}\0\u{109c92}.W\u{ca505}\\¥\u{e0cf8}\0`4x\u{feff}×ꡔL\u{b16bc}\u{c1e8f}TYu\u{e7dae}\u{49a55}: ": 43, "\u{9e}%MWȺ`$\u{202e}:z?🕴\u{6}\u{a0}㶬{\u{5}<\u{6}*": bafk6bzaceaont65a5awr6tb55msdtvepr4xwa62mgsp4gyyfmbd62ey4ts4z6, "¥`?=%🕴`L": true, "¥\u{7aff2}Òb[?\u{8}\u{923a6}»\\»": false, "¦\u{6a307}": null, "Ѩ🕴T¥$*?\u{6}𪵅\u{98110}": -6.640335043931752e20, "\u{1bd53}Ѩd%@": null, "🕴3\u{feff}&\u{dd024}Ѩ=\u{c62d5}p\u{7f}�'AѨ|`g\u{635c5}h": 22, "𣅝\"\u{8b95b}\u{fbfeb}\u{cd3f3}A'\u{167f7}𘑦&🕴\u{94f3b}H⼣\u{95077}": "", "𮎾ýK\u{feff}qȺ\"\u{64e75}\u{70dd2}¥\u{41885}l": bafy6bzacedp25hwogiwuqmaa52iwzgx42gznm36zcpjwo2togshwzpsae6p26, "𲊳": [226, 239, 217, 38, 87, 223, 233, 194, 149, 219, 168, 60, 210, 19, 135, 178, 111, 92, 80, 108, 128, 121, 241, 63, 190, 148, 170, 24], "\u{3393b}\0¥\u{3b6e9}F\u{93d46}X=\u{d5dd8}:\u{feff}%'\t\u{95303}'*\u{aca4a}", "\0\\\"s\u{feff}M:b\u{8}H/\u{6cafc}\u{f1224}z\u{8}8P🕴\u{a2f8a}\u{8}\\": true, "\u{1}=\u{3134e}$\0v{K\"({Ѩ/�𪓆 \u{b3be1}�\"{$¥//🕴\0\u{604de}\u{a3afa}$\u{3}q": [43, 187, 50, 198], "\u{2}'Ⱥ\t\t\u{10b73d}\"\u{8}Ѩ:": [177, 214, 28, 238, 196, 162, 205, 149, 100, 179, 184, 109, 52, 32, 41, 6, 221, 134, 161, 95, 164, 2, 124, 18, 111, 203, 147, 153, 196, 122, 109, 242, 133, 28, 125, 73, 247, 14, 254, 57, 169, 190, 203], "\u{4}x1\u{b}\u{d46f0}k¥�'%\u{1007c4}": -41, "\u{6}/": 34, "\u{1b}2": -25, "\u{1b}\u{202e}\u{c9725}\u{80c81}?/\u{5}\u{e1f43}m\u{f878e}\u{feff}?M": bafyobzacecdottam3btcdm3lil44fdpksyl4ry22wyyufv5d3h6yuc2xic62a, "\u{1b}\u{4d55c}?|L¥ù\0\u{b}¥?[\u{f871d}k{.\u{9b}ć🕴$a\u{2}\u{db167}`\"": baguqeerag4wvi75skxsukegz4at6o2jjuwnsmujpjfkds3vdu4ybvdnqszfa, "\"[ .[": baguqefrayopq7znw46ljb72uinasfv3w2iuuvycues24ag6px6cwn7uhxgzq, "\"\u{eb96e}\u{3}7𩼱/\u{7f}?<\r�9\u{1b}'\u{b}<\u{56215}\u{612fc}*:'�EȺ/Ý\u{41397}-": -0.0, "$\u{3551b}$🕴\u{4051d}渴": -14, "%=�\t\u{cadf7}\"\0\"=z\u{f357}\u{a23f8}\":\u{f77ac}🕴\tJ\t$": [15, 242, 91, 76, 243, 187, 224, 9, 73, 10, 254, 67, 51, 52, 17, 243, 78, 108, 39, 226, 242, 59, 201, 164, 49, 64, 177, 101, 28, 139, 30, 14, 186, 53, 73, 148, 250, 157, 77, 84, 1, 36, 211, 17, 227, 29, 58, 144, 4, 192, 22, 128], "%\u{671d5}\u{5} =\r?'b.\u{1b}\u{7cbe2}𱛣\\\u{1}¹Sb\r/%\u{108071}ȺE$x\u{1b}": true, "&'\u{1398c}🕴\u{91} ": [35, 228, 163, 135, 36, 120, 223, 108, 37, 185, 72, 109, 236, 64, 172, 51, 97, 158, 76, 34, 242, 182, 40, 28, 179, 167, 49, 193, 42, 24, 222, 201, 26, 63, 190, 143, 222, 12, 8, 41, 47, 5, 168, 168, 222, 18, 54, 55, 98, 164, 82, 143, 124, 88, 254, 159, 177, 101, 205, 232, 17, 198, 34, 34, 105, 60, 25, 16, 173, 94, 188, 33, 68, 226, 245, 233, 154, 122, 243, 23, 198, 255, 31, 226, 251], "*]\u{8bb40}&.-Io\u{2ee60}%\u{b}\u{b4204}\u{b}\u{6fa9e}\u{1b}\t[\\\\uS=¥\u{7f}": -0.0, "*\u{1f84e}`𡣶\u{b}\"\u{df68f}A\u{33626}\u{202e}|\0\"\u{862b5}\u{7df5d}\u{1b}<\u{87a2b}\u{8dd82}~\tP\u{202e}Ѩ": null, "*\u{38df8}p\u{d2c21}\"\\\u{202e}\u{82032}㿶1&ѨU`H8�\"&": [38, 69, 84, 226, 186, 180, 254, 85, 31, 178, 18, 43, 46, 2, 151, 54, 154, 241, 244, 226, 93, 189, 139, 83, 133, 109], ".%\u{fd55f}?,/.`\r\u{dadcb}%\0\u{f3420}\"%#\\Ⱥ\u{95792}R¥}\\\r𡂂\u{69eab}\u{feff}\u{7f}{": [117, 59, 134, 197, 33, 33, 196, 45, 250, 203, 13, 97, 151, 48, 4, 203, 100, 245, 18, 88, 41, 62, 165, 72, 69, 218, 126, 161, 161, 184, 57], "/\u{5}\"I28qȺ|«㖃$c`\u{2}*?\u{ee2e9}\"\u{51724}": false, "/.\r¥": -1.2787723284947307e-308, "/J\u{10519c}%%/$.%\u{65527}\rW|Ⱥ{\\¥\u{8201f}\u{7f}\u{b}&%c=Ѩ\u{9bf3f}": 2.35302505714783e265, "/\u{52490}=|:%w\rl\u{90}": null, "5Q%\u{561e8}^r.\u{b8932}\u{92f2d}4\u{7f}+⩾\u{7f}/.¥\\\u{1}\rS1\0%": bafkreigrelqjps75gjoqf4ijk5muyqd3eo7nkw5cm5phlkiaaoy4yuiy6q, ":`\u{7112e}": baguqehraddx54dt5qiorol2h2cg5mnr7pjvahh3d43il2ernaikugjuvyymq, "?\u{75c14}\t龒Ⱥ\u{e5509}\u{534e4}<>\u{1b}\u{d3cef}\u{7f}\u{417fd}\u{7f}\u{6c856}%'\u{202e}>\u{43519}U[:\0\u{b}&\u{df77e}/&": -4.024585944763422e193, "K*\t\t\u{e140}Ø\r o\u{c3cad}2/{Ⱥ\u{7f}s\u{7f}¥\u{b71fe}&¥\u{e34cb}Ⱥ\u{135f0}\u{b}$\t2$$": -10, "OB\u{4}\u{7f}\rP5\u{80012}\u{16348}\\z\rÍ\u{1b}\u{3b61b}?": bafkreiaimlx7a6sb2ezufrqz5kqv3p2y4fadnzakimbrhq3ajzjg7ue75m, "P\u{93db9} &zG\u{7}?🕴\u{7f}'\u{10ec52}\u{feff}\t:\0\u{63df4}m\t\\": false, "U\u{2}\u{9ff8c}¥?\0\u{57a21}\u{9bb0e}ef\u{2}Ⱥp\u{f73a7}4\u{b}$\u{7f}\u{1b}9\u{7f}{\u{9e15c}\u{d804b}\u{705a1}\u{e750c}Ⱥ\u{7f}¾\u{7c515}": [182, 236, 56, 93, 36, 111, 103, 217, 53, 16, 250, 85, 194, 3, 183, 131, 156, 114, 43, 35, 38, 181, 143, 123, 141, 23, 109, 247, 10, 205, 121, 24, 25, 35, 178, 125, 3, 138, 141, 76, 31, 49, 144, 42, 23, 111, 9, 180, 199, 19, 28, 28, 36, 135, 72, 100, 199, 115], "Xa\u{b1ab2}$(\u{9e}\u{3812e}\u{fe6c}\u{6acee}\t8$=\u{7aa19}He\u{202e}|2*\u{6813f}Ⱥ:$%\u{70ca6}`\u{e4b58}o\u{a0}?": null, "Z\u{87bae}\0\u{f48d0}Æng": 49, "\\": [161, 24, 206, 12, 94, 242, 137, 40, 92, 187, 103, 166, 57, 108, 16, 46, 30, 140, 111, 96, 213, 196, 3], "\\\u{e7806}\u{7}%\r\u{7f}:<\u{95ec9}\u{10b262}\ry\u{d494a}g\u{5c182}'&.\u{2}\u{750dc}]Kq\t": -1.3364526424136176e63, "^\u{50332}\u{3}\u{7f}'$$Ѩ\u{7f}\"D?": -33, "`\"*\u{6989b}\u{a0552}Ⱥ&d\u{4}Ⱥ🕴Ѩ\u{f863f}Q\u{202e}\\=\u{202e}*}\u{6e906}\u{994d4}\t\u{10cc54}\u{3}_坢Ⱥ\u{ef59f}F\u{e82f1}": "'*𰀏Ѩ\\%\u{4}.*\u{202e}\u{96457}%|\u{100bbc}�\u{5046b}H\u{d9ba9}HL\u{5}*\r\u{8a1e4}\u{b}\":<\u{7f}\u{9f}Ѩ\u{7f}s{�\u{a46b4}\toh\u{7f}*", false, 46, 14, "�\u{102e58}'\u{f5c0e}\t\"E\\cM<'l", false, [135, 235, 189, 222, 190, 18, 166, 127, 220, 237, 86, 153, 31, 245, 85, 90, 198, 38, 141, 133, 190, 152, 45, 202, 134, 204, 48, 11, 139, 218, 166, 185, 151, 185, 14, 83, 198, 240, 223, 59, 82, 165, 180, 107, 145, 62, 145, 69, 120, 158, 111, 248, 129, 252, 131, 205, 198, 215, 133, 171, 133, 47, 12, 202, 128, 214, 166, 137, 1, 192, 251, 117, 7, 234, 236, 17, 204, 31, 74, 10, 168, 79, 30, 0, 231, 29, 135, 146, 72], "RvѨs$\u{75d03}Ѩ(<\t\t𤡧s`\0\u{1b}.\\\\$\u{1616b}\u{7f}\u{3cf54}", false, [235, 223, 146, 172, 43, 51, 24, 21, 97, 241, 49, 100, 109, 54, 82, 186, 134, 207, 194, 77, 228, 210, 37, 8, 203, 94, 183, 5, 197, 144, 138, 15, 165, 27, 102, 4, 88, 13, 236, 223, 176, 178, 233, 90, 97, 161, 185, 38, 223, 154, 255, 212, 108, 179, 214, 107, 224, 221, 198, 190, 85, 135, 36, 226, 102, 69, 206, 155, 37, 70, 205, 167, 176, 184, 47, 239, 155], -1.1621607219028973e-308, "\u{59b57}/x\"\u{7f}\u{7fb3b}\u{3}&2&\u{b}\u{2}*🕴l\u{9d87e}%\u{1b}\u{b}RA", -24, -18, null, [95, 22, 213, 216, 209, 22, 236, 29, 48, 196, 106, 1, 159, 4, 224, 154, 54, 81], true, bafyb4ieb5rs6wukn4nc3vkm5ej4qrmgt7ycs7lmkc65m3wcxmpgo2xms4u, bafkr4ieb5eefx4xbgebjbqtjvimlfqjwrqcrg7vlzoiqrnntc2avr3g5g4, bafyb4ibwxgkbwxtmkhdb5bwhyi7caxvo3ke6mk42emzae5iini52xuhlpy, -37, false, false, -28, "\u{8}1\u{79fd2}\0\u{ca356}\u{59494}O𫔓{\u{7f}Q\u{3269c}]\u{202e}\u{2}\u{97bce}\t\u{578dd}cP\0\r", null], [158, 31, 209, 14, 252, 175, 225, 4, 36, 138, 244, 197, 42, 56, 92], null, false, [52, 230, 174, 76, 132, 211, 203, 241, 115, 11, 214, 245, 111, 133, 43, 90, 154, 170, 151, 64, 144, 238, 229, 61, 189, 206, 192, 76, 209, 181, 128, 129, 149, 185, 71, 119, 213, 172, 36, 212, 47, 194, 112, 62, 251, 65, 101, 24, 90, 161, 131, 1, 164, 160, 99, 211, 197, 197, 201, 100, 159, 72, 192, 14, 231, 60, 84, 118, 134, 195, 67, 16, 246, 204, 36, 246, 158, 191, 131, 237, 231, 155, 25, 200, 124, 214], -1.6606481206991028e-178, bafybwif7fjc3n3ymfa23uyjq64sksrh45thlzmm76bb5bohapwgol2thvq, null, -1.346334315879978e109, "衴", [119, 113, 165, 145, 116, 9, 189, 140, 253, 252, 3, 148, 133, 255, 48, 161, 243], "\u{8}u/G\u{a6e8d}?\u{60a32}=ó\r\\?🕴\u{7}%a/\u{1b}¥'\u{7f}:\u{60bc8}\0i`\u{1037a2}A\u{c10fd}\u{b}>\u{202e}", "", {"\0\0¥": true, "\u{3}=Ѩ\u{5ce31}:/\u{97}\r\u{7f}/\\-\u{e2599}?\0\u{202e}`w0\u{7f}\u{b}\u{10ed54}5\u{84}%\"": "$\tE&\u{43d48}S\u{fb913}ÿzVt\u{e7bed}L&{²\u{6}\u{7f}", "\t\u{5}\u{a8a7f}\u{89589}sJ<'𫮹碗\"Ѩ1i/𦏲x🕴\u{a3b5e}\u{ee69}?": null, "\u{b}'\u{83bfd}\t/*㷰\t\u{97282}\u{5}I{": [223, 168, 69, 95, 72, 43, 203, 89, 110, 174, 115, 77, 84, 89, 26, 159, 57, 147, 74, 158, 40, 74, 218, 178, 183, 135, 65, 67, 200, 1, 1, 140, 39, 233, 54, 33, 28, 99, 87, 116, 176, 91, 93, 98, 3, 13, 238, 112, 18, 248, 83, 152, 221, 158, 134, 229, 80, 111, 168, 248, 209, 124, 19, 203, 55, 80, 89, 127, 31, 233, 114, 8, 236, 122, 44, 128], "\u{b}G;%&YP🕴t\u{4}E�.{\r\\/�K|\"\r": bafyrmifvkoneefkqfiseviqruma2fucp5bha2di7eg7jlyk3zrf46fw4na, "\u{b}d'S\u{4dc81}%\t)🕴\u{202e}.🂉\u{49dd2}𗹆\u{461bb}\\*c": true, "\r:*$㝩\u{98464}\u{6}𰂜.\u{1b}\u{b}=0\u{8d206}¥\u{95e7d}.\0\r:¥$\u{fd075}w{\u{202e}\u{7f}À{/\u{aee3d}": -54, "\u{1b}]Ѩr\u{46a43}Ⱥ\u{76dd1}\"@%6%�*L\u{10fe25}\u{cb0c9}::\u{82}{": baguqfiheaiqmnsckfvqrzdszfgxhnfdlbgv4tv2umb4ebrla4e4ovkqzr7qtl2q, "\u{1b}\u{202e}=\t%>\u{64bd7}=\t¥.?&\u{202e}\"=\u{feff}\0\u{7ed05}\u{7f}Ⱥ\u{3b248}": true, "\"ȺA\rZ": [182, 24, 119, 121, 203, 152, 112, 93, 47, 116, 134, 100], "$": bafyr4idvqlsptx6ct75oj6n343bqpaabhltktomqf74lrhnjwj5zwvrz5m, "$\u{2}\u{9f}¥{\0$\u{202e}c\u{1b}\u{736d1}\u{7e350}#K🕴´\u{44a44}.🕴Ⱥ\u{401cb}%\r//\u{44014}r\u{1b}é¥": bafyr4icg62pibjpimmwgopvait4hdkb3lkz4ry3duekmfx4expvn4rrwqm, "$)ѨÊ\u{69a2e}:»Ⱥ\u{99}\u{f3438}\u{b}\u{c0d9c}*": 17, "$/??{\t\0á<\u{6d90c}.\\\u{5ab70}W*D.`\u{81dac}`": "🕴j\u{202e}`\u{97293}Z<²\u{aabdb}¬\u{202e}.&=Ùh\u{5680a}\"𪻏", "*\"\0'`<ѨjȺ$\u{8d55d}Ⱥ\u{d1704}$\u{69be0}]\u{feff}%..": "Ⱥl'Ⱥ\r�<;\u{edd5d}<\0\u{a2820}\0\u{16271}\u{ccc45}\\:/\u{202e}\u{4}(%\u{7}", "*/$\u{3b185}/�:\u{c8c4f}CA&4\tc,>\r+": 0.0, ".{^\u{feff}\u{b31b8}\u{f0ff0}=\u{1b}\u{cf39b}&Ⱥ\u{cc0f0}ꟊj\u{7f}2*¥n.FѨ": bafkreifagebl3ayuazlaokrf45fopvx2mxh52amx5e7yh4w3pxhkken3vq, ".\u{202e}": true, "6\u{3}\u{febc8}P\u{6a6d5}~\u{3279b}p삣6\u{b}\u{b}K": baguqehrak3yvnpqo6ab5rdeccamokfdova7l5vpi4xq7skax4mmzw75slkeq, ":\\\u{1009ca}{'\u{7f}\u{9b51b}/ek%🕴=Ⱥ\u{7f}{\u{9ecd8}/Ⱥ`\u{aaa83}\u{feff}\u{1b}{\u{5ebd0}�\u{c68be}T": 9.620946678080735e-61, ":\u{feff}A*&\u{839f8}\u{86fd8}\r4{;�<`.<$\0\u{b1b8a}>": "𠑛%Ⱥ�\u{1}<\u{1b}\05$OѨ? =Io\u{3}Ⱥ\u{4a654}\u{95}\u{2}\\", ":\u{f12fc}\u{8f80d}\u{c4396}𣝛=\u{7}\u{92a55}🕴(&\u{91993}\u{e357e}\u{8ac6d}\u{7f}Ⱥ襥\"':�\u{5}": baguqegza2ch32i7qyewr7mke33b5bpzm7jep5ei5kpard5moafylxh46gpma, "<\"\u{fe42e}\r\u{103930}O3=\u{feff}𐽱Ѩ\u{c7c2a}<.譒\u{ec02e}/\u{520c0}FA㣂\u{b}\rù": -43, "<\u{b8112}}B\u{1}\u{e22e4}$[¥=\"\u{b}`$=": bafyobzacecdbrhr5ehtimriiz6mxvinsq5tuyh7dqu3pderxxdmgtepquklbe, "?¸H/%\u{feff}\\K\u{cc4ea}y:🕴\"@8:\0:𢭤¥<\u{c9d0a}\u{7f}\u{8}\u{7f}\u{5}": bafkrwidvq2ib5wbdwi6evstk4n4zuo2ptkqx62jlqzp65xevm54zh6caiy, "@\u{66e54}Ѩ$u": null, "C𫏦.\u{b}/'8\u{49182}ȺȺ&\u{c91e4}\t/": -46, "OS\u{b}\u{7}𩚡4?Ⱥ\u{feff}'\r¥\u{202e}\u{b}B🕴f\u{202e}.": null, "Q\u{d12fe}&`\t*\0g\u{1b}\u{8}2D/$)\r{?𠯚\u{fed96}x\\û": 5, "U^\"?d\u{f8edf}R\u{a3374}<🕴\\\u{202e}>%\u{feff}\u{a2d62}\u{ab6e8}\\\tP\u{81d20}W\u{998d7}\0\0\u{d2250}\u{74ab2}.:W*%": [212, 136, 153, 95, 249, 147, 220, 254, 102, 118, 32, 185, 203, 25, 119, 125, 56, 0, 225, 234, 220, 127, 234, 201, 81, 152, 213, 225, 133, 23, 85, 200, 158, 231, 88, 27, 12], "\\Ѩ&\u{ce722}\u{74bbb}i\u{9caf6}f}<\u{feff}Uo\u{f89a7}\u{2}$\0𰔭\u{63b24}𘥆&þ": null, "`\t\u{7abdf}Ð)&g&": "{Ѩ\u{e4a29}*nȺ\u{e491e}M\u{89c42}%a\u{8}\u{82}", "`r\r\u{8}=🕴\t\u{440b0}Ⱥ\u{4}�%*{\u{202e}\u{151ad}\u{55675}K": "\u{4}\u{5d2d2}:Úv\u{f704f}\0\u{7}\u{ad34a}¥A<\u{aebdd}\u{898f2}<\u{d3097}\u{8}W/`Ⱥ\t\rѨ\u{10361f}\u{6d1ed}", "e&#{`\u{34996}?'/\u{1b}\u{2}\u{19c0c}\u{6716f}\t$%\u{db2ea}[{": 22, "n?": 52, "o\u{106e1b}?\u{1b}{*/$<Ѩ\u{7f}\u{7f}.'\"`6\r🕴�\u{202e}\u{77895}�𩈷)�\u{f670f}\u{202e}`:\u{4fb5c}>`x\u{35613}(e\u{202e}µ\u{ae9bd}\u{83}Ѩ¥\u{cf5b4}¥~\u{1}\\\u{10bc4b}hl(:#z": "\u{b}<{\u{5}\u{3c7ce}\t\u{b0200}`Ⱥ\u{58ab1}2*\"\u{ff4a9}Dw🕴&", "葜\u{1b}`�:\u{10e218}<2\u{1b}`\u{10b2e2}C\u{7f}\u{202e}\"¥s\u{1b}\0*\u{b}%\u{9d41a}=\u{e573}": [true, [32, 206, 206, 97, 143, 10, 81, 82, 157, 249, 61, 22, 250, 48, 255, 194, 232, 184, 202, 22, 232, 158, 227, 32, 56, 8, 142, 212, 153, 240, 168, 65, 248, 231, 48, 108, 237, 135, 114, 246, 246, 204, 239, 24, 162, 41, 103, 211, 37, 81, 230, 208, 111, 248, 4, 9, 197, 153, 105, 20, 217, 206, 146], true, -5.25899467531148e-128, "M<0%\u{202e}\u{b}/\u{46e36}\u{108dc}\u{a2ef8}\u{7f}.]\u{b96d6}\u{b41c1}\u{4e68e}", "\u{5d121}\u{b}\u{cf745}\u{3}\u{5badc}\u{202e}\u{d984a}", bafybwic7uqdxsofaghtkiqawch55haewwlxbtyy7dzua22bjcif4y2jmri, false, null, 29, bafkr4idbn64n3h5bfrab6gnwlyggbaxq2lyvtcpvyzp5z4h3myyl7tkzui, "ѨSm¥L\u{7}?\u{7c63b}`x%qN\u{a501c}1.Ѩ\u{5}:\u{6120c}·{*7", false, [112, 107, 102, 23, 200, 228, 14, 201, 117, 98, 1, 76, 10, 156, 40, 126, 28, 147, 14], null, "%Ѩ\u{b}=\u{e06d3}\"\0\u{b}\u{105e48}\u{dd959}㮴a{N*¥\u{4}\u{6c856}", [1, 150, 20, 111, 201, 216, 245, 91, 220, 0, 194, 252, 33, 40, 135, 13, 44, 29, 164, 248, 22, 137, 89, 237, 222, 28, 4, 157, 221, 9, 100, 197, 235, 73, 41, 84, 55, 116, 103, 40, 48, 206, 112, 238, 68, 248, 66, 52, 206, 58, 27, 54, 24, 51, 14, 70, 8, 203, 39, 212, 210, 72, 151, 65, 50, 3, 131, 97, 121, 181, 227, 173, 63, 242, 250, 82, 244, 186, 53, 96, 222, 46, 102, 216, 26], -5.797387639150806e-228, true, [228, 221, 48, 49, 157, 129, 203, 175, 12, 252, 218, 247, 178, 191, 214, 73, 7, 40, 52, 53, 19, 147, 108, 202, 231, 162, 31, 28, 67, 99, 121, 1, 129, 25, 41, 204, 153, 97, 233, 32, 246, 185, 128], -2.3032544649066786e268, true, false, 29, -23, [119, 152, 15, 167, 240, 199, 14, 99, 170, 252, 95, 162, 82, 87, 28, 38, 176, 157, 185, 58, 156, 67, 186, 0, 230, 43, 167, 184, 246, 129, 63, 121, 129, 247, 200, 218], false, -0.0, true, true, 41, -1.9024538231552825e-308, null, 0, "%\u{5}\u{86d1a}ð\\\u{5}\u{1b}\u{3}n\u{feff}F\u{10d421}> From<&Object> for Named { + // FIXME probbaly needs to be a try_from + fn from(obj: &Object) -> Self { + let btree = Object::entries(obj) + .iter() + .map(|entry| { + let entry = Array::from(&entry); + let key = entry.get(0).as_string().unwrap(); // FIXME + let value = T::try_from(entry.get(1)).unwrap().0; // FIXME + (key, value) + }) + .collect::>(); + + Named(btree) + } +} + +#[cfg(target_arch = "wasm32")] +impl From> for JsValue { + fn from(arguments: Named) -> Self { + arguments + .0 + .iter() + .fold(Map::new(), |map, (ref k, v)| { + map.set(&JsValue::from_str(k), &JsValue::from(v.clone())); + map + }) + .into() + } +} + +#[cfg(target_arch = "wasm32")] +impl TryFrom for Named { + type Error = TryFromJsValueError; + + fn try_from(js: JsValue) -> Result { + match T::try_from(js) { + Err(()) => Err(TryFromJsValueError::NotIpld), + Ok(Ipld::Map(map)) => Ok(Named(map)), + Ok(_wrong_ipld) => Err(TryFromJsValueError::NotAMap), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +pub enum TryFromJsValueError { + #[error("Not a map")] + NotAMap, + + #[error("Not Ipld")] + NotIpld, +} + +impl From> for Named> { + fn from(named: Named) -> Named> { + let btree: BTreeMap> = named + .into_iter() + .map(|(k, v)| (k, promise::Any::from_ipld(v))) + .collect(); + + Named(btree) + } +} + +impl From> for Named { + fn from(named: Named) -> Named { + let btree: BTreeMap = + named.into_iter().map(|(k, v)| (k, v.into())).collect(); + + Named(btree) + } +} + +impl TryFrom> for Named { + type Error = Pending; + + fn try_from(named: Named) -> Result { + named.iter().try_fold(Named::new(), |mut acc, (ref k, v)| { + let ipld = v.clone().try_into()?; + acc.insert(k.to_string(), ipld); + Ok(acc) + }) + } +} + +impl TryFrom>> for Named +where + Ipld: TryFrom, +{ + type Error = promise::Any>; + + fn try_from(resolves: promise::Any>) -> Result { + resolves + .clone() + .try_resolve()? + .into_iter() + .try_fold(Named::new(), |mut btree, (k, v)| { + let ipld = v.try_into().map_err(|_| ())?; + btree.insert(k, ipld); + Ok(btree) + }) + .map_err(|_: ()| resolves) // FIXME + } +} + +/// Errors for [`arguments::Named`][Named]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +pub enum NamedError { + /// A required field was missing. + #[error("Missing arguments::Named field {0}")] + FieldMissing(String), + + /// The value at the named field didn't match the expected value. + #[error("arguments::Named field {0}: value doesn't match")] + FieldValueMismatch(String), +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Named { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..20) + .prop_map(|newtype_map| { + newtype_map + .into_iter() + .fold(Named::new(), |mut named, (k, v)| { + named.insert(k, v.0); + named + }) + }) + .boxed() + } +} diff --git a/src/ability/command.rs b/src/ability/command.rs new file mode 100644 index 00000000..040ec2ea --- /dev/null +++ b/src/ability/command.rs @@ -0,0 +1,66 @@ +//! Ability command utilities +//! +//! Commands are the `cmd` field of a UCAN, and set the shape of the `args` field. +//! +//! ```js +//! // Here is a UCAN payload: +//! { +//! "iss": "did:example:123", +//! "aud": "did:example:456", +//! "cmd": "/msg/send", // <--- This is the command +//! "args": { // ┐ +//! "to": "mailto:alice@example.com", // ├─ The shape of the args is determined by the cmd +//! "message": "Hello, World!", // │ +//! } // ┘ +//! "exp": 1234567890 +//! } +//! ``` + +/// Attach a `cmd` field to a type +/// +/// Commands are the `cmd` field of a UCAN, and set the shape of the `args` field. +/// The `COMMAND` attaches this to types so that they can be serialized appropriately. +/// +/// # Examples +/// +/// ```rust +/// # use ucan::ability::command::Command; +/// # +/// struct Upload { +/// pub gb_quota: u64, +/// pub mime_types: Vec, +/// } +/// +/// impl Command for Upload { +/// const COMMAND: &'static str = "/storage/upload"; +/// } +/// +/// assert_eq!(Upload::COMMAND, "/storage/upload"); +/// ``` +pub trait Command { + /// The value that will be placed in the UCAN's `cmd` field for the given type + /// + /// FIXME + /// This is a `const` because it *must not*[^dynamic] depend on the runtime values of a type + /// in order to ensure type safety. + /// + /// [^dynamic]: Note that if the `dynamic` feature is enabled, the exception is + /// a special ability called [`Dynamic`][super::dynamic::Dynamic] (for e.g. JS FFI) + /// that uses a non-exported code path separate from the [`Command`] trait. + const COMMAND: &'static str; +} + +// NOTE do not export; this is used to limit the Hierarchy +// interface to [Parentful] and [Parentless] while enabling [Dynamic] +// FIXME ^^^^ NOT ANYMORE? +// Either that needs to be re-locked down, or (because it's all abstract anyways) +// just note that you probably don;t want this one. +pub trait ToCommand { + fn to_command(&self) -> String; +} + +impl ToCommand for T { + fn to_command(&self) -> String { + T::COMMAND.to_string() + } +} diff --git a/src/ability/crud.rs b/src/ability/crud.rs new file mode 100644 index 00000000..7a47a611 --- /dev/null +++ b/src/ability/crud.rs @@ -0,0 +1,251 @@ +//! Abilties for [CRUD] (create, read, update, and destroy) interfaces +//! +//! An overview of the hierarchy can be found on [`crud::Any`][`Any`]. +//! +//! # Wrapping External Resources +//! +//! In most cases, the Subject _is_ the resource being acted +//! on with a CRUD interface. To model external resources directly +//! (i.e. without a URL), generate a unique [`Did`] that represents the +//! specific resource (i.e. the `sub`) directly. This makes the +//! UCAN self-certifying, and can give multiple names to a single +//! resource (which can be important if operating over an open network +//! such as a DHT or gossip). It also provides an abstraction if, +//! for example, the the domain name of a service changes. +//! +//! # `path` Field +//! +//! All variants of CRUD abilities include an *optional* `path` field. +//! +//! There are cases where a Subject acts as a gateway for *external* +//! resources, such as web services or hierarchical file systems. +//! Both of these contain sub-resources expressed via path. +//! If you are issued access to the root, and can attenuate that access to +//! any sub-path, or a single leaf resource. +//! +//! ```js +//! { +//! "sub: "did:example:1234", // <-- e.g. Wraps a web API +//! "cmd": "/crud/update", +//! "args": { +//! "path": "/some/path/to/a/resource", +//! }, +//! // ... +//! } +//! ``` +//! +//! [CRUD]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete +//! [`Did`]: crate::did::Did + +pub mod create; +pub mod destroy; +pub mod read; +pub mod update; + +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError, ParsePromised}, + }, + invocation::promise::Resolvable, + ipld, +}; +use create::{Create, PromisedCreate}; +use destroy::{Destroy, PromisedDestroy}; +use libipld_core::ipld::Ipld; +use read::{PromisedRead, Read}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use update::{PromisedUpdate, Update}; + +#[cfg(target_arch = "wasm32")] +pub mod js; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Crud { + Create(Create), + Read(Read), + Update(Update), + Destroy(Destroy), +} + +impl From for arguments::Named { + fn from(crud: Crud) -> Self { + match crud { + Crud::Create(create) => create.into(), + Crud::Read(read) => read.into(), + Crud::Update(update) => update.into(), + Crud::Destroy(destroy) => destroy.into(), + } + } +} + +impl From for Crud { + fn from(create: Create) -> Self { + Crud::Create(create) + } +} + +impl From for Crud { + fn from(read: Read) -> Self { + Crud::Read(read) + } +} + +impl From for Crud { + fn from(update: Update) -> Self { + Crud::Update(update) + } +} + +impl From for Crud { + fn from(destroy: Destroy) -> Self { + Crud::Destroy(destroy) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PromisedCrud { + Create(PromisedCreate), + Read(PromisedRead), + Update(PromisedUpdate), + Destroy(PromisedDestroy), +} + +impl ParsePromised for PromisedCrud { + type PromisedArgsError = InvalidArgs; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match PromisedCreate::try_parse_promised(cmd, args.clone()) { + Ok(create) => return Ok(PromisedCrud::Create(create)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Create(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match PromisedRead::try_parse_promised(cmd, args.clone()) { + Ok(read) => return Ok(PromisedCrud::Read(read)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Read(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match PromisedUpdate::try_parse_promised(cmd, args.clone()) { + Ok(update) => return Ok(PromisedCrud::Update(update)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Update(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match PromisedDestroy::try_parse_promised(cmd, args) { + Ok(destroy) => return Ok(PromisedCrud::Destroy(destroy)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Destroy(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + Err(ParseAbilityError::UnknownCommand(cmd.into())) + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum InvalidArgs { + #[error("Invalid args for create: {0}")] + Create(create::FromPromisedArgsError), + + #[error("Invalid args for read: {0}")] + Read(read::FromPromisedArgsError), + + #[error("Invalid args for update: {0}")] + Update(update::FromPromisedArgsError), + + #[error("Invalid args for destroy: {0}")] + Destroy(destroy::FromPromisedArgsError), +} + +impl ParseAbility for Crud { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match Create::try_parse(cmd, args.clone()) { + Ok(create) => return Ok(Crud::Create(create)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match Read::try_parse(cmd, args.clone()) { + Ok(read) => return Ok(Crud::Read(read)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match Update::try_parse(cmd, args.clone()) { + Ok(update) => return Ok(Crud::Update(update)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match Destroy::try_parse(cmd, args) { + Ok(destroy) => return Ok(Crud::Destroy(destroy)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + Err(ParseAbilityError::UnknownCommand(cmd.into())) + } +} + +impl ToCommand for Crud { + fn to_command(&self) -> String { + match self { + Crud::Create(create) => create.to_command(), + Crud::Read(read) => read.to_command(), + Crud::Update(update) => update.to_command(), + Crud::Destroy(destroy) => destroy.to_command(), + } + } +} + +impl ToCommand for PromisedCrud { + fn to_command(&self) -> String { + match self { + PromisedCrud::Create(create) => create.to_command(), + PromisedCrud::Read(read) => read.to_command(), + PromisedCrud::Update(update) => update.to_command(), + PromisedCrud::Destroy(destroy) => destroy.to_command(), + } + } +} +impl Resolvable for Crud { + type Promised = PromisedCrud; +} + +impl From for arguments::Named { + fn from(promised: PromisedCrud) -> Self { + match promised { + PromisedCrud::Create(create) => create.into(), + PromisedCrud::Read(read) => read.into(), + PromisedCrud::Update(update) => update.into(), + PromisedCrud::Destroy(destroy) => destroy.into(), + } + } +} diff --git a/src/ability/crud/create.rs b/src/ability/crud/create.rs new file mode 100644 index 00000000..69acac8c --- /dev/null +++ b/src/ability/crud/create.rs @@ -0,0 +1,256 @@ +//! Create new resources. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `crud/create` ability. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// create("crud/create") +/// end +/// end +/// end +/// +/// createpromise("crud::create::PromisedCreate") +/// createready("crud::create::Create") +/// +/// top --> any --> mutate --> create +/// create -.->|invoke| createpromise -.->|resolve| createready -.-> exe{{execute}} +/// +/// style createready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Create { + /// An optional path to a sub-resource that is to be created. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// Optional arguments for creation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>, +} + +impl From for Ipld { + fn from(create: Create) -> Self { + let mut map = BTreeMap::new(); + + if let Some(path) = create.path { + map.insert("path".to_string(), path.display().to_string().into()); + } + + if let Some(args) = create.args { + map.insert("args".to_string(), args.into()); + } + + Ipld::Map(map) + } +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/create` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// create("crud/create") +/// end +/// end +/// end +/// +/// createpromise("crud::create::PromisedCreate") +/// createready("crud::create::Create") +/// +/// top --> any --> mutate --> create +/// create -.->|invoke| createpromise -.->|resolve| createready -.-> exe{{execute}} +/// +/// style createpromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedCreate { + /// An optional path to a sub-resource that is to be created. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option>, + + /// Optional arguments for creation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>>, +} + +const COMMAND: &str = "/crud/create"; + +impl Command for Create { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedCreate { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for PromisedCreate { + type Error = FromPromisedArgsError; + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, prom) in arguments { + match k.as_str() { + "path" => match prom { + ipld::Promised::String(s) => { + path = Some(promise::Any::Resolved(PathBuf::from(s)).into()); + } + ipld::Promised::WaitOk(cid) => { + path = Some(promise::Any::PendingOk(cid).into()); + } + ipld::Promised::WaitErr(cid) => { + path = Some(promise::Any::PendingErr(cid).into()); + } + ipld::Promised::WaitAny(cid) => { + path = Some(promise::Any::PendingAny(cid).into()); + } + _ => return Err(FromPromisedArgsError::InvalidPath(k)), + }, + + "args" => { + args = match prom { + ipld::Promised::Map(map) => { + Some(promise::Any::Resolved(arguments::Named(map)).into()) + } + ipld::Promised::WaitOk(cid) => Some(promise::Any::PendingOk(cid)), + ipld::Promised::WaitErr(cid) => Some(promise::Any::PendingErr(cid)), + ipld::Promised::WaitAny(cid) => Some(promise::Any::PendingAny(cid)), + _ => return Err(FromPromisedArgsError::InvalidArgs(prom)), + } + } + _ => return Err(FromPromisedArgsError::InvalidMapKey(k)), + } + } + + Ok(PromisedCreate { path, args }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Invalid path {0}")] + InvalidPath(String), + + #[error("Invalid args {0}")] + InvalidArgs(ipld::Promised), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl TryFrom> for Create { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, ipld) in arguments { + match k.as_str() { + "path" => { + if let Ipld::String(s) = ipld { + path = Some(PathBuf::from(s)); + } else { + return Err(()); + } + } + "args" => { + args = Some(ipld.try_into().map_err(|_| ())?); + } + _ => return Err(()), + } + } + + Ok(Create { path, args }) + } +} + +impl From for PromisedCreate { + fn from(r: Create) -> PromisedCreate { + PromisedCreate { + path: r.path.map(|inner_path| promise::Any::Resolved(inner_path)), + + args: r + .args + .map(|inner_args| promise::Any::Resolved(inner_args.into())), + } + } +} + +impl promise::Resolvable for Create { + type Promised = PromisedCreate; +} + +impl From for arguments::Named { + fn from(create: Create) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = create.path { + named.insert("path".to_string(), path.display().to_string().into()); + } + + if let Some(args) = create.args { + named.insert("args".to_string(), args.into()); + } + + named + } +} + +impl From for arguments::Named { + fn from(promised: PromisedCreate) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path_prom) = promised.path { + named.insert("path".to_string(), path_prom.to_promised_ipld()); + } + + if let Some(args_prom) = promised.args { + named.insert("args".to_string(), args_prom.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/crud/destroy.rs b/src/ability/crud/destroy.rs new file mode 100644 index 00000000..3f52084a --- /dev/null +++ b/src/ability/crud/destroy.rs @@ -0,0 +1,244 @@ +//! Destroy a resource. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `crud/destroy` ability. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// destroy("crud/destroy") +/// end +/// end +/// end +/// +/// destroypromise("crud::destroy::Promised") +/// destroyready("crud::destroy::Destroy") +/// +/// top --> any --> mutate --> destroy +/// destroy -.->|invoke| destroypromise -.->|resolve| destroyready -.-> exe{{execute}} +/// +/// style destroyready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Destroy { + /// An optional path to a sub-resource that is to be destroyed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, +} + +impl From for Ipld { + fn from(destroy: Destroy) -> Self { + let mut map = BTreeMap::new(); + + if let Some(path) = destroy.path { + map.insert("path".to_string(), path.display().to_string().into()); + } + + Ipld::Map(map) + } +} + +const COMMAND: &'static str = "/crud/destroy"; + +impl Command for Destroy { + const COMMAND: &'static str = COMMAND; +} + +impl From for arguments::Named { + fn from(ready: Destroy) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = ready.path { + named.insert( + "path".to_string(), + path.into_os_string() + .into_string() + .expect("PathBuf to generate valid paths") // FIXME reasonable assumption? + .into(), + ); + } + + named + } +} + +impl TryFrom> for Destroy { + type Error = TryFromArgsError; + + fn try_from(args: arguments::Named) -> Result { + let mut path = None; + + for (k, ipld) in args { + match k.as_str() { + "path" => { + if let Ipld::String(s) = ipld { + path = Some(PathBuf::from(s)); + } else { + return Err(TryFromArgsError::NotAPathBuf); + } + } + s => return Err(TryFromArgsError::InvalidField(s.into())), + } + } + + Ok(Destroy { path }) + } +} + +#[derive(Error, Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum TryFromArgsError { + #[error("Path value is not a PathBuf")] + NotAPathBuf, + + #[error("Invalid map key {0}")] + InvalidField(String), +} + +impl promise::Resolvable for Destroy { + type Promised = PromisedDestroy; +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/destroy` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// destroy("crud/destroy") +/// end +/// end +/// end +/// +/// destroypromise("crud::destroy::Promised") +/// destroyready("crud::destroy::Destroy") +/// +/// top --> any --> mutate --> destroy +/// destroy -.->|invoke| destroypromise -.->|resolve| destroyready -.-> exe{{execute}} +/// +/// style destroypromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedDestroy { + /// An optional path to a sub-resource that is to be destroyed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option>, +} + +impl TryFrom> for PromisedDestroy { + type Error = FromPromisedArgsError; + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + + for (k, prom) in arguments { + match k.as_str() { + "path" => match prom { + ipld::Promised::String(s) => { + path = Some(promise::Any::Resolved(PathBuf::from(s)).into()); + } + ipld::Promised::WaitOk(cid) => { + path = Some(promise::Any::PendingOk(cid).into()); + } + ipld::Promised::WaitErr(cid) => { + path = Some(promise::Any::PendingErr(cid).into()); + } + ipld::Promised::WaitAny(cid) => { + path = Some(promise::Any::PendingAny(cid).into()); + } + _ => return Err(FromPromisedArgsError::InvalidPath(k)), + }, + _ => return Err(FromPromisedArgsError::InvalidMapKey(k)), + } + } + + Ok(PromisedDestroy { path }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Invalid path {0}")] + InvalidPath(String), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl Command for PromisedDestroy { + const COMMAND: &'static str = COMMAND; +} + +// impl From for arguments::Named { +// fn from(promised: PromisedDestroy) -> Self { +// let mut named = arguments::Named::new(); +// +// if let Some(path_res) = promised.path { +// named.insert( +// "path".to_string(), +// path_res.map(|p| ipld::Newtype::from(p).0).into(), +// ); +// } +// +// named +// } +// } + +impl From for PromisedDestroy { + fn from(r: Destroy) -> PromisedDestroy { + PromisedDestroy { + path: r + .path + .map(|inner_path| promise::Any::Resolved(inner_path).into()), + } + } +} + +impl From for arguments::Named { + fn from(promised: PromisedDestroy) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = promised.path { + named.insert("path".to_string(), path.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/crud/js.rs b/src/ability/crud/js.rs new file mode 100644 index 00000000..57a56bff --- /dev/null +++ b/src/ability/crud/js.rs @@ -0,0 +1,35 @@ +//! JavaScript bindings for the CRUD abilities. + +use super::read; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct CrudRead(#[wasm_bindgen(skip)] pub read::Ready); + +#[wasm_bindgen] +impl CrudRead { + pub fn to_jsvalue(self) -> JsValue { + ipld::Newtype(Ipld::from(self.0)).into() + } + + pub fn from_jsvalue(js_val: JsValue) -> Result { + ipld::Newtype::try_into_jsvalue(js_val).map(CrudRead) + } + + pub fn to_command(&self) -> String { + Read::to_command() + } + + pub fn check_same(&self, proof: &CrudRead) -> Result<(), JsError> { + self.0.check_same(&proof.0).map_err(Into::into) + } + + // FIXME more than any + pub fn check_parent(&self, proof: &CrudAny) -> Result<(), JsError> { + self.0.check_parent(&proof.0).map_err(Into::into) + } +} + +// FIXME needs bindings +#[wasm_bindgen] +pub struct CrudReadPromise(#[wasm_bindgen(skip)] pub read::Promised); diff --git a/src/ability/crud/read.rs b/src/ability/crud/read.rs new file mode 100644 index 00000000..873636e2 --- /dev/null +++ b/src/ability/crud/read.rs @@ -0,0 +1,252 @@ +//! Read from a resource. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::{error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// This ability is used to fetch messages from other actors. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// any("crud/*") +/// +/// subgraph Invokable +/// read("crud/read") +/// end +/// end +/// +/// readpromise("crud::read::Promised") +/// readready("crud::read::Read") +/// +/// top --> any --> read +/// read -.->|invoke| readpromise -.->|resolve| readready -.-> exe{{execute}} +/// +/// style readready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Read { + /// An optional path to a sub-resource that is to be read. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// Optional arguments to modify the read request. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>, +} + +impl From for Ipld { + fn from(ready: Read) -> Self { + let mut map = BTreeMap::new(); + + if let Some(path) = ready.path { + map.insert("path".to_string(), Ipld::String(path.display().to_string())); + } + + if let Some(args) = ready.args { + map.insert("args".to_string(), args.into()); + } + + map.into() + } +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/read` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// subgraph Invokable +/// read("crud/read") +/// end +/// end +/// end +/// +/// readpromise("crud::read::Promised") +/// readready("crud::read::Read") +/// +/// top --> any --> read +/// read -.->|invoke| readpromise -.->|resolve| readready -.-> exe{{execute}} +/// +/// style readpromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedRead { + /// An optional path to a sub-resource that is to be read. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option>, + + /// Optional arguments to modify the read request. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>>, +} + +impl TryFrom> for PromisedRead { + type Error = FromPromisedArgsError; + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, prom) in arguments { + match k.as_str() { + "path" => match prom { + ipld::Promised::String(s) => { + path = Some(promise::Any::Resolved(PathBuf::from(s)).into()); + } + ipld::Promised::WaitOk(cid) => { + path = Some(promise::Any::PendingOk(cid).into()); + } + ipld::Promised::WaitErr(cid) => { + path = Some(promise::Any::PendingErr(cid).into()); + } + ipld::Promised::WaitAny(cid) => { + path = Some(promise::Any::PendingAny(cid).into()); + } + _ => return Err(FromPromisedArgsError::InvalidPath(k)), + }, + + "args" => { + args = match prom { + ipld::Promised::Map(map) => { + Some(promise::Any::Resolved(arguments::Named(map)).into()) + } + ipld::Promised::WaitOk(cid) => Some(promise::Any::PendingOk(cid).into()), + ipld::Promised::WaitErr(cid) => Some(promise::Any::PendingErr(cid).into()), + ipld::Promised::WaitAny(cid) => Some(promise::Any::PendingAny(cid).into()), + _ => return Err(FromPromisedArgsError::InvalidArgs(prom)), + } + } + _ => return Err(FromPromisedArgsError::InvalidMapKey(k)), + } + } + + Ok(PromisedRead { path, args }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Invalid path {0}")] + InvalidPath(String), + + #[error("Invalid args {0}")] + InvalidArgs(ipld::Promised), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +const COMMAND: &'static str = "/crud/read"; + +impl Command for Read { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedRead { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom for Read { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl From for arguments::Named { + fn from(ready: Read) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = ready.path { + named.insert( + "path".to_string(), + path.into_os_string() + .into_string() + .expect("PathBuf should make a valid path") + .into(), + ); + } + + if let Some(args) = ready.args { + named.insert("args".to_string(), args.into()); + } + + named + } +} + +impl TryFrom> for Read { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, v) in arguments.into_iter() { + match k.as_str() { + "path" => { + if let Ipld::String(string) = v { + path = Some(PathBuf::from(string)); + } else { + return Err(()); + } + } + "args" => { + args = Some(arguments::Named::try_from(v).map_err(|_| ())?); + } + _ => return Err(()), + } + } + + Ok(Read { path, args }) + } +} + +impl promise::Resolvable for Read { + type Promised = PromisedRead; +} + +impl From for arguments::Named { + fn from(promised: PromisedRead) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path_res) = promised.path { + named.insert("path".to_string(), path_res.to_promised_ipld()); + } + + if let Some(args_res) = promised.args { + named.insert("args".to_string(), args_res.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/crud/update.rs b/src/ability/crud/update.rs new file mode 100644 index 00000000..b0f7a6bb --- /dev/null +++ b/src/ability/crud/update.rs @@ -0,0 +1,298 @@ +//! Update existing resources. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `crud/create` ability. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// update("crud/update") +/// end +/// end +/// end +/// +/// updatepromise("crud::update::Promised") +/// updateready("crud::update::Update") +/// +/// top --> any --> mutate --> update +/// update -.->|invoke| updatepromise -.->|resolve| updateready -.-> exe{{execute}} +/// +/// style updateready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Update { + /// An optional path to a sub-resource that is to be updated. + #[serde(default, skip_serializing_if = "Option::is_none")] + path: Option, + + /// Optional arguments to be passed in the update. + #[serde(default, skip_serializing_if = "Option::is_none")] + args: Option>, +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/update` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// update("crud/update") +/// end +/// end +/// end +/// +/// updatepromise("crud::update::Promised") +/// updateready("crud::update::Update") +/// +/// top --> any --> mutate --> update +/// update -.->|invoke| updatepromise -.->|resolve| updateready -.-> exe{{execute}} +/// +/// style updatepromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedUpdate { + /// An optional path to a sub-resource that is to be updated. + #[serde(default, skip_serializing_if = "Option::is_none")] + path: Option>, + + /// Optional arguments to be passed in the update. + #[serde(default, skip_serializing_if = "Option::is_none")] + args: Option>>, +} + +const COMMAND: &'static str = "/crud/update"; + +impl Command for Update { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedUpdate { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for PromisedUpdate { + type Error = FromPromisedArgsError; + + fn try_from(named: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (key, prom) in named { + match key.as_str() { + "path" => match Ipld::try_from(prom) { + Err(pending) => { + path = Some(pending.into()); + } + Ok(ipld) => match ipld { + Ipld::String(s) => path = Some(promise::Any::Resolved(PathBuf::from(s))), + other => return Err(FromPromisedArgsError::PathBodyNotAString(other)), + }, + }, + + "args" => match prom { + ipld::Promised::Map(map) => { + args = Some(promise::Any::Resolved(arguments::Named(map))) + } + ipld::Promised::WaitOk(cid) => args = Some(promise::Any::PendingOk(cid)), + ipld::Promised::WaitErr(cid) => args = Some(promise::Any::PendingErr(cid)), + ipld::Promised::WaitAny(cid) => { + args = Some(promise::Any::PendingAny(cid)); + } + _ => return Err(FromPromisedArgsError::InvalidArgs(prom)), + }, + + _ => return Err(FromPromisedArgsError::InvalidMapKey(key)), + } + } + + Ok(PromisedUpdate { path, args }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Path body is not a string")] + PathBodyNotAString(Ipld), + + #[error("Invalid args {0}")] + InvalidArgs(ipld::Promised), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl TryFrom> for Update { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (key, ipld) in named { + match key.as_str() { + "path" => { + if let Ipld::String(s) = ipld { + path = Some(PathBuf::from(s)); + } else { + return Err(()); + } + } + "args" => { + if let Ipld::Map(map) = ipld { + args = Some(arguments::Named(map)); + } else { + return Err(()); + } + } + _ => return Err(()), + } + } + + Ok(Update { path, args }) + } +} + +impl From for arguments::Named { + fn from(create: Update) -> Self { + let mut named = arguments::Named::::new(); + + if let Some(path) = create.path { + named.insert("path".to_string(), Ipld::String(path.display().to_string())); + } + + if let Some(args) = create.args { + named.insert("args".to_string(), args.into()); + } + + named + } +} + +impl TryFrom for Update { + type Error = TryFromIpldError; + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::Map(map) = ipld { + if map.len() > 2 { + return Err(TryFromIpldError::TooManyKeys); + } + + Ok(Update { + path: map + .get("path") + .map(|ipld| { + (ipld::Newtype(ipld.clone())) + .try_into() + .map_err(TryFromIpldError::InvalidPath) + }) + .transpose()?, + + args: map + .get("args") + .map(|ipld| { + arguments::Named::::try_from(ipld.clone()) + .map_err(|_| TryFromIpldError::InvalidArgs) + }) + .transpose()?, + }) + } else { + Err(TryFromIpldError::NotAMap) + } + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum TryFromIpldError { + #[error("Not a map")] + NotAMap, + + #[error("Too many keys")] + TooManyKeys, + + #[error("Invalid path: {0}")] + InvalidPath(ipld::newtype::NotAString), + + #[error("Invalid args: not a map")] + InvalidArgs, +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedUpdateError { + #[error("Unresolved args")] + UnresolvedArgs(promise::Any>), + + #[error("Args pending")] + ArgsPending(>::Error), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl From for PromisedUpdate { + fn from(r: Update) -> PromisedUpdate { + PromisedUpdate { + path: r.path.map(|inner_path| promise::Any::Resolved(inner_path)), + + args: r + .args + .map(|inner_args| promise::Any::Resolved(inner_args.into())), + } + } +} + +impl promise::Resolvable for Update { + type Promised = PromisedUpdate; +} + +impl From for arguments::Named { + fn from(promised: PromisedUpdate) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = promised.path { + named.insert("path".to_string(), path.to_promised_ipld()); + } + + if let Some(args) = promised.args { + named.insert("args".to_string(), args.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/dynamic.rs b/src/ability/dynamic.rs new file mode 100644 index 00000000..e5cbe65e --- /dev/null +++ b/src/ability/dynamic.rs @@ -0,0 +1,130 @@ +//! This module is for dynamic abilities, especially for FFI and Wasm support + +use super::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError}, +}; +use libipld_core::{error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use js_sys; + +// NOTE the lack of checking functions! + +/// A "dynamic" ability with the bare minimum of statics +/// +///
+/// This should be a last resort, and only for e.g. FFI. The Dynamic ability is +/// not recommended for typical Rust usage. +/// +/// This is instead meant to be embedded inside of structs that have e.g. FFI bindings to +/// a validation function, such as `js_sys::Function` for JS, `magnus::function!` for Ruby, +/// and so on. +///
+/// +/// [`Dynamic`] uses none of the typical ability traits directly. Rather, it must be wrapped +/// in [`Reader`][crate::reader::Reader], which wires up dynamic dispatch for the +/// relevant traits using a configuration struct. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] // FIXME serialize / deserilaize? +pub struct Dynamic { + /// The `cmd` field (hooks into a dynamic version of [`Command`][crate::ability::command::Command]) + pub cmd: String, + + /// Unstructured, named arguments + /// + /// The only requirement is that the keys are strings and the values are [`Ipld`] + pub args: arguments::Named, +} + +impl ParseAbility for Dynamic { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + Ok(Dynamic { + cmd: cmd.to_string(), + args, + }) + } +} + +impl ToCommand for Dynamic { + fn to_command(&self) -> String { + self.cmd.clone() + } +} + +impl From for arguments::Named { + fn from(dynamic: Dynamic) -> Self { + dynamic.args + } +} + +#[cfg(target_arch = "wasm32")] +impl From for js_sys::Map { + fn from(ability: Dynamic) -> Self { + let args = js_sys::Map::new(); + for (k, v) in ability.args.0 { + args.set(&k.into(), &ipld::Newtype(v).into()); + } + + let map = js_sys::Map::new(); + map.set(&"args".into(), &js_sys::Object::from(args).into()); + map.set(&"cmd".into(), &ability.cmd.into()); + map + } +} + +#[cfg(target_arch = "wasm32")] +impl TryFrom for Dynamic { + type Error = JsValue; + + fn try_from(map: js_sys::Map) -> Result { + if let (Some(cmd), js_args) = ( + map.get(&("cmd".into())).as_string(), + &map.get(&("args".into())), + ) { + let obj_args = js_sys::Object::try_from(js_args).ok_or(wasm_bindgen::JsValue::NULL)?; + let keys = js_sys::Object::keys(obj_args); + let values = js_sys::Object::values(obj_args); + + let mut btree = BTreeMap::new(); + for (k, v) in keys.iter().zip(values) { + if let Some(k) = k.as_string() { + btree.insert(k, ipld::Newtype::try_from(v).expect("FIXME").0); + } else { + return Err(k); + } + } + + Ok(Dynamic { + cmd, + args: arguments::Named(btree), + }) + } else { + Err(JsValue::NULL) // FIXME + } + } +} + +impl From for Ipld { + fn from(dynamic: Dynamic) -> Self { + dynamic.into() + } +} + +impl TryFrom for Dynamic { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} diff --git a/src/ability/js.rs b/src/ability/js.rs new file mode 100644 index 00000000..e66d92f2 --- /dev/null +++ b/src/ability/js.rs @@ -0,0 +1,19 @@ +//! Bindings for the JavaScript via Wasm +//! +//! Note that these are all [`wasm_bindgen`]-specific, +//! and are not recommended elsewhere due to limited +//! type safety, poorer performance, and restrictions +//! on the API placed by [`wasm_bindgen`]. +//! +//! The overall pattern is roughly: "JS code hands the +//! Rust code a config object with handlers at runtime". +//! The Rust takes those handlers, and dispatches them +//! as part of the normal flow. +//! +//! When compiled for Wasm, the other abilities in this +//! crate export JS bindings. This allows them to be +//! plugged into e.g. ability hierarchies from the JS +//! side as an extension mechanism. + +pub mod parentful; +pub mod parentless; diff --git a/src/ability/js/config.rs b/src/ability/js/config.rs new file mode 100644 index 00000000..2f42905b --- /dev/null +++ b/src/ability/js/config.rs @@ -0,0 +1,120 @@ +//! JavaScript interface for abilities that *do* require a parent hierarchy + +use crate::{ + ability::{arguments, command::ToCommand, dynamic}, + reader::Reader, +}; +use js_sys::{Function, JsString, Map}; +use libipld_core::ipld::Ipld; +use std::collections::BTreeMap; +use wasm_bindgen::{prelude::*, JsValue}; + +// FIXME rename +type WithParents = Reader>; + +// FIXME just make into a general config? + +/// The configuration object that expresses an ability (with parents) from JS +#[derive(Debug, Clone, PartialEq, Default)] +#[wasm_bindgen(getter_with_clone)] +pub struct ParentfulConfig { + pub command: String, + + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub is_nonce_meaningful: bool, + + #[wasm_bindgen(js_name = validateShape)] + pub validate_shape: Function, +} + +// NOTE if changed, please update this in the docs for `ParentfulArgs` below +#[wasm_bindgen(typescript_custom_section)] +const PARENTFUL_ARGS: &str = r#" +interface ParentfulArgs { + command: string, + isNonceMeaningful: boolean, + validateShape: Function, +} +"#; + +#[wasm_bindgen] +extern "C" { + /// Named constructor arguments for `ParentfulConfig` + /// + /// This forms the basis for configuring an ability. + /// These values will be used at runtime to perform + /// checks on the ability (e.g. during delegation), + /// for indexing, and storage (among others). + /// + /// ```typescript + /// // TypeScript + /// interface ParentfulArgs { + /// command: string, + /// isNonceMeaningful: boolean, + /// validateShape: Function, + /// } + /// ``` + #[wasm_bindgen(typescript_type = "ParentfulArgs")] + pub type ParentfulArgs; + + /// Get the [`Command`][crate::ability::command::Command] string + #[wasm_bindgen(js_name = command)] + pub fn command(this: &ParentfulArgs) -> String; + + /// Whether the nonce should factor into a receipt's global index ([`task::Id`]) + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub fn is_nonce_meaningful(this: &ParentfulArgs) -> bool; + + /// Parser validator + #[wasm_bindgen(js_name = validateShape)] + pub fn validate_shape(this: &ParentfulArgs) -> Function; +} + +#[wasm_bindgen] +impl ParentfulConfig { + /// Construct a new `ParentfulConfig` from JavaScript + /// + /// # Examples + /// + /// ```javascript + /// // JavaScript + /// const msgSendConfig = new ParentfulConfig({ + /// command: "msg/send", + /// isNonceMeaningful: true, + /// validateShape: (args) => { + /// if (args.to && args.message && args.length() === 2) { + /// return true; + /// } + /// return false; + /// } + /// } + /// ); + /// ``` + #[wasm_bindgen(constructor)] + pub fn new(js_obj: ParentfulArgs) -> Result { + Ok(ParentfulConfig { + command: command(&js_obj), + is_nonce_meaningful: is_nonce_meaningful(&js_obj), + validate_shape: validate_shape(&js_obj), + }) + } +} + +impl From for dynamic::Dynamic { + fn from(js: WithParents) -> Self { + dynamic::Dynamic { + cmd: js.env.command, + args: js.val, + } + } +} + +impl ToCommand for ParentfulConfig { + fn to_command(&self) -> String { + self.command.clone() + } +} + +impl Checkable for WithParents { + type Hierarchy = Parentful; +} diff --git a/src/ability/js/parentful.rs b/src/ability/js/parentful.rs new file mode 100644 index 00000000..2f42905b --- /dev/null +++ b/src/ability/js/parentful.rs @@ -0,0 +1,120 @@ +//! JavaScript interface for abilities that *do* require a parent hierarchy + +use crate::{ + ability::{arguments, command::ToCommand, dynamic}, + reader::Reader, +}; +use js_sys::{Function, JsString, Map}; +use libipld_core::ipld::Ipld; +use std::collections::BTreeMap; +use wasm_bindgen::{prelude::*, JsValue}; + +// FIXME rename +type WithParents = Reader>; + +// FIXME just make into a general config? + +/// The configuration object that expresses an ability (with parents) from JS +#[derive(Debug, Clone, PartialEq, Default)] +#[wasm_bindgen(getter_with_clone)] +pub struct ParentfulConfig { + pub command: String, + + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub is_nonce_meaningful: bool, + + #[wasm_bindgen(js_name = validateShape)] + pub validate_shape: Function, +} + +// NOTE if changed, please update this in the docs for `ParentfulArgs` below +#[wasm_bindgen(typescript_custom_section)] +const PARENTFUL_ARGS: &str = r#" +interface ParentfulArgs { + command: string, + isNonceMeaningful: boolean, + validateShape: Function, +} +"#; + +#[wasm_bindgen] +extern "C" { + /// Named constructor arguments for `ParentfulConfig` + /// + /// This forms the basis for configuring an ability. + /// These values will be used at runtime to perform + /// checks on the ability (e.g. during delegation), + /// for indexing, and storage (among others). + /// + /// ```typescript + /// // TypeScript + /// interface ParentfulArgs { + /// command: string, + /// isNonceMeaningful: boolean, + /// validateShape: Function, + /// } + /// ``` + #[wasm_bindgen(typescript_type = "ParentfulArgs")] + pub type ParentfulArgs; + + /// Get the [`Command`][crate::ability::command::Command] string + #[wasm_bindgen(js_name = command)] + pub fn command(this: &ParentfulArgs) -> String; + + /// Whether the nonce should factor into a receipt's global index ([`task::Id`]) + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub fn is_nonce_meaningful(this: &ParentfulArgs) -> bool; + + /// Parser validator + #[wasm_bindgen(js_name = validateShape)] + pub fn validate_shape(this: &ParentfulArgs) -> Function; +} + +#[wasm_bindgen] +impl ParentfulConfig { + /// Construct a new `ParentfulConfig` from JavaScript + /// + /// # Examples + /// + /// ```javascript + /// // JavaScript + /// const msgSendConfig = new ParentfulConfig({ + /// command: "msg/send", + /// isNonceMeaningful: true, + /// validateShape: (args) => { + /// if (args.to && args.message && args.length() === 2) { + /// return true; + /// } + /// return false; + /// } + /// } + /// ); + /// ``` + #[wasm_bindgen(constructor)] + pub fn new(js_obj: ParentfulArgs) -> Result { + Ok(ParentfulConfig { + command: command(&js_obj), + is_nonce_meaningful: is_nonce_meaningful(&js_obj), + validate_shape: validate_shape(&js_obj), + }) + } +} + +impl From for dynamic::Dynamic { + fn from(js: WithParents) -> Self { + dynamic::Dynamic { + cmd: js.env.command, + args: js.val, + } + } +} + +impl ToCommand for ParentfulConfig { + fn to_command(&self) -> String { + self.command.clone() + } +} + +impl Checkable for WithParents { + type Hierarchy = Parentful; +} diff --git a/src/ability/msg.rs b/src/ability/msg.rs new file mode 100644 index 00000000..0e7e35d1 --- /dev/null +++ b/src/ability/msg.rs @@ -0,0 +1,142 @@ +//! Message abilities + +pub mod receive; +pub mod send; + +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError, ParsePromised}, + }, + invocation::promise::Resolvable, + ipld, +}; +use libipld_core::ipld::Ipld; +use receive::{PromisedReceive, Receive}; +use send::{PromisedSend, Send}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test_utils")] +use proptest_derive::Arbitrary; + +/// A family of abilities for sending and receiving messages. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "test_utils", derive(Arbitrary))] +pub enum Msg { + /// The ability for sending messages. + Send(Send), + + /// The ability for receiving messages. + Receive(Receive), +} + +impl From for arguments::Named { + fn from(msg: Msg) -> Self { + match msg { + Msg::Send(send) => send.into(), + Msg::Receive(receive) => receive.into(), + } + } +} + +impl From for Ipld { + fn from(msg: Msg) -> Self { + match msg { + Msg::Send(send) => send.into(), + Msg::Receive(receive) => receive.into(), + } + } +} + +/// A promised version of the [`Msg`] ability. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PromisedMsg { + /// The promised ability for sending messages. + Send(PromisedSend), + + /// The promised ability for receiving messages. + Receive(PromisedReceive), +} + +impl ToCommand for Msg { + fn to_command(&self) -> String { + match self { + Msg::Send(send) => send.to_command(), + Msg::Receive(receive) => receive.to_command(), + } + } +} + +impl ToCommand for PromisedMsg { + fn to_command(&self) -> String { + match self { + PromisedMsg::Send(send) => send.to_command(), + PromisedMsg::Receive(receive) => receive.to_command(), + } + } +} + +impl ParsePromised for PromisedMsg { + type PromisedArgsError = (); + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + if let Ok(send) = PromisedSend::try_parse_promised(cmd, args.clone()) { + return Ok(PromisedMsg::Send(send)); + } + + if let Ok(receive) = PromisedReceive::try_parse_promised(cmd, args) { + return Ok(PromisedMsg::Receive(receive)); + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +impl Resolvable for Msg { + type Promised = PromisedMsg; +} + +impl From for arguments::Named { + fn from(promised: PromisedMsg) -> Self { + match promised { + PromisedMsg::Send(send) => send.into(), + PromisedMsg::Receive(receive) => receive.into(), + } + } +} + +impl ParseAbility for Msg { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + if let Ok(send) = Send::try_parse(cmd, args.clone()) { + return Ok(Msg::Send(send)); + } + + if let Ok(receive) = Receive::try_parse(cmd, args) { + return Ok(Msg::Receive(receive)); + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +// #[cfg(feature = "test_utils")] +// impl Arbitrary for Payload +// where +// T::Strategy: 'static, +// DID::Parameters: Clone, +// { +// type Parameters = (T::Parameters, DID::Parameters); +// type Strategy = BoxedStrategy; +// +// fn arbitrary_with((t_args, did_args): Self::Parameters) -> Self::Strategy { +// } +// } diff --git a/src/ability/msg/receive.rs b/src/ability/msg/receive.rs new file mode 100644 index 00000000..60abcd7f --- /dev/null +++ b/src/ability/msg/receive.rs @@ -0,0 +1,156 @@ +//! The ability to receive messages + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, url, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test_utils")] +use proptest_derive::Arbitrary; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The ability to receive messages +/// +/// This ability is used to receive messages from other actors. +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of message abilities is as follows: +/// +/// ```mermaid +/// flowchart TB +/// top("*") +/// +/// subgraph Message Abilities +/// any("msg/*") +/// +/// subgraph Invokable +/// rec("msg/receive") +/// end +/// end +/// +/// recrun{{"invoke"}} +/// +/// top --> any +/// any --> rec -.-> recrun +/// +/// style rec stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "test_utils", derive(Arbitrary))] +#[serde(deny_unknown_fields)] +pub struct Receive { + /// An *optional* URL (e.g. email, DID, socket) to receive messages from. + /// This assumes that the `subject` has the authority to issue such a capability. + pub from: Option, +} + +const COMMAND: &'static str = "/msg/receive"; + +impl Command for Receive { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedReceive { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for Receive { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut from = None; + + for (key, ipld) in arguments { + match key.as_str() { + "from" => { + from = Some(url::Newtype::try_from(ipld).map_err(|_| ())?); + } + _ => return Err(()), + } + } + + Ok(Receive { from }) + } +} + +impl From for arguments::Named { + fn from(receive: Receive) -> Self { + let mut args = arguments::Named::new(); + + if let Some(from) = receive.from { + args.insert("from".into(), from.into()); + } + + args + } +} + +impl From for Ipld { + fn from(receive: Receive) -> Self { + arguments::Named::::from(receive).into() + } +} + +impl TryFrom for Receive { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::Map(map) = ipld { + arguments::Named::(map).try_into().map_err(|_| ()) + } else { + Err(()) + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PromisedReceive { + pub from: Option>, +} + +impl promise::Resolvable for Receive { + type Promised = PromisedReceive; +} + +impl TryFrom> for PromisedReceive { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut from = None; + + for (key, prom) in arguments { + match key.as_str() { + "from" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => { + from = Some(promise::Any::Resolved( + url::Newtype::parse(s.as_str()).map_err(|_| ())?, + )); + } + Err(pending) => from = Some(pending.into()), + _ => return Err(()), + }, + _ => return Err(()), + } + } + + Ok(PromisedReceive { from }) + } +} + +impl From for arguments::Named { + fn from(promised: PromisedReceive) -> Self { + let mut args = arguments::Named::new(); + + if let Some(from) = promised.from { + let _ = from.to_promised_ipld().with_resolved(|ipld| { + args.insert("from".into(), ipld.into()); + }); + } + + args + } +} diff --git a/src/ability/msg/send.rs b/src/ability/msg/send.rs new file mode 100644 index 00000000..c3e47ab5 --- /dev/null +++ b/src/ability/msg/send.rs @@ -0,0 +1,260 @@ +//! The ability to send messages + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, url, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test_utils")] +use proptest_derive::Arbitrary; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `msg/send` ability. +/// +/// # Lifecycle +/// +/// The hierarchy of message abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// any("msg/*") +/// +/// subgraph Invokable +/// send("msg/send") +/// end +/// end +/// +/// sendpromise("msg::send::Promised") +/// sendrun("msg::send::Send") +/// +/// top --> any +/// any --> send -.->|invoke| sendpromise -.->|resolve| sendrun -.-> exe{{execute}} +/// +/// style sendrun stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "test_utils", derive(Arbitrary))] +#[serde(deny_unknown_fields)] +pub struct Send { + /// The recipient of the message + pub to: url::Newtype, + + /// The sender address of the message + /// + /// This *may* be a URL (such as an email address). + /// If provided, the `subject` must have the right to send from this address. + pub from: url::Newtype, + + /// The main body of the message + pub message: String, +} + +impl From for arguments::Named { + fn from(send: Send) -> Self { + arguments::Named::from_iter([ + ("to".to_string(), send.to.into()), + ("from".to_string(), send.from.into()), + ("message".to_string(), send.message.into()), + ]) + } +} + +impl From for Ipld { + fn from(send: Send) -> Self { + let args = arguments::Named::from(send); + Ipld::Map(args.0) + } +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The invoked variant of the `msg/send` ability +/// +/// This variant may be linked to other invoked abilities by [`Promise`][crate::invocation::Promise]s. +/// +/// # Lifecycle +/// +/// The hierarchy of message abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// any("msg/*") +/// +/// subgraph Invokable +/// send("msg/send") +/// end +/// end +/// +/// sendpromise("msg::send::Promised") +/// sendrun("msg::send::Send") +/// +/// top --> any +/// any --> send -.->|invoke| sendpromise -.->|resolve| sendrun -.-> exe{{execute}} +/// +/// style sendpromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedSend { + /// The recipient of the message + pub to: promise::Any, + + /// The sender address of the message + /// + /// This *may* be a URL (such as an email address). + /// If provided, the `subject` must have the right to send from this address. + pub from: promise::Any, + + /// The main body of the message + pub message: promise::Any, +} + +impl promise::Resolvable for Send { + type Promised = PromisedSend; +} + +impl TryFrom> for Send { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut to = None; + let mut from = None; + let mut message = None; + + for (key, value) in named.0 { + match key.as_str() { + "to" => match Ipld::try_from(value) { + Ok(Ipld::String(s)) => { + to = Some(url::Newtype::parse(s.as_str()).map_err(|_| ())?) + } + _ => return Err(()), + }, + "from" => match Ipld::try_from(value) { + Ok(Ipld::String(s)) => { + from = Some(url::Newtype::parse(s.as_str()).map_err(|_| ())?) + } + _ => return Err(()), + }, + "message" => match Ipld::try_from(value) { + Ok(Ipld::String(s)) => message = Some(s), + _ => return Err(()), + }, + _ => return Err(()), + } + } + + Ok(Send { + to: to.ok_or(())?, + from: from.ok_or(())?, + message: message.ok_or(())?, + }) + } +} + +impl TryFrom> for PromisedSend { + type Error = (); + + fn try_from(args: arguments::Named) -> Result { + let mut to = None; + let mut from = None; + let mut message = None; + + for (key, prom) in args.0 { + match key.as_str() { + "to" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => { + to = Some(promise::Any::Resolved( + url::Newtype::parse(s.as_str()).map_err(|_| ())?, + )); + } + Err(pending) => to = Some(pending.into()), + _ => return Err(()), + }, + "from" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => { + from = Some(promise::Any::Resolved( + url::Newtype::parse(s.as_str()).map_err(|_| ())?, + )); + } + Err(pending) => from = Some(pending.into()), + _ => return Err(()), + }, + "message" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => message = Some(promise::Any::Resolved(s)), + Err(pending) => to = Some(pending.into()), + _ => return Err(()), + }, + _ => return Err(()), + } + } + + Ok(PromisedSend { + to: to.ok_or(())?, + from: from.ok_or(())?, + message: message.ok_or(())?, + }) + } +} + +impl From for arguments::Named { + fn from(p: PromisedSend) -> Self { + arguments::Named::from_iter([ + ("to".into(), p.to.into()), + ("from".into(), p.from.into()), + ("message".into(), p.message.into()), + ]) + } +} + +const COMMAND: &'static str = "/msg/send"; + +impl Command for Send { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedSend { + const COMMAND: &'static str = COMMAND; +} + +impl From for PromisedSend { + fn from(r: Send) -> Self { + PromisedSend { + to: promise::Any::Resolved(r.to), + from: promise::Any::Resolved(r.from), + message: promise::Any::Resolved(r.message), + } + } +} + +impl TryFrom for Send { + type Error = PromisedSend; + + fn try_from(p: PromisedSend) -> Result { + match p { + PromisedSend { + to: promise::Any::Resolved(to), + from: promise::Any::Resolved(from), + message: promise::Any::Resolved(message), + } => Ok(Send { to, from, message }), + _ => Err(p), + } + } +} + +impl From for arguments::Named { + fn from(p: PromisedSend) -> Self { + arguments::Named::from_iter([ + ("to".into(), p.to.to_promised_ipld()), + ("from".into(), p.from.to_promised_ipld()), + ("message".into(), p.message.to_promised_ipld()), + ]) + } +} diff --git a/src/ability/parse.rs b/src/ability/parse.rs new file mode 100644 index 00000000..73f9745a --- /dev/null +++ b/src/ability/parse.rs @@ -0,0 +1,68 @@ +use super::command::Command; +use crate::{ability::arguments, ipld}; +use libipld_core::ipld::Ipld; +use std::fmt; +use thiserror::Error; + +pub trait ParseAbility: Sized { + type ArgsErr: fmt::Debug; + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result>; +} + +#[derive(Debug, Clone, Error)] +pub enum ParseAbilityError { + #[error("Unknown command: {0}")] + UnknownCommand(String), + + #[error(transparent)] + InvalidArgs(#[from] E), +} + +impl>> ParseAbility for T +where + >>::Error: fmt::Debug, +{ + type ArgsErr = >>::Error; + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result>>::Error>> { + if cmd != T::COMMAND { + return Err(ParseAbilityError::UnknownCommand(cmd.to_string())); + } + + Self::try_from(args).map_err(ParseAbilityError::InvalidArgs) + } +} + +pub trait ParsePromised: Sized { + type PromisedArgsError; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result>; +} + +impl>> ParsePromised for T +where + >>::Error: fmt::Debug, +{ + type PromisedArgsError = >>::Error; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + if cmd != T::COMMAND { + return Err(ParseAbilityError::UnknownCommand(cmd.to_string())); + } + + Self::try_from(args).map_err(ParseAbilityError::InvalidArgs) + } +} diff --git a/src/ability/pipe.rs b/src/ability/pipe.rs new file mode 100644 index 00000000..e31e3bc2 --- /dev/null +++ b/src/ability/pipe.rs @@ -0,0 +1,22 @@ +use crate::{crypto::varsig, delegation, did::Did, ipld}; +use libipld_core::{codec::Codec, ipld::Ipld}; + +pub struct Pipe, C: Codec> { + pub source: Cap, + pub sink: Cap, +} + +pub enum Cap, C: Codec> { + Proof(delegation::Proof), + Literal(Ipld), +} + +pub struct PromisedPipe, C: Codec> { + pub source: PromisedCap, + pub sink: PromisedCap, +} + +pub enum PromisedCap, C: Codec> { + Proof(delegation::Proof), + Promised(ipld::Promised), +} diff --git a/src/ability/preset.rs b/src/ability/preset.rs new file mode 100644 index 00000000..8adec291 --- /dev/null +++ b/src/ability/preset.rs @@ -0,0 +1,180 @@ +use super::{ + crud::{self, Crud, PromisedCrud}, + msg::{Msg, PromisedMsg}, + ucan::revoke::{PromisedRevoke, Revoke}, + wasm::run as wasm, +}; +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError, ParsePromised}, + }, + invocation::promise::Resolvable, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Preset { + Crud(Crud), + Msg(Msg), + Ucan(Revoke), + Wasm(wasm::Run), +} + +impl From for Preset +where + Crud: From, +{ + fn from(t: T) -> Self { + Preset::Crud(Crud::from(t)) + } +} + +impl ToCommand for Preset { + fn to_command(&self) -> String { + match self { + Preset::Crud(crud) => crud.to_command(), + Preset::Msg(msg) => msg.to_command(), + Preset::Ucan(ucan) => ucan.to_command(), + Preset::Wasm(wasm) => wasm.to_command(), + } + } +} + +impl From for arguments::Named { + fn from(preset: Preset) -> Self { + match preset { + Preset::Crud(crud) => crud.into(), + Preset::Msg(msg) => msg.into(), + Preset::Ucan(ucan) => ucan.into(), + Preset::Wasm(wasm) => wasm.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] //, Serialize, Deserialize)] +pub enum PromisedPreset { + Crud(PromisedCrud), + Msg(PromisedMsg), + Ucan(PromisedRevoke), + Wasm(wasm::PromisedRun), +} + +impl Resolvable for Preset { + type Promised = PromisedPreset; +} + +impl ToCommand for PromisedPreset { + fn to_command(&self) -> String { + match self { + PromisedPreset::Crud(promised) => promised.to_command(), + PromisedPreset::Msg(promised) => promised.to_command(), + PromisedPreset::Ucan(promised) => promised.to_command(), + PromisedPreset::Wasm(promised) => promised.to_command(), + } + } +} + +impl ParsePromised for PromisedPreset { + type PromisedArgsError = ParsePromisedError; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match PromisedCrud::try_parse_promised(cmd, args.clone()) { + Ok(promised) => return Ok(PromisedPreset::Crud(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => { + return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Crud( + err, + ))) + } + } + + match PromisedMsg::try_parse_promised(cmd, args.clone()) { + Ok(promised) => return Ok(PromisedPreset::Msg(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(_err) => return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Msg)), + } + + match wasm::PromisedRun::try_parse_promised(cmd, args.clone()) { + Ok(promised) => return Ok(PromisedPreset::Wasm(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(_err) => return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Wasm)), + } + + match PromisedRevoke::try_parse_promised(cmd, args) { + Ok(promised) => return Ok(PromisedPreset::Ucan(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(_err) => return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Ucan)), + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +#[derive(Debug, Clone, Error)] +pub enum ParsePromisedError { + #[error("Crud error: {0}")] + Crud(ParseAbilityError), + + #[error("Msg error")] + Msg, // FIXME + + #[error("Wasm error")] + Wasm, // FIXME + + #[error("Ucan error")] + Ucan, // FIXME +} + +impl ParseAbility for Preset { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match Msg::try_parse(cmd, args.clone()) { + Ok(msg) => return Ok(Preset::Msg(msg)), + Err(ParseAbilityError::UnknownCommand(_)) => (), // FIXME + Err(err) => return Err(err), + } + + match Crud::try_parse(cmd, args.clone()) { + Ok(crud) => return Ok(Preset::Crud(crud)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => return Err(err), + } + + match wasm::Run::try_parse(cmd, args.clone()) { + Ok(wasm) => return Ok(Preset::Wasm(wasm)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => return Err(err), + } + + match Revoke::try_parse(cmd, args) { + Ok(ucan) => return Ok(Preset::Ucan(ucan)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => return Err(err), + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +impl From for arguments::Named { + fn from(promised: PromisedPreset) -> Self { + match promised { + PromisedPreset::Crud(promised) => promised.into(), + PromisedPreset::Msg(promised) => promised.into(), + PromisedPreset::Ucan(promised) => promised.into(), + PromisedPreset::Wasm(promised) => promised.into(), + } + } +} diff --git a/src/ability/ucan.rs b/src/ability/ucan.rs new file mode 100644 index 00000000..220034b6 --- /dev/null +++ b/src/ability/ucan.rs @@ -0,0 +1,5 @@ +//! Abilities for and about UCANs themselves + +pub mod assert; +pub mod batch; +pub mod revoke; diff --git a/src/ability/ucan/assert.rs b/src/ability/ucan/assert.rs new file mode 100644 index 00000000..410d0148 --- /dev/null +++ b/src/ability/ucan/assert.rs @@ -0,0 +1,30 @@ +use crate::ability::command::Command; +use crate::task::Task; +use libipld_core::cid::Cid; + +// Things that you can assert include content and receipts + +#[derive(Debug, PartialEq)] +pub struct Ran { + ran: Cid, + out: Box>, + fx: Vec, // FIXME may be more than "just" a task +} + +impl Command for Ran { + const COMMAND: &'static str = "/ucan/assert/ran"; + // const COMMAND: &'static str = "/ucan/ran";???? +} + +/////////////// +/////////////// +/////////////// + +#[derive(Debug, PartialEq)] +pub struct Claim { + claim: T, +} // Where Ipld: From + +impl Command for Claim { + const COMMAND: &'static str = "/ucan/assert/claim"; +} diff --git a/src/ability/ucan/batch.rs b/src/ability/ucan/batch.rs new file mode 100644 index 00000000..983500df --- /dev/null +++ b/src/ability/ucan/batch.rs @@ -0,0 +1,18 @@ +// use crate::{crypto::varsig, delegation::Delegation, did::Did}; +// use libipld_core::{cid::Cid, codec::Codec, ipld::Ipld}; +// use std::collections::BTreeMap; +// +// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +// pub struct Batch, Enc: Codec> { +// pub batch: Vec>, // FIXME not quite right; would be nice to include meta etc +// } +// +// pub struct Step, Enc: Codec> { +// pub subject: DID, +// pub audience: Option, +// pub ability: A, // FIXME promise version instead? Promised version shoudl be able to promise any field +// pub cause: Option, +// pub metadata: BTreeMap, +// +// pub cap: Vec>, +// } diff --git a/src/ability/ucan/revoke.rs b/src/ability/ucan/revoke.rs new file mode 100644 index 00000000..37c4f051 --- /dev/null +++ b/src/ability/ucan/revoke.rs @@ -0,0 +1,111 @@ +//! This is an ability for revoking [`Delegation`][crate::delegation::Delegation]s by their [`Cid`]. +//! +//! For more, see the [UCAN Revocation spec](https://github.com/ucan-wg/revocation). + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +/// The fully resolved variant: ready to execute. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Revoke { + /// The UCAN to revoke + pub ucan: Cid, + // FIXME pub witness +} + +impl From for arguments::Named { + fn from(revoke: Revoke) -> Self { + arguments::Named::from_iter([("ucan".to_string(), Ipld::Link(revoke.ucan).into())]) + } +} + +const COMMAND: &'static str = "/ucan/revoke"; + +impl Command for Revoke { + const COMMAND: &'static str = COMMAND; +} +impl Command for PromisedRevoke { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for Revoke { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let ipld: Ipld = arguments.get("ucan").ok_or(())?.clone(); + let nt: ipld::cid::Newtype = ipld.try_into().map_err(|_| ())?; + + Ok(Revoke { ucan: nt.cid }) + } +} + +impl promise::Resolvable for Revoke { + type Promised = PromisedRevoke; +} + +impl From for arguments::Named { + fn from(promised: PromisedRevoke) -> Self { + arguments::Named::from_iter([("ucan".into(), Ipld::from(promised.ucan).into())]) + } +} + +/// A variant where arguments may be [`Promise`][crate::invocation::promise]s. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PromisedRevoke { + pub ucan: promise::Any, +} + +impl TryFrom> for PromisedRevoke { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut ucan = None; + + for (k, prom) in arguments { + match k.as_str() { + "ucan" => match Ipld::try_from(prom) { + Ok(Ipld::Link(cid)) => { + ucan = Some(promise::Any::Resolved(cid)); + } + Err(pending) => ucan = Some(pending.into()), + _ => return Err(()), + }, + _ => (), + } + } + + Ok(PromisedRevoke { + ucan: ucan.ok_or(())?, + }) + } +} + +impl From for PromisedRevoke { + fn from(r: Revoke) -> PromisedRevoke { + PromisedRevoke { + ucan: promise::Any::Resolved(r.ucan), + } + } +} + +impl From for arguments::Named { + fn from(p: PromisedRevoke) -> arguments::Named { + arguments::Named::from_iter([("ucan".into(), p.ucan.into())]) + } +} + +impl TryFrom for Revoke { + type Error = (); + + fn try_from(p: PromisedRevoke) -> Result { + Ok(Revoke { + ucan: p.ucan.try_resolve().map_err(|_| ())?, + }) + } +} diff --git a/src/ability/wasm.rs b/src/ability/wasm.rs new file mode 100644 index 00000000..3aa1e67b --- /dev/null +++ b/src/ability/wasm.rs @@ -0,0 +1,4 @@ +//! [WebAssembly](https://webassembly.org/) abilities + +pub mod module; +pub mod run; diff --git a/src/ability/wasm/module.rs b/src/ability/wasm/module.rs new file mode 100644 index 00000000..c17726d7 --- /dev/null +++ b/src/ability/wasm/module.rs @@ -0,0 +1,89 @@ +//! Wasm module representations + +use crate::ipld; +use base64::{display::Base64Display, engine::general_purpose::STANDARD, Engine as _}; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, link::Link, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; + +/// Ways to represent a Wasm module in a `wasm/run` payload. +#[derive(Debug, Clone, PartialEq)] +pub enum Module { + /// The raw bytes of the Wasm module + /// + /// Encodes as a `data:` URL + Inline(Vec), + + /// A [`Cid`] link to the Wasm module + Remote(Link>), +} + +impl From for Ipld { + fn from(module: Module) -> Self { + match module { + Module::Inline(bytes) => Ipld::Bytes(bytes), + Module::Remote(cid) => Ipld::Link(*cid), + } + } +} + +impl TryFrom for Module { + type Error = SerdeError; + + fn try_from(nt: ipld::Newtype) -> Result { + ipld_serde::from_ipld(nt.0) + } +} + +impl TryFrom for Module { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl Serialize for Module { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + match self { + Module::Remote(link) => link.cid().serialize(serializer), + Module::Inline(bytes) => format!( + "data:application/wasm;base64,{}", + Base64Display::new(bytes.as_ref(), &STANDARD) + ) + .serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for Module { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s.starts_with("data:") { + let data = s + .split(',') + .nth(1) + .ok_or_else(|| serde::de::Error::custom("missing base64 data"))?; + + let bytes = STANDARD + .decode(data) + .map_err(|err| serde::de::Error::custom(err))?; + + Ok(Module::Inline(bytes)) + } else { + let cid = Cid::try_from(s).map_err(serde::de::Error::custom)?; + Ok(Module::Remote(Link::new(cid))) + } + } +} + +impl From for ipld::Promised { + fn from(module: Module) -> Self { + module.into() + } +} diff --git a/src/ability/wasm/run.rs b/src/ability/wasm/run.rs new file mode 100644 index 00000000..77f95abc --- /dev/null +++ b/src/ability/wasm/run.rs @@ -0,0 +1,146 @@ +//! Ability to run a Wasm module + +use super::module::Module; +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; + +const COMMAND: &'static str = "/wasm/run"; + +impl Command for Run { + const COMMAND: &'static str = COMMAND; +} + +// FIXME autogenerate for resolvable? +impl Command for PromisedRun { + const COMMAND: &'static str = COMMAND; +} + +/// The ability to run a Wasm module on the subject's machine +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Run { + /// The Wasm module to run + pub module: Module, + + /// The function from the module to run + pub function: String, + + /// Arguments to pass to the function + pub args: Vec, +} + +impl From for arguments::Named { + fn from(run: Run) -> Self { + arguments::Named::from_iter([ + ("mod".into(), Ipld::from(run.module)), + ("fun".into(), run.function.into()), + ("args".into(), run.args.into()), + ]) + } +} + +impl TryFrom> for Run { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut module = None; + let mut function = None; + let mut args = None; + + for (key, ipld) in named { + match key.as_str() { + "mod" => { + module = Some(ipld.try_into().map_err(|_| ())?); + } + "fun" => { + if let Ipld::String(s) = ipld { + function = Some(s); + } else { + return Err(()); + } + } + "args" => { + if let Ipld::List(list) = ipld { + args = Some(list); + } else { + return Err(()); + } + } + _ => return Err(()), + } + } + + Ok(Run { + module: module.ok_or(())?, + function: function.ok_or(())?, + args: args.ok_or(())?, + }) + } +} + +impl promise::Resolvable for Run { + type Promised = PromisedRun; +} + +/// A variant meant for linking together invocations with promises +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PromisedRun { + pub module: promise::Any, + pub function: promise::Any, + pub args: promise::Any>, +} + +impl TryFrom> for PromisedRun { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut module = None; + let mut function = None; + let mut args = None; + + for (key, prom) in named { + match key.as_str() { + "module" => module = Some(prom.to_promise_any().map_err(|_| ())?), + "function" => function = Some(prom.to_promise_any_string()?), + "args" => { + if let ipld::Promised::List(list) = prom.into() { + args = Some(promise::Any::Resolved(list)); + } else { + return Err(()); + } + } + _ => return Err(()), + } + } + + Ok(PromisedRun { + module: module.ok_or(())?, + function: function.ok_or(())?, + args: args.ok_or(())?, + }) + } +} + +impl From for PromisedRun { + fn from(run: Run) -> Self { + PromisedRun { + module: promise::Any::Resolved(run.module), + function: promise::Any::Resolved(run.function), + args: promise::Any::Resolved(run.args.iter().map(|ipld| ipld.clone().into()).collect()), + } + } +} + +impl From for arguments::Named { + fn from(promised: PromisedRun) -> Self { + arguments::Named::from_iter([ + ("module".into(), promised.module.to_promised_ipld()), + ("function".into(), promised.function.to_promised_ipld()), + ("args".into(), promised.args.to_promised_ipld()), + ]) + } +} diff --git a/src/builder.rs b/src/builder.rs deleted file mode 100644 index ef062361..00000000 --- a/src/builder.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! A builder for creating UCANs - -use async_signature::AsyncSigner; -use cid::multihash; -use serde::{de::DeserializeOwned, Serialize}; -use signature::Signer; - -use crate::{ - capability::{Capabilities, Capability, CapabilityParser, DefaultCapabilityParser}, - crypto::{JWSSignature, SignerDid}, - error::Error, - time, - ucan::{Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, - CidString, DefaultFact, -}; - -/// A builder for creating UCANs -#[derive(Debug, Clone)] -pub struct UcanBuilder { - version: Option, - audience: Option, - nonce: Option, - capabilities: Capabilities, - lifetime: Option, - expiration: Option, - not_before: Option, - facts: Option, - proofs: Option>, -} - -impl Default for UcanBuilder { - fn default() -> Self { - Self { - version: Default::default(), - audience: Default::default(), - nonce: Default::default(), - capabilities: Default::default(), - lifetime: Default::default(), - expiration: Default::default(), - not_before: Default::default(), - facts: Default::default(), - proofs: Default::default(), - } - } -} - -impl UcanBuilder -where - F: Clone + Serialize, - C: CapabilityParser, -{ - /// Set the UCAN version - pub fn version(mut self, version: &str) -> Self { - self.version = Some(version.to_string()); - self - } - - /// Set the audience of the UCAN - pub fn for_audience(mut self, audience: impl AsRef) -> Self { - self.audience = Some(audience.as_ref().to_string()); - self - } - - /// Set the nonce of the UCAN - pub fn with_nonce(mut self, nonce: impl AsRef) -> Self { - self.nonce = Some(nonce.as_ref().to_string()); - self - } - - /// Set the lifetime of the UCAN - pub fn with_lifetime(mut self, seconds: u64) -> Self { - self.lifetime = Some(seconds); - self - } - - /// Set the expiration of the UCAN - pub fn with_expiration(mut self, timestamp: u64) -> Self { - self.expiration = Some(timestamp); - self - } - - /// Set the not before of the UCAN - pub fn not_before(mut self, timestamp: u64) -> Self { - self.not_before = Some(timestamp); - self - } - - /// Set the fact of the UCAN - pub fn with_fact(mut self, fact: F) -> Self { - self.facts = Some(fact); - self - } - - /// Add a witness to the proofs of the UCAN - pub fn witnessed_by( - mut self, - authority: &Ucan, - hasher: Option, - ) -> Self - where - F2: Clone + DeserializeOwned, - C2: CapabilityParser, - { - match authority.to_cid(hasher) { - Ok(cid) => { - self.proofs - .get_or_insert(Default::default()) - .push(CidString(cid)); - } - Err(e) => panic!("Failed to add authority: {}", e), - } - - self - } - - /// Claim a capability for the UCAN - pub fn claiming_capability(mut self, capability: Capability) -> Self { - self.capabilities.push(capability); - self - } - - /// Claim multiple capabilities for the UCAN - pub fn claiming_capabilities(mut self, capabilities: &[Capability]) -> Self { - self.capabilities.extend_from_slice(capabilities); - self - } - - /// Sign the UCAN with the given signer - pub fn sign(self, signer: &S) -> Result, Error> - where - S: Signer + SignerDid, - K: JWSSignature, - { - let version = self.version.unwrap_or_else(|| UCAN_VERSION.to_string()); - - let issuer = signer.did().map_err(|e| Error::SigningError { - msg: format!("failed to construct DID, {}", e), - })?; - - let Some(audience) = self.audience else { - return Err(Error::SigningError { - msg: "an audience is required".to_string(), - }); - }; - - let header = jose_b64::serde::Json::new(UcanHeader { - alg: K::ALGORITHM.to_string(), - typ: "JWT".to_string(), - }) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let expiration = match (self.expiration, self.lifetime) { - (None, None) => None, - (None, Some(lifetime)) => Some(time::now() + lifetime), - (Some(expiration), None) => Some(expiration), - (Some(_), Some(_)) => { - return Err(Error::SigningError { - msg: "only one of expiration or lifetime may be set".to_string(), - }) - } - }; - - let payload = jose_b64::serde::Json::new(UcanPayload { - ucv: version, - iss: issuer, - aud: audience, - exp: expiration, - nbf: self.not_before, - nnc: self.nonce, - cap: self.capabilities, - fct: self.facts, - prf: self.proofs, - }) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let header_b64 = serde_json::to_value(&header) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let payload_b64 = serde_json::to_value(&payload) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let signed_data = format!( - "{}.{}", - header_b64.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of header".to_string(), - })?, - payload_b64.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of payload".to_string(), - })?, - ); - - let signature = signer.sign(signed_data.as_bytes()).to_vec().into(); - - Ok(Ucan:: { - header, - payload, - signature, - }) - } - - /// Sign the UCAN with the given async signer - pub async fn sign_async(self, signer: &S) -> Result, Error> - where - S: AsyncSigner + SignerDid, - K: JWSSignature + 'static, - { - let version = self.version.unwrap_or_else(|| UCAN_VERSION.to_string()); - - let issuer = signer.did().map_err(|e| Error::SigningError { - msg: format!("failed to construct DID, {}", e), - })?; - - let Some(audience) = self.audience else { - return Err(Error::SigningError { - msg: "an audience is required".to_string(), - }); - }; - - let header = jose_b64::serde::Json::new(UcanHeader { - alg: K::ALGORITHM.to_string(), - typ: "JWT".to_string(), - }) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let expiration = match (self.expiration, self.lifetime) { - (None, None) => None, - (None, Some(lifetime)) => Some(time::now() + lifetime), - (Some(expiration), None) => Some(expiration), - (Some(_), Some(_)) => { - return Err(Error::SigningError { - msg: "only one of expiration or lifetime may be set".to_string(), - }) - } - }; - - let payload = jose_b64::serde::Json::new(UcanPayload { - ucv: version, - iss: issuer, - aud: audience, - exp: expiration, - nbf: self.not_before, - nnc: self.nonce, - cap: self.capabilities, - fct: self.facts, - prf: self.proofs, - }) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let header_b64 = serde_json::to_value(&header) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let payload_b64 = serde_json::to_value(&payload) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let signed_data = format!( - "{}.{}", - header_b64.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of header".to_string(), - })?, - payload_b64.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of payload".to_string(), - })?, - ); - - let signature = signer - .sign_async(signed_data.as_bytes()) - .await - .map_err(|e| Error::SigningError { msg: e.to_string() })? - .to_vec() - .into(); - - Ok(Ucan:: { - header, - payload, - signature, - }) - } -} - -#[cfg(test)] -mod tests { - use signature::rand_core; - use std::str::FromStr; - - use crate::did_verifier::DidVerifierMap; - - use super::*; - - #[test] - fn test_round_trip_validate() -> Result<(), anyhow::Error> { - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .sign(&iss_key)?; - - let token = ucan.encode()?; - let decoded: Ucan = Ucan::from_str(&token)?; - - assert!(decoded.validate(0, &did_verifier_map).is_ok()); - - Ok(()) - } -} diff --git a/src/capability.rs b/src/capability.rs deleted file mode 100644 index de4377ad..00000000 --- a/src/capability.rs +++ /dev/null @@ -1,405 +0,0 @@ -//! Capabilities, and traits for deserializing them - -use std::collections::BTreeMap; - -use serde::{ - de::{DeserializeSeed, IgnoredAny, Visitor}, - Deserialize, Deserializer, Serialize, Serializer, -}; -use url::Url; - -use crate::semantics::{ - ability::{Ability, TopAbility}, - caveat::{Caveat, EmptyCaveat}, - resource::Resource, -}; - -/// The default capability handler, when deserializing a UCAN -pub type DefaultCapabilityParser = PluginCapability; - -/// A capability -#[derive(Debug, Clone)] -pub struct Capability { - /// The resource - resource: Box, - /// The ability - ability: Box, - /// The caveat - caveat: Box, -} - -impl Capability { - /// Creates a new capability - pub fn new(resource: R, ability: A, caveat: C) -> Self - where - R: Resource, - A: Ability, - C: Caveat, - { - Self { - resource: Box::new(resource), - ability: Box::new(ability), - caveat: Box::new(caveat), - } - } - - /// Creates a new capability by cloning the resource, ability, and caveat as trait objects - pub fn clone_box(resource: &dyn Resource, ability: &dyn Ability, caveat: &dyn Caveat) -> Self { - Self { - resource: dyn_clone::clone_box(resource), - ability: dyn_clone::clone_box(ability), - caveat: dyn_clone::clone_box(caveat), - } - } - - /// Returns the resource - pub fn resource(&self) -> &dyn Resource { - &*self.resource - } - - /// Returns the ability - pub fn ability(&self) -> &dyn Ability { - &*self.ability - } - - /// Returns the caveat - pub fn caveat(&self) -> &dyn Caveat { - &*self.caveat - } - - /// Returns true if self is subsumed by other - pub fn is_subsumed_by(&self, other: &Capability) -> bool { - if !self.resource.is_valid_attenuation(&*other.resource) { - return false; - } - - if !(other.ability.is::() || self.ability.is_valid_attenuation(&*other.ability)) - { - return false; - } - - other.caveat.is::() || self.caveat.is_valid_attenuation(&*other.caveat) - } -} - -/// A collection of capabilities -#[derive(Clone, Debug)] -pub struct Capabilities { - inner: Vec, - _marker: std::marker::PhantomData C>, -} - -impl Default for Capabilities { - fn default() -> Self { - Self { - inner: Default::default(), - _marker: Default::default(), - } - } -} - -impl Capabilities { - /// Creates a new collection of capabilities from a vector - pub fn new(inner: Vec) -> Self { - Self { - inner, - _marker: Default::default(), - } - } - - /// Pushes a capability to the collection - pub fn push(&mut self, capability: Capability) { - self.inner.push(capability); - } - - /// Extends the collection with the capabilities from a slice of capabilities - pub fn extend_from_slice(&mut self, capabilities: &[Capability]) { - self.inner.extend_from_slice(capabilities); - } - - /// Returns an iterator over the capabilities - pub fn iter(&self) -> impl Iterator { - self.inner.iter() - } -} - -/// Handles deserializing capabilities -pub trait CapabilityParser: Clone { - /// Tries to deserialize a capability from a resource_uri, ability, and a deserilizer for the caveat - fn try_handle( - resource_uri: &Url, - ability: &str, - caveat_deserializer: &mut dyn erased_serde::Deserializer<'_>, - ) -> Result, anyhow::Error> - where - Self: Sized; -} - -/// A capability handler that deserializes using the registered plugins -#[derive(Clone, Debug)] -pub struct PluginCapability {} - -impl CapabilityParser for PluginCapability { - fn try_handle( - resource_uri: &Url, - ability: &str, - caveat_deserializer: &mut dyn erased_serde::Deserializer<'_>, - ) -> Result, anyhow::Error> { - let resource_scheme = resource_uri.scheme(); - - for plugin in crate::plugins::plugins().filter(|p| p.scheme() == resource_scheme) { - let Some(resource) = plugin.try_handle_resource(resource_uri)? else { - continue; - }; - - let Some(ability) = plugin.try_handle_ability(&resource, ability)? else { - continue; - }; - - let Some(caveat) = plugin.try_handle_caveat(&resource, &ability, caveat_deserializer)? - else { - continue; - }; - - return Ok(Some(Capability { - resource, - ability, - caveat, - })); - } - - Ok(None) - } -} - -impl Serialize for Capabilities -where - Cap: CapabilityParser, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut capabilities: BTreeMap>> = - Default::default(); - - for capability in self.iter() { - let resource_uri = capability.resource().to_string(); - let ability_key = capability.ability().to_string(); - let caveat = capability.caveat(); - - capabilities - .entry(resource_uri) - .or_default() - .entry(ability_key) - .or_default() - .push(caveat); - } - - capabilities.serialize(serializer) - } -} - -impl<'de, C> Deserialize<'de> for Capabilities -where - C: CapabilityParser, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct CapabilitiesVisitor { - _marker: std::marker::PhantomData C>, - } - - impl<'de, C> Visitor<'de> for CapabilitiesVisitor - where - C: CapabilityParser, - { - type Value = Vec; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "a map of capabilities") - } - - fn visit_map
(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let mut capabilities = Vec::new(); - - while let Some(resource_key) = map.next_key::()? { - let resource_uri = - Url::parse(&resource_key).map_err(serde::de::Error::custom)?; - - map.next_value_seed(Abilities:: { - resource_uri, - capabilities: &mut capabilities, - _marker: Default::default(), - })?; - } - - Ok(capabilities) - } - } - - let caps = deserializer.deserialize_map(CapabilitiesVisitor:: { - _marker: Default::default(), - })?; - - Ok(Self::new(caps)) - } -} - -struct Abilities<'a, C> { - resource_uri: Url, - capabilities: &'a mut Vec, - _marker: std::marker::PhantomData C>, -} - -impl<'de, 'a, C> DeserializeSeed<'de> for Abilities<'a, C> -where - C: CapabilityParser, -{ - type Value = (); - - fn deserialize(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct AbilitiesVisitor<'a, C> { - resource_uri: Url, - capabilities: &'a mut Vec, - _marker: std::marker::PhantomData C>, - } - - impl<'de, 'a, C> Visitor<'de> for AbilitiesVisitor<'a, C> - where - C: CapabilityParser, - { - type Value = (); - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "a map of abilities for {}", self.resource_uri) - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - while let Some(ability_key) = map.next_key::()? { - map.next_value_seed(Caveats:: { - resource_uri: self.resource_uri.clone(), - ability_key: ability_key.clone(), - capabilities: self.capabilities, - _marker: Default::default(), - })?; - } - - Ok(()) - } - } - - deserializer.deserialize_map(AbilitiesVisitor:: { - resource_uri: self.resource_uri, - capabilities: self.capabilities, - _marker: Default::default(), - }) - } -} - -struct Caveats<'a, C> { - resource_uri: Url, - ability_key: String, - capabilities: &'a mut Vec, - _marker: std::marker::PhantomData C>, -} - -impl<'de, 'a, C> DeserializeSeed<'de> for Caveats<'a, C> -where - C: CapabilityParser, -{ - type Value = (); - - fn deserialize(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct CaveatsVisitor<'a, C> { - resource_uri: Url, - ability_key: String, - capabilities: &'a mut Vec, - _marker: std::marker::PhantomData C>, - } - - impl<'de, 'a, C> Visitor<'de> for CaveatsVisitor<'a, C> - where - C: CapabilityParser, - { - type Value = (); - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - formatter, - "a map of caveats for {} : {}", - self.resource_uri, self.ability_key - ) - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - while let Some(element) = seq.next_element_seed(CaveatSeed:: { - resource_uri: self.resource_uri.clone(), - ability_key: self.ability_key.clone(), - _marker: Default::default(), - })? { - if let Some(capability) = element { - self.capabilities.push(capability); - } - } - - Ok(()) - } - } - - deserializer.deserialize_seq(CaveatsVisitor:: { - resource_uri: self.resource_uri, - ability_key: self.ability_key, - capabilities: self.capabilities, - _marker: Default::default(), - }) - } -} - -struct CaveatSeed { - resource_uri: Url, - ability_key: String, - _marker: std::marker::PhantomData Cap>, -} - -impl<'de, Cap> DeserializeSeed<'de> for CaveatSeed -where - Cap: CapabilityParser, -{ - type Value = Option; - - fn deserialize(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let mut deserializer = >::erase(deserializer); - - let Some(capability) = - Cap::try_handle(&self.resource_uri, &self.ability_key, &mut deserializer) - .map_err(serde::de::Error::custom)? - else { - erased_serde::deserialize::(&mut deserializer) - .map_err(serde::de::Error::custom)?; - return Ok(None); - }; - - Ok(Some(capability)) - } -} diff --git a/src/capsule.rs b/src/capsule.rs new file mode 100644 index 00000000..af2f1b2e --- /dev/null +++ b/src/capsule.rs @@ -0,0 +1,55 @@ +//! Capsule type utilities. +//! +//! Capsule types are a pattern where you associate a string to type, +//! and use the tag as a key and the payload as a value in a map. +//! This helps disambiguate types when serializing and deserializing. +//! +//! Unlike a `type` field, the fact that it's on the outside of the payload +//! is often helpful in improving serializaion and deserialization performance. +//! It also avoids needing fields on nested structures where the inner types are known. +//! +//! Some simple examples include: +//! +//! ```javascript +//! {"u32": 42} +//! {"i64": 99} +//! {"coord": {"x": 1, "y": 2}} +//! { +//! "boundary": [ +//! {"x": 1, "y": 2}, // ─┐ +//! {"x": 3, "y": 4}, // ├─ Untagged coords inside "boundary" capsule +//! {"x": 5, "y": 6}, // │ +//! {"x": 7, "y": 8} // ─┘ +//! ] +//! } +//! ``` +//! +//! UCAN uses these in payload wrappers, such as [`Delegation`][crate::delegation::Delegation]. + +/// The primary capsule trait +/// +/// # Examples +/// +/// ```rust +/// # use ucan::capsule::Capsule; +/// # use std::collections::BTreeMap; +/// # +/// # #[derive(Debug, PartialEq)] +/// struct Coord { +/// x: i32, +/// y: i32 +/// } +/// +/// impl Capsule for Coord { +/// const TAG: &'static str = "coordinate"; +/// } +/// +/// let coord = Coord { x: 1, y: 2 }; +/// let capsuled = BTreeMap::from_iter([(Coord::TAG.to_string(), coord)]); +/// +/// assert_eq!(capsuled.get("coordinate"), Some(&Coord { x: 1, y: 2 })); +/// ```` +pub trait Capsule { + /// The tag to use when constructing or matching on the capsule + const TAG: &'static str; +} diff --git a/src/crypto.rs b/src/crypto.rs index 75009dbb..4bc82072 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,36 +1,22 @@ -//! Cryptography utilities +//! Cryptographic signature utilities -use signature::SignatureEncoding; +mod domain_separator; +mod nonce; + +pub mod signature; +pub mod varsig; + +pub use domain_separator::DomainSeparator; +pub use nonce::*; #[cfg(feature = "bls")] -pub mod bls; -#[cfg(feature = "eddsa")] -pub mod eddsa; -#[cfg(feature = "es256")] -pub mod es256; -#[cfg(feature = "es256k")] -pub mod es256k; -#[cfg(feature = "es384")] -pub mod es384; +pub mod bls12381; + #[cfg(feature = "es512")] pub mod es512; -#[cfg(feature = "ps256")] -pub mod ps256; + #[cfg(feature = "rs256")] pub mod rs256; -/// A trait for mapping a SignatureEncoding to its algorithm name under JWS -pub trait JWSSignature: SignatureEncoding { - /// The algorithm name under JWS - // I'd originally referenced JWA types directly here, but supporting - // unspecified algorithms, like BLS, means leaving things more open-ended. - const ALGORITHM: &'static str; -} - -/// A trait for mapping a Signer to its DID. In most cases, this will -/// be a DID with method did-key, however other methods can be supported -/// by implementing this trait for a custom signer. -pub trait SignerDid { - /// The DID of the signer - fn did(&self) -> Result; -} +#[cfg(feature = "rs512")] +pub mod rs512; diff --git a/src/crypto/bls.rs b/src/crypto/bls.rs deleted file mode 100644 index f7362b62..00000000 --- a/src/crypto/bls.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! BLS12-381 signature support - -use anyhow::anyhow; -use blst::BLST_ERROR; -use signature::SignatureEncoding; - -use super::JWSSignature; - -/// A BLS12-381 G1 signature -#[derive(Debug, Clone)] -pub struct Bls12381G1Sha256SswuRoNulSignature(pub blst::min_sig::Signature); - -impl<'a> TryFrom<&'a [u8]> for Bls12381G1Sha256SswuRoNulSignature { - type Error = BLST_ERROR; - - fn try_from(bytes: &'a [u8]) -> Result { - Ok(Self(blst::min_sig::Signature::uncompress(bytes)?)) - } -} - -impl From for [u8; 48] { - fn from(sig: Bls12381G1Sha256SswuRoNulSignature) -> Self { - sig.0.compress() - } -} - -impl SignatureEncoding for Bls12381G1Sha256SswuRoNulSignature { - type Repr = [u8; 48]; -} - -impl JWSSignature for Bls12381G1Sha256SswuRoNulSignature { - const ALGORITHM: &'static str = "Bls12381G1"; -} - -/// A BLS12-381 G2 signature -#[derive(Debug, Clone)] -pub struct Bls12381G2Sha256SswuRoNulSignature(pub blst::min_pk::Signature); - -impl<'a> TryFrom<&'a [u8]> for Bls12381G2Sha256SswuRoNulSignature { - type Error = BLST_ERROR; - - fn try_from(bytes: &'a [u8]) -> Result { - Ok(Self(blst::min_pk::Signature::uncompress(bytes)?)) - } -} - -impl From for [u8; 96] { - fn from(sig: Bls12381G2Sha256SswuRoNulSignature) -> Self { - sig.0.compress() - } -} - -impl SignatureEncoding for Bls12381G2Sha256SswuRoNulSignature { - type Repr = [u8; 96]; -} - -impl JWSSignature for Bls12381G2Sha256SswuRoNulSignature { - const ALGORITHM: &'static str = "Bls12381G2"; -} - -/// A verifier for BLS12-381 G1 signatures -#[cfg(feature = "bls-verifier")] -pub fn bls_12_381_g1_sha256_sswu_ro_nul_verifier( - key: &[u8], - payload: &[u8], - signature: &[u8], -) -> Result<(), anyhow::Error> { - let dst = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; - let aug = &[]; - - let key = - blst::min_sig::PublicKey::uncompress(key).map_err(|_| anyhow!("invalid BLS12-381 key"))?; - - let signature = blst::min_sig::Signature::uncompress(signature) - .map_err(|_| anyhow!("invalid BLS12-381 signature"))?; - - match signature.verify(true, payload, dst, aug, &key, true) { - BLST_ERROR::BLST_SUCCESS => Ok(()), - BLST_ERROR::BLST_BAD_ENCODING => Err(anyhow!("bad encoding")), - BLST_ERROR::BLST_POINT_NOT_ON_CURVE => Err(anyhow!("point not on curve")), - BLST_ERROR::BLST_POINT_NOT_IN_GROUP => Err(anyhow!("bad point not in group")), - BLST_ERROR::BLST_AGGR_TYPE_MISMATCH => Err(anyhow!("aggregate type mismatch")), - BLST_ERROR::BLST_VERIFY_FAIL => Err(anyhow!("signature mismatch")), - BLST_ERROR::BLST_PK_IS_INFINITY => Err(anyhow!("public key is infinity")), - BLST_ERROR::BLST_BAD_SCALAR => Err(anyhow!("bad scalar")), - } -} - -/// A verifier for BLS12-381 G2 signatures -#[cfg(feature = "bls-verifier")] -pub fn bls_12_381_g2_sha256_sswu_ro_nul_verifier( - key: &[u8], - payload: &[u8], - signature: &[u8], -) -> Result<(), anyhow::Error> { - let dst = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; - let aug = &[]; - - let key = - blst::min_pk::PublicKey::uncompress(key).map_err(|_| anyhow!("invalid BLS12-381 key"))?; - - let signature = blst::min_pk::Signature::uncompress(signature) - .map_err(|_| anyhow!("invalid BLS12-381 signature"))?; - - match signature.verify(true, payload, dst, aug, &key, true) { - BLST_ERROR::BLST_SUCCESS => Ok(()), - BLST_ERROR::BLST_BAD_ENCODING => Err(anyhow!("bad encoding")), - BLST_ERROR::BLST_POINT_NOT_ON_CURVE => Err(anyhow!("point not on curve")), - BLST_ERROR::BLST_POINT_NOT_IN_GROUP => Err(anyhow!("bad point not in group")), - BLST_ERROR::BLST_AGGR_TYPE_MISMATCH => Err(anyhow!("aggregate type mismatch")), - BLST_ERROR::BLST_VERIFY_FAIL => Err(anyhow!("signature mismatch")), - BLST_ERROR::BLST_PK_IS_INFINITY => Err(anyhow!("public key is infinity")), - BLST_ERROR::BLST_BAD_SCALAR => Err(anyhow!("bad scalar")), - } -} diff --git a/src/crypto/bls12381.rs b/src/crypto/bls12381.rs new file mode 100644 index 00000000..2ad56f6f --- /dev/null +++ b/src/crypto/bls12381.rs @@ -0,0 +1,5 @@ +//! BLS12-381 signature support + +pub mod error; +pub mod min_pk; +pub mod min_sig; diff --git a/src/crypto/bls12381/error.rs b/src/crypto/bls12381/error.rs new file mode 100644 index 00000000..8475a875 --- /dev/null +++ b/src/crypto/bls12381/error.rs @@ -0,0 +1,52 @@ +use blst::BLST_ERROR; +use enum_as_inner::EnumAsInner; +use thiserror::Error; + +/// Errors that can occur during BLS verification. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error, EnumAsInner)] +pub enum VerificationError { + /// Signature mismatch. + #[error("signature mismatch")] + VerifyMsgFail, + + /// Bad encoding. + #[error("bad encoding")] + BadEncoding, + + /// Point not on curve. + #[error("point not on curve")] + PointNotOnCurve, + + /// Point not in group. + #[error("bad point not in group")] + PointNotInGroup, + + /// Aggregate type mismatch. + #[error("aggregate type mismatch")] + AggrTypeMismatch, + + /// Public key is infinity. + #[error("public key is infinity")] + PkIsInfinity, + + /// Bad scalar. + #[error("bad scalar")] + BadScalar, +} + +impl TryFrom for VerificationError { + type Error = (); + + fn try_from(err: BLST_ERROR) -> Result { + match err { + BLST_ERROR::BLST_SUCCESS => Err(()), + BLST_ERROR::BLST_VERIFY_FAIL => Ok(VerificationError::VerifyMsgFail), + BLST_ERROR::BLST_BAD_ENCODING => Ok(VerificationError::BadEncoding), + BLST_ERROR::BLST_POINT_NOT_ON_CURVE => Ok(VerificationError::PointNotOnCurve), + BLST_ERROR::BLST_POINT_NOT_IN_GROUP => Ok(VerificationError::PointNotInGroup), + BLST_ERROR::BLST_AGGR_TYPE_MISMATCH => Ok(VerificationError::AggrTypeMismatch), + BLST_ERROR::BLST_PK_IS_INFINITY => Ok(VerificationError::PkIsInfinity), + BLST_ERROR::BLST_BAD_SCALAR => Ok(VerificationError::BadScalar), + } + } +} diff --git a/src/crypto/bls12381/min_pk.rs b/src/crypto/bls12381/min_pk.rs new file mode 100644 index 00000000..87a58f38 --- /dev/null +++ b/src/crypto/bls12381/min_pk.rs @@ -0,0 +1,53 @@ +use super::error::VerificationError; +use crate::crypto::domain_separator::DomainSeparator; +use blst::BLST_ERROR; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// A BLS12-381 MinPubKey signature +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub blst::min_pk::Signature); + +impl DomainSeparator for Signature { + /// From the [IETF BLS Signature Spec](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#section-4.2.1) + const DST: &'static [u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = BLST_ERROR; + + fn try_from(bytes: &'a [u8]) -> Result { + Ok(Self(blst::min_pk::Signature::uncompress(bytes)?)) + } +} + +impl From for [u8; 96] { + fn from(sig: Signature) -> Self { + sig.0.compress() + } +} + +impl SignatureEncoding for Signature { + type Repr = [u8; 96]; +} + +impl Signer for blst::min_pk::SecretKey { + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(Signature(self.sign(msg, Signature::DST, &[]))) + } +} + +impl Verifier for blst::min_pk::PublicKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + match VerificationError::try_from(signature.0.verify( + true, + msg, + Signature::DST, + &[], + &self, + true, + )) { + Ok(err) => Err(signature::Error::from_source(err)), + Err(_) => Ok(()), + } + } +} diff --git a/src/crypto/bls12381/min_sig.rs b/src/crypto/bls12381/min_sig.rs new file mode 100644 index 00000000..1298f098 --- /dev/null +++ b/src/crypto/bls12381/min_sig.rs @@ -0,0 +1,53 @@ +use super::error::VerificationError; +use crate::crypto::domain_separator::DomainSeparator; +use blst::BLST_ERROR; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// A BLS12-381 MinSig signature +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub blst::min_sig::Signature); + +impl DomainSeparator for Signature { + /// From the [IETF BLS Signature Spec](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#section-4.2.1) + const DST: &'static [u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = BLST_ERROR; + + fn try_from(bytes: &'a [u8]) -> Result { + Ok(Self(blst::min_sig::Signature::uncompress(bytes)?)) + } +} + +impl From for [u8; 48] { + fn from(sig: Signature) -> Self { + sig.0.compress() + } +} + +impl SignatureEncoding for Signature { + type Repr = [u8; 48]; +} + +impl Signer for blst::min_sig::SecretKey { + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(Signature(self.sign(msg, Signature::DST, &[]))) + } +} + +impl Verifier for blst::min_sig::PublicKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + match VerificationError::try_from(signature.0.verify( + true, + msg, + Signature::DST, + &[], + &self, + true, + )) { + Ok(err) => Err(signature::Error::from_source(err)), + Err(_) => Ok(()), + } + } +} diff --git a/src/crypto/domain_separator.rs b/src/crypto/domain_separator.rs new file mode 100644 index 00000000..fbe325f3 --- /dev/null +++ b/src/crypto/domain_separator.rs @@ -0,0 +1,7 @@ +//! Domain separation utilities. + +/// Static domain separator for the DID method. +pub trait DomainSeparator { + /// The domain separator bytes; + const DST: &'static [u8]; +} diff --git a/src/crypto/eddsa.rs b/src/crypto/eddsa.rs deleted file mode 100644 index 6bfc130f..00000000 --- a/src/crypto/eddsa.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! EdDSA signature support - -#[cfg(feature = "eddsa-verifier")] -use anyhow::anyhow; -#[cfg(feature = "eddsa-verifier")] -use signature::Verifier; - -use multibase::Base; - -use super::{JWSSignature, SignerDid}; - -impl JWSSignature for ed25519::Signature { - const ALGORITHM: &'static str = "EdDSA"; -} - -impl SignerDid for ed25519_dalek::SigningKey { - fn did(&self) -> Result { - let mut buf = unsigned_varint::encode::u128_buffer(); - let multicodec = unsigned_varint::encode::u128(0xed, &mut buf); - - Ok(format!( - "did:key:{}", - multibase::encode( - Base::Base58Btc, - [multicodec, self.verifying_key().to_bytes().as_ref()].concat() - ) - )) - } -} - -/// A verifier for Ed25519 signatures using the `ed25519-dalek` crate -#[cfg(feature = "eddsa-verifier")] -pub fn eddsa_verifier(key: &[u8], payload: &[u8], signature: &[u8]) -> Result<(), anyhow::Error> { - let key = ed25519_dalek::VerifyingKey::try_from(key) - .map_err(|e| anyhow!("invalid Ed25519 key, {}", e))?; - - let signature = ed25519_dalek::Signature::try_from(signature) - .map_err(|e| anyhow!("invalid Ed25519 signature, {}", e))?; - - key.verify(payload, &signature) - .map_err(|e| anyhow!("signature mismatch, {}", e)) -} diff --git a/src/crypto/es256.rs b/src/crypto/es256.rs deleted file mode 100644 index 48f030ca..00000000 --- a/src/crypto/es256.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! ES256 signature support - -#[cfg(feature = "es256-verifier")] -use anyhow::anyhow; -#[cfg(feature = "es256-verifier")] -use signature::Verifier; - -use super::JWSSignature; - -impl JWSSignature for p256::ecdsa::Signature { - const ALGORITHM: &'static str = "ES256"; -} - -/// A verifier for PS256 signatures -#[cfg(feature = "es256-verifier")] -pub fn es256_verifier(key: &[u8], payload: &[u8], signature: &[u8]) -> Result<(), anyhow::Error> { - let key = p256::ecdsa::VerifyingKey::try_from(key).map_err(|_| anyhow!("invalid P-256 key"))?; - - let signature = - p256::ecdsa::Signature::try_from(signature).map_err(|_| anyhow!("invalid P-256 key"))?; - - key.verify(payload, &signature) - .map_err(|e| anyhow!("signature mismatch, {}", e)) -} diff --git a/src/crypto/es256k.rs b/src/crypto/es256k.rs deleted file mode 100644 index e521b50e..00000000 --- a/src/crypto/es256k.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! ES256K signature support - -#[cfg(feature = "es256k-verifier")] -use anyhow::anyhow; -#[cfg(feature = "es256k-verifier")] -use signature::Verifier; - -use super::JWSSignature; - -impl JWSSignature for k256::ecdsa::Signature { - const ALGORITHM: &'static str = "ES256K"; -} - -/// A verifier for ES256k signatures -#[cfg(feature = "es256k-verifier")] -pub fn es256k_verifier(key: &[u8], payload: &[u8], signature: &[u8]) -> Result<(), anyhow::Error> { - let key = - k256::ecdsa::VerifyingKey::try_from(key).map_err(|_| anyhow!("invalid secp256k1 key"))?; - - let signature = k256::ecdsa::Signature::try_from(signature) - .map_err(|_| anyhow!("invalid secp256k1 key"))?; - - key.verify(payload, &signature) - .map_err(|e| anyhow!("signature mismatch, {}", e)) -} diff --git a/src/crypto/es384.rs b/src/crypto/es384.rs deleted file mode 100644 index 8f8687dc..00000000 --- a/src/crypto/es384.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! ES384 signature support - -#[cfg(feature = "es384-verifier")] -use anyhow::anyhow; -#[cfg(feature = "es384-verifier")] -use signature::Verifier; - -use super::JWSSignature; - -impl JWSSignature for p384::ecdsa::Signature { - const ALGORITHM: &'static str = "ES384"; -} - -/// A verifier for ES384 signatures -#[cfg(feature = "es384-verifier")] -pub fn es384_verifier(key: &[u8], payload: &[u8], signature: &[u8]) -> Result<(), anyhow::Error> { - let key = p384::ecdsa::VerifyingKey::try_from(key).map_err(|_| anyhow!("invalid P-384 key"))?; - - let signature = - p384::ecdsa::Signature::try_from(signature).map_err(|_| anyhow!("invalid P-384 key"))?; - - key.verify(payload, &signature) - .map_err(|e| anyhow!("signature mismatch, {}", e)) -} diff --git a/src/crypto/es512.rs b/src/crypto/es512.rs index f62653ca..18a31e25 100644 --- a/src/crypto/es512.rs +++ b/src/crypto/es512.rs @@ -1,7 +1,33 @@ -//! ES512 signature support +//! ES512 signature support (P-512) -use super::JWSSignature; +use p521; +use signature::Verifier; +use std::fmt; -impl JWSSignature for ecdsa::Signature { - const ALGORITHM: &'static str = "ES512"; +/// The verifying/public key for ES512. +#[derive(Clone)] // FIXME , Serialize, Deserialize)] +pub struct VerifyingKey(pub p521::ecdsa::VerifyingKey); + +impl fmt::Debug for VerifyingKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("VerifyingKey").finish() + } +} + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + self.0.to_encoded_point(true) == other.0.to_encoded_point(true) + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify( + &self, + msg: &[u8], + signature: &p521::ecdsa::Signature, + ) -> Result<(), signature::Error> { + self.0.verify(msg, &signature) + } } diff --git a/src/crypto/nonce.rs b/src/crypto/nonce.rs new file mode 100644 index 00000000..ee1a3b53 --- /dev/null +++ b/src/crypto/nonce.rs @@ -0,0 +1,227 @@ +//! [Nonce]s & utilities. +//! +//! [Nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + +use enum_as_inner::EnumAsInner; +use getrandom::getrandom; +use libipld_core::{ipld::Ipld, multibase::Base::Base32HexLower}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// Known [`Nonce`] types +#[derive(Clone, Debug, EnumAsInner, Serialize, Deserialize)] +pub enum Nonce { + /// 128-bit, 16-byte nonce + Nonce16([u8; 16]), + + /// Dynamic sized nonce + Custom(Vec), +} + +impl PartialEq for Nonce { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Nonce::Nonce16(a), Nonce::Nonce16(b)) => a == b, + (Nonce::Custom(a), Nonce::Custom(b)) => a == b, + (Nonce::Custom(a), Nonce::Nonce16(b)) => a.as_slice() == b, + (Nonce::Nonce16(a), Nonce::Custom(b)) => a == b.as_slice(), + } + } +} + +impl From<[u8; 16]> for Nonce { + fn from(s: [u8; 16]) -> Self { + Nonce::Nonce16(s) + } +} + +impl From for Vec { + fn from(nonce: Nonce) -> Self { + match nonce { + Nonce::Nonce16(nonce) => nonce.to_vec(), + Nonce::Custom(nonce) => nonce, + } + } +} + +impl From> for Nonce { + fn from(nonce: Vec) -> Self { + if let Ok(sixteen) = <[u8; 16]>::try_from(nonce.clone()) { + return sixteen.into(); + } + + Nonce::Custom(nonce) + } +} + +impl Nonce { + /// Generate a 128-bit, 16-byte nonce + /// + /// # Arguments + /// + /// * `salt` - A salt. This may be left empty, but is recommended to avoid collision. + /// + /// # Example + /// + /// ```rust + /// # use ucan::crypto::Nonce; + /// # use ucan::did::Did; + /// # + /// let mut salt = "did:example:123".as_bytes().to_vec(); + /// let nonce = Nonce::generate_16(); + /// + /// assert_eq!(Vec::from(nonce).len(), 16); + /// ``` + pub fn generate_16() -> Nonce { + let mut buf = [0; 16]; + getrandom(&mut buf).expect("irrecoverable getrandom failure"); + Nonce::Nonce16(buf) + } +} + +impl fmt::Display for Nonce { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Nonce::Nonce16(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + Nonce::Custom(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + } + } +} + +impl From for Ipld { + fn from(nonce: Nonce) -> Self { + match nonce { + Nonce::Nonce16(nonce) => Ipld::Bytes(nonce.to_vec()), + Nonce::Custom(nonce) => Ipld::Bytes(nonce), + } + } +} + +impl TryFrom for Nonce { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::Bytes(v) = ipld { + match v.len() { + 16 => Ok(Nonce::Nonce16( + v.try_into() + .expect("16 bytes because we checked in the match"), + )), + _ => Ok(Nonce::Custom(v)), + } + } else { + Err(()) + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Nonce { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + any::<[u8; 16]>().prop_map(Nonce::Nonce16), + any::>().prop_map(Nonce::Custom) + ] + .boxed() + } +} + +// FIXME move module? +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[wasm_bindgen] +/// A JavaScript-compatible wrapper for [`Nonce`] +pub struct JsNonce(#[wasm_bindgen(skip)] pub Nonce); + +#[cfg(target_arch = "wasm32")] +impl From for Nonce { + fn from(newtype: JsNonce) -> Self { + newtype.0 + } +} + +#[cfg(target_arch = "wasm32")] +impl From for JsNonce { + fn from(nonce: Nonce) -> Self { + JsNonce(nonce) + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl JsNonce { + /// Generate a 128-bit, 16-byte nonce + /// + /// # Arguments + /// + /// * `salt` - A salt. This may be left empty, but is recommended to avoid collision. + pub fn generate_16() -> JsNonce { + Nonce::generate_16().into() + } + + /// Directly lift a 12-byte `Uint8Array` into a [`JsNonce`] + /// + /// # Arguments + /// + /// * `nonce` - The exact nonce to convert to a [`JsNonce`] + pub fn from_uint8_array(arr: Box<[u8]>) -> JsNonce { + Nonce::from(arr.to_vec()).into() + } + + /// Expose the underlying bytes of a [`JsNonce`] as a 12-byte `Uint8Array` + /// + /// # Arguments + /// + /// * `self` - The [`JsNonce`] to convert to a `Uint8Array` + pub fn to_uint8_array(&self) -> Box<[u8]> { + match &self.0 { + Nonce::Nonce12(nonce) => nonce.to_vec().into_boxed_slice(), + Nonce::Nonce16(nonce) => nonce.to_vec().into_boxed_slice(), + Nonce::Custom(nonce) => nonce.clone().into_boxed_slice(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + // FIXME prop test with lots of inputs + #[test] + fn ipld_roundtrip_16() { + let gen = Nonce::generate_16(); + let ipld = Ipld::from(gen.clone()); + + let inner = if let Nonce::Nonce16(nonce) = gen { + Ipld::Bytes(nonce.to_vec()) + } else { + panic!("No conversion!") + }; + + assert_eq!(ipld, inner); + assert_eq!(gen, ipld.try_into().unwrap()); + } + + // FIXME prop test with lots of inputs + // #[test] + // fn ser_de() { + // let gen = Nonce::generate_16(); + // let ser = serde_json::to_string(&gen).unwrap(); + // let de = serde_json::from_str(&ser).unwrap(); + + // assert_eq!(gen, de); + // } +} diff --git a/src/crypto/p521.rs b/src/crypto/p521.rs new file mode 100644 index 00000000..e5b13a37 --- /dev/null +++ b/src/crypto/p521.rs @@ -0,0 +1,32 @@ +use p521; +use serde::{Deserialize, Serialize}; +use signature::Verifier; +use std::fmt; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct VerifyingKey(pub p521::ecdsa::VerifyingKey); + +impl fmt::Debug for VerifyingKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("VerifyingKey").finish() + } +} + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + self.0.to_encoded_point(true) == other.0.to_encoded_point(true) + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify( + &self, + msg: &[u8], + signature: &p521::ecdsa::Signature, + ) -> Result<(), signature::Error> { + self.0.verify(msg, &signature) + } +} diff --git a/src/crypto/ps256.rs b/src/crypto/ps256.rs deleted file mode 100644 index 547548d7..00000000 --- a/src/crypto/ps256.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! PS256 signature support - -#[cfg(feature = "ps256-verifier")] -use anyhow::anyhow; -#[cfg(feature = "ps256-verifier")] -use signature::Verifier; - -use super::JWSSignature; - -impl JWSSignature for rsa::pss::Signature { - const ALGORITHM: &'static str = "PS256"; -} - -/// A verifier for RS256 signatures -#[cfg(feature = "ps256-verifier")] -pub fn ps256_verifier(key: &[u8], payload: &[u8], signature: &[u8]) -> Result<(), anyhow::Error> { - let key = rsa::pkcs1::DecodeRsaPublicKey::from_pkcs1_der(key) - .map_err(|e| anyhow!("invalid PKCS#1 key, {}", e))?; - - let key = rsa::pss::VerifyingKey::::new(key); - - let signature = rsa::pss::Signature::try_from(signature) - .map_err(|e| anyhow!("invalid RSASSA-PKCS1-v1_5 signature, {}", e))?; - - key.verify(payload, &signature) - .map_err(|e| anyhow!("signature mismatch, {}", e)) -} diff --git a/src/crypto/rs256.rs b/src/crypto/rs256.rs index 9f39c1ff..de6205cd 100644 --- a/src/crypto/rs256.rs +++ b/src/crypto/rs256.rs @@ -1,27 +1,67 @@ -//! RS256 signature support +//! RS256 signature support (2048-bit RSA PKCS #1 v1.5). -#[cfg(feature = "rs256-verifier")] -use anyhow::anyhow; -#[cfg(feature = "rs256-verifier")] -use signature::Verifier; +use rsa; +use signature::{SignatureEncoding, Signer, Verifier}; -use super::JWSSignature; +/// The verifying/public key for RS256. +#[derive(Debug, Clone)] +pub struct VerifyingKey(pub rsa::pkcs1v15::VerifyingKey); -impl JWSSignature for rsa::pkcs1v15::Signature { - const ALGORITHM: &'static str = "RS256"; +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + self.0.as_ref() == other.0.as_ref() + } } -/// A verifier for RS256 signatures -#[cfg(feature = "rs256-verifier")] -pub fn rs256_verifier(key: &[u8], payload: &[u8], signature: &[u8]) -> Result<(), anyhow::Error> { - let key = rsa::pkcs1::DecodeRsaPublicKey::from_pkcs1_der(key) - .map_err(|e| anyhow!("invalid PKCS#1 key, {}", e))?; +impl Eq for VerifyingKey {} - let key = rsa::pkcs1v15::VerifyingKey::::new(key); +impl Verifier for VerifyingKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0.verify(msg, &signature.0) + } +} + +/// The signing/secret key for RS256. +#[derive(Debug, Clone)] +pub struct SigningKey(pub rsa::pkcs1v15::SigningKey); + +impl Signer for SigningKey { + fn try_sign(&self, msg: &[u8]) -> Result { + self.0.try_sign(msg).map(Signature) + } +} + +/// The signature for RS256. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub rsa::pkcs1v15::Signature); + +impl SignatureEncoding for Signature { + type Repr = [u8; 256]; +} + +impl From<[u8; 256]> for Signature { + fn from(bytes: [u8; 256]) -> Self { + Signature( + rsa::pkcs1v15::Signature::try_from(bytes.as_ref()) + .expect("passed in [u8; 256], so should succeed"), + ) + } +} + +impl From for [u8; 256] { + fn from(sig: Signature) -> [u8; 256] { + sig.0 + .to_bytes() + .as_ref() + .try_into() + .expect("Signature should be exactly 256 bytes") + } +} - let signature = rsa::pkcs1v15::Signature::try_from(signature) - .map_err(|e| anyhow!("invalid RSASSA-PKCS1-v1_5 signature, {}", e))?; +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = signature::Error; - key.verify(payload, &signature) - .map_err(|e| anyhow!("signature mismatch, {}", e)) + fn try_from(bytes: &'a [u8]) -> Result { + rsa::pkcs1v15::Signature::try_from(bytes).map(Signature) + } } diff --git a/src/crypto/rs512.rs b/src/crypto/rs512.rs new file mode 100644 index 00000000..32a739a7 --- /dev/null +++ b/src/crypto/rs512.rs @@ -0,0 +1,67 @@ +//! RS512 signature support (4096-bit RSA PKCS #1 v1.5). + +use rsa; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// The verifying/public key for RS512. +#[derive(Debug, Clone)] // FIXME , Serialize, Deserialize)] +pub struct VerifyingKey(pub rsa::pkcs1v15::VerifyingKey); + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + rsa::RsaPublicKey::from(self.0.clone()) == rsa::RsaPublicKey::from(other.0.clone()) + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0.verify(msg, &signature.0) + } +} + +/// The signing/secret key for RS512. +#[derive(Debug, Clone)] // FIXME , Serialize, Deserialize)] +pub struct SigningKey(pub rsa::pkcs1v15::SigningKey); + +impl Signer for SigningKey { + fn try_sign(&self, msg: &[u8]) -> Result { + self.0.try_sign(msg).map(Signature) + } +} + +/// The signature for RS512. +#[derive(Debug, Clone, PartialEq, Eq)] // FIXME , Serialize, Deserialize)] +pub struct Signature(pub rsa::pkcs1v15::Signature); + +impl SignatureEncoding for Signature { + type Repr = [u8; 512]; +} + +impl From<[u8; 512]> for Signature { + fn from(bytes: [u8; 512]) -> Self { + Signature( + rsa::pkcs1v15::Signature::try_from(bytes.as_ref()) + .expect("passed in [u8; 512], so should succeed"), + ) + } +} + +impl From for [u8; 512] { + fn from(sig: Signature) -> [u8; 512] { + sig.0 + .to_bytes() + .as_ref() + .try_into() + .expect("Signature should be exactly 512 bytes") + } +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = signature::Error; + + fn try_from(bytes: &'a [u8]) -> Result { + rsa::pkcs1v15::Signature::try_from(bytes).map(Signature) + } +} diff --git a/src/crypto/signature.rs b/src/crypto/signature.rs new file mode 100644 index 00000000..41e90739 --- /dev/null +++ b/src/crypto/signature.rs @@ -0,0 +1,5 @@ +//! Signatures and cryptographic envelopes. + +mod envelope; + +pub use envelope::*; diff --git a/src/crypto/signature/envelope.rs b/src/crypto/signature/envelope.rs new file mode 100644 index 00000000..60165def --- /dev/null +++ b/src/crypto/signature/envelope.rs @@ -0,0 +1,244 @@ +use crate::ability::arguments::Named; +use crate::crypto::varsig::Header; +use crate::{capsule::Capsule, crypto::varsig, did::Did}; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + error::Result, + ipld::Ipld, + multihash::{Code, MultihashDigest}, +}; +use signature::SignatureEncoding; +use signature::Verifier; +use std::collections::BTreeMap; +use std::io::Write; +use thiserror::Error; + +pub trait Envelope: Sized { + type DID: Did; + type Payload: Clone + Capsule + TryFrom> + Into>; + type VarsigHeader: varsig::Header + Clone; + type Encoder: Codec; + + fn varsig_header(&self) -> &Self::VarsigHeader; + fn signature(&self) -> &::Signature; + fn payload(&self) -> &Self::Payload; + fn verifier(&self) -> &Self::DID; + + fn construct( + varsig_header: Self::VarsigHeader, + signature: ::Signature, + payload: Self::Payload, + ) -> Self; + + fn to_ipld_envelope(&self) -> Ipld { + let wrapped_payload = Self::wrap_payload(self.payload().clone()); + let header_bytes: Vec = self.varsig_header().clone().into(); + let header: Ipld = vec![header_bytes.into(), wrapped_payload].into(); + let sig_bytes: Ipld = self.signature().to_vec().into(); + + vec![sig_bytes.into(), header].into() + } + + fn wrap_payload(payload: Self::Payload) -> Ipld { + let inner_args: Named = payload.into(); + let inner_ipld: Ipld = inner_args.into(); + BTreeMap::from_iter([(Self::Payload::TAG.into(), inner_ipld)]).into() + } + + fn try_from_ipld_envelope( + ipld: Ipld, + ) -> Result>>::Error>> { + let Ipld::List(list) = ipld else { + return Err(FromIpldError::InvalidSignatureContainer); + }; + + let [Ipld::Bytes(sig), Ipld::List(inner)] = list.as_slice() else { + return Err(FromIpldError::InvalidSignatureContainer); + }; + + let [Ipld::Bytes(varsig_header), Ipld::Map(btree)] = inner.as_slice() else { + return Err(FromIpldError::InvalidVarsigContainer); + }; + + let (1, Some(Ipld::Map(inner))) = ( + btree.len(), + btree.get(::TAG.into()), + ) else { + return Err(FromIpldError::InvalidPayloadCapsule); + }; + + let payload = Self::Payload::try_from(Named(inner.clone())) + .map_err(FromIpldError::CannotParsePayload)?; + + let varsig_header = Self::VarsigHeader::try_from(varsig_header.as_slice()) + .map_err(|_| FromIpldError::CannotParseVarsigHeader)?; + + let signature = ::Signature::try_from(sig.as_slice()) + .map_err(|_| FromIpldError::CannotParseSignature)?; + + Ok(Self::construct(varsig_header, signature, payload)) + } + + fn varsig_encode(&self, mut w: W) -> Result + where + Ipld: Encode, + { + let codec = self.varsig_header().codec().clone(); + self.to_ipld_envelope().encode(codec, &mut w)?; + Ok(w) + } + + /// Attempt to sign some payload with a given signer. + /// + /// # Arguments + /// + /// * `signer` - The signer to use to sign the payload. + /// * `payload` - The payload to sign. + /// + /// # Errors + /// + /// * [`SignError`] - the payload can't be encoded or the signature fails. + // FIXME ported + fn try_sign( + signer: &::Signer, + varsig_header: Self::VarsigHeader, + payload: Self::Payload, + ) -> Result + where + Ipld: Encode, + Named: From, + { + Self::try_sign_generic(signer, varsig_header, payload) + } + + /// Attempt to sign some payload with a given signer and specific codec. + /// + /// # Arguments + /// + /// * `signer` - The signer to use to sign the payload. + /// * `codec` - The codec to use to encode the payload. + /// * `payload` - The payload to sign. + /// + /// # Errors + /// + /// * [`SignError`] - the payload can't be encoded or the signature fails. + /// + /// # Example + /// + fn try_sign_generic( + signer: &::Signer, + varsig_header: Self::VarsigHeader, + payload: Self::Payload, + ) -> Result + where + Ipld: Encode, + Named: From, + { + let ipld = Self::wrap_payload(payload.clone()); + let mut buffer = vec![]; + ipld.encode(*varsig_header.codec(), &mut buffer) + .map_err(SignError::PayloadEncodingError)?; + + let signature = + signature::Signer::try_sign(signer, &buffer).map_err(SignError::SignatureError)?; + + Ok(Self::construct(varsig_header, signature, payload)) + } + + /// Attempt to validate a signature. + /// + /// # Arguments + /// + /// * `self` - The envelope to validate. + /// + /// # Errors + /// + /// * [`ValidateError`] - the payload can't be encoded or the signature fails. + /// + /// # Exmaples + /// + /// FIXME + fn validate_signature(&self) -> Result<(), ValidateError> + where + Ipld: Encode, + Named: From, + { + let mut encoded = vec![]; + let ipld: Ipld = BTreeMap::from_iter([( + Self::Payload::TAG.to_string(), + Named::::from(self.payload().clone()).into(), + )]) + .into(); + + ipld.encode( + *varsig::header::Header::codec(self.varsig_header()), + &mut encoded, + ) + .map_err(ValidateError::PayloadEncodingError)?; + + self.verifier() + .verify(&encoded, &self.signature()) + .map_err(ValidateError::VerifyError) + } + + fn cid(&self) -> Result + where + Ipld: Encode, + { + let encoded = self.varsig_encode(Vec::new())?; + let multihash = Code::Sha2_256.digest(&encoded); + + Ok(Cid::new_v1( + varsig::header::Header::codec(self.varsig_header()) + .clone() + .into(), + multihash, + )) + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum FromIpldError { + #[error("Invalid signature container")] + InvalidSignatureContainer, + + #[error("Invalid varsig container")] + InvalidVarsigContainer, + + #[error("Cannot parse payload: {0}")] + CannotParsePayload(#[from] E), + + #[error("Cannot parse varsig header")] + CannotParseVarsigHeader, + + #[error("Cannot parse signature")] + CannotParseSignature, + + #[error("Invalid payload capsule")] + InvalidPayloadCapsule, +} + +/// Errors that can occur when signing a [`siganture::Envelope`][Envelope]. +#[derive(Debug, Error)] +pub enum SignError { + /// Unable to encode the payload. + #[error("Unable to encode payload")] + PayloadEncodingError(#[from] libipld_core::error::Error), + + /// Error while signing. + #[error("Signature error: {0}")] + SignatureError(#[from] signature::Error), +} + +/// Errors that can occur when validating a [`signature::Envelope`][Envelope]. +#[derive(Debug, Error)] +pub enum ValidateError { + /// Unable to encode the payload. + #[error("Unable to encode payload")] + PayloadEncodingError(#[from] libipld_core::error::Error), + + /// Error while verifying the signature. + #[error("Signature verification failed: {0}")] + VerifyError(#[from] signature::Error), +} diff --git a/src/crypto/varsig.rs b/src/crypto/varsig.rs new file mode 100644 index 00000000..9308f13e --- /dev/null +++ b/src/crypto/varsig.rs @@ -0,0 +1,4 @@ +pub mod encoding; +pub mod header; + +pub use header::Header; diff --git a/src/crypto/varsig/encoding.rs b/src/crypto/varsig/encoding.rs new file mode 100644 index 00000000..237cb0ef --- /dev/null +++ b/src/crypto/varsig/encoding.rs @@ -0,0 +1,3 @@ +mod preset; + +pub use preset::Preset; diff --git a/src/crypto/varsig/encoding/preset.rs b/src/crypto/varsig/encoding/preset.rs new file mode 100644 index 00000000..e69a5a87 --- /dev/null +++ b/src/crypto/varsig/encoding/preset.rs @@ -0,0 +1,147 @@ +use crate::crypto::signature::Envelope; +use crate::delegation::Delegation; +use libipld_core::codec::Codec; +use libipld_core::codec::Encode; +use libipld_core::ipld::Ipld; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Preset { + Identity = 0x5f, + DagPb = 0x70, + DagCbor = 0x71, + DagJson = 0x0129, + Jwt = 0x6a77, // FIXME break out Jwt & EIP-191? + Eip191 = 0xe191, +} + +impl Encode for Ipld { + fn encode( + &self, + c: Preset, + w: &mut W, + ) -> Result<(), libipld_core::error::Error> { + match c { + Preset::Identity => todo!(), + Preset::DagPb => todo!(), + Preset::DagCbor => self.encode(libipld_cbor::DagCborCodec, w), + Preset::DagJson => todo!(), + Preset::Jwt => todo!(), + Preset::Eip191 => todo!(), + } + } +} + +impl Encode for Delegation { + fn encode( + &self, + c: Preset, + w: &mut W, + ) -> Result<(), libipld_core::error::Error> { + self.clone().to_ipld_envelope().encode(c, w) + } +} + +impl TryFrom for Preset { + type Error = libipld_core::error::UnsupportedCodec; + + fn try_from(value: u64) -> Result { + match value { + 0x5f => Ok(Preset::Identity), + 0x70 => Ok(Preset::DagPb), + 0x71 => Ok(Preset::DagCbor), + 0x0129 => Ok(Preset::DagJson), + 0x6a77 => Ok(Preset::Jwt), + 0xe191 => Ok(Preset::Eip191), + // 0xe1 => Ok(Preset::MerkleBatchSig), + _ => Err(libipld_core::error::UnsupportedCodec(value)), + } + } +} + +impl From for u64 { + fn from(encoding: Preset) -> u64 { + encoding as u64 + } +} + +impl Codec for Preset {} + +// FIXME pub struct MerkleSig + +impl<'a> TryFrom<&'a [u8]> for Preset { + type Error = (); + + fn try_from(bytes: &'a [u8]) -> Result { + if let (encoding_info, &[]) = unsigned_varint::decode::u64(&bytes).map_err(|_| ())? { + return match encoding_info { + 0x5f => Ok(Preset::Identity), + 0x70 => Ok(Preset::DagPb), + 0x71 => Ok(Preset::DagCbor), + 0x0129 => Ok(Preset::DagJson), + 0x6a77 => Ok(Preset::Jwt), + 0xe191 => Ok(Preset::Eip191), + // 0xe1 => { + // let merkle_proof = Vec::new(); + // Ok(Preset::MerkleBatchSig(merkle_proof)) + // } + _ => Err(()), + }; + }; + + Err(()) + } +} + +impl AsRef<[u8]> for Preset { + fn as_ref(&self) -> &[u8] { + match self { + Preset::Identity => &[0x5f], + Preset::DagPb => &[0x70], + Preset::DagCbor => &[0x71], + Preset::DagJson => &[0x01, 0x29], + Preset::Jwt => &[0x6a, 0x77], + Preset::Eip191 => &[0xe1, 0x91], + // Preset::Eip191(inner) => { + // let mut buffer = vec![0xe191]; + // buffer.extend(inner.as_ref()); + // buffer.as_ref() + // } // Preset::MerkleBatchSig(merkle_proof) => { + // let mut buffer = vec![0xe1]; + // buffer.extend(merkle_proof.as_ref()); + // buffer.as_ref() + // } + } + } +} + +impl From for u32 { + fn from(encoding: Preset) -> u32 { + match encoding { + Preset::Identity => 0x5f, + Preset::DagPb => 0x70, + Preset::DagCbor => 0x71, + Preset::DagJson => 0x0129, + Preset::Jwt => 0x6a77, + Preset::Eip191 => 0xe191, + // Preset::MerkleBatchSig(_) => 0xe1, + } + } +} + +impl TryFrom for Preset { + type Error = libipld_core::error::UnsupportedCodec; + + fn try_from(value: u32) -> Result { + match value { + 0x5f => Ok(Preset::Identity), + 0x70 => Ok(Preset::DagPb), + 0x71 => Ok(Preset::DagCbor), + 0x0129 => Ok(Preset::DagJson), + 0x6a77 => Ok(Preset::Jwt), + 0xe191 => Ok(Preset::Eip191), + // 0xe1 => Ok(Preset::MerkleBatchSig), + _ => Err(libipld_core::error::UnsupportedCodec(value as u64)), + } + } +} diff --git a/src/crypto/varsig/header.rs b/src/crypto/varsig/header.rs new file mode 100644 index 00000000..17005ac6 --- /dev/null +++ b/src/crypto/varsig/header.rs @@ -0,0 +1,17 @@ +mod eddsa; +mod es256; +mod es256k; +mod es512; +mod preset; +mod rs256; +mod rs512; +mod traits; + +pub use eddsa::EdDsaHeader; +pub use es256::Es256Header; +pub use es256k::Es256kHeader; +pub use es512::Es512Header; +pub use preset::Preset; +pub use rs256::Rs256Header; +pub use rs512::Rs512Header; +pub use traits::Header; diff --git a/src/crypto/varsig/header/eddsa.rs b/src/crypto/varsig/header/eddsa.rs new file mode 100644 index 00000000..d16d7137 --- /dev/null +++ b/src/crypto/varsig/header/eddsa.rs @@ -0,0 +1,43 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct EdDsaHeader { + pub codec: C, +} + +impl> TryFrom<&[u8]> for EdDsaHeader { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0xed, inner)) = unsigned_varint::decode::u8(&bytes) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&inner) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(EdDsaHeader { codec }); + } + } + + return Err(()); + } +} + +impl + Clone> From> for Vec { + fn from(ed: EdDsaHeader) -> Vec { + let mut tag_buf: [u8; 2] = Default::default(); + let tag: &[u8] = unsigned_varint::encode::u8(0xed, &mut tag_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc: &[u8] = unsigned_varint::encode::u64(ed.codec.into(), &mut enc_buf); + + [tag, enc].concat().into() + } +} + +impl Header for EdDsaHeader { + type Signature = ed25519_dalek::Signature; + type Verifier = ed25519_dalek::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/es256.rs b/src/crypto/varsig/header/es256.rs new file mode 100644 index 00000000..ba57f06d --- /dev/null +++ b/src/crypto/varsig/header/es256.rs @@ -0,0 +1,48 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Es256Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Es256Header { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1200, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x12, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Es256Header { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(es: Es256Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1200, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x12, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(es.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl Header for Es256Header { + type Signature = p256::ecdsa::Signature; + type Verifier = p256::ecdsa::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/es256k.rs b/src/crypto/varsig/header/es256k.rs new file mode 100644 index 00000000..17bd7c67 --- /dev/null +++ b/src/crypto/varsig/header/es256k.rs @@ -0,0 +1,48 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Es256kHeader { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Es256kHeader { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0xe7, inner)) = unsigned_varint::decode::u8(&bytes) { + if let Ok((0x12, more)) = unsigned_varint::decode::u8(&inner).map_err(|_| ()) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Es256kHeader { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(es: Es256kHeader) -> Vec { + let mut tag_buf: [u8; 2] = Default::default(); + let tag = unsigned_varint::encode::u8(0xe7, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x12, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(es.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl Header for Es256kHeader { + type Signature = k256::ecdsa::Signature; + type Verifier = k256::ecdsa::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/es512.rs b/src/crypto/varsig/header/es512.rs new file mode 100644 index 00000000..c14b4723 --- /dev/null +++ b/src/crypto/varsig/header/es512.rs @@ -0,0 +1,48 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Es512Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Es512Header { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1202, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x13, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Es512Header { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(es: Es512Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1202, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x13, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(es.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl Header for Es512Header { + type Signature = p521::ecdsa::Signature; + type Verifier = p521::ecdsa::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/preset.rs b/src/crypto/varsig/header/preset.rs new file mode 100644 index 00000000..80664d5e --- /dev/null +++ b/src/crypto/varsig/header/preset.rs @@ -0,0 +1,106 @@ +use super::{eddsa, es256, es256k, es512, rs256, rs512, Header}; +use crate::{crypto::varsig::encoding, did::key}; + +#[derive(Clone, Debug, PartialEq)] +pub enum Preset { + EdDsa(eddsa::EdDsaHeader), + Es256(es256::Es256Header), + Es256k(es256k::Es256kHeader), + Es512(es512::Es512Header), + Rs256(rs256::Rs256Header), + Rs512(rs512::Rs512Header), + // FIXME BLS? needs varsig specs + // FIXME Es384 needs varsig specs +} + +impl From> for Preset { + fn from(ed: eddsa::EdDsaHeader) -> Self { + Preset::EdDsa(ed) + } +} + +impl From> for Preset { + fn from(rs256: rs256::Rs256Header) -> Self { + Preset::Rs256(rs256) + } +} + +impl From> for Preset { + fn from(rs512: rs512::Rs512Header) -> Self { + Preset::Rs512(rs512) + } +} + +impl From> for Preset { + fn from(es256: es256::Es256Header) -> Self { + Preset::Es256(es256) + } +} + +impl From> for Preset { + fn from(es256k: es256k::Es256kHeader) -> Self { + Preset::Es256k(es256k) + } +} + +impl From for Vec { + fn from(preset: Preset) -> Vec { + match preset { + Preset::EdDsa(ed) => ed.into(), + Preset::Rs256(rs256) => rs256.into(), + Preset::Rs512(rs512) => rs512.into(), + Preset::Es256(es256) => es256.into(), + Preset::Es256k(es256k) => es256k.into(), + Preset::Es512(es512) => es512.into(), + } + } +} + +impl<'a> TryFrom<&'a [u8]> for Preset { + type Error = (); + + fn try_from(bytes: &'a [u8]) -> Result { + if let Ok(ed) = eddsa::EdDsaHeader::try_from(bytes) { + return Ok(Preset::EdDsa(ed)); + } + + if let Ok(rs256) = rs256::Rs256Header::::try_from(bytes) { + return Ok(Preset::Rs256(rs256)); + } + + if let Ok(rs512) = rs512::Rs512Header::::try_from(bytes) { + return Ok(Preset::Rs512(rs512)); + } + + if let Ok(es256) = es256::Es256Header::::try_from(bytes) { + return Ok(Preset::Es256(es256)); + } + + if let Ok(es256k) = es256k::Es256kHeader::::try_from(bytes) { + return Ok(Preset::Es256k(es256k)); + } + + if let Ok(es512) = es512::Es512Header::::try_from(bytes) { + return Ok(Preset::Es512(es512)); + } + + Err(()) + } +} + +impl Header for Preset { + type Signature = key::Signature; + type Verifier = key::Verifier; + + fn codec(&self) -> &encoding::Preset { + match self { + Preset::EdDsa(ed) => ed.codec(), + Preset::Rs256(rs256) => rs256.codec(), + Preset::Rs512(rs512) => rs512.codec(), + Preset::Es256(es256) => es256.codec(), + Preset::Es256k(es256k) => es256k.codec(), + Preset::Es512(es512) => es512.codec(), + // Preset::Bls + } + } +} diff --git a/src/crypto/varsig/header/rs256.rs b/src/crypto/varsig/header/rs256.rs new file mode 100644 index 00000000..4f559d80 --- /dev/null +++ b/src/crypto/varsig/header/rs256.rs @@ -0,0 +1,61 @@ +use super::Header; +use crate::crypto::rs256::{Signature, VerifyingKey}; +use libipld_core::codec::Codec; +use thiserror::Error; + +#[derive(Clone, Debug, PartialEq)] +pub struct Rs256Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Rs256Header { + type Error = ParseFromBytesError<>::Error>; + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1205, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x12, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = + C::try_from(codec_info).map_err(ParseFromBytesError::CodecPrefixError)?; + + return Ok(Rs256Header { codec }); + } + } + } + + Err(ParseFromBytesError::InvalidHeader) + } +} + +#[derive(Debug, PartialEq, Clone, Error)] +pub enum ParseFromBytesError { + #[error("Invalid header")] + InvalidHeader, + + #[error("Codec prefix error: {0}")] + CodecPrefixError(#[from] C), +} + +impl> From> for Vec { + fn from(rs: Rs256Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1205, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x12, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(rs.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl Header for Rs256Header { + type Signature = Signature; + type Verifier = VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/rs512.rs b/src/crypto/varsig/header/rs512.rs new file mode 100644 index 00000000..60c77adf --- /dev/null +++ b/src/crypto/varsig/header/rs512.rs @@ -0,0 +1,49 @@ +use super::Header; +use crate::crypto::rs512::{Signature, VerifyingKey}; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Rs512Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Rs512Header { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1205, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x13, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Rs512Header { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(rs: Rs512Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1205, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x13, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(rs.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl Header for Rs512Header { + type Signature = Signature; + type Verifier = VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/traits.rs b/src/crypto/varsig/header/traits.rs new file mode 100644 index 00000000..e6da044c --- /dev/null +++ b/src/crypto/varsig/header/traits.rs @@ -0,0 +1,42 @@ +use libipld_core::codec::{Codec, Encode}; +use signature::Verifier; +use thiserror::Error; + +pub trait Header: for<'a> TryFrom<&'a [u8]> + Into> { + type Signature: signature::SignatureEncoding; + type Verifier: signature::Verifier; + + fn codec(&self) -> &Enc; + + fn encode_payload, Buf: std::io::Write>( + &self, + payload: T, + buffer: &mut Buf, + ) -> Result<(), libipld_core::error::Error> { + payload.encode(Self::codec(self).clone(), buffer) + } + + fn try_verify<'a, T: Encode>( + &self, + verifier: &'a Self::Verifier, + signature: &'a Self::Signature, + payload: T, + ) -> Result<(), VerifyError> { + let mut buffer = vec![]; + self.encode_payload(payload, &mut buffer) + .map_err(VerifyError::CodecError)?; + + verifier + .verify(&buffer, signature) + .map_err(VerifyError::SignatureError) + } +} + +#[derive(Debug, Error)] +pub enum VerifyError { + #[error("Varsig codec error: {0}")] + CodecError(libipld_core::error::Error), + + #[error("varsig signature error: {0}")] + SignatureError(signature::Error), +} diff --git a/src/delegation.rs b/src/delegation.rs new file mode 100644 index 00000000..a452f6b3 --- /dev/null +++ b/src/delegation.rs @@ -0,0 +1,199 @@ +//! A [`Delegation`] is the way to grant someone else the use of [`Ability`][crate::ability]. +//! +//! ## Data +//! +//! - [`Delegation`] is the top-level, signed data struture. +//! - [`Payload`] is the fields unique to an invocation. +//! - [`Preset`] is an [`Delegation`] preloaded with this library's [preset abilities](crate::ability::preset::Ready). +//! - [`Predicate`]s are syntactically-driven validation rules for [`Delegation`]s. +//! +//! ## Stateful Helpers +//! +//! - [`Agent`] is a high-level interface for sessions that will involve more than one invoctaion. +//! - [`store`] is an interface for caching [`Delegation`]s. + +pub mod policy; +pub mod store; + +mod agent; +mod payload; + +pub use agent::Agent; +pub use payload::*; + +use crate::{ + ability::arguments::Named, + capsule::Capsule, + crypto::{signature::Envelope, varsig, Nonce}, + did::{self, Did}, + time::{TimeBoundError, Timestamp}, +}; +use libipld_core::{codec::Codec, ipld::Ipld, link::Link}; +use policy::Predicate; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use web_time::SystemTime; + +/// A [`Delegation`] is a signed delegation [`Payload`] +/// +/// A [`Payload`] on its own is not a valid [`Delegation`], as it must be signed by the issuer. +#[derive(Clone, Debug, PartialEq)] +pub struct Delegation< + DID: Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + pub varsig_header: V, + pub payload: Payload, + pub signature: DID::Signature, + _marker: std::marker::PhantomData, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Proof< + DID: Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + pub prf: Vec>>, +} + +impl, C: Codec> Capsule for Proof { + const TAG: &'static str = "ucan/prf"; +} + +impl, C: Codec> Delegation { + pub fn new( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Delegation { + Delegation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + /// Retrive the `issuer` of a [`Delegation`] + pub fn issuer(&self) -> &DID { + &self.payload.issuer + } + + /// Retrive the `subject` of a [`Delegation`] + pub fn subject(&self) -> Option<&DID> { + self.payload.subject.as_ref() + } + + /// Retrive the `audience` of a [`Delegation`] + pub fn audience(&self) -> &DID { + &self.payload.audience + } + + /// Retrieve the `via` of a [`Delegation`] + pub fn via(&self) -> Option<&DID> { + self.payload.via.as_ref() + } + + /// Retrieve the `command` of a [`Delegation`] + pub fn command(&self) -> &String { + &self.payload.command + } + + /// Retrive the `policy` of a [`Delegation`] + pub fn policy(&self) -> &Vec { + &self.payload.policy + } + + /// Retrive the `metadata` of a [`Delegation`] + pub fn metadata(&self) -> &BTreeMap { + &self.payload.metadata + } + + /// Retrive the `nonce` of a [`Delegation`] + pub fn nonce(&self) -> &Nonce { + &self.payload.nonce + } + + /// Retrive the `not_before` of a [`Delegation`] + pub fn not_before(&self) -> Option<&Timestamp> { + self.payload.not_before.as_ref() + } + + /// Retrive the `expiration` of a [`Delegation`] + pub fn expiration(&self) -> Option<&Timestamp> { + self.payload.expiration.as_ref() + } + + pub fn check_time(&self, now: SystemTime) -> Result<(), TimeBoundError> { + self.payload.check_time(now) + } +} + +impl + Clone, C: Codec> Envelope for Delegation +where + Payload: TryFrom>, + Named: From>, +{ + type DID = DID; + type Payload = Payload; + type VarsigHeader = V; + type Encoder = C; + + fn construct( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Delegation { + Delegation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + fn varsig_header(&self) -> &V { + &self.varsig_header + } + + fn payload(&self) -> &Payload { + &self.payload + } + + fn signature(&self) -> &DID::Signature { + &self.signature + } + + fn verifier(&self) -> &DID { + &self.payload.issuer + } +} + +impl + Clone, C: Codec> Serialize for Delegation +where + Payload: TryFrom>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_ipld_envelope().serialize(serializer) + } +} + +impl<'de, DID: Did + Clone, V: varsig::Header + Clone, C: Codec> Deserialize<'de> + for Delegation +where + Payload: TryFrom>, + as TryFrom>>::Error: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Self::try_from_ipld_envelope(ipld).map_err(serde::de::Error::custom) + } +} diff --git a/src/delegation/agent.rs b/src/delegation/agent.rs new file mode 100644 index 00000000..9b050878 --- /dev/null +++ b/src/delegation/agent.rs @@ -0,0 +1,146 @@ +use super::{payload::Payload, policy::Predicate, store::Store, Delegation}; +use crate::{ + ability::arguments::Named, + crypto::{signature::Envelope, varsig, Nonce}, + did, + did::Did, + time::Timestamp, +}; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + ipld::Ipld, +}; +use std::{collections::BTreeMap, marker::PhantomData}; +use thiserror::Error; +use web_time::SystemTime; + +/// A stateful agent capable of delegating to others, and being delegated to. +/// +/// This is helpful for sessions where more than one delegation will be made. +#[derive(Debug)] +pub struct Agent< + S: Store, + DID: Did + Clone = did::preset::Verifier, + V: varsig::Header + Clone = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + /// The [`Did`][Did] of the agent. + pub did: DID, + + /// The attached [`deleagtion::Store`][super::store::Store]. + pub store: S, + + signer: ::Signer, + _marker: PhantomData<(V, C)>, +} + +impl + Clone, DID: Did + Clone, V: varsig::Header + Clone, C: Codec> + Agent +where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + pub fn new(did: DID, signer: ::Signer, store: S) -> Self { + Self { + did, + store, + signer, + _marker: PhantomData, + } + } + + pub fn delegate( + &self, + audience: DID, + subject: Option<&DID>, + via: Option, + command: String, + new_policy: Vec, + metadata: BTreeMap, + expiration: Option, + not_before: Option, + now: SystemTime, + varsig_header: V, + ) -> Result, DelegateError> { + let nonce = Nonce::generate_16(); + + let (subject, policy) = match subject { + Some(subject) if *subject == self.did => (Some(subject.clone()), new_policy), + None => (None, new_policy), + Some(subject) => { + let proofs = &self + .store + .get_chain(&self.did, &subject, &command, vec![], now) + .map_err(DelegateError::StoreError)? + .ok_or(DelegateError::ProofsNotFound)?; + let to_delegate = proofs.first().1.payload(); + + let mut policy = to_delegate.policy.clone(); + policy.extend(new_policy); + (Some(subject.clone()), policy) + } + }; + + let payload: Payload = Payload { + issuer: self.did.clone(), + audience, + subject, + via, + command, + metadata, + nonce, + expiration, + not_before, + policy, + }; + + Ok(Delegation::try_sign(&self.signer, varsig_header, payload).expect("FIXME")) + } + + pub fn receive( + &self, + cid: Cid, // FIXME remove and generate from the capsule header? + delegation: Delegation, + ) -> Result<(), ReceiveError> { + if self.store.get(&cid).is_ok() { + return Ok(()); + } + + if delegation.audience() != &self.did { + return Err(ReceiveError::WrongAudience(delegation.audience().clone())); + } + + delegation + .validate_signature() + .map_err(|_| ReceiveError::InvalidSignature(cid))?; + + self.store.insert_keyed(cid, delegation).map_err(Into::into) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Error)] +pub enum DelegateError { + #[error("The current agent does not have the necessary proofs to delegate.")] + ProofsNotFound, + + #[error(transparent)] + StoreError(#[from] StoreErr), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Error)] +pub enum ReceiveError { + #[error("The current agent ({0}) is not the intended audience of the delegation.")] + WrongAudience(DID), + + #[error("Signature for UCAN with CID {0} is invalid.")] + InvalidSignature(Cid), + + #[error(transparent)] + StoreError(#[from] StoreErr), +} diff --git a/src/delegation/payload.rs b/src/delegation/payload.rs new file mode 100644 index 00000000..4446623b --- /dev/null +++ b/src/delegation/payload.rs @@ -0,0 +1,495 @@ +use super::policy::{predicate, Predicate}; +use crate::ability::arguments::Named; +use crate::time; +use crate::{ + capsule::Capsule, + crypto::Nonce, + did::{Did, Verifiable}, + time::{TimeBoundError, Timestamp}, +}; +use core::str::FromStr; +use derive_builder::Builder; +use libipld_core::ipld::Ipld; +use std::{collections::BTreeMap, fmt::Debug}; +use thiserror::Error; +use web_time::SystemTime; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld; + +/// The payload portion of a [`Delegation`][super::Delegation]. +/// +/// This contains the semantic information about the delegation, including the +/// issuer, subject, audience, the delegated ability, time bounds, and so on. +#[derive(Debug, Clone, PartialEq, Builder)] // FIXME Serialize, Deserialize, Builder)] +pub struct Payload { + /// The subject of the [`Delegation`]. + /// + /// This role *must* have issued the earlier (root) + /// delegation in the chain. This makes the chains + /// self-certifying. + /// + /// The semantics of the delegation are established + /// by the subject. + /// + /// [`Delegation`]: super::Delegation + pub subject: Option, + + /// The issuer of the [`Delegation`]. + /// + /// This [`Did`] *must* match the signature on + /// the outer layer of [`Delegation`]. + /// + /// [`Delegation`]: super::Delegation + pub issuer: DID, + + /// The agent being delegated to. + pub audience: DID, + + /// A [`Did`] that must be in the delegation chain at invocation time. + #[builder(default)] + pub via: Option, + + /// The command being delegated. + pub command: String, + + /// Any [`Predicate`] policies that constrain the `args` on an [`Invocation`][crate::invocation::Invocation]. + #[builder(default)] + pub policy: Vec, + + /// Extensible, free-form fields. + #[builder(default)] + pub metadata: BTreeMap, + + /// A [cryptographic nonce] to ensure that the UCAN's [`Cid`] is unique. + /// + /// [cryptograpgic nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + /// [`Cid`]: libipld_core::cid::Cid ; + #[builder(default = "Nonce::generate_16()")] + pub nonce: Nonce, + + /// The latest wall-clock time that the UCAN is valid until, + /// given as a [Unix timestamp]. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + #[builder(default)] + pub expiration: Option, + + /// An optional earliest wall-clock time that the UCAN is valid from, + /// given as a [Unix timestamp]. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + #[builder(default)] + pub not_before: Option, +} + +impl Payload { + pub fn check_time(&self, now: SystemTime) -> Result<(), TimeBoundError> { + let ts_now = &Timestamp::postel(now); + + if let Some(ref exp) = self.expiration { + if exp < ts_now { + return Err(TimeBoundError::Expired); + } + } + + if let Some(ref nbf) = self.not_before { + if nbf > ts_now { + return Err(TimeBoundError::NotYetValid); + } + } + + Ok(()) + } +} + +impl Capsule for Payload { + const TAG: &'static str = "ucan/d@1.0.0-rc.1"; +} + +impl Verifiable for Payload { + fn verifier(&self) -> &DID { + &self.issuer + } +} + +impl TryFrom> for Payload +where + ::Err: Debug, +{ + type Error = ParseError; + + fn try_from(args: Named) -> Result { + let mut subject = None; + let mut issuer = None; + let mut audience = None; + let mut via = None; + let mut command = None; + let mut policy = None; + let mut metadata = None; + let mut nonce = None; + let mut expiration = None; + let mut not_before = None; + + for (k, ipld) in args { + match k.as_str() { + "sub" => { + subject = Some(match ipld { + Ipld::Null => None, + Ipld::String(s) => { + Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("sub".to_string(), bad)), + }) + } + "iss" => match ipld { + Ipld::String(s) => { + issuer = Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("iss".to_string(), bad)), + }, + "aud" => match ipld { + Ipld::String(s) => { + audience = + Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("aud".to_string(), bad)), + }, + "via" => match ipld { + Ipld::String(s) => { + via = Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("via".to_string(), bad)), + }, + "cmd" => match ipld { + Ipld::String(s) => command = Some(s), + bad => return Err(ParseError::WrongTypeForField("cmd".to_string(), bad)), + }, + "pol" => match ipld { + Ipld::List(xs) => { + let result: Result, ParseError> = + xs.iter().try_fold(vec![], |mut acc, ipld| { + let pred = Predicate::try_from(ipld.clone())?; + acc.push(pred); + Ok(acc) + }); + + policy = Some(result?); + } + bad => return Err(ParseError::WrongTypeForField("pol".to_string(), bad)), + }, + "meta" => match ipld { + Ipld::Map(m) => metadata = Some(m), + bad => return Err(ParseError::WrongTypeForField("meta".to_string(), bad)), + }, + "nonce" => match ipld { + Ipld::Bytes(b) => nonce = Some(Nonce::from(b).into()), + bad => return Err(ParseError::WrongTypeForField("nonce".to_string(), bad)), + }, + "exp" => match ipld { + Ipld::Integer(i) => { + expiration = Some(Some( + Timestamp::try_from(i).map_err(ParseError::BadTimestamp)?, + )) + } + Ipld::Null => expiration = Some(None), + bad => return Err(ParseError::WrongTypeForField("exp".to_string(), bad)), + }, + "nbf" => match ipld { + Ipld::Integer(i) => { + not_before = Some(Timestamp::try_from(i).map_err(ParseError::BadTimestamp)?) + } + bad => return Err(ParseError::WrongTypeForField("nbf".to_string(), bad)), + }, + other => return Err(ParseError::UnknownField(other.to_string())), + } + } + + Ok(Payload { + subject: subject.ok_or(ParseError::MissingSub)?, + issuer: issuer.ok_or(ParseError::MissingIss)?, + audience: audience.ok_or(ParseError::MissingAud)?, + via, + command: command.ok_or(ParseError::MissingCmd)?, + policy: policy.ok_or(ParseError::MissingPol)?, + metadata: metadata.unwrap_or_default(), + nonce: nonce.ok_or(ParseError::MissingNonce)?, + expiration: expiration.ok_or(ParseError::MissingExp)?, + not_before, + }) + } +} + +#[derive(Debug, Error)] +pub enum ParseError +where + ::Err: Debug, +{ + #[error("Unknown field: {0}")] + UnknownField(String), + + #[error("Missing sub field")] + MissingSub, + + #[error("Missing iss field")] + MissingIss, + + #[error("Missing aud field")] + MissingAud, + + #[error("Missing cmd field")] + MissingCmd, + + #[error("Missing pol field")] + MissingPol, + + #[error("Missing nonce field")] + MissingNonce, + + #[error("Missing exp field")] + MissingExp, + + #[error("Wrong type for field {0}: {1:?}")] + WrongTypeForField(String, Ipld), + + #[error("Cannot parse DID")] + DidParseError(::Err), + + #[error("Cannot parse timestamp: {0}")] + BadTimestamp(#[from] time::OutOfRangeError), + + #[error("Cannot parse policy predicate: {0}")] + InvalidPolicy(#[from] predicate::FromIpldError), +} + +impl From> for Ipld { + fn from(payload: Payload) -> Self { + let named: Named = payload.into(); + Ipld::Map(named.0) + } +} + +impl TryFrom for Payload +where + DID: Did + FromStr, + ::Err: Debug, +{ + type Error = TryFromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::Map(map) => { + let named = Named::(map); + Payload::try_from(named).map_err(TryFromIpldError::MapParseError) + } + _ => Err(TryFromIpldError::NotAMap), + } + } +} + +#[derive(Debug, Error)] +pub enum TryFromIpldError +where + ::Err: Debug, +{ + NotAMap, + MapParseError(ParseError), +} + +impl From> for Named { + fn from(payload: Payload) -> Self { + let mut args = Named::::from_iter([ + ("iss".to_string(), Ipld::String(payload.issuer.to_string())), + ( + "aud".to_string(), + Ipld::String(payload.audience.to_string()), + ), + ("cmd".to_string(), Ipld::String(payload.command)), + ("pol".to_string(), { + Ipld::List(payload.policy.into_iter().map(|p| p.into()).collect()) + }), + ("nonce".to_string(), payload.nonce.into()), + ( + "exp".to_string(), + payload.expiration.map_or(Ipld::Null, |e| e.into()), + ), + ]); + + if let Some(subject) = payload.subject { + args.insert("sub".to_string(), Ipld::String(subject.to_string())); + } else { + args.insert("sub".to_string(), Ipld::Null); + } + + if let Some(via) = payload.via { + args.insert("via".to_string(), Ipld::String(via.to_string())); + } + + if let Some(not_before) = payload.not_before { + args.insert("nbf".to_string(), Ipld::from(not_before)); + } + + if !payload.metadata.is_empty() { + args.insert("meta".to_string(), Ipld::Map(payload.metadata)); + } + + args + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Payload +where + DID::Parameters: Clone, +{ + type Parameters = (DID::Parameters, ::Parameters); + type Strategy = BoxedStrategy; + + fn arbitrary_with((did_args, pred_args): Self::Parameters) -> Self::Strategy { + ( + Option::::arbitrary(), + DID::arbitrary_with(did_args.clone()), + DID::arbitrary_with(did_args), + String::arbitrary(), + Nonce::arbitrary(), + Option::::arbitrary(), + Option::::arbitrary(), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..5).prop_map(|m| { + m.into_iter() + .map(|(k, v)| (k, v.0)) + .collect::>() + }), + prop::collection::vec(Predicate::arbitrary_with(pred_args), 0..10), + Option::::arbitrary(), + ) + .prop_map( + |( + subject, + issuer, + audience, + command, + nonce, + expiration, + not_before, + metadata, + policy, + via, + )| { + Payload { + issuer, + subject, + audience, + command, + policy, + metadata, + nonce, + expiration, + not_before, + via, + } + }, + ) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test_log::test] + fn test_ipld_round_trip(payload in Payload::::arbitrary()) { + let observed: Ipld = payload.clone().into(); + let parsed = Payload::::try_from(observed); + + prop_assert!(parsed.is_ok()); + prop_assert_eq!(parsed.unwrap(), payload); + } + + #[test_log::test] + fn test_ipld_has_correct_fields(payload in Payload::::arbitrary()) { + let observed: Ipld = payload.clone().into(); + + if let Ipld::Map(named) = observed { + prop_assert!(named.len() >= 6); + prop_assert!(named.len() <= 10); + + for key in named.keys() { + prop_assert!(matches!(key.as_str(), "sub" | "iss" | "aud" | "via" | "cmd" | "pol" | "meta" | "nonce" | "exp" | "nbf")); + } + } else { + prop_assert!(false, "ipld map"); + } + } + + #[test_log::test] + fn test_ipld_field_types(payload in Payload::::arbitrary()) { + let named: Named = payload.clone().into(); + + let iss = named.get("iss".into()); + let aud = named.get("aud".into()); + let cmd = named.get("cmd".into()); + let pol = named.get("pol".into()); + let nonce = named.get("nonce".into()); + let exp = named.get("exp".into()); + + // Required Fields + prop_assert_eq!(iss.unwrap(), &Ipld::String(payload.issuer.to_string())); + prop_assert_eq!(aud.unwrap(), &Ipld::String(payload.audience.to_string())); + prop_assert_eq!(cmd.unwrap(), &Ipld::String(payload.command.clone())); + prop_assert_eq!(pol.unwrap(), &Ipld::List(payload.policy.clone().into_iter().map(|p| p.into()).collect())); + prop_assert_eq!(nonce.unwrap(), &payload.nonce.into()); + prop_assert_eq!(exp.unwrap(), &payload.expiration.map_or(Ipld::Null, |e| e.into())); + + // Optional Fields + match (payload.subject, named.get("sub")) { + (Some(sub), Some(Ipld::String(s))) => { + prop_assert_eq!(&sub.to_string(), s); + } + (None, Some(Ipld::Null)) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.via, named.get("via")) { + (Some(via), Some(Ipld::String(s))) => { + prop_assert_eq!(&via.to_string(), s); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.metadata.is_empty(), named.get("meta")) { + (false, Some(Ipld::Map(btree))) => { + prop_assert_eq!(&payload.metadata, btree); + } + (true, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.not_before, named.get("nbf")) { + (Some(nbf), Some(Ipld::Integer(i))) => { + prop_assert_eq!(&i128::from(nbf), i); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + } + + #[test_log::test] + fn test_non_payload(ipld in ipld::Newtype::arbitrary()) { + // Just ensuring that a negative test shows up + let parsed = Payload::::try_from(ipld.0); + prop_assert!(parsed.is_err()) + } + } +} diff --git a/src/delegation/policy.rs b/src/delegation/policy.rs new file mode 100644 index 00000000..2bbc2abe --- /dev/null +++ b/src/delegation/policy.rs @@ -0,0 +1,10 @@ +//! Policy language. +//! +//! The policy language is a simple predicate language extended with [`jq`]-style selectors. +//! +//! [`jq`]: https://stedolan.github.io/jq/ + +pub mod selector; + +pub mod predicate; +pub use predicate::*; diff --git a/src/delegation/policy/predicate.rs b/src/delegation/policy/predicate.rs new file mode 100644 index 00000000..18ee1b98 --- /dev/null +++ b/src/delegation/policy/predicate.rs @@ -0,0 +1,1758 @@ +use super::selector::filter::Filter; +use super::selector::{Select, SelectorError}; +use crate::ipld; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use std::str::FromStr; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq)] +pub enum Predicate { + // Comparison + Equal(Select, ipld::Newtype), + + GreaterThan(Select, ipld::Number), + GreaterThanOrEqual(Select, ipld::Number), + + LessThan(Select, ipld::Number), + LessThanOrEqual(Select, ipld::Number), + + Like(Select, String), + + // Connectives + Not(Box), + And(Box, Box), + Or(Box, Box), + + // Collection iteration + Every(Select, Box), // ∀x ∈ xs + Some(Select, Box), // ∃x ∈ xs +} + +#[derive(Debug, Clone, PartialEq, EnumAsInner)] +pub enum Harmonization { + Equal, // e.g. x > 10 vs x > 10 + Conflict, // e.g. x == 1 vs x == 2 + LhsWeaker, // e.g. x > 10 vs x > 100 (AKA compatible but rhs narrower than lhs) + LhsStronger, // e.g. x > 10 vs x > 1 (AKA compatible lhs narrower than rhs) + StrongerTogether, // e.g. x > 10 vs x < 100 (AKA both narrow each other) + IncomparablePath, // e.g. .foo and .bar +} + +impl Harmonization { + pub fn complement(self) -> Self { + match self { + Harmonization::Equal => Harmonization::Conflict, + Harmonization::Conflict => Harmonization::Equal, // FIXME Correct? + Harmonization::LhsWeaker => Harmonization::LhsStronger, + Harmonization::LhsStronger => Harmonization::LhsWeaker, + Harmonization::StrongerTogether => Harmonization::StrongerTogether, + Harmonization::IncomparablePath => Harmonization::IncomparablePath, + } + } + + pub fn flip(self) -> Self { + match self { + Harmonization::Equal => Harmonization::Equal, + Harmonization::Conflict => Harmonization::Conflict, + Harmonization::LhsWeaker => Harmonization::LhsStronger, + Harmonization::LhsStronger => Harmonization::LhsWeaker, + Harmonization::StrongerTogether => Harmonization::StrongerTogether, + Harmonization::IncomparablePath => Harmonization::IncomparablePath, + } + } +} + +impl Predicate { + // FIXME make &self? + pub fn run(self, data: &Ipld) -> Result { + Ok(match self { + Predicate::Equal(lhs, rhs_data) => lhs.get(data)? == rhs_data, + Predicate::GreaterThan(lhs, rhs_data) => lhs.get(data)? > rhs_data, + Predicate::GreaterThanOrEqual(lhs, rhs_data) => lhs.get(data)? >= rhs_data, + Predicate::LessThan(lhs, rhs_data) => lhs.get(data)? < rhs_data, + Predicate::LessThanOrEqual(lhs, rhs_data) => lhs.get(data)? <= rhs_data, + Predicate::Like(lhs, rhs_data) => glob(&lhs.get(data)?, &rhs_data), + Predicate::Not(inner) => !inner.run(data)?, + Predicate::And(lhs, rhs) => lhs.run(data)? && rhs.run(data)?, + Predicate::Or(lhs, rhs) => lhs.run(data)? || rhs.run(data)?, + Predicate::Every(xs, p) => xs + .get(data)? + .to_vec() + .iter() + .try_fold(true, |acc, each_datum| { + Ok(acc && p.clone().run(&each_datum.0)?) + })?, + Predicate::Some(xs, p) => xs + .get(data)? + .to_vec() + .iter() + .try_fold(false, |acc, each_datum| { + Ok(acc || p.clone().run(&each_datum.0)?) + })?, + }) + } + + // FIXME check paths are subsets, becase that changes some of these + pub fn harmonize( + &self, + other: &Self, + lhs_ctx: Vec, + rhs_ctx: Vec, + ) -> Harmonization { + match (self, other) { + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::Equal(rhs_selector, rhs_ipld), + ) => { + // FIXME include ctx in path? + if lhs_selector.is_related(rhs_selector) { + if lhs_ipld == rhs_ipld { + Harmonization::Equal + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + // FIXME lhs + rhs selector must be exact + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num > *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num >= *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num < *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num <= *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + /*********** + * Strings * + ***********/ + (Predicate::Like(lhs_selector, lhs_str), Predicate::Like(rhs_selector, rhs_str)) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_str == rhs_str { + Harmonization::Equal + } else { + // FIXME actually not accurate; need to walk both in case of inner patterns + match (glob(lhs_str, rhs_str), glob(rhs_str, lhs_str)) { + (true, true) => Harmonization::StrongerTogether, + _ => Harmonization::Conflict, + } + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + (Predicate::Equal(lhs_selector, lhs_ipld), Predicate::Like(rhs_selector, rhs_str)) => { + if lhs_selector.is_related(rhs_selector) { + if let Ipld::String(lhs_str) = &lhs_ipld.0 { + if glob(&lhs_str, rhs_str) { + // FIXME? + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + // NOTE Predicate::Like forces this to unify as a string, so anything else fails + // ...so this is not *not* a type checker + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + (lhs @ Predicate::Like(_, _), rhs @ Predicate::Equal(_, _)) => { + rhs.harmonize(lhs, rhs_ctx, lhs_ctx).complement() + } + + /**************** + * Greater Than * + ***************/ + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num > rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num < rhs_num { + Harmonization::LhsWeaker + } else { + Harmonization::LhsStronger + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /************************* + * Greater Than Or Equal * + *************************/ + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num > rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num <= rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num < rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /********************** + * Less Than Or Equal * + **********************/ + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::LhsWeaker + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num >= rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /************* + * Less Than * + *************/ + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::LhsStronger + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /*************** + * Connectives * + ***************/ + (_self, Predicate::Not(rhs_inner)) => { + self.harmonize(rhs_inner, lhs_ctx, rhs_ctx).complement() + } + (Predicate::Not(lhs_inner), rhs) => { + lhs_inner.harmonize(rhs, lhs_ctx, rhs_ctx).complement() + } + (_self, Predicate::And(and_left, and_right)) => { + let rhs_raw_pred1: Predicate = *and_left.clone(); + let rhs_raw_pred2: Predicate = *and_right.clone(); + + match ( + self.harmonize(&rhs_raw_pred1, lhs_ctx.clone(), rhs_ctx.clone()), + self.harmonize(&rhs_raw_pred2, lhs_ctx, rhs_ctx), + ) { + (Harmonization::Conflict, _) => Harmonization::Conflict, + (_, Harmonization::Conflict) => Harmonization::Conflict, + (Harmonization::IncomparablePath, right) => right, + (left, Harmonization::IncomparablePath) => left, + (Harmonization::Equal, rhs) => rhs, + (lhs, Harmonization::Equal) => lhs, + (Harmonization::LhsWeaker, Harmonization::LhsWeaker) => { + Harmonization::LhsWeaker + } + (Harmonization::LhsStronger, Harmonization::LhsStronger) => { + Harmonization::LhsStronger + } + (Harmonization::LhsStronger, Harmonization::LhsWeaker) => { + Harmonization::StrongerTogether + } + (Harmonization::LhsWeaker, Harmonization::LhsStronger) => { + Harmonization::StrongerTogether + } + (Harmonization::StrongerTogether, _) => Harmonization::StrongerTogether, + (_, Harmonization::StrongerTogether) => Harmonization::StrongerTogether, + } + } + (lhs @ Predicate::And(_, _), rhs) => lhs.harmonize(rhs, lhs_ctx, rhs_ctx).flip(), + (_self, Predicate::Or(or_left, or_right)) => { + let rhs_raw_pred1: Predicate = *or_left.clone(); + let rhs_raw_pred2: Predicate = *or_right.clone(); + + match ( + self.harmonize(&rhs_raw_pred1, lhs_ctx.clone(), rhs_ctx.clone()), + self.harmonize(&rhs_raw_pred2, lhs_ctx, rhs_ctx), + ) { + (Harmonization::Conflict, Harmonization::Conflict) => Harmonization::Conflict, + (lhs, Harmonization::Conflict) => lhs, + (Harmonization::Conflict, rhs) => rhs, + (Harmonization::IncomparablePath, right) => right, + (left, Harmonization::IncomparablePath) => left, + (Harmonization::Equal, rhs) => rhs, + (lhs, Harmonization::Equal) => lhs, + (Harmonization::LhsWeaker, Harmonization::LhsWeaker) => { + Harmonization::LhsWeaker + } + (Harmonization::LhsStronger, Harmonization::LhsStronger) => { + Harmonization::LhsStronger + } + (_, Harmonization::LhsWeaker) => Harmonization::LhsWeaker, + (Harmonization::LhsWeaker, _) => Harmonization::LhsWeaker, + (Harmonization::LhsStronger, Harmonization::StrongerTogether) => { + Harmonization::LhsStronger + } + (Harmonization::StrongerTogether, Harmonization::LhsStronger) => { + Harmonization::LhsStronger + } + (Harmonization::StrongerTogether, Harmonization::StrongerTogether) => { + Harmonization::StrongerTogether + } + } + } + (lhs @ Predicate::Or(_, _), rhs) => lhs.harmonize(rhs, lhs_ctx, rhs_ctx).flip(), + // /****************** + // * Quantification * + // ******************/ + // Predicate::Every(rhs_selector, rhs_inner) => { + // let rhs_raw_pred: Predicate = *rhs_inner.clone(); + // // TODO FIXME exact path + // todo!() + // // match self.harmonize(&rhs_raw_pred, lhs_ctx, rhs_ctx) { + // // Harmonization::LhsPassed => Harmonization::LhsPassed, + // // Harmonization::LhsWeaker => Harmonization::LhsWeaker, + // // Harmonization::IncomparablePath => Harmonization::IncomparablePath, + // // Harmonization::Conflict => { + // // Harmonization::Conflict + // // } + // // } + // } + // Predicate::Some(rhs_selector, rhs_inner) => { + // let rhs_raw_pred: Predicate = *rhs_inner.clone(); + // // TODO FIXME As long as the lhs path doens't terminate earlier, then pass + // todo!() + // // match self.harmonize(&rhs_raw_pred, lhs_ctx, rhs_ctx) { + // // Harmonization::LhsPassed => Harmonization::LhsPassed, + // // Harmonization::LhsWeaker => Harmonization::LhsWeaker, + // // Harmonization::IncomparablePath => Harmonization::IncomparablePath, + // // Harmonization::Conflict => { + // // Harmonization::Conflict + // // } + // // } + // } + // }, + _ => todo!(), + } + } +} + +pub fn glob(input: &str, pattern: &str) -> bool { + if pattern.is_empty() { + return input == ""; + } + + // Parsing pattern + let (saw_escape, mut patterns, mut working) = pattern.chars().fold( + (false, vec![], "".to_string()), + |(saw_escape, mut acc, mut working), c| { + match c { + '*' => { + if saw_escape { + working.push('*'); + (false, acc, working) + } else { + acc.push(working); + working = "".to_string(); + (false, acc, working) + } + } + '\\' => { + if saw_escape { + // Push prev escape + working.push('\\'); + } + (true, acc, working) + } + _ => { + if saw_escape { + working.push('\\'); + } + + working.push(c); + (false, acc, working) + } + } + }, + ); + + if saw_escape { + working.push('\\'); + } + + patterns.push(working); + + // Test input against the pattern + patterns + .iter() + .enumerate() + .try_fold(input, |acc, (idx, pattern_frag)| { + if let Some((pre, post)) = acc.split_once(pattern_frag) { + if idx == 0 && !pattern.starts_with("*") && !pre.is_empty() { + Err(()) + } else if idx == patterns.len() - 1 && !pattern.ends_with("*") && !post.is_empty() { + Err(()) + } else { + Ok(post) + } + } else { + Err(()) + } + }) + .is_ok() +} + +impl TryFrom for Predicate { + type Error = FromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::List(v) => match v.as_slice() { + [Ipld::String(s), inner] if s == "not" => { + let inner = Box::new(Predicate::try_from(inner.clone())?); + Ok(Predicate::Not(inner)) + } + [Ipld::String(op_str), Ipld::String(sel_str), val] => match op_str.as_str() { + "==" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidIpldSelector)?; + + Ok(Predicate::Equal(sel, ipld::Newtype(val.clone()))) + } + ">" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + + Ok(Predicate::GreaterThan(sel, num)) + } + ">=" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + Ok(Predicate::GreaterThanOrEqual(sel, num)) + } + "<" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + + Ok(Predicate::LessThan(sel, num)) + } + "<=" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + + Ok(Predicate::LessThanOrEqual(sel, num)) + } + "like" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidStringSelector)?; + + if let Ipld::String(s) = val { + Ok(Predicate::Like(sel, s.to_string())) + } else { + Err(FromIpldError::NotAString(val.clone())) + } + } + "every" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidCollectionSelector)?; + + let p = Box::new(Predicate::try_from(val.clone())?); + Ok(Predicate::Every(sel, p)) + } + "some" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidCollectionSelector)?; + + let p = Box::new(Predicate::try_from(val.clone())?); + Ok(Predicate::Some(sel, p)) + } + _ => Err(FromIpldError::UnrecognizedTripleTag(op_str.to_string())), + }, + [Ipld::String(op_str), lhs, rhs] => match op_str.as_str() { + "and" => { + let lhs = Box::new(Predicate::try_from(lhs.clone())?); + let rhs = Box::new(Predicate::try_from(rhs.clone())?); + Ok(Predicate::And(lhs, rhs)) + } + "or" => { + let lhs = Box::new(Predicate::try_from(lhs.clone())?); + let rhs = Box::new(Predicate::try_from(rhs.clone())?); + Ok(Predicate::Or(lhs, rhs)) + } + _ => Err(FromIpldError::UnrecognizedTripleTag(op_str.to_string())), + }, + _ => Err(FromIpldError::UnrecognizedShape), + }, + _ => Err(FromIpldError::NotATuple(ipld)), + } + } +} + +#[derive(Debug, PartialEq, Error)] +pub enum FromIpldError { + #[error("Invalid Ipld selector {0:?}")] + InvalidIpldSelector( as FromStr>::Err), + + #[error("Invalid ipld::Number selector {0:?}")] + InvalidNumberSelector( as FromStr>::Err), + + #[error("Invalid ipld::Collection selector {0:?}")] + InvalidCollectionSelector( as FromStr>::Err), + + #[error("Invalid String selector {0:?}")] + InvalidStringSelector( as FromStr>::Err), + + #[error("Cannot parse ipld::Number {0:?}")] + CannotParseIpldNumber(>::Error), + + #[error("Not a string: {0:?}")] + NotAString(Ipld), + + #[error("Unrecognized triple tag {0}")] + UnrecognizedTripleTag(String), + + #[error("Unrecognized shape")] + UnrecognizedShape, + + #[error("Not a predicate tuple {0:?}")] + NotATuple(Ipld), +} + +impl From for Ipld { + fn from(p: Predicate) -> Self { + match p { + Predicate::Equal(lhs, rhs) => { + Ipld::List(vec![Ipld::String("==".to_string()), lhs.into(), rhs.into()]) + } + Predicate::GreaterThan(lhs, rhs) => { + Ipld::List(vec![Ipld::String(">".to_string()), lhs.into(), rhs.into()]) + } + Predicate::GreaterThanOrEqual(lhs, rhs) => { + Ipld::List(vec![Ipld::String(">=".to_string()), lhs.into(), rhs.into()]) + } + Predicate::LessThan(lhs, rhs) => { + Ipld::List(vec![Ipld::String("<".to_string()), lhs.into(), rhs.into()]) + } + Predicate::LessThanOrEqual(lhs, rhs) => { + Ipld::List(vec![Ipld::String("<=".to_string()), lhs.into(), rhs.into()]) + } + Predicate::Like(lhs, rhs) => Ipld::List(vec![ + Ipld::String("like".to_string()), + lhs.into(), + rhs.into(), + ]), + Predicate::Not(inner) => { + let unboxed = *inner; + Ipld::List(vec![Ipld::String("not".to_string()), unboxed.into()]) + } + Predicate::And(lhs, rhs) => Ipld::List(vec![ + Ipld::String("and".to_string()), + (*lhs).into(), + (*rhs).into(), + ]), + Predicate::Or(lhs, rhs) => Ipld::List(vec![ + Ipld::String("or".to_string()), + (*lhs).into(), + (*rhs).into(), + ]), + Predicate::Every(xs, p) => Ipld::List(vec![ + Ipld::String("every".to_string()), + xs.into(), + (*p).into(), + ]), + Predicate::Some(xs, p) => Ipld::List(vec![ + Ipld::String("some".to_string()), + xs.into(), + (*p).into(), + ]), + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Predicate { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_params: Self::Parameters) -> Self::Strategy { + let leaf = prop_oneof![ + (Select::arbitrary(), ipld::Newtype::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::Equal(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::GreaterThan(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::GreaterThanOrEqual(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::LessThan(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::LessThanOrEqual(lhs, rhs) }), + (Select::arbitrary(), String::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::Like(lhs, rhs) }) + ]; + + let connective = leaf.clone().prop_recursive(8, 16, 4, |inner| { + prop_oneof![ + (inner.clone(), inner.clone()) + .prop_map(|(lhs, rhs)| { Predicate::And(Box::new(lhs), Box::new(rhs)) }), + (inner.clone(), inner.clone()) + .prop_map(|(lhs, rhs)| { Predicate::Or(Box::new(lhs), Box::new(rhs)) }), + ] + }); + + let quantified = leaf.clone().prop_recursive(8, 16, 4, |inner| { + prop_oneof![ + (Select::arbitrary(), inner.clone()) + .prop_map(|(xs, p)| { Predicate::Every(xs, Box::new(p)) }), + (Select::arbitrary(), inner.clone()) + .prop_map(|(xs, p)| { Predicate::Some(xs, Box::new(p)) }), + ] + }); + + prop_oneof![leaf, connective, quantified].boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod glob { + use super::*; + + #[test_log::test] + fn test_concrete() -> TestResult { + let got = glob(&"hello world", &"hello world"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_concrete_fail() -> TestResult { + let got = glob(&"hello world", &"NOPE"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_empty_pattern_fail() -> TestResult { + let got = glob(&"hello world", &""); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_escaped_star() -> TestResult { + let got = glob(&"*", &r#"\*"#); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_inner_escaped_star() -> TestResult { + let got = glob(&"hello, * world*", &r#"hello*\**\*"#); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_empty_string_fail() -> TestResult { + let got = glob(&"", &"NOPE"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_left_star() -> TestResult { + let got = glob(&"hello world", &"*world"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_left_star_failure() -> TestResult { + let got = glob(&"hello world", &"*NOPE"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_right_star() -> TestResult { + let got = glob(&"hello world", &"hello*"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_right_star_failure() -> TestResult { + let got = glob(&"hello world", &"NOPE*"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_only_star() -> TestResult { + let got = glob(&"hello world", &"*"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_two_stars() -> TestResult { + let got = glob(&"hello world", &"* *"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_two_stars_fail() -> TestResult { + let got = glob(&"hello world", &"*@*"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_multiple_inner_stars() -> TestResult { + let got = glob(&"hello world", &"h*l*o*w*r*d"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_multiple_inner_stars_fail() -> TestResult { + let got = glob(&"hello world", &"a*b*c*d*e*f"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_concrete_with_multiple_inner_stars() -> TestResult { + let got = glob(&"hello world", &"hello* *world"); + assert!(got); + Ok(()) + } + } + + mod run { + use super::*; + use libipld::ipld; + + fn simple() -> Ipld { + ipld!({ + "foo": 42, + "bar": "baz".to_string(), + "qux": true + }) + } + + fn email() -> Ipld { + ipld!({ + "from": "alice@example.com", + "to": ["bob@example.com", "fraud@example.com"], + "cc": ["carol@example.com"], + "subject": "Quarterly Reports", + "body": "Here's Q2 the reports ..." + }) + } + + fn wasm() -> Ipld { + ipld!({ + "mod": "data:application/wasm;base64,SOMEBASE64GOESHERE", + "fun": "test", + "input": [0, 1, 2 ,3] + }) + } + + #[test_log::test] + fn test_eq() -> TestResult { + let p = Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".not_from?").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_dot_field_ending_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from.not?").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_dot_field_inner_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".nope?.not").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_root_try_not_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".?").unwrap(), Ipld::Null.into()); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_try_not_null() -> TestResult { + let p = Predicate::Equal( + Select::from_str(".from?").unwrap(), + "alice@example.com".into(), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_nested_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from?.not?").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_fail_same_type() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from").unwrap(), "NOPE".into()); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_bad_selector() -> TestResult { + let p = Predicate::Equal( + Select::from_str(".NOPE").unwrap(), + "alice@example.com".into(), + ); + + assert!(p.run(&email()).is_err()); + Ok(()) + } + + #[test_log::test] + fn test_eq_fail_different_type() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from").unwrap(), 42.into()); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_gt() -> TestResult { + let p = Predicate::GreaterThan(Select::from_str(".foo").unwrap(), (41.9).into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_gt_fail() -> TestResult { + let p = Predicate::GreaterThan(Select::from_str(".foo").unwrap(), 42.into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_gte() -> TestResult { + let p = Predicate::GreaterThanOrEqual(Select::from_str(".foo").unwrap(), 42.into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_gte_fail() -> TestResult { + let p = Predicate::GreaterThanOrEqual(Select::from_str(".foo").unwrap(), (42.1).into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lt() -> TestResult { + let p = Predicate::LessThan(Select::from_str(".foo").unwrap(), (42.1).into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lt_fail() -> TestResult { + let p = Predicate::LessThan(Select::from_str(".foo").unwrap(), 42.into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lte() -> TestResult { + let p = Predicate::LessThanOrEqual(Select::from_str(".foo").unwrap(), 42.into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lte_fail() -> TestResult { + let p = Predicate::LessThanOrEqual(Select::from_str(".foo").unwrap(), (41.9).into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_like() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "alice@*".into()); + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_concrete() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "NOPE".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_left_star() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "*NOPE".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_right_star() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "NOPE*".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_both_stars() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "*NOPE*".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_not() -> TestResult { + let p = Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + ))); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_double_negative() -> TestResult { + let p = Predicate::Not(Box::new(Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + ))))); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_not_fail() -> TestResult { + let p = Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + ))); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_both_succeed() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_left_fail() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_right_fail() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_both_fail() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_both_succeed() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_left_fail() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_right_fail() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_both_fail() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + // FIXME nested, too + #[test_log::test] + fn test_every() -> TestResult { + let p = Predicate::Every( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 100.into(), + )), + ); + + assert!(p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_every_failure() -> TestResult { + let p = Predicate::Every( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 1.into(), + )), + ); + + assert!(!p.run(&wasm())?); + Ok(()) + } + + // FIXME nested, too + #[test_log::test] + fn test_some_all_succeed() -> TestResult { + let p = Predicate::Some( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 100.into(), + )), + ); + + assert!(p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_some_not_all() -> TestResult { + let p = Predicate::Some( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 1.into(), + )), + ); + + assert!(p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_some_all_fail() -> TestResult { + let p = Predicate::Some( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 0.into(), + )), + ); + + assert!(!p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_alternate_every_and_some() -> TestResult { + // ["every", ".a", ["some", ".b[]", ["==", ".", 0]]] + let p = Predicate::Every( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Some( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope, but ok because "some" + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [-1, 0, 1] + } + ] + } + ); + + assert!(p.run(&nested_data)?); + Ok(()) + } + + #[test_log::test] + fn test_alternate_fail_every_and_some() -> TestResult { + // ["every", ".a", ["some", ".b[]", ["==", ".", 0]]] + let p = Predicate::Every( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Some( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope, but ok because "some" + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [-1, 42, 1] // No 0, so fail "every" + } + ] + } + ); + + assert!(!p.run(&nested_data)?); + Ok(()) + } + + // FIXME + #[test_log::test] + fn test_alternate_some_and_every() -> TestResult { + // ["some", ".a", ["every", ".b[]", ["==", ".", 0]]] + let p = Predicate::Some( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope, so fail this every, but... + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [0, 0, 0] // This every succeeds, so the outer "some" succeeds + } + ] + } + ); + + assert!(p.run(&nested_data)?); + Ok(()) + } + + // FIXME + #[test_log::test] + fn test_alternate_fail_some_and_every() -> TestResult { + // ["some", ".a", ["every", ".b[]", ["==", ".", 0]]] + let p = Predicate::Some( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [-1, 42, 1] // Also nope, so fail + } + ] + } + ); + + assert!(!p.run(&nested_data)?); + Ok(()) + } + + #[test_log::test] + fn test_deeply_alternate_some_and_every() -> TestResult { + // ["some", ".a", + // ["every", ".b.c[]", + // ["some", ".d", + // ["every", ".e[]", + // ["==", ".f.g", 0] + // ] + // ] + // ] + // ] + let p = Predicate::Some( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".b.c[]").unwrap(), + Box::new(Predicate::Some( + Select::from_str(".d").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".e[]").unwrap(), + Box::new(Predicate::Equal( + Select::from_str(".f.g").unwrap(), + 0.into(), + )), + )), + )), + )), + ); + + let deeply_nested_data = ipld!( + { + // Some + "a": [ + { + "b": { + "c": { + // Every + "c1": { + // Some + "d": [ + { + // Every + "e": { + "e1": { + "f": { + "g": 0 + }, + "nope": -10 + }, + "e2": { + "_": "not selected", + "f": { + "g": 0 + }, + } + } + } + ] + }, + "c2": { + // Some + "*": "avoid", + "d": [ + { + // Every + "e": { + "e1": { + "f": { + "g": 0 + }, + "nope": -10 + }, + "e2": { + "_": "not selected", + "f": { + "g": 0 + }, + } + } + } + ] + } + } + } + } + ], + "z": "doesn't read this" + } + ); + + assert!(p.run(&deeply_nested_data)?); + Ok(()) + } + } +} diff --git a/src/delegation/policy/selector.rs b/src/delegation/policy/selector.rs new file mode 100644 index 00000000..f33cc7f3 --- /dev/null +++ b/src/delegation/policy/selector.rs @@ -0,0 +1,324 @@ +pub mod filter; + +mod error; +mod select; +mod selectable; + +pub use error::{ParseError, SelectorErrorReason}; +pub use select::Select; +pub use selectable::Selectable; + +use filter::Filter; +use nom::{self, character::complete::char, multi::many0, sequence::preceded}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::cmp::Ordering; +use std::{fmt, str::FromStr}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Selector(pub Vec); + +impl Selector { + pub fn new() -> Self { + Selector(vec![]) + } + + pub fn is_related(&self, other: &Selector) -> bool { + self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b) + } +} + +impl fmt::Display for Selector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ops = self.0.iter(); + + if let Some(field) = ops.next() { + if !field.is_dot_field() { + write!(f, ".")?; + } + + write!(f, "{}", field)?; + } else { + write!(f, ".")?; + } + + for op in ops { + op.fmt(f)?; + } + + Ok(()) + } +} + +impl FromStr for Selector { + type Err = nom::Err; + + fn from_str(s: &str) -> Result { + if !s.starts_with(".") { + return Err(nom::Err::Error(ParseError::MissingStartingDot( + s.to_string(), + ))); + } + + if s.starts_with("..") { + return Err(nom::Err::Error(ParseError::StartsWithDoubleDot( + s.to_string(), + ))); + } + + let working; + let mut acc = vec![]; + + if let Ok((more, found)) = + nom::branch::alt((filter::parse_try_dot_field, filter::parse_dot_field))(s) + { + working = more; + acc.push(found); + } else { + working = &s[1..]; + } + + match preceded(many0(char('?')), many0(filter::parse))(working) { + Ok(("", ops)) => { + let mut mut_ops = ops.clone(); + acc.append(&mut mut_ops); + Ok(Selector(acc)) + } + Ok((more, _ops)) => Err(nom::Err::Error(ParseError::TrailingInput(more.to_string()))), + Err(err) => Err(err.map(|input| ParseError::UnknownPattern(input.to_string()))), + } + } +} +impl Serialize for Selector { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Selector { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Selector::from_str(&s).map_err(serde::de::Error::custom) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +#[error("Selector {selector} encountered runtime error: {reason}")] +pub struct SelectorError { + pub selector: Selector, + pub reason: SelectorErrorReason, +} + +impl SelectorError { + pub fn from_refs(path_refs: &Vec<&Filter>, reason: SelectorErrorReason) -> SelectorError { + SelectorError { + selector: Selector(path_refs.iter().map(|op| (*op).clone()).collect()), + reason, + } + } +} + +impl PartialOrd for Selector { + fn partial_cmp(&self, other: &Self) -> Option { + if self == other { + return Some(Ordering::Equal); + } + + if self.0.starts_with(&other.0) { + return Some(Ordering::Greater); + } + + if other.0.starts_with(&self.0) { + return Some(Ordering::Less); + } + + None + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Selector { + type Parameters = ::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + prop::collection::vec(Filter::arbitrary_with(args), 0..12) + .prop_map(|ops| Selector(ops)) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod serialization { + use super::*; + + proptest! { + #[test] + fn test_selector_round_trip(sel: Selector) { + let serialized = sel.to_string(); + let deserialized = serialized.parse(); + prop_assert_eq!(Ok(sel), deserialized); + } + } + + #[test_log::test] + fn test_bare_dot() -> TestResult { + pretty::assert_eq!(Selector::from_str("."), Ok(Selector(vec![]))); + Ok(()) + } + + #[test_log::test] + fn test_dot_try() -> TestResult { + pretty::assert_eq!(Selector::from_str(".?"), Ok(Selector(vec![]))); + Ok(()) + } + + #[test_log::test] + fn test_dot_many_tries() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".?????????????????????"), + Ok(Selector(vec![])) + ); + Ok(()) + } + + #[test_log::test] + fn test_inner_try_is_null() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".nope?.not"), + Ok(Selector(vec![ + Filter::Try(Box::new(Filter::Field("nope".into()))), + Filter::Field("not".into()) + ])) + ); + Ok(()) + } + + #[test_log::test] + fn test_dot_many_tries_and_dot_field() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".?????????????????????.foo"), + Ok(Selector(vec![Filter::Field("foo".to_string())])) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_question_marks() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".foo??????????????"), + Ok(Selector(vec![Filter::Try(Box::new(Filter::Field( + "foo".to_string() + )))])) + ); + Ok(()) + } + + #[test_log::test] + fn test_fails_trailing_dot() -> TestResult { + let got = Selector::from_str(".foo."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_leading_double_dot() -> TestResult { + let got = Selector::from_str("..foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_inner_double_dot() -> TestResult { + let got = Selector::from_str(".foo..bar"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_multiple_leading_dots() -> TestResult { + let got = Selector::from_str(".."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fail_missing_leading_dot() -> TestResult { + let got = Selector::from_str("[22]"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_dot_field() -> TestResult { + let got = Selector::from_str(".foo"); + pretty::assert_eq!(got, Ok(Selector(vec![Filter::Field("foo".to_string())]))); + Ok(()) + } + + #[test_log::test] + fn test_multiple_dot_fields() -> TestResult { + let got = Selector::from_str(".foo.bar.baz"); + pretty::assert_eq!( + got, + Ok(Selector(vec![ + Filter::Field("foo".to_string()), + Filter::Field("bar".to_string()), + Filter::Field("baz".to_string()) + ])) + ); + Ok(()) + } + + #[test_log::test] + fn test_fairly_complex() -> TestResult { + let got = Selector::from_str(r#".foo.bar[].baz[0][]["42"]._quux?[8]"#); + pretty::assert_eq!( + got, + Ok(Selector(vec![ + Filter::Field("foo".to_string()), + Filter::Field("bar".to_string()), + Filter::Values, + Filter::Field("baz".to_string()), + Filter::ArrayIndex(0), + Filter::Values, + Filter::Field("42".to_string()), + Filter::Try(Box::new(Filter::Field("_quux".to_string()))), + Filter::ArrayIndex(8) + ])) + ); + + Ok(()) + } + + #[test_log::test] + fn test_very_complex() -> TestResult { + let got = Selector::from_str(r#".???.foo.bar[].baz[0][]["42"]._quux??[8]"#); + pretty::assert_eq!( + got, + Ok(Selector(vec![ + Filter::Field("foo".to_string()), + Filter::Field("bar".to_string()), + Filter::Values, + Filter::Field("baz".to_string()), + Filter::ArrayIndex(0), + Filter::Values, + Filter::Field("42".to_string()), + Filter::Try(Box::new(Filter::Field("_quux".to_string()))), + Filter::ArrayIndex(8) + ])) + ); + + Ok(()) + } + } +} diff --git a/src/delegation/policy/selector/error.rs b/src/delegation/policy/selector/error.rs new file mode 100644 index 00000000..37f663d6 --- /dev/null +++ b/src/delegation/policy/selector/error.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq, Serialize, Deserialize)] +pub enum ParseError { + #[error("unmatched trailing input")] + TrailingInput(String), + + #[error("unknown pattern: {0}")] + UnknownPattern(String), + + #[error("missing starting dot: {0}")] + MissingStartingDot(String), + + #[error("starts with double dot: {0}")] + StartsWithDoubleDot(String), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Error)] +pub enum SelectorErrorReason { + #[error("Index out of bounds")] + IndexOutOfBounds, + + #[error("Key not found")] + KeyNotFound, + + #[error("Not a list")] + NotAList, + + #[error("Not a map")] + NotAMap, + + #[error("Not a collection")] + NotACollection, + + #[error("Not a number")] + NotANumber, + + #[error("Not a string")] + NotAString, +} diff --git a/src/delegation/policy/selector/filter.rs b/src/delegation/policy/selector/filter.rs new file mode 100644 index 00000000..a9d089d2 --- /dev/null +++ b/src/delegation/policy/selector/filter.rs @@ -0,0 +1,600 @@ +use super::error::ParseError; +use enum_as_inner::EnumAsInner; +use nom::{ + self, + branch::alt, + bytes::complete::tag, + character::complete::{alphanumeric1, char, digit1}, + combinator::map_res, + error::context, + multi::many1, + sequence::{delimited, preceded, terminated}, + IResult, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::{fmt, str::FromStr}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, EnumAsInner)] +pub enum Filter { + ArrayIndex(i32), // [2] + Field(String), // ["key"] (or .key) + Values, // .[] + Try(Box), // ? +} + +impl Filter { + pub fn is_in(&self, other: &Self) -> bool { + match (self, other) { + (Filter::ArrayIndex(a), Filter::ArrayIndex(b)) => a == b, + (Filter::Field(a), Filter::Field(b)) => a == b, + (Filter::Values, Filter::Values) => true, + (Filter::ArrayIndex(_a), Filter::Values) => true, + (Filter::Field(_k), Filter::Values) => true, + (Filter::Try(a), Filter::Try(b)) => a.is_in(b), + _ => false, + } + } + + pub fn is_dot_field(&self) -> bool { + match self { + Filter::Field(k) => { + if let Some(first) = k.chars().next() { + (first.is_alphabetic() || first == '_') + && k.chars().all(|c| char::is_alphanumeric(c) || c == '_') + } else { + false + } + } + _ => false, + } + } +} + +impl fmt::Display for Filter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Filter::ArrayIndex(i) => write!(f, "[{}]", i), + Filter::Field(k) => { + if self.is_dot_field() { + write!(f, ".{}", k) + } else { + write!(f, "[\"{}\"]", k) + } + } + Filter::Values => write!(f, "[]"), + Filter::Try(inner) => write!(f, "{}?", inner), + } + } +} + +pub fn parse(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_try, parse_non_try)); + context("selector_op", p)(input) +} + +pub fn parse_try(input: &str) -> IResult<&str, Filter> { + let p = map_res( + terminated(parse_non_try, many1(tag("?"))), + |found: Filter| Ok::(Filter::Try(Box::new(found))), + ); + + context("try", p)(input) +} + +pub fn parse_try_dot_field(input: &str) -> IResult<&str, Filter> { + let p = map_res( + terminated(parse_dot_field, many1(tag("?"))), + |found: Filter| Ok::(Filter::Try(Box::new(found))), + ); + + context("try", p)(input) +} + +pub fn parse_non_try(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_values, parse_field, parse_array_index)); + context("non_try", p)(input) +} + +pub fn parse_array_index(input: &str) -> IResult<&str, Filter> { + let num = nom::combinator::recognize(preceded(nom::combinator::opt(tag("-")), digit1)); + + let array_index = map_res(delimited(char('['), num, char(']')), |found| { + let idx = i32::from_str(found).map_err(|_| ())?; + Ok::(Filter::ArrayIndex(idx)) + }); + + context("array_index", array_index)(input) +} + +pub fn parse_values(input: &str) -> IResult<&str, Filter> { + context("values", tag("[]"))(input).map(|(rest, _)| (rest, Filter::Values)) +} + +pub fn parse_field(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_delim_field, parse_dot_field)); + + context("map_field", p)(input) +} + +pub fn parse_dot_field(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_dot_alpha_field, parse_dot_underscore_field)); + context("dot_field", p)(input) +} + +fn dot_starter(input: &str) -> IResult<&str, &str> { + if input.len() < 2 { + return Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag, + ))); + } + + let bytes = input.as_bytes(); + + if bytes[0] == b'.' { + if char::from(bytes[1]).is_alphabetic() || bytes[1] == b'_' { + return Ok((&input[2..], &input[..2])); + } + } + + Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag, + ))) +} + +fn is_allowed_in_dot_field(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +pub fn parse_dot_alpha_field(input: &str) -> IResult<&str, Filter> { + let p = map_res( + preceded( + dot_starter, + nom::multi::many0(nom::character::complete::satisfy(is_allowed_in_dot_field)), + ), + |found: Vec| { + let inner = [input.as_bytes()[1] as char] + .iter() + .chain(found.iter()) + .collect::(); + Ok::(Filter::Field(inner)) + }, + ); + + context("dot_field", p)(input) +} + +pub fn parse_dot_underscore_field(input: &str) -> IResult<&str, Filter> { + let p = map_res(preceded(tag("._"), alphanumeric1), |found: &str| { + let key = format!("{}{}", '_', found); + Ok::(Filter::Field(key)) + }); + + context("dot_field", p)(input) +} + +pub fn parse_empty_quotes_field(input: &str) -> IResult<&str, Filter> { + let p = map_res(tag("[\"\"]"), |_: &str| { + Ok::(Filter::Field("".to_string())) + }); + + context("empty_quotes_field", p)(input) +} + +pub fn unicode_or_space(input: &str) -> IResult<&str, &str> { + #[derive(Copy, Clone, PartialEq, Debug)] + enum Status { + Looking, + FoundQuote, + Done, + Failed, + } + + let (status, len) = + input + .as_bytes() + .iter() + .fold((Status::Looking, 0), |(status, len), byte| { + if status == Status::Failed { + return (status, len); + } + + if status == Status::Done { + return (status, len); + } + + let c = char::from(*byte); + + if status == Status::FoundQuote { + if c == ']' { + return (Status::Done, len + 1); + } else { + return (Status::Looking, len + 1); + } + } + + if c == '"' { + return (Status::FoundQuote, len + 1); + } + + if c == ' ' || (!nom_unicode::is_whitespace(c) && !nom_unicode::is_control(c)) { + return (Status::Looking, len + 1); + } + + (Status::Failed, 0) + }); + + match (status, len) { + (Status::Done, len) => Ok((&input[len - 2..], &input[..len - 2])), + _ => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::TakeWhile1, + ))), + } +} + +pub fn parse_delim_field(input: &str) -> IResult<&str, Filter> { + let p = map_res( + delimited(tag(r#"[""#), unicode_or_space, tag(r#""]"#)), + |found: &str| Ok::(Filter::Field(found.to_string())), + ); + + context("delimited_field", alt((p, parse_empty_quotes_field)))(input) +} + +impl FromStr for Filter { + type Err = nom::Err; + + fn from_str(s: &str) -> Result { + match parse(s).map_err(|e| nom::Err::Failure(ParseError::UnknownPattern(e.to_string())))? { + ("", found) => Ok(found), + (rest, _) => Err(nom::Err::Failure(ParseError::TrailingInput(rest.into()))), + } + } +} +impl Serialize for Filter { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Filter { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Filter::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Filter { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_params: Self::Parameters) -> Self::Strategy { + prop_oneof![ + i32::arbitrary().prop_map(|i| Filter::ArrayIndex(i)), + "[a-zA-Z_ ]*".prop_map(Filter::Field), + "[a-zA-Z_][a-zA-Z0-9_]*".prop_map(Filter::Field), + Just(Filter::Values), + // FIXME prop_recursive::lazy(|_| { Filter::arbitrary_with(()).prop_map(Filter::Try) }), + ] + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod serialization { + use super::*; + + proptest! { + #[test] + fn test_filter_round_trip(filter: Filter) { + let serialized = filter.to_string(); + let deserialized = serialized.parse(); + prop_assert_eq!(Ok(filter), deserialized); + } + } + + #[test_log::test] + fn test_fails_on_empty() -> TestResult { + let got = Filter::from_str(""); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_bare_dot() -> TestResult { + // NOTE this passes as a Selector, but not a Filter + let got = Filter::from_str("."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_multiple_bare_dots() -> TestResult { + let got = Filter::from_str(".."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_leading_dots() -> TestResult { + let got = Filter::from_str("..foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_empty_whitespace() -> TestResult { + let got = Filter::from_str(" "); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_leading_whitespace() -> TestResult { + let got = Filter::from_str(" .foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_trailing_whitespace() -> TestResult { + let got = Filter::from_str(".foo "); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_values() -> TestResult { + let got = Filter::from_str("[]"); + pretty::assert_eq!(got, Ok(Filter::Values)); + Ok(()) + } + + #[test_log::test] + fn test_values_fails_inner_whitespace() -> TestResult { + let got = Filter::from_str("[ ]"); + pretty::assert_eq!(got.is_err(), true); + Ok(()) + } + + #[test_log::test] + fn test_array_index_zero() -> TestResult { + let got = Filter::from_str("[0]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(0))); + Ok(()) + } + + #[test_log::test] + fn test_array_index_small() -> TestResult { + let got = Filter::from_str("[2]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(2))); + Ok(()) + } + + #[test_log::test] + fn test_array_index_large() -> TestResult { + let got = Filter::from_str("[1234567890]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(1234567890))); + Ok(()) + } + + #[test_log::test] + fn test_array_from_end() -> TestResult { + let got = Filter::from_str("[-42]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(-42))); + Ok(()) + } + + #[test_log::test] + fn test_array_fails_spaces() -> TestResult { + let got = Filter::from_str("[ 42]"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_dot_field() -> TestResult { + let got = Filter::from_str(".F0o"); + pretty::assert_eq!(got, Ok(Filter::Field("F0o".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_dot_field_starting_underscore() -> TestResult { + let got = Filter::from_str("._foo"); + pretty::assert_eq!(got, Ok(Filter::Field("_foo".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_dot_field_trailing_underscore() -> TestResult { + let got = Filter::from_str(".fO0_"); + pretty::assert_eq!(got, Ok(Filter::Field("fO0_".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_fails_dot_field_with_leading_number() -> TestResult { + let got = Filter::from_str(".1foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_dot_field_with_inner_symbol() -> TestResult { + let got = Filter::from_str(".fo%o"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_delim_field() -> TestResult { + let got = Filter::from_str(r#"["F0o"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("F0o".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_fails_without_quotes() -> TestResult { + let got = Filter::from_str(r#"[F0o]"#); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_fails_if_missing_right_brace() -> TestResult { + let got = Filter::from_str(r#"["F0o""#); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_starting_underscore() -> TestResult { + let got = Filter::from_str(r#"["_foo"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("_foo".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_trailing_underscore() -> TestResult { + let got = Filter::from_str(r#"["fO0_"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("fO0_".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_with_leading_number() -> TestResult { + let got = Filter::from_str(r#"["1foo"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("1foo".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_with_inner_symbol() -> TestResult { + let got = Filter::from_str(r#"[".fo%o"]"#); + pretty::assert_eq!(got, Ok(Filter::Field(".fo%o".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_try() -> TestResult { + let got = Filter::from_str(".foo?"); + pretty::assert_eq!( + got, + Ok(Filter::Try(Box::new(Filter::Field("foo".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_try() -> TestResult { + let got = parse(".foo?"); + pretty::assert_eq!( + got, + Ok(("", Filter::Try(Box::new(Filter::Field("foo".to_string()))))) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_dot_field() -> TestResult { + pretty::assert_eq!( + Filter::from_str(".foo???????????????????"), + Ok(Filter::Try(Box::new(Filter::Field("foo".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_multiple_tries_after_dot_field() -> TestResult { + pretty::assert_eq!( + parse(".foo???????????????????"), + Ok(("", Filter::Try(Box::new(Filter::Field("foo".to_string()))))) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_multiple_tries_after_dot_field_trailing() -> TestResult { + pretty::assert_eq!( + parse(".foo???????????????????abc"), + Ok(( + "abc", + Filter::Try(Box::new(Filter::Field("foo".to_string()))) + )) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_many0_multiple_tries_after_dot_field() -> TestResult { + pretty::assert_eq!( + nom::multi::many0(parse)(".foo???????????????????abc"), + Ok(( + "abc", + vec![Filter::Try(Box::new(Filter::Field("foo".to_string())))] + )) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_delim_field() -> TestResult { + pretty::assert_eq!( + Filter::from_str(r#"["foo"]???????"#), + Ok(Filter::Try(Box::new(Filter::Field("foo".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_delim_field_inner_questionmarks() -> TestResult { + let got = Filter::from_str(r#"["f?o"]???????"#); + pretty::assert_eq!( + got, + Ok(Filter::Try(Box::new(Filter::Field("f?o".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_values() -> TestResult { + let got = Filter::from_str("[]???????"); + pretty::assert_eq!(got, Ok(Filter::Try(Box::new(Filter::Values)))); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_index() -> TestResult { + let got = Filter::from_str("[42]???????"); + pretty::assert_eq!(got, Ok(Filter::Try(Box::new(Filter::ArrayIndex(42))))); + Ok(()) + } + + #[test_log::test] + fn test_fails_bare_try() -> TestResult { + let got = Filter::from_str("?"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_dot_try() -> TestResult { + let got = Filter::from_str(".?"); + assert!(got.is_err()); + Ok(()) + } + } +} diff --git a/src/delegation/policy/selector/select.rs b/src/delegation/policy/selector/select.rs new file mode 100644 index 00000000..67f3eaa9 --- /dev/null +++ b/src/delegation/policy/selector/select.rs @@ -0,0 +1,259 @@ +use super::Selector; +use super::{error::SelectorErrorReason, filter::Filter, Selectable, SelectorError}; +use libipld_core::ipld::Ipld; +use std::cmp::Ordering; +use std::fmt; +use std::str::FromStr; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Clone)] +pub struct Select { + filters: Vec, + _marker: std::marker::PhantomData, +} + +impl fmt::Debug for Select { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Select({:?})", self.filters) + } +} + +impl PartialEq for Select { + fn eq(&self, other: &Self) -> bool { + Selector(self.filters.clone()) == Selector(other.filters.clone()) + } +} + +impl Select { + pub fn new(filters: Vec) -> Self { + Self { + filters, + _marker: std::marker::PhantomData, + } + } + + pub fn is_related(&self, other: &Select) -> bool + where + Ipld: From + From, + { + Selector(self.filters.clone()).is_related(&Selector(other.filters.clone())) + } +} + +impl Select { + pub fn get(self, ctx: &Ipld) -> Result { + let got = self.filters.iter().try_fold( + (ctx.clone(), vec![], false), + |(ipld, mut seen_ops, is_try), op| { + seen_ops.push(op); + + match op { + Filter::Try(inner) => { + let op: Filter = *inner.clone(); + let ipld: Ipld = + Select::::new(vec![op]).get(ctx).unwrap_or(Ipld::Null); + + Ok((ipld, seen_ops.clone(), true)) + } + Filter::ArrayIndex(i) => { + let result = { + match ipld { + Ipld::List(xs) => { + if i.abs() as usize > xs.len() { + return Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )); + }; + + xs.get((xs.len() as i32 + *i) as usize) + .ok_or(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )) + .cloned() + } + _ => Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::NotAList, + ), + )), + } + }; + + Ok((result?, seen_ops.clone(), is_try)) + } + Filter::Field(k) => { + let result = match ipld { + Ipld::Map(xs) => xs + .get(k) + .ok_or(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::KeyNotFound, + ), + )) + .cloned(), + _ => Err(( + is_try, + SelectorError::from_refs(&seen_ops, SelectorErrorReason::NotAMap), + )), + }; + + Ok((result?, seen_ops.clone(), is_try)) + } + Filter::Values => { + let result = match ipld { + Ipld::List(xs) => Ok(Ipld::List(xs)), + Ipld::Map(xs) => Ok(Ipld::List(xs.values().cloned().collect())), + _ => Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::NotACollection, + ), + )), + }; + + Ok((result?, seen_ops.clone(), is_try)) + } + } + }, + ); + + let (ipld, path) = match got { + Ok((ipld, seen_ops, _)) => Ok((ipld, seen_ops)), + Err((is_try, ref e @ SelectorError { ref selector, .. })) => { + if is_try { + Ok((Ipld::Null, selector.0.iter().map(|x| x).collect::>())) + } else { + Err(e.clone()) + } + } + }?; + + T::try_select(ipld).map_err(|e| SelectorError::from_refs(&path, e)) + } +} + +impl From> for Ipld +where + Ipld: From, +{ + fn from(s: Select) -> Self { + Selector(s.filters).to_string().into() + } +} + +impl FromStr for Select { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let selector = Selector::from_str(s).map_err(ParseError)?; + Ok(Select { + filters: selector.0, + _marker: std::marker::PhantomData, + }) + } +} + +#[derive(Debug, PartialEq, Error)] +#[error("Failed to parse selector: {0}")] +pub struct ParseError(#[from] nom::Err); + +impl PartialOrd for Select { + fn partial_cmp(&self, other: &Self) -> Option { + Selector(self.filters.clone()).partial_cmp(&Selector(other.filters.clone())) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Select { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + prop::collection::vec(Filter::arbitrary(), 1..10) + .prop_map(Select::new) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ipld; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod get { + use super::*; + + fn nested_data() -> Ipld { + Ipld::Map( + vec![ + ("name".to_string(), Ipld::String("Alice".to_string())), + ("age".to_string(), Ipld::Integer(42)), + ( + "friends".to_string(), + Ipld::List(vec![ + Ipld::String("Bob".to_string()), + Ipld::String("Charlie".to_string()), + ]), + ), + ] + .into_iter() + .collect(), + ) + } + + proptest! { + #[test_log::test] + fn test_identity(data: ipld::Newtype) { + let selector = Select::::from_str(".")?; + prop_assert_eq!(selector.get(&data.0)?, data); + } + + #[test_log::test] + fn test_try_missing_is_null(data: ipld::Newtype) { + let selector = Select::::from_str(".foo?")?; + let cleaned_data = match data.0.clone() { + Ipld::Map(mut m) => { + m.remove("foo").map_or(Ipld::Null, |v| v) + } + ipld => ipld + }; + prop_assert_eq!(selector.get(&cleaned_data)?, Ipld::Null); + } + + #[test_log::test] + fn test_try_missing_plus_trailing_is_null(data: ipld::Newtype, more: Vec) { + let mut filters = vec![Filter::Try(Box::new(Filter::Field("foo".into())))]; + filters.append(&mut more.clone()); + + let selector: Select = Select::new(filters); + + let cleaned_data = match data.0.clone() { + Ipld::Map(mut m) => { + m.remove("foo").map_or(Ipld::Null, |v| v) + } + ipld => ipld + }; + prop_assert_eq!(selector.get(&cleaned_data)?, Ipld::Null); + } + } + } +} diff --git a/src/delegation/policy/selector/selectable.rs b/src/delegation/policy/selector/selectable.rs new file mode 100644 index 00000000..9fa22186 --- /dev/null +++ b/src/delegation/policy/selector/selectable.rs @@ -0,0 +1,62 @@ +use super::error::SelectorErrorReason; +use crate::ipld; +use libipld_core::ipld::Ipld; +use std::collections::BTreeMap; + +pub trait Selectable: Sized { + fn try_select(ipld: Ipld) -> Result; +} + +impl Selectable for Ipld { + fn try_select(ipld: Ipld) -> Result { + Ok(ipld) + } +} + +impl Selectable for ipld::Newtype { + fn try_select(ipld: Ipld) -> Result { + Ok(ipld::Newtype(ipld)) + } +} + +impl Selectable for ipld::Number { + fn try_select(ipld: Ipld) -> Result { + match ipld { + Ipld::Integer(i) => Ok(ipld::Number::Integer(i)), + Ipld::Float(f) => Ok(ipld::Number::Float(f)), + _ => Err(SelectorErrorReason::NotANumber), + } + } +} + +impl Selectable for String { + fn try_select(ipld: Ipld) -> Result { + match ipld { + Ipld::String(s) => Ok(s), + _ => Err(SelectorErrorReason::NotAString), + } + } +} + +impl Selectable for ipld::Collection { + fn try_select(ipld: Ipld) -> Result { + match ipld { + Ipld::List(xs) => Ok(ipld::Collection::Array(xs.into_iter().try_fold( + vec![], + |mut acc, v| { + acc.push(Selectable::try_select(v)?); + Ok(acc) + }, + )?)), + Ipld::Map(xs) => Ok(ipld::Collection::Map(xs.into_iter().try_fold( + BTreeMap::new(), + |mut map, (k, v)| { + let value = Selectable::try_select(v)?; + map.insert(k, value); + Ok(map) + }, + )?)), + _ => Err(SelectorErrorReason::NotACollection), + } + } +} diff --git a/src/delegation/store.rs b/src/delegation/store.rs new file mode 100644 index 00000000..22050f87 --- /dev/null +++ b/src/delegation/store.rs @@ -0,0 +1,7 @@ +//! Storage interface for [`Delegation`][super::Delegation]s. + +mod memory; +mod traits; + +pub use memory::MemoryStore; +pub use traits::*; diff --git a/src/delegation/store/memory.rs b/src/delegation/store/memory.rs new file mode 100644 index 00000000..bf1c4cab --- /dev/null +++ b/src/delegation/store/memory.rs @@ -0,0 +1,816 @@ +use super::Store; +use crate::ability::arguments::Named; +use crate::delegation; +use crate::{ + crypto::varsig, + delegation::{policy::Predicate, Delegation}, + did::{self, Did}, +}; +use libipld_core::codec::Encode; +use libipld_core::ipld::Ipld; +use libipld_core::{cid::Cid, codec::Codec}; +use nonempty::NonEmpty; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + convert::Infallible, + sync::{Arc, Mutex, MutexGuard}, +}; +use web_time::SystemTime; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// A simple in-memory store for delegations. +/// +/// The store is laid out as follows: +/// +/// `{Subject => {Audience => {Cid => Delegation}}}` +/// +/// ```mermaid +/// flowchart LR +/// subgraph Subjects +/// direction TB +/// +/// Akiko +/// Boris +/// Carol +/// +/// subgraph aud[Boris's Audiences] +/// direction TB +/// +/// Denzel +/// Erin +/// Frida +/// Georgia +/// Hugo +/// +/// subgraph cid[Frida's CIDs] +/// direction LR +/// +/// CID1 --> Delegation1 +/// CID2 --> Delegation2 +/// CID3 --> Delegation3 +/// end +/// end +/// end +/// +/// Akiko ~~~ Hugo +/// Carol ~~~ Hugo +/// Boris --> Frida --> CID2 +/// +/// Boris -.-> Denzel +/// Boris -.-> Erin +/// Boris -.-> Georgia +/// Boris -.-> Hugo +/// +/// Frida -.-> CID1 +/// Frida -.-> CID3 +/// +/// style Boris stroke:orange; +/// style Frida stroke:orange; +/// style CID2 stroke:orange; +/// style Delegation2 stroke:orange; +/// +/// linkStyle 5 stroke:orange; +/// linkStyle 6 stroke:orange; +/// linkStyle 1 stroke:orange; +/// ``` +#[derive(Debug, Clone)] +pub struct MemoryStore< + DID: did::Did + Ord = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + inner: Arc>>, +} + +#[derive(Debug, Clone, PartialEq)] +struct MemoryStoreInner< + DID: did::Did + Ord = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + ucans: BTreeMap>>, + index: BTreeMap, BTreeMap>>, + revocations: BTreeSet, +} + +impl, C: Codec> MemoryStore { + pub fn new() -> Self { + Self::default() + } + + pub fn len(&self) -> usize { + self.lock().ucans.len() + } + + pub fn is_empty(&self) -> bool { + self.lock().ucans.is_empty() // FIXME account for revocations? + } + + fn lock(&self) -> MutexGuard<'_, MemoryStoreInner> { + match self.inner.lock() { + Ok(guard) => guard, + Err(poison) => { + // We ignore lock poisoning for simplicity + poison.into_inner() + } + } + } +} + +impl, C: Codec> Default for MemoryStore { + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + +impl, C: Codec> Default for MemoryStoreInner { + fn default() -> Self { + MemoryStoreInner { + ucans: BTreeMap::new(), + index: BTreeMap::new(), + revocations: BTreeSet::new(), + } + } +} + +// FIXME check that UCAN is valid +impl + Clone, Enc: Codec> Store + for MemoryStore +where + Named: From>, + delegation::Payload: TryFrom>, + Ipld: Encode, +{ + type Error = Infallible; + + fn get(&self, cid: &Cid) -> Result>>, Self::Error> { + // cheap Arc clone + Ok(self.lock().ucans.get(cid).cloned()) + // FIXME + } + + fn insert_keyed( + &self, + cid: Cid, + delegation: Delegation, + ) -> Result<(), Self::Error> { + let mut tx = self.lock(); + + tx.index + .entry(delegation.subject().cloned()) + .or_default() + .entry(delegation.audience().clone()) + .or_default() + .insert(cid); + + tx.ucans.insert(cid.clone(), Arc::new(delegation)); + + Ok(()) + } + + fn revoke(&self, cid: Cid) -> Result<(), Self::Error> { + self.lock().revocations.insert(cid); + Ok(()) + } + + fn get_chain( + &self, + aud: &DID, + subject: &DID, + command: &str, + policy: Vec, + now: SystemTime, + ) -> Result>)>>, Self::Error> { + let blank_set = BTreeSet::new(); + let blank_map = BTreeMap::new(); + let tx = self.lock(); + + let all_powerlines = tx.index.get(&None).unwrap_or(&blank_map); + let all_aud_for_subject = tx.index.get(&Some(subject.clone())).unwrap_or(&blank_map); + let powerline_candidates = all_powerlines.get(aud).unwrap_or(&blank_set); + let sub_candidates = all_aud_for_subject.get(aud).unwrap_or(&blank_set); + + let mut parent_candidate_stack = + vec![sub_candidates.iter().chain(powerline_candidates.iter())]; + let mut hypothesis_chain = vec![]; + + let corrected_target_command = if command.ends_with('/') { + Cow::Borrowed(command) + } else { + Cow::Owned(format!("{command}/")) + }; + + 'outer: loop { + if let Some(parent_cid_candidates) = parent_candidate_stack.last_mut() { + if parent_cid_candidates.clone().collect::>().is_empty() { + parent_candidate_stack.pop(); + continue; + } + + 'inner: for cid in parent_cid_candidates { + // CHECKS + if tx.revocations.contains(cid) { + continue; + } + + if let Some(delegation) = tx.ucans.get(cid) { + if delegation.check_time(now).is_err() { + continue; + } + + // FIXME extract + let corrected_delegation_command = + if delegation.payload.command.ends_with('/') { + delegation.payload.command.clone() + } else { + format!("{}/", delegation.payload.command) + }; + + if !corrected_target_command.starts_with(&corrected_delegation_command) { + continue; + } + + // FIXME + // for target_pred in policy.iter() { + // for delegate_pred in delegation.payload.policy.iter() { + // let comparison = + // target_pred.harmonize(delegate_pred, vec![], vec![]); + + // if comparison.is_conflict() || comparison.is_lhs_weaker() { + // continue 'inner; + // } + // } + // } + + // PASSED CHECKS, so processing + hypothesis_chain.push((cid.clone(), Arc::clone(delegation))); + + let issuer = delegation.issuer().clone(); + + // Hit a root delegation, AKA base case + if Some(&issuer) == delegation.subject() { + break 'outer; + } + + let new_aud_candidates = + all_aud_for_subject.get(&issuer).unwrap_or(&blank_set); + + if !new_aud_candidates.is_empty() || !all_powerlines.get(&issuer).is_none() + { + parent_candidate_stack.push( + new_aud_candidates.iter().chain( + all_powerlines.get(&issuer).unwrap_or(&blank_set).iter(), + ), + ); + + break 'inner; + } + } + } + } else { + parent_candidate_stack.pop(); + break 'outer; + } + } + + Ok(NonEmpty::from_vec(hypothesis_chain)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::varsig::encoding; + use crate::crypto::varsig::header; + use crate::{crypto::signature::Envelope, delegation::store::Store}; + + use libipld_core::cid::Cid; + use nonempty::nonempty; + use pretty_assertions as pretty; + use rand::thread_rng; + use std::time::SystemTime; + use testresult::TestResult; + + fn gen_did() -> (crate::did::preset::Verifier, crate::did::preset::Signer) { + let sk = ed25519_dalek::SigningKey::generate(&mut thread_rng()); + let verifier = + crate::did::preset::Verifier::Key(crate::did::key::Verifier::EdDsa(sk.verifying_key())); + let signer = crate::did::preset::Signer::Key(crate::did::key::Signer::EdDsa(sk)); + + (verifier, signer) + } + + #[test_log::test] + fn test_get_fail() -> TestResult { + let store = MemoryStore::< + did::preset::Verifier, + varsig::header::Preset, + varsig::encoding::Preset, + >::default(); + store.get(&Cid::default())?; + pretty::assert_eq!(store.get(&Cid::default()), Ok(None)); + Ok(()) + } + + #[test_log::test] + fn test_insert_get_roundtrip() -> TestResult { + let (did, signer) = gen_did(); + + let store = MemoryStore::default(); + let varsig_header = header::Preset::EdDsa(header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + }); + + let deleg = Delegation::try_sign( + &signer, + varsig_header, + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(did.clone()) + .audience(did.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(deleg.clone())?; + let retrieved = store.get(&deleg.cid()?)?.ok_or("failed to retrieve")?; + + pretty::assert_eq!(deleg, *retrieved); + + Ok(()) + } + + #[test_log::test] + fn test_insert_is_idempotent() -> TestResult { + let (did, signer) = gen_did(); + + let store = MemoryStore::default(); + let varsig_header = header::Preset::EdDsa(header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + }); + + let deleg = Delegation::try_sign( + &signer, + varsig_header, + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(did.clone()) + .audience(did.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + + let retrieved = store.get(&deleg.cid()?)?.ok_or("failed to retrieve")?; + + pretty::assert_eq!(deleg, *retrieved); + pretty::assert_eq!(store.len(), 1); + + Ok(()) + } + + mod get_chain { + use super::*; + + #[test_log::test] + fn test_simple_fail() -> TestResult { + let (server, _server_signer) = gen_did(); + let (nope, _nope_signer) = gen_did(); + + let store = MemoryStore::< + did::preset::Verifier, + varsig::header::Preset, + varsig::encoding::Preset, + >::default(); + let got = store.get_chain(&server, &nope, "/".into(), vec![], SystemTime::now())?; + + pretty::assert_eq!(got, None); + Ok(()) + } + + #[test_log::test] + fn test_with_one() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, _bob_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let deleg = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(deleg.clone())?; + + let got = store.get_chain(&bob, &alice, "/".into(), vec![], SystemTime::now())?; + pretty::assert_eq!(got, Some(nonempty![(deleg.cid()?, Arc::new(deleg))].into())); + Ok(()) + } + + #[test_log::test] + fn test_with_one_with_others_in_store() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, _carol_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let noise = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/example".into()) + .build()?, + )?; + + store.insert(noise.clone())?; + + let deleg = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(deleg.clone())?; + + let more_noise = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(carol.clone()) + .command("/test".into()) + .build()?, + )?; + + store.insert(more_noise.clone())?; + + let got = store.get_chain(&bob, &alice, "/".into(), vec![], SystemTime::now())?; + pretty::assert_eq!(got, Some(nonempty![(deleg.cid()?, Arc::new(deleg))].into())); + Ok(()) + } + + #[test_log::test] + fn test_with_two() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, _carol_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let deleg_1 = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(deleg_1.clone())?; + + let deleg_2 = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(deleg_2.clone())?; + + let got = store.get_chain(&carol, &alice, "/".into(), vec![], SystemTime::now())?; + + pretty::assert_eq!( + got, + Some( + nonempty![ + (deleg_2.cid()?, Arc::new(deleg_2)), + (deleg_1.cid()?, Arc::new(deleg_1)), + ] + .into() + ) + ); + Ok(()) + } + + #[test_log::test] + fn test_looking_for_narrower_command() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, _carol_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let deleg_1 = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/test".into()) + .build()?, + )?; + + store.insert(deleg_1.clone())?; + + let deleg_2 = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/test/me".into()) + .build()?, + )?; + + store.insert(deleg_2.clone())?; + + let got = store.get_chain( + &carol, + &alice, + "/test/me/now".into(), + vec![], + SystemTime::now(), + )?; + + pretty::assert_eq!( + got, + Some( + nonempty![ + (deleg_2.cid()?, Arc::new(deleg_2)), + (deleg_1.cid()?, Arc::new(deleg_1)), + ] + .into() + ) + ); + Ok(()) + } + + #[test_log::test] + fn test_broken_chain() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, _bob_signer) = gen_did(); + let (carol, carol_signer) = gen_did(); + let (dan, _dan_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let alice_to_bob = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/test".into()) + .build()?, + )?; + + store.insert(alice_to_bob.clone())?; + + let carol_to_dan = crate::Delegation::try_sign( + &carol_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(carol.clone()) + .audience(dan.clone()) + .command("/test/me".into()) + .build()?, + )?; + + store.insert(carol_to_dan.clone())?; + + let got = store.get_chain( + &carol, + &alice, + "/test/me/now".into(), + vec![], + SystemTime::now(), + )?; + + pretty::assert_eq!(got, None); + Ok(()) + } + + #[test_log::test] + fn test_long_chain() -> TestResult { + // Scenario + // ======== + // 1. bob -*-> carol + // 2. carol -a-> dave + // 3. alice -d-> bob + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, carol_signer) = gen_did(); + let (dave, _) = gen_did(); + + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let store = crate::delegation::store::MemoryStore::default(); + + // 1. bob -*-> carol + let bob_to_carol = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/".into()) + .build()?, + )?; + + // 2. carol -a-> dave + let carol_to_dave = crate::Delegation::try_sign( + &carol_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(carol.clone()) + .audience(dave.clone()) + .command("/".into()) + .build()?, // I don't love this is now failable + )?; + + // 3. alice -d-> bob + let alice_to_bob = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(bob_to_carol.clone())?; + store.insert(carol_to_dave.clone())?; + store.insert(alice_to_bob.clone())?; + + let got: Vec = store + .get_chain(&dave, &alice, "/".into(), vec![], SystemTime::now()) + .map_err(|e| e.to_string())? + .ok_or("failed during proof lookup")? + .iter() + .map(|(cid, _)| cid) + .cloned() + .collect(); + + pretty::assert_eq!( + got, + vec![ + carol_to_dave.cid()?, + bob_to_carol.cid()?, + alice_to_bob.cid()? + ] + ); + + Ok(()) + } + + #[test_log::test] + fn test_long_powerline() -> TestResult { + // Scenario + // ======== + // 1. bob -*-> carol + // 2. carol -a-> dave + // 3. alice -d-> bob + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, carol_signer) = gen_did(); + let (dave, _) = gen_did(); + + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let store = crate::delegation::store::MemoryStore::default(); + + // 1. bob -*-> carol + let bob_to_carol = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/".into()) + .build()?, + )?; + + // 2. carol -a-> dave + let carol_to_dave = crate::Delegation::try_sign( + &carol_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(carol.clone()) + .audience(dave.clone()) + .command("/".into()) + .build()?, // I don't love this is now failable + )?; + + // 3. alice -d-> bob + let alice_to_bob = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .build()?, + )?; + + store.insert(bob_to_carol.clone())?; + store.insert(carol_to_dave.clone())?; + store.insert(alice_to_bob.clone())?; + + let got: Vec = store + .get_chain(&dave, &alice.clone(), "/".into(), vec![], SystemTime::now()) + .map_err(|e| e.to_string())? + .ok_or("failed during proof lookup")? + .iter() + .map(|(cid, _)| cid) + .cloned() + .collect(); + + pretty::assert_eq!( + got, + vec![ + carol_to_dave.cid()?, + bob_to_carol.cid()?, + alice_to_bob.cid()? + ] + ); + + Ok(()) + } + } +} diff --git a/src/delegation/store/traits.rs b/src/delegation/store/traits.rs new file mode 100644 index 00000000..76486f40 --- /dev/null +++ b/src/delegation/store/traits.rs @@ -0,0 +1,125 @@ +use crate::{ + ability::arguments::Named, + crypto::signature::Envelope, + crypto::varsig, + delegation::payload::Payload, + delegation::{policy::Predicate, Delegation}, + did::Did, +}; +use libipld_core::codec::Encode; +use libipld_core::ipld::Ipld; +use libipld_core::{cid::Cid, codec::Codec}; +use nonempty::NonEmpty; +use std::{fmt::Debug, sync::Arc}; +use thiserror::Error; +use web_time::SystemTime; + +pub trait Store + Clone, C: Codec> +where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + type Error: Debug; + + fn get(&self, cid: &Cid) -> Result>>, Self::Error>; + + fn insert( + &self, + delegation: Delegation, + ) -> Result<(), DelegationInsertError> { + self.insert_keyed(delegation.cid()?, delegation) + .map_err(DelegationInsertError::StoreError) + } + + fn insert_keyed(&self, cid: Cid, delegation: Delegation) -> Result<(), Self::Error>; + + // FIXME validate invocation + // store invocation + // just... move to invocation + fn revoke(&self, cid: Cid) -> Result<(), Self::Error>; + + fn get_chain( + &self, + audience: &DID, + subject: &DID, + command: &str, + policy: Vec, + now: SystemTime, + ) -> Result>)>>, Self::Error>; + + fn get_chain_cids( + &self, + audience: &DID, + subject: &DID, + command: &str, + policy: Vec, + now: SystemTime, + ) -> Result>, Self::Error> { + self.get_chain(audience, subject, command, policy, now) + .map(|chain| chain.map(|chain| chain.map(|(cid, _)| cid))) + } + + fn can_delegate( + &self, + issuer: DID, + audience: &DID, + command: &str, + policy: Vec, + now: SystemTime, + ) -> Result { + self.get_chain(audience, &issuer, command, policy, now) + .map(|chain| chain.is_some()) + } + + fn get_many( + &self, + cids: &[Cid], + ) -> Result>>>, Self::Error> { + cids.iter() + .map(|cid| self.get(cid)) + .collect::>() + } +} + +impl, DID: Did + Clone, V: varsig::Header + Clone, C: Codec> Store + for &T +where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + type Error = >::Error; + + fn get(&self, cid: &Cid) -> Result>>, Self::Error> { + (**self).get(cid) + } + + fn insert_keyed(&self, cid: Cid, delegation: Delegation) -> Result<(), Self::Error> { + (**self).insert_keyed(cid, delegation) + } + + fn revoke(&self, cid: Cid) -> Result<(), Self::Error> { + (**self).revoke(cid) + } + + fn get_chain( + &self, + audience: &DID, + subject: &DID, + command: &str, + policy: Vec, + now: SystemTime, + ) -> Result>)>>, Self::Error> { + (**self).get_chain(audience, subject, command, policy, now) + } +} + +#[derive(Debug, Error)] +pub enum DelegationInsertError { + #[error("Cannot make CID from delegation based on supplied Varsig")] + CannotMakeCid(#[from] libipld_core::error::Error), + + #[error("Store error: {0}")] + StoreError(E), +} diff --git a/src/did.rs b/src/did.rs new file mode 100644 index 00000000..65a00d29 --- /dev/null +++ b/src/did.rs @@ -0,0 +1,12 @@ +//! Decentralized Identifier ([DID][wiki]) utilities. +//! +//! [wiki]: https://en.wikipedia.org/wiki/Decentralized_identifier + +mod newtype; +mod traits; + +pub mod key; +pub mod preset; + +pub use newtype::{FromIpldError, Newtype}; +pub use traits::{Did, Verifiable}; diff --git a/src/did/dns.rs b/src/did/dns.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/did/dns.rs @@ -0,0 +1 @@ + diff --git a/src/did/key.rs b/src/did/key.rs new file mode 100644 index 00000000..6be3a7bb --- /dev/null +++ b/src/did/key.rs @@ -0,0 +1,11 @@ +//! Support for the [`did:key`](https://w3c-ccg.github.io/did-method-key/) DID method. + +mod signature; +mod verifier; +mod signer; + +pub mod traits; + +pub use signature::Signature; +pub use verifier::*; +pub use signer::*; diff --git a/src/did/key/signature.rs b/src/did/key/signature.rs new file mode 100644 index 00000000..1392eec6 --- /dev/null +++ b/src/did/key/signature.rs @@ -0,0 +1,116 @@ +use enum_as_inner::EnumAsInner; + +#[cfg(feature = "eddsa")] +use ed25519_dalek; + +#[cfg(feature = "es256")] +use p256; + +#[cfg(feature = "es256k")] +use k256; + +#[cfg(feature = "es384")] +use p384; + +#[cfg(feature = "es512")] +use ::p521 as ext_p521; + +#[cfg(feature = "rs256")] +use crate::crypto::rs256; + +#[cfg(feature = "rs512")] +use crate::crypto::rs512; + +#[cfg(feature = "bls")] +use crate::crypto::bls12381; + +/// Signature types that are verifiable by `did:key` [`Verifier`]s. +#[derive(Debug, Clone, PartialEq, Eq, EnumAsInner)] +pub enum Signature { + /// `EdDSA` signature. + #[cfg(feature = "eddsa")] + EdDsa(ed25519_dalek::Signature), + + /// `ES256K` (`secp256k1`) signature. + #[cfg(feature = "es256k")] + Es256k(k256::ecdsa::Signature), + + /// `P-256` signature. + #[cfg(feature = "es256")] + P256(p256::ecdsa::Signature), + + /// `P-384` signature. + #[cfg(feature = "es384")] + P384(p384::ecdsa::Signature), + + /// `P-521` signature. + #[cfg(feature = "es512")] + P521(ext_p521::ecdsa::Signature), + + /// `RS256` signature. + #[cfg(feature = "rs256")] + Rs256(rs256::Signature), + + /// `RS512` signature. + #[cfg(feature = "rs512")] + Rs512(rs512::Signature), + + /// `BLS 12-381` signature for the "min pub key" variant. + #[cfg(feature = "bls")] + BlsMinPk(bls12381::min_pk::Signature), + + /// `BLS 12-381` signature for the "min sig" variant. + #[cfg(feature = "bls")] + BlsMinSig(bls12381::min_sig::Signature), + + /// An unknown signature type. + /// + /// This is primarily for parsing, where reification is delayed + /// until the DID method is known. + Unknown(Vec), +} + +impl signature::SignatureEncoding for Signature { + type Repr = Vec; +} + +impl From for Vec { + fn from(sig: Signature) -> Vec { + match sig { + #[cfg(feature = "eddsa")] + Signature::EdDsa(sig) => sig.to_vec(), + + #[cfg(feature = "es256k")] + Signature::Es256k(sig) => sig.to_vec(), + + #[cfg(feature = "es256")] + Signature::P256(sig) => sig.to_vec(), + + #[cfg(feature = "es384")] + Signature::P384(sig) => sig.to_vec(), + + #[cfg(feature = "es512")] + Signature::P521(sig) => sig.to_vec(), + + #[cfg(feature = "rs256")] + Signature::Rs256(sig) => <[u8; 256]>::from(sig).into(), + + #[cfg(feature = "rs512")] + Signature::Rs512(sig) => <[u8; 512]>::from(sig).into(), + + #[cfg(feature = "bls")] + Signature::BlsMinPk(sig) => <[u8; 96]>::from(sig).into(), + + #[cfg(feature = "bls")] + Signature::BlsMinSig(sig) => <[u8; 48]>::from(sig).into(), + + Signature::Unknown(vec) => vec, + } + } +} + +impl From<&[u8]> for Signature { + fn from(arr: &[u8]) -> Signature { + Signature::Unknown(arr.to_vec()) + } +} diff --git a/src/did/key/signer.rs b/src/did/key/signer.rs new file mode 100644 index 00000000..7d69fa94 --- /dev/null +++ b/src/did/key/signer.rs @@ -0,0 +1,123 @@ +use super::Signature; +use enum_as_inner::EnumAsInner; + +#[cfg(feature = "eddsa")] +use ed25519_dalek; + +#[cfg(feature = "es256")] +use p256; + +#[cfg(feature = "es256k")] +use k256; + +#[cfg(feature = "es384")] +use p384; + +#[cfg(feature = "es512")] +use ::p521 as ext_p521; + +#[cfg(feature = "rs256")] +use crate::crypto::rs256; + +#[cfg(feature = "rs512")] +use crate::crypto::rs512; + +/// Signer types that are verifiable by `did:key` [`Verifier`]s. +#[derive(Clone, EnumAsInner)] +pub enum Signer { + /// `EdDSA` signer. + #[cfg(feature = "eddsa")] + EdDsa(ed25519_dalek::SigningKey), + + /// `ES256K` (`secp256k1`) signer. + #[cfg(feature = "es256k")] + Es256k(k256::ecdsa::SigningKey), + + /// `P-256` signer. + #[cfg(feature = "es256")] + P256(p256::ecdsa::SigningKey), + + /// `P-384` signer. + #[cfg(feature = "es384")] + P384(p384::ecdsa::SigningKey), + + /// `P-521` signer. + #[cfg(feature = "es512")] + P521(ext_p521::ecdsa::SigningKey), + + /// `RS256` signer. + #[cfg(feature = "rs256")] + Rs256(rs256::SigningKey), + + /// `RS512` signer. + #[cfg(feature = "rs512")] + Rs512(rs512::SigningKey), + + /// `BLS 12-381` signer for the "min pub key" variant. + #[cfg(feature = "bls")] + BlsMinPk(blst::min_pk::SecretKey), + + /// `BLS 12-381` signer for the "min sig" variant. + #[cfg(feature = "bls")] + BlsMinSig(blst::min_sig::SecretKey), +} + +impl signature::Signer for Signer { + fn try_sign(&self, msg: &[u8]) -> Result { + match self { + #[cfg(feature = "eddsa")] + Signer::EdDsa(signer) => { + let sig = signer.sign(msg); + Ok(Signature::EdDsa(sig)) + } + + #[cfg(feature = "es256k")] + Signer::Es256k(signer) => { + let sig = signer.sign(msg); + Ok(Signature::Es256k(sig)) + } + + #[cfg(feature = "es256")] + Signer::P256(signer) => { + let sig = signer.sign(msg); + Ok(Signature::P256(sig)) + } + + #[cfg(feature = "es384")] + Signer::P384(signer) => { + let sig = signer.sign(msg); + Ok(Signature::P384(sig)) + } + + #[cfg(feature = "es512")] + Signer::P521(signer) => { + let sig = signer.sign(msg); + Ok(Signature::P521(sig)) + } + + #[cfg(feature = "rs256")] + Signer::Rs256(signer) => { + let sig = signer.sign(msg); + Ok(Signature::Rs256(sig)) + } + + #[cfg(feature = "rs512")] + Signer::Rs512(signer) => { + let sig = signer.sign(msg); + Ok(Signature::Rs512(sig)) + } + + #[cfg(feature = "bls")] + Signer::BlsMinPk(signer) => { + let sig = signer.try_sign(msg)?; + Ok(Signature::BlsMinPk(sig)) + } + + #[cfg(feature = "bls")] + Signer::BlsMinSig(signer) => { + let sig = signer.try_sign(msg)?; + Ok(Signature::BlsMinSig(sig)) + } + } + } +} diff --git a/src/did/key/traits.rs b/src/did/key/traits.rs new file mode 100644 index 00000000..7a1eb9f1 --- /dev/null +++ b/src/did/key/traits.rs @@ -0,0 +1,81 @@ +/// A trait aligning signatures with keys. + +use crate::crypto::{bls12381, es512, rs256, rs512}; +use ::p521 as ext_p521; +use ed25519_dalek; +use k256; +use p256; +use p384; + +// FIXME +// also: e.g. HSM? + +pub trait DidKey: signature::Verifier { + const BASE58_PREFIX: &'static str; + + type Signer: signature::Signer; + type Signature: signature::SignatureEncoding; +} + +impl DidKey for ed25519_dalek::VerifyingKey { + const BASE58_PREFIX: &'static str = "6Mk"; + + type Signer = ed25519_dalek::SigningKey; + type Signature = ed25519_dalek::Signature; +} + +impl DidKey for p256::ecdsa::VerifyingKey { + const BASE58_PREFIX: &'static str = "Dn"; + + type Signer = p256::ecdsa::SigningKey; + type Signature = p256::ecdsa::Signature; +} + +impl DidKey for k256::ecdsa::VerifyingKey { + const BASE58_PREFIX: &'static str = "Q3s"; + + type Signer = k256::ecdsa::SigningKey; + type Signature = k256::ecdsa::Signature; +} + +impl DidKey for p384::ecdsa::VerifyingKey { + const BASE58_PREFIX: &'static str = "82"; + + type Signer = p384::ecdsa::SigningKey; + type Signature = p384::ecdsa::Signature; +} + +impl DidKey for es512::VerifyingKey { + const BASE58_PREFIX: &'static str = "2J9"; + + type Signer = ext_p521::ecdsa::SigningKey; + type Signature = ext_p521::ecdsa::Signature; +} + +impl DidKey for rs256::VerifyingKey { + const BASE58_PREFIX: &'static str = "4MX"; + + type Signer = rs256::SigningKey; + type Signature = rs256::Signature; +} + +impl DidKey for rs512::VerifyingKey { + const BASE58_PREFIX: &'static str = "zgg"; + + type Signer = rs512::SigningKey; + type Signature = rs512::Signature; +} + +impl DidKey for blst::min_sig::PublicKey { + const BASE58_PREFIX: &'static str = "UC7"; + + type Signer = blst::min_sig::SecretKey; + type Signature = bls12381::min_sig::Signature; +} + +impl DidKey for blst::min_pk::PublicKey { + const BASE58_PREFIX: &'static str = "UC7"; + + type Signer = blst::min_pk::SecretKey; + type Signature = bls12381::min_pk::Signature; +} diff --git a/src/did/key/verifier.rs b/src/did/key/verifier.rs new file mode 100644 index 00000000..4dcced0e --- /dev/null +++ b/src/did/key/verifier.rs @@ -0,0 +1,593 @@ +use super::Signature; +use blst::BLST_ERROR; +use did_url::DID; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use multibase; +use multibase::Base; +use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; +use serde::{Deserialize, Serialize}; +use signature as sig; +use std::{fmt::Display, str::FromStr}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "eddsa")] +use ed25519_dalek; + +#[cfg(feature = "es256")] +use p256; + +#[cfg(feature = "es256k")] +use k256; + +#[cfg(feature = "es384")] +use p384; + +#[cfg(feature = "es512")] +use crate::crypto::es512; + +#[cfg(feature = "rs256")] +use crate::crypto::rs256; + +#[cfg(feature = "rs512")] +use crate::crypto::rs512; + +#[cfg(feature = "bls")] +use blst; + +/// Verifiers (public/verifying keys) for `did:key`. +#[derive(Debug, Clone, PartialEq, Eq, EnumAsInner)] +pub enum Verifier { + /// `EdDSA` verifying key. + #[cfg(feature = "eddsa")] + EdDsa(ed25519_dalek::VerifyingKey), + + /// `ES256K` (`secp256k1`) verifying key. + #[cfg(feature = "es256k")] + Es256k(k256::ecdsa::VerifyingKey), + + /// `P-256` verifying key. + #[cfg(feature = "es256")] + P256(p256::ecdsa::VerifyingKey), + + /// `P-384` verifying key. + #[cfg(feature = "es384")] + P384(p384::ecdsa::VerifyingKey), + + /// `P-521` verifying key. + #[cfg(feature = "es512")] + P521(es512::VerifyingKey), + + /// `RS256` verifying key. + #[cfg(feature = "rs256")] + Rs256(rs256::VerifyingKey), + + /// `RS512` verifying key. + #[cfg(feature = "rs512")] + Rs512(rs512::VerifyingKey), + + /// `BLS 12-381` verifying key for the "min pub key" variant. + #[cfg(feature = "bls")] + BlsMinPk(blst::min_pk::PublicKey), + + /// `BLS 12-381` verifying key for the "min sig" variant. + #[cfg(feature = "bls")] + BlsMinSig(blst::min_sig::PublicKey), +} + +impl PartialOrd for Verifier { + fn partial_cmp(&self, other: &Self) -> Option { + self.to_string().partial_cmp(&other.to_string()) + } +} + +impl Ord for Verifier { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.to_string().cmp(&other.to_string()) + } +} + +impl signature::Verifier for Verifier { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + match (self, signature) { + (Verifier::EdDsa(vk), Signature::EdDsa(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::Es256k(vk), Signature::Es256k(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::P256(vk), Signature::P256(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::P384(vk), Signature::P384(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::P521(vk), Signature::P521(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::Rs256(vk), Signature::Rs256(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::Rs512(vk), Signature::Rs512(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::BlsMinPk(vk), Signature::BlsMinPk(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::BlsMinSig(vk), Signature::BlsMinSig(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (_, _) => Err(signature::Error::from_source( + "invalid signature type for verifier", + )), + } + } +} + +impl Display for Verifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = match self { + Verifier::EdDsa(ed25519_pk) => { + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xed, &mut buf); + + let mut payload: Vec = tag.to_vec(); + let bytes = ed25519_pk.to_bytes(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::Es256k(secp256k1_pk) => { + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xe7, &mut buf); + + let mut payload = tag.to_vec(); + let bytes = secp256k1_pk.to_sec1_bytes(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::P256(p256_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1200, &mut buf); + + let mut payload = tag.to_vec(); + let point = p256_key.to_encoded_point(true); + payload.extend_from_slice(point.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::P384(p384_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1201, &mut buf); + + let mut payload = tag.to_vec(); + let point = p384_key.to_encoded_point(true); + payload.extend_from_slice(point.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::P521(p521_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1202, &mut buf); + + let mut payload = tag.to_vec(); + let point = p521_key.0.to_encoded_point(true); + payload.extend_from_slice(point.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::Rs256(rsa2048_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1205, &mut buf); + + let mut payload = tag.to_vec(); + let raw = rsa2048_key.0.to_pkcs1_der().map_err(|_| std::fmt::Error)?; // NOTE: technically should never fail + payload.extend_from_slice(raw.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::Rs512(rsa4096_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1205, &mut buf); + + let mut payload = tag.to_vec(); + let raw = rsa4096_key.0.to_pkcs1_der().map_err(|_| std::fmt::Error)?; // NOTE: technically should never fail + payload.extend_from_slice(raw.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::BlsMinPk(bls_minpk_pk) => { + let bytes = bls_minpk_pk.compress(); + + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xeb, &mut buf); + + let mut payload = tag.to_vec(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::BlsMinSig(bls_minsig_pk) => { + let bytes = bls_minsig_pk.compress(); + + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xeb, &mut buf); + + let mut payload = tag.to_vec(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + }; + + write!(f, "did:key:{}", inner) + } +} + +impl FromStr for Verifier { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + if s.len() < 32 { + // Smallest key size + return Err(FromStrError::TooShort); + } + + match s.split_at(8) { + ("did:key:", more) => { + let (_base, varint_bytes): (multibase::Base, Vec) = multibase::decode(more)?; + let (tag, rest) = unsigned_varint::decode::u16(&varint_bytes)?; + + // FIXME also check max length on bytes + match tag { + 0xed => { + let arr: [u8; 32] = rest.try_into().map_err(|_| FromStrError::TooShort)?; + + let vk = ed25519_dalek::VerifyingKey::from_bytes(&arr) + .map_err(FromStrError::CannotParseEdDsa)?; + + Ok(Verifier::EdDsa(vk)) + } + 0xe7 => { + let vk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&rest) + .map_err(FromStrError::CannotParseEs256k)?; + + Ok(Verifier::Es256k(vk)) + } + 0x1200 => { + let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(rest) + .map_err(FromStrError::CannotParseP256)?; + + Ok(Verifier::P256(vk)) + } + 0x1201 => { + let vk = p384::ecdsa::VerifyingKey::from_sec1_bytes(rest) + .map_err(FromStrError::CannotParseP384)?; + + Ok(Verifier::P384(vk)) + } + 0x1202 => { + let vk = p521::ecdsa::VerifyingKey::from_sec1_bytes(rest) + .map_err(FromStrError::CannotParseP521)?; + + Ok(Verifier::P521(es512::VerifyingKey(vk))) + } + 0x1205 => match rest.len() { + // 256-bytes plus params + 270 => { + let vk = rsa::pkcs1v15::VerifyingKey::from_pkcs1_der(rest) + .map_err(FromStrError::CannotParseRs256)?; + + Ok(Verifier::Rs256(rs256::VerifyingKey(vk))) + } + // 512-bytes plus params + 526 => { + let vk = rsa::pkcs1v15::VerifyingKey::from_pkcs1_der(rest) + .map_err(FromStrError::CannotParseRs512)?; + + Ok(Verifier::Rs512(rs512::VerifyingKey(vk))) + } + len => Err(FromStrError::InvalidRsaLength(len)), + }, + 0xeb => match rest.len() { + 48 => { + let pk = blst::min_pk::PublicKey::deserialize(rest) + .map_err(FromStrError::CannotParseBlsMinPk)?; + + Ok(Verifier::BlsMinPk(pk)) + } + 96 => { + let pk = blst::min_sig::PublicKey::deserialize(rest) + .map_err(FromStrError::CannotParseBlsMinSig)?; + + Ok(Verifier::BlsMinSig(pk)) + } + len => Err(FromStrError::InvalidBlsLength(len)), + }, + word => Err(FromStrError::UnexpectedPrefix(word)), + } + } + + (s, _) => Err(FromStrError::UnexpectedHeader(s.to_string())), + } + } +} + +impl From for Ipld { + fn from(v: Verifier) -> Self { + v.to_string().into() + } +} + +impl TryFrom for Verifier { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::String(s) = ipld { + Verifier::from_str(&s).map_err(|_| ()) + } else { + Err(()) + } + } +} + +#[derive(Debug, Error)] +pub enum FromStrError { + #[error("not a did:key prefix: {0}")] + NotADidKey(usize), + + #[error("unexpected prefix: {0:?}")] + UnexpectedPrefix(u16), + + #[error("unexpected header: {0}")] + UnexpectedHeader(String), + + #[error("unexpected BLS length: {0}")] + InvalidBlsLength(usize), + + #[error("Invalid RSA length: {0}")] + InvalidRsaLength(usize), + + #[error("key too short")] + TooShort, + + #[error("cannot parse EdDSA key: {0}")] + CannotParseEdDsa(sig::Error), + + #[error("cannot parse ES256K key: {0}")] + CannotParseEs256k(sig::Error), + + #[error("cannot parse P-256 key: {0}")] + CannotParseP256(sig::Error), + + #[error("cannot parse P-384 key: {0}")] + CannotParseP384(sig::Error), + + #[error("cannot parse P-521 key: {0}")] + CannotParseP521(sig::Error), + + #[error("cannot parse RS256 key: {0}")] + CannotParseRs256(rsa::pkcs1::Error), + + #[error("cannot parse RS512 key: {0}")] + CannotParseRs512(rsa::pkcs1::Error), + + #[error("cannot parse BLS min pk key: {0:?}")] + CannotParseBlsMinPk(BLST_ERROR), + + #[error("cannot parse BLS min sig key: {0:?}")] + CannotParseBlsMinSig(BLST_ERROR), + + #[error("cannot decode multibase: {0}")] + CannotDecodeMultibase(#[from] multibase::Error), + + #[error("cannot parse tag: {0}")] + CannotParseTag(#[from] unsigned_varint::decode::Error), +} + +impl PartialEq for FromStrError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (FromStrError::NotADidKey(a), FromStrError::NotADidKey(b)) => a == b, + (FromStrError::UnexpectedPrefix(a), FromStrError::UnexpectedPrefix(b)) => a == b, + (FromStrError::TooShort, FromStrError::TooShort) => true, + (FromStrError::CannotParseEdDsa(a), FromStrError::CannotParseEdDsa(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseEs256k(a), FromStrError::CannotParseEs256k(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseP256(a), FromStrError::CannotParseP256(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseP384(a), FromStrError::CannotParseP384(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseP521(a), FromStrError::CannotParseP521(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseRs256(a), FromStrError::CannotParseRs256(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseRs512(a), FromStrError::CannotParseRs512(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseBlsMinPk(a), FromStrError::CannotParseBlsMinPk(b)) => a == b, + (FromStrError::CannotParseBlsMinSig(a), FromStrError::CannotParseBlsMinSig(b)) => { + a == b + } + ( + FromStrError::CannotDecodeMultibase(lhs), + FromStrError::CannotDecodeMultibase(rhs), + ) => lhs == rhs, + (FromStrError::CannotParseTag(lhs), FromStrError::CannotParseTag(rhs)) => lhs == rhs, + _ => false, + } + } +} + +impl Serialize for Verifier { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Verifier { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Verifier::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl From for DID { + fn from(v: Verifier) -> Self { + DID::parse(&v.to_string()).expect("verifier to be a valid DID") + } +} + +impl TryFrom for Verifier { + type Error = FromStrError; + + fn try_from(did: DID) -> Result { + Verifier::from_str(&did.to_string()) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Verifier { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // NOTE these are just the test vectors from `did:key` v0.7 + prop_oneof![ + // ed25519 + Just("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"), + + // secp256k1 + Just("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"), + Just("did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"), + Just("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"), + + // BLS + Just("did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"), + Just("did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"), + + // P-256 + Just("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"), + Just("did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"), + + // P-384 + Just("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"), + Just("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"), + + // P-521 + Just("did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"), + Just("did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"), + + // RSA-2048 + Just("did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"), + + // RSA-4096 + Just("did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2") + ] + .prop_map(|s: &str| Verifier::from_str(s).expect("did:key spec test vectors to work")) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions as pretty; + use testresult::TestResult; + + mod serialization { + use super::*; + + fn roundtrip(s: &str) -> TestResult { + let v = Verifier::from_str(s)?; + let serialized = v.to_string(); + pretty::assert_eq!(s, serialized); + Ok(()) + } + + #[test_log::test] + fn test_ed25519_parse() -> TestResult { + roundtrip("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK") + } + + #[test_log::test] + fn test_secp256k_1_parse() -> TestResult { + roundtrip("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme") + } + + #[test_log::test] + fn test_secp256k_2_parse() -> TestResult { + roundtrip("did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2") + } + + #[test_log::test] + fn test_secp256k_3_parse() -> TestResult { + roundtrip("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N") + } + + #[test_log::test] + fn test_bls_1_parse() -> TestResult { + roundtrip("did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY") + } + + #[test_log::test] + fn test_bls_2_parse() -> TestResult { + roundtrip("did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW") + } + + #[test_log::test] + fn test_p256_1_parse() -> TestResult { + roundtrip("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169") + } + + #[test_log::test] + fn test_p256_2_parse() -> TestResult { + roundtrip("did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv") + } + + #[test_log::test] + fn test_p384_1_parse() -> TestResult { + roundtrip( + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + ) + } + + #[test_log::test] + fn test_p384_2_parse() -> TestResult { + roundtrip( + "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54", + ) + } + + #[test_log::test] + fn test_p521_1_parse() -> TestResult { + roundtrip("did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7") + } + + #[test_log::test] + fn test_p521_2_parse() -> TestResult { + roundtrip("did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f") + } + + #[test_log::test] + fn test_rs256_parse() -> TestResult { + roundtrip("did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i") + } + + #[test_log::test] + fn test_rs512_parse() -> TestResult { + roundtrip("did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2") + } + } +} diff --git a/src/did/newtype.rs b/src/did/newtype.rs new file mode 100644 index 00000000..1057247b --- /dev/null +++ b/src/did/newtype.rs @@ -0,0 +1,156 @@ +use did_url::DID; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::{fmt, string::ToString}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// A [Decentralized Identifier (DID)][wiki] +/// +/// This is a newtype wrapper around the [`DID`] type from the [`did_url`] crate. +/// +/// # Examples +/// +/// ```rust +/// # use ucan::did; +/// # +/// let did = did::Newtype::try_from("did:example:123".to_string()).unwrap(); +/// assert_eq!(did.0.method(), "example"); +/// ``` +/// +/// [wiki]: https://en.wikipedia.org/wiki/Decentralized_identifier +pub struct Newtype(pub DID); + +impl Serialize for Newtype { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + String::from(self.clone()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Newtype { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + Newtype::try_from(string).map_err(serde::de::Error::custom) + } +} + +impl From for String { + fn from(did: Newtype) -> Self { + did.0.to_string() + } +} + +impl TryFrom for Newtype { + type Error = >::Error; + + fn try_from(string: String) -> Result { + DID::parse(&string).map(Newtype) + } +} + +impl fmt::Display for Newtype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_string()) + } +} + +impl From for Ipld { + fn from(did: Newtype) -> Self { + did.into() + } +} + +impl TryFrom for Newtype { + type Error = FromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::String(string) => { + Newtype::try_from(string).map_err(FromIpldError::StructuralError) + } + other => Err(FromIpldError::NotAnIpldString(other)), + } + } +} + +/// Errors that can occur when converting to or from a [`Newtype`] +#[derive(Debug, Clone, EnumAsInner, PartialEq, Error)] +pub enum FromIpldError { + /// Strutural errors in the [`Newtype`] + #[error(transparent)] + StructuralError(#[from] did_url::Error), + + /// The [`Ipld`] was not a string + #[error("Not an IPLD String")] + NotAnIpldString(Ipld), +} + +impl Serialize for FromIpldError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for FromIpldError { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Ok(FromIpldError::NotAnIpldString(ipld)) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // NOTE these are just the test vectors from `did:key` v0.7 + prop_oneof![ + // did:key + Just("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"), + + // secp256k1 + Just("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"), + Just("did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"), + Just("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"), + + // BLS + Just("did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"), + Just("did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"), + + // P-256 + Just("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"), + Just("did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"), + + // P-384 + Just("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"), + Just("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"), + + // P-521 + Just("did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"), + Just("did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"), + + // RSA-2048 + Just("did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"), + + // RSA-4096 + Just("did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2") + ].prop_map(|s: &str| Newtype(did_url::DID::parse(s).expect("did:key spec test vectors to work"))).boxed() + } +} diff --git a/src/did/preset.rs b/src/did/preset.rs new file mode 100644 index 00000000..ff34c699 --- /dev/null +++ b/src/did/preset.rs @@ -0,0 +1,115 @@ +use super::key; +use super::Did; +use did_url::DID; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::{fmt::Display, str::FromStr}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// The set of [`Did`] types that ship with this library ("presets"). +#[derive(Debug, Clone, EnumAsInner, PartialEq, PartialOrd, Ord, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Verifier { + /// `did:key` DIDs. + Key(key::Verifier), + // + // FIXME Dns(did_url::DID), +} + +impl From for Ipld { + fn from(verifier: Verifier) -> Self { + match verifier { + Verifier::Key(verifier) => verifier.into(), + } + } +} + +impl TryFrom for Verifier { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + key::Verifier::try_from(ipld) + .map(Verifier::Key) + .map_err(|_| ()) + } +} + +impl From for DID { + fn from(verifier: Verifier) -> Self { + match verifier { + Verifier::Key(verifier) => verifier.into(), + } + } +} + +#[derive(Clone, EnumAsInner)] +pub enum Signer { + Key(key::Signer), + // FIXME Dns(did_url::DID), +} + +impl std::fmt::Debug for Signer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Signer::Key(_signer) => write!(f, "Signer::Key(HIDDEN)"), + } + } +} + +impl Did for Verifier { + type Signature = key::Signature; + type Signer = self::Signer; +} + +impl TryFrom for Verifier { + type Error = key::FromStrError; + + fn try_from(did: DID) -> Result { + key::Verifier::try_from(did).map(Verifier::Key) + } +} + +impl signature::Signer for Signer { + fn try_sign(&self, message: &[u8]) -> Result { + match self { + Signer::Key(signer) => signer.try_sign(message), + } + } +} + +impl signature::Verifier for Verifier { + fn verify(&self, message: &[u8], signature: &key::Signature) -> Result<(), signature::Error> { + match self { + Verifier::Key(verifier) => verifier.verify(message, signature), + } + } +} + +impl Display for Verifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Verifier::Key(verifier) => verifier.fmt(f), + } + } +} + +impl FromStr for Verifier { + type Err = key::FromStrError; + + fn from_str(s: &str) -> Result { + key::Verifier::from_str(s).map(Verifier::Key) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Verifier { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + key::Verifier::arbitrary().prop_map(Verifier::Key).boxed() + } +} diff --git a/src/did/traits.rs b/src/did/traits.rs new file mode 100644 index 00000000..8e55df2d --- /dev/null +++ b/src/did/traits.rs @@ -0,0 +1,11 @@ +use std::fmt; +use std::str::FromStr; + +pub trait Did: PartialEq + ToString + FromStr + signature::Verifier + Ord { + type Signature: signature::SignatureEncoding + PartialEq + fmt::Debug; + type Signer: signature::Signer + fmt::Debug; +} + +pub trait Verifiable { + fn verifier<'a>(&'a self) -> &'a DID; +} diff --git a/src/did_verifier.rs b/src/did_verifier.rs deleted file mode 100644 index 62c5ab59..00000000 --- a/src/did_verifier.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! DID verifier methods - -use core::fmt; -use std::collections::HashMap; - -use crate::error::Error; - -#[cfg(feature = "did-key")] -pub mod did_key; - -/// A map from did method to verifier -#[derive(Debug)] -pub struct DidVerifierMap { - map: HashMap>, -} - -impl Default for DidVerifierMap { - fn default() -> Self { - #[allow(unused_mut)] - let mut did_verifier_map = Self { - map: HashMap::new(), - }; - - #[cfg(feature = "did-key")] - did_verifier_map.register(did_key::DidKeyVerifier::default()); - - did_verifier_map - } -} - -impl DidVerifierMap { - /// Register a verifier - pub fn register(&mut self, verifier: V) -> &mut Self - where - V: DidVerifier + 'static, - { - self.map - .insert(verifier.method().to_string(), Box::new(verifier)); - self - } - - /// Register a verifier that's already boxed - pub fn register_box(&mut self, verifier: Box) -> &mut Self { - self.map.insert(verifier.method().to_string(), verifier); - self - } - - /// Verify a signature using the registered verifier for the given method - pub fn verify( - &self, - method: &str, - identifier: &str, - payload: &[u8], - signature: &[u8], - ) -> Result<(), Error> { - self.map - .get(method) - .ok_or_else(|| Error::VerifyingError { - msg: format!("Unrecognized DID method, {}", method), - })? - .verify(identifier, payload, signature) - .map_err(|e| Error::VerifyingError { msg: e.to_string() }) - } -} - -impl FromIterator> for DidVerifierMap { - fn from_iter>>(iter: T) -> Self { - let mut map = Self::default(); - for verifier in iter { - map.register_box(verifier); - } - - map - } -} - -impl Extend> for DidVerifierMap { - fn extend>>(&mut self, iter: T) { - for verifier in iter { - self.register_box(verifier); - } - } -} - -/// A trait for implementing DID method verification -pub trait DidVerifier { - /// The DID method for this verifier - fn method(&self) -> &'static str; - - /// Verify a signature - fn verify( - &self, - identifier: &str, - payload: &[u8], - signature: &[u8], - ) -> Result<(), anyhow::Error>; -} - -impl fmt::Debug for dyn DidVerifier { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DidVerifier") - .field("method", &self.method()) - .finish() - } -} diff --git a/src/did_verifier/did_key.rs b/src/did_verifier/did_key.rs deleted file mode 100644 index 4c2f658f..00000000 --- a/src/did_verifier/did_key.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! did:key method verifier - -#[cfg(feature = "eddsa-verifier")] -use crate::crypto::eddsa::eddsa_verifier; -#[cfg(feature = "es256-verifier")] -use crate::crypto::es256::es256_verifier; -#[cfg(feature = "es256k-verifier")] -use crate::crypto::es256k::es256k_verifier; -#[cfg(feature = "es384-verifier")] -use crate::crypto::es384::es384_verifier; -#[cfg(feature = "ps256-verifier")] -use crate::crypto::ps256::ps256_verifier; -#[cfg(feature = "rs256-verifier")] -use crate::crypto::rs256::rs256_verifier; - -use core::fmt; -use std::{any::TypeId, collections::HashMap}; - -use anyhow::anyhow; -use multibase::Base; - -use super::DidVerifier; - -/// A closure for verifying a signature -pub type SignatureVerifier = dyn Fn(&[u8], &[u8], &[u8]) -> Result<(), anyhow::Error>; - -/// did:key method verifier -pub struct DidKeyVerifier { - /// map from type id of signature to verifier function - verifier_map: HashMap>, -} - -impl Default for DidKeyVerifier { - fn default() -> Self { - #[allow(unused_mut)] - let mut did_key_verifier = Self { - verifier_map: HashMap::new(), - }; - - #[cfg(feature = "eddsa-verifier")] - did_key_verifier.set::(eddsa_verifier); - - #[cfg(feature = "es256-verifier")] - did_key_verifier.set::(es256_verifier); - - #[cfg(feature = "es256k-verifier")] - did_key_verifier.set::(es256k_verifier); - - #[cfg(feature = "es384-verifier")] - did_key_verifier.set::(es384_verifier); - - #[cfg(feature = "ps256-verifier")] - did_key_verifier.set::(ps256_verifier); - - #[cfg(feature = "rs256-verifier")] - did_key_verifier.set::(rs256_verifier); - - did_key_verifier - } -} - -impl DidKeyVerifier { - /// set verifier function for type `T` - pub fn set(&mut self, f: F) -> &mut Self - where - T: 'static, - F: Fn(&[u8], &[u8], &[u8]) -> Result<(), anyhow::Error> + 'static, - { - self.verifier_map.insert(TypeId::of::(), Box::new(f)); - self - } - - /// check if verifier function for type `T` is set - pub fn has(&self) -> bool - where - T: 'static, - { - self.verifier_map.contains_key(&TypeId::of::()) - } -} - -impl DidVerifier for DidKeyVerifier { - fn method(&self) -> &'static str { - "key" - } - - fn verify( - &self, - identifier: &str, - payload: &[u8], - signature: &[u8], - ) -> Result<(), anyhow::Error> { - let (base, data) = multibase::decode(identifier).map_err(|e| anyhow!(e))?; - - let Base::Base58Btc = base else { - return Err(anyhow!("expected base58btc, got {:?}", base)); - }; - - let (multicodec, public_key) = - unsigned_varint::decode::u128(&data).map_err(|e| anyhow!(e))?; - - let multicodec_pub_key = MulticodecPubKey::try_from(multicodec)?; - - multicodec_pub_key.validate_pub_key_len(public_key)?; - - #[allow(unreachable_patterns)] - let verifier = match multicodec_pub_key { - #[cfg(feature = "es256k")] - MulticodecPubKey::Secp256k1Compressed => self - .verifier_map - .get(&TypeId::of::()), - #[cfg(feature = "eddsa")] - MulticodecPubKey::X25519 => return Err(anyhow!("x25519 not supported for signing")), - #[cfg(feature = "eddsa")] - MulticodecPubKey::Ed25519 => self.verifier_map.get(&TypeId::of::()), - #[cfg(feature = "es256")] - MulticodecPubKey::P256Compressed => self - .verifier_map - .get(&TypeId::of::()), - #[cfg(feature = "es384")] - MulticodecPubKey::P384Compressed => self - .verifier_map - .get(&TypeId::of::()), - #[cfg(feature = "es521")] - MulticodecPubKey::P521Compressed => self - .verifier_map - .get(&TypeId::of::>()), - #[cfg(feature = "rs256")] - MulticodecPubKey::RSAPKCS1 => self - .verifier_map - .get(&TypeId::of::()), - _ => Option::<&Box>::None, - } - .ok_or_else(|| anyhow!("no registered verifier for signature type"))?; - - verifier(public_key, payload, signature) - } -} - -impl fmt::Debug for DidKeyVerifier { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DidKeyVerifier").finish() - } -} - -/// Multicodec public key -#[derive(Debug)] -pub enum MulticodecPubKey { - /// secp256k1 compressed public key - #[cfg(feature = "es256k")] - Secp256k1Compressed, - /// x25519 public key - #[cfg(feature = "eddsa")] - X25519, - /// ed25519 public key - #[cfg(feature = "eddsa")] - Ed25519, - /// p256 compressed public key - #[cfg(feature = "es256")] - P256Compressed, - /// p384 compressed public key - #[cfg(feature = "es384")] - P384Compressed, - /// p521 compressed public key - #[cfg(feature = "es521")] - P521Compressed, - /// rsa pkcs1 public key - #[cfg(feature = "rs256")] - RSAPKCS1, -} - -impl MulticodecPubKey { - #[allow(unused_variables)] - fn validate_pub_key_len(&self, pub_key: &[u8]) -> Result<(), anyhow::Error> { - #[allow(unreachable_patterns)] - match self { - #[cfg(feature = "es256k")] - MulticodecPubKey::Secp256k1Compressed => { - if pub_key.len() != 33 { - return Err(anyhow!( - "expected 33 bytes for secp256k1 compressed public key, got {}", - pub_key.len() - )); - } - } - #[cfg(feature = "eddsa")] - MulticodecPubKey::X25519 => { - if pub_key.len() != 32 { - return Err(anyhow!( - "expected 32 bytes for x25519 public key, got {}", - pub_key.len() - )); - } - } - #[cfg(feature = "eddsa")] - MulticodecPubKey::Ed25519 => { - if pub_key.len() != 32 { - return Err(anyhow!( - "expected 32 bytes for ed25519 public key, got {}", - pub_key.len() - )); - } - } - #[cfg(feature = "es256")] - MulticodecPubKey::P256Compressed => { - if pub_key.len() != 33 { - return Err(anyhow!( - "expected 33 bytes for p256 compressed public key, got {}", - pub_key.len() - )); - } - } - #[cfg(feature = "es384")] - MulticodecPubKey::P384Compressed => { - if pub_key.len() != 49 { - return Err(anyhow!( - "expected 49 bytes for p384 compressed public key, got {}", - pub_key.len() - )); - } - } - #[cfg(feature = "es521")] - MulticodecPubKey::P521Compressed => { - if pub_key.len() > 67 { - return Err(anyhow!( - "expected <= 67 bytes for p521 compressed public key, got {}", - pub_key.len() - )); - } - } - #[cfg(feature = "rs256")] - MulticodecPubKey::RSAPKCS1 => match pub_key.len() { - 94 | 126 | 162 | 226 | 294 | 422 | 546 => {} - n => { - return Err(anyhow!( - "expected 94, 126, 162, 226, 294, 422, or 546 bytes for RSA PKCS1 public key, got {}", - n - )); - } - }, - _ => return Err(anyhow!("unsupported public key type")), - }; - - #[allow(unreachable_code)] - Ok(()) - } -} - -impl TryFrom for MulticodecPubKey { - type Error = anyhow::Error; - - fn try_from(value: u128) -> Result { - match value { - #[cfg(feature = "es256k")] - 0xe7 => Ok(MulticodecPubKey::Secp256k1Compressed), - #[cfg(feature = "eddsa")] - 0xec => Ok(MulticodecPubKey::X25519), - #[cfg(feature = "eddsa")] - 0xed => Ok(MulticodecPubKey::Ed25519), - #[cfg(feature = "es256")] - 0x1200 => Ok(MulticodecPubKey::P256Compressed), - #[cfg(feature = "es384")] - 0x1201 => Ok(MulticodecPubKey::P384Compressed), - #[cfg(feature = "es521")] - 0x1202 => Ok(MulticodecPubKey::P521Compressed), - #[cfg(feature = "rs256")] - 0x1205 => Ok(MulticodecPubKey::RSAPKCS1), - _ => Err(anyhow!("unsupported multicodec")), - } - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 98d665b4..00000000 --- a/src/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Error types for UCAN - -use thiserror::Error; - -/// Error types for UCAN -#[derive(Error, Debug)] -pub enum Error { - /// Parsing errors - #[error("An error occurred while parsing the token: {msg}")] - TokenParseError { - /// Error message - msg: String, - }, - /// Verification errors - #[error("An error occurred while verifying the token: {msg}")] - VerifyingError { - /// Error message - msg: String, - }, - /// Signing errors - #[error("An error occurred while signing the token: {msg}")] - SigningError { - /// Error message - msg: String, - }, - /// Plugin errors - #[error(transparent)] - PluginError(PluginError), - /// Internal errors - #[error("An unexpected error occurred in rs-ucan: {msg}\n\nThis is a bug: please consider filing an issue at https://github.com/ucan-wg/rs-ucan/issues")] - InternalUcanError { - /// Error message - msg: String, - }, -} - -/// Error types for plugins -#[derive(Error, Debug)] -#[error(transparent)] -pub struct PluginError { - #[from] - inner: anyhow::Error, -} - -impl From for Error { - fn from(inner: anyhow::Error) -> Self { - Self::PluginError(PluginError { inner }) - } -} diff --git a/src/invocation.rs b/src/invocation.rs new file mode 100644 index 00000000..f9ffb106 --- /dev/null +++ b/src/invocation.rs @@ -0,0 +1,254 @@ +//! An [`Invocation`] is a request to use an [`Ability`][crate::ability]. +//! +//! ## Data +//! +//! - [`Invocation`] is the top-level, signed data struture. +//! - [`Payload`] is the fields unique to an invocation. +//! - [`Preset`] is an [`Invocation`] preloaded with this library's [preset abilities](crate::ability::preset::Ready). +//! - [`promise`]s are a mechanism to chain invocations together. +//! +//! ## Stateful Helpers +//! +//! - [`Agent`] is a high-level interface for sessions that will involve more than one invoctaion. +//! - [`store`] is an interface for caching [`Invocation`]s. + +pub mod agent; +pub mod payload; + +pub mod promise; +pub mod store; + +pub use agent::Agent; +pub use payload::*; + +use crate::ability::arguments::Named; +use crate::ability::command::ToCommand; +use crate::ability::parse::ParseAbility; +use crate::{ + crypto::{signature::Envelope, varsig}, + did::{self, Did}, + time::{Expired, Timestamp}, +}; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + ipld::Ipld, +}; +use serde::{Deserialize, Serialize}; +use web_time::SystemTime; + +/// The complete, signed [`invocation::Payload`][Payload]. +/// +/// Invocations are the actual "doing" in the UCAN lifecycle. +/// Unlike [`Delegation`][crate::Delegation]s, which live for some period of time and +/// can be used multiple times, [`Invocation`]s are unique and single-use. +/// +/// # Expiration +/// +/// `Invocations` include an optional expiration field which behaves like a timeout: +/// "if this isn't run by a the expiration time, I'm going to assume that it didn't happen." +/// This is a best practice in message-passing distributed systems because the network is +/// [unreliable](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing). +#[derive(Debug, Clone, PartialEq)] +pub struct Invocation< + A, + DID: did::Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + pub varsig_header: V, + pub payload: Payload, + pub signature: DID::Signature, + _marker: std::marker::PhantomData, +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Clone + did::Did, + V: Clone + varsig::Header, + C: Codec, + > Encode for Invocation +where + Ipld: Encode, + Named: From + From>, + Payload: TryFrom>, + // >>::Error: std::fmt::Debug, +{ + fn encode(&self, c: C, w: &mut W) -> Result<(), libipld_core::error::Error> { + self.to_ipld_envelope().encode(c, w) + } +} + +impl, C: Codec> + Invocation +where + Ipld: Encode, +{ + pub fn new(varsig_header: V, signature: DID::Signature, payload: Payload) -> Self { + Invocation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + pub fn audience(&self) -> &Option { + &self.payload.audience + } + + pub fn normalized_audience(&self) -> &DID { + if let Some(audience) = &self.payload.audience { + audience + } else { + &self.payload.subject + } + } + + pub fn issuer(&self) -> &DID { + &self.payload.issuer + } + + pub fn subject(&self) -> &DID { + &self.payload.subject + } + + pub fn ability(&self) -> &A { + &self.payload.ability + } + + pub fn map_ability(self, f: F) -> Invocation + where + F: FnOnce(A) -> Z, + Z: ParseAbility + ToCommand, + { + Invocation::new( + self.varsig_header, + self.signature, + self.payload.map_ability(f), + ) + } + + pub fn proofs(&self) -> &Vec { + &self.payload.proofs + } + + pub fn issued_at(&self) -> &Option { + &self.payload.issued_at + } + + pub fn expiration(&self) -> &Option { + &self.payload.expiration + } + + pub fn check_time(&self, now: SystemTime) -> Result<(), Expired> { + self.payload.check_time(now) + } +} + +impl, C: Codec> did::Verifiable + for Invocation +{ + fn verifier(&self) -> &DID { + &self.payload.verifier() + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec, + > From> for Ipld +where + Named: From, + Payload: TryFrom>, +{ + fn from(invocation: Invocation) -> Self { + invocation.to_ipld_envelope() + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec, + > Envelope for Invocation +where + Named: From, + Payload: TryFrom>, +{ + type DID = DID; + type Payload = Payload; + type VarsigHeader = V; + type Encoder = C; + + fn construct( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Invocation { + Invocation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + fn varsig_header(&self) -> &V { + &self.varsig_header + } + + fn payload(&self) -> &Payload { + &self.payload + } + + fn signature(&self) -> &DID::Signature { + &self.signature + } + + fn verifier(&self) -> &DID { + &self.payload.issuer + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec, + > Serialize for Invocation +where + Named: From, + Payload: TryFrom>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_ipld_envelope().serialize(serializer) + } +} + +impl< + 'de, + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec, + > Deserialize<'de> for Invocation +where + Named: From, + Payload: TryFrom>, + as TryFrom>>::Error: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Self::try_from_ipld_envelope(ipld).map_err(serde::de::Error::custom) + } +} diff --git a/src/invocation/agent.rs b/src/invocation/agent.rs new file mode 100644 index 00000000..ee040a21 --- /dev/null +++ b/src/invocation/agent.rs @@ -0,0 +1,768 @@ +use super::{ + payload::{Payload, ValidationError}, + store::Store, + Invocation, +}; +use crate::{ + ability::{self, arguments, arguments::Named, command::ToCommand, parse::ParseAbility}, + crypto::{ + signature::{self, Envelope}, + varsig, Nonce, + }, + delegation, + did::{self, Did}, + time::Timestamp, +}; +use enum_as_inner::EnumAsInner; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + ipld::Ipld, +}; +use std::{collections::BTreeMap, marker::PhantomData}; +use thiserror::Error; +use web_time::SystemTime; + +#[derive(Debug)] +pub struct Agent< + S: Store, + D: delegation::store::Store, + T: ToCommand = ability::preset::Preset, + DID: Did + Clone = did::preset::Verifier, + V: varsig::Header + Clone = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> where + Ipld: Encode, + delegation::Payload: TryFrom>, + Named: From>, +{ + /// The agent's [`DID`]. + pub did: DID, + + /// A [`delegation::Store`][delegation::store::Store]. + pub delegation_store: D, + + /// A [`Store`][Store] for the agent's [`Invocation`]s. + pub invocation_store: S, + + signer: ::Signer, + marker: PhantomData<(T, V, C)>, +} + +impl Agent +where + Ipld: Encode, + T: ToCommand + Clone + ParseAbility, + Named: From, + Payload: TryFrom>, + delegation::Payload: Clone, + DID: Did + Clone, + S: Store, + D: delegation::store::Store, + V: varsig::Header + Clone, + C: Codec, + delegation::Payload: TryFrom>, + Named: From>, +{ + pub fn new( + did: DID, + signer: ::Signer, + invocation_store: S, + delegation_store: D, + ) -> Self { + Self { + did, + invocation_store, + delegation_store, + signer, + marker: PhantomData, + } + } + + pub fn invoke( + &self, + audience: Option, + subject: DID, + ability: T, + metadata: BTreeMap, + cause: Option, + expiration: Option, + issued_at: Option, + now: SystemTime, + varsig_header: V, + ) -> Result, InvokeError> { + let proofs = if subject == self.did { + vec![] + } else { + self.delegation_store + .get_chain_cids(&self.did, &subject, &ability.to_command(), vec![], now)? // FIXME policy + .ok_or(InvokeError::ProofsNotFound)? + .into() + }; + + let payload = Payload { + issuer: self.did.clone(), + subject, + audience, + ability, + proofs, + metadata, + nonce: Nonce::generate_16(), + cause, + expiration, + issued_at, + }; + + Ok(Invocation::try_sign(&self.signer, varsig_header, payload) + .map_err(InvokeError::SignError)?) + } + + // pub fn invoke_promise( + // &mut self, + // audience: Option<&DID>, + // subject: DID, + // ability: T::Promised, + // metadata: BTreeMap, + // cause: Option, + // expiration: Option, + // issued_at: Option, + // now: SystemTime, + // varsig_header: V, + // ) -> Result< + // Invocation, + // InvokeError< + // D::DelegationStoreError, + // ParseAbilityError<()>, // FIXME errs + // >, + // > + // where + // Named: From, + // Payload: TryFrom>, + // { + // let proofs = self + // .delegation_store + // .get_chain(self.did, &Some(subject.clone()), "/".into(), vec![], now) + // .map_err(InvokeError::DelegationStoreError)? + // .map(|chain| chain.map(|(cid, _)| cid).into()) + // .unwrap_or(vec![]); + + // let mut seed = vec![]; + + // let payload = Payload { + // issuer: self.did.clone(), + // subject, + // audience: audience.cloned(), + // ability, + // proofs, + // metadata, + // nonce: Nonce::generate_12(&mut seed), + // cause, + // expiration, + // issued_at, + // }; + + // Ok(Invocation::try_sign(self.signer, varsig_header, payload) + // .map_err(InvokeError::SignError)?) + // } + + pub fn receive( + &self, + invocation: Invocation, + ) -> Result>, ReceiveError> + where + arguments::Named: From, + Payload: TryFrom>, + Invocation: Clone + Encode, + { + self.generic_receive(invocation, SystemTime::now()) + } + + pub fn generic_receive( + &self, + invocation: Invocation, + now: SystemTime, + ) -> Result>, ReceiveError> + where + arguments::Named: From, + Payload: TryFrom>, + Invocation: Clone + Encode, + { + let cid: Cid = invocation.cid().map_err(ReceiveError::EncodingError)?; + + invocation + .validate_signature() + .map_err(ReceiveError::SigVerifyError)?; + + // FIXME validate signature directly in inv store + + self.invocation_store + .put(cid.clone(), invocation.clone()) + .map_err(ReceiveError::InvocationStoreError)?; + + let proofs = &self + .delegation_store + .get_many(&invocation.proofs()) + .map_err(ReceiveError::DelegationStoreError)?; + let proof_payloads: Vec<&delegation::Payload> = proofs + .iter() + .zip(invocation.proofs().iter()) + .map(|(d, cid)| { + Ok(&d + .as_ref() + .ok_or(ReceiveError::DelegationNotFound(*cid))? + .payload) + }) + .collect::>>()?; + + let _ = &invocation + .payload + .check(proof_payloads, now) + .map_err(ReceiveError::ValidationError)?; + + Ok(if invocation.normalized_audience() != &self.did { + Recipient::Other(invocation.payload) + } else { + Recipient::You(invocation.payload) + }) + } + + // pub fn revoke( + // &self, + // subject: DID, + // cause: Option, + // cid: Cid, + // now: Timestamp, + // varsig_header: V, + // // FIXME return type + // ) -> Result, ()> + // where + // Named: From, + // T: From, + // Payload: TryFrom>, + // { + // let ability: T = Revoke { ucan: cid.clone() }.into(); + // let proofs = if &subject == self.did { + // vec![] + // } else { + // todo!("update to latest trait interface"); // FIXME + // // self.delegation_store + // // .get_chain(&subject, &Some(self.did.clone()), vec![], now.into()) + // // .map_err(|_| ())? + // // .map(|chain| chain.map(|(index_cid, _)| index_cid).into()) + // // .unwrap_or(vec![]) + // }; + + // let payload = Payload { + // issuer: self.did.clone(), + // subject: self.did.clone(), + // audience: Some(self.did.clone()), + // ability, + // proofs, + // cause, + // metadata: BTreeMap::new(), + // nonce: Nonce::generate_12(&mut vec![]), + // expiration: None, + // issued_at: None, + // }; + + // let invocation = + // Invocation::try_sign(self.signer, varsig_header, payload).map_err(|_| ())?; + + // self.delegation_store.revoke(cid).map_err(|_| ())?; + // Ok(invocation) + // } +} + +#[derive(Debug, PartialEq, Clone, EnumAsInner)] +pub enum Recipient { + // FIXME change to status? + You(T), + Other(T), + Unresolved(Cid), +} + +#[derive(Debug, Error, EnumAsInner)] +pub enum ReceiveError, V: varsig::Header, C: Codec> { + #[error("couldn't find delegation: {0}")] + DelegationNotFound(Cid), + + #[error("encoding error: {0}")] + EncodingError(#[from] libipld_core::error::Error), + + #[error("signature verification error: {0}")] + SigVerifyError(#[from] signature::ValidateError), + + #[error("invocation store error: {0}")] + InvocationStoreError(#[source] >::Error), + + #[error("delegation store error: {0}")] + DelegationStoreError(#[source] D), + + #[error("delegation validation error: {0}")] + ValidationError(#[source] ValidationError), +} + +#[derive(Debug, Error)] +pub enum InvokeError { + #[error("delegation store error: {0}")] + DelegationStoreError(#[from] D), + + #[error("The current agent does not have the necessary proofs to invoke.")] + ProofsNotFound, + + #[error("store error: {0}")] + SignError(#[source] signature::SignError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ability::{arguments::Named, command::Command, crud::read::Read}, + crypto::{ + signature::Envelope, + varsig, + varsig::{encoding, header}, + }, + delegation::store::Store, + invocation::{ + payload::ValidationError, + promise::{CantResolve, Resolvable}, + Agent, + }, + ipld, + }; + use libipld_core::{cid::Cid, ipld::Ipld}; + use pretty_assertions as pretty; + use rand::thread_rng; + use std::{ + ops::{Add, Sub}, + time::{Duration, SystemTime}, + }; + use testresult::TestResult; + + #[derive(Debug, Clone, PartialEq)] + pub struct AccountManage; + + impl Command for AccountManage { + const COMMAND: &'static str = "/account/info"; + } + + impl From for Named { + fn from(_: AccountManage) -> Self { + Default::default() + } + } + + impl TryFrom> for AccountManage { + type Error = (); + + fn try_from(args: Named) -> Result { + if args == Default::default() { + Ok(AccountManage) + } else { + Err(()) + } + } + } + + impl From for Named { + fn from(_: AccountManage) -> Self { + Default::default() + } + } + + impl TryFrom> for AccountManage { + type Error = (); + + fn try_from(args: Named) -> Result { + if args == Default::default() { + Ok(AccountManage) + } else { + Err(()) + } + } + } + + impl Resolvable for AccountManage { + type Promised = AccountManage; + + fn try_resolve(promised: Self::Promised) -> Result> { + Ok(promised) + } + } + + fn gen_did() -> (crate::did::preset::Verifier, crate::did::preset::Signer) { + let sk = ed25519_dalek::SigningKey::generate(&mut thread_rng()); + let verifier = + crate::did::preset::Verifier::Key(crate::did::key::Verifier::EdDsa(sk.verifying_key())); + let signer = crate::did::preset::Signer::Key(crate::did::key::Signer::EdDsa(sk)); + + (verifier, signer) + } + + fn setup_agent( + ) -> Agent { + let (did, signer) = gen_did(); + let inv_store = crate::invocation::store::MemoryStore::default(); + let del_store = crate::delegation::store::MemoryStore::default(); + + crate::invocation::agent::Agent::new(did, signer, inv_store, del_store) + } + + fn setup_valid_time() -> (Timestamp, Timestamp, Timestamp) { + let now = SystemTime::UNIX_EPOCH.add(Duration::from_secs(60 * 60 * 24 * 30)); + let exp = now.add(std::time::Duration::from_secs(60)); + let nbf = now.sub(std::time::Duration::from_secs(60)); + + ( + nbf.try_into().expect("valid nbf time"), + now.try_into().expect("valid now time"), + exp.try_into().expect("valid exp time"), + ) + } + + mod receive { + use super::*; + use assert_matches::assert_matches; + + #[test_log::test] + fn test_invoker_is_sub_implicit_aud() -> TestResult { + let (_nbf, now, exp) = setup_valid_time(); + let agent = setup_agent(); + + let invocation = agent.invoke( + None, + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(exp.try_into()?), + Some(now.try_into()?), + now.into(), + varsig::header::Preset::EdDsa(varsig::header::EdDsaHeader { + codec: varsig::encoding::Preset::DagCbor, + }), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into())?; + pretty::assert_eq!(observed, Recipient::You(invocation.payload)); + Ok(()) + } + + #[test_log::test] + fn test_invoker_is_sub_and_aud() -> TestResult { + let (_nbf, now, exp) = setup_valid_time(); + let agent = setup_agent(); + + let invocation = agent.invoke( + Some(agent.did.clone()), + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(exp.try_into()?), + Some(now.try_into()?), + now.into(), + header::Preset::EdDsa(header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + }), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into())?; + pretty::assert_eq!(observed, Recipient::You(invocation.payload)); + + Ok(()) + } + + #[test_log::test] + fn test_other_recipient() -> TestResult { + let (_nbf, now, exp) = setup_valid_time(); + let agent = setup_agent(); + + let (not_server, _) = gen_did(); + + let invocation = agent.invoke( + Some(not_server), + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(exp.try_into()?), + Some(now.try_into()?), + now.into(), + varsig::header::Preset::EdDsa(varsig::header::EdDsaHeader { + codec: varsig::encoding::Preset::DagCbor, + }), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into())?; + pretty::assert_eq!(observed, Recipient::Other(invocation.payload)); + Ok(()) + } + + #[test_log::test] + fn test_expired() -> TestResult { + let (past, now, _exp) = setup_valid_time(); + let agent = setup_agent(); + + let invocation = agent.invoke( + None, + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(past.try_into()?), + Some(now.try_into()?), + now.into(), + header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + } + .into(), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into()); + pretty::assert_eq!( + observed + .unwrap_err() + .as_validation_error() + .ok_or("not a validation error")?, + &ValidationError::Expired + ); + Ok(()) + } + + #[test_log::test] + fn test_invalid_sig() -> TestResult { + let (_past, now, _exp) = setup_valid_time(); + let agent = setup_agent(); + let server = &agent.did; + + let mut invocation = agent.invoke( + None, + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + None, + Some(now.try_into()?), + now.into(), + header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + } + .into(), + )?; + + let (not_server, _) = gen_did(); + + invocation.payload.issuer = not_server.clone(); + invocation.payload.audience = Some(server.clone()); + invocation.payload.subject = not_server; + + let observed = agent.generic_receive(invocation, now.into()); + + assert_matches!( + observed, + Err(ReceiveError::SigVerifyError( + crate::crypto::signature::ValidateError::VerifyError(_) + )) + ); + + Ok(()) + } + } + + mod chain { + use super::*; + use assert_matches::assert_matches; + + struct Ctx { + varsig_header: crate::crypto::varsig::header::Preset, + dnslink_len: usize, + inv_store: crate::invocation::store::MemoryStore, + del_store: crate::delegation::store::MemoryStore, + account_invocation: Invocation, + server: crate::did::preset::Verifier, + server_signer: crate::did::preset::Signer, + device: crate::did::preset::Verifier, + dnslink: crate::did::preset::Verifier, + } + + fn setup_test_chain() -> Result> { + let (server, server_signer) = gen_did(); + let (account, account_signer) = gen_did(); + let (device, device_signer) = gen_did(); + let (dnslink, dnslink_signer) = gen_did(); + + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let inv_store = crate::invocation::store::MemoryStore::default(); + let del_store = crate::delegation::store::MemoryStore::default(); + + // Scenario + // ======== + // + // Delegations + // 1. account -*-> server + // 2. server -a-> device + // 3. dnslink -d-> account + // + // Invocation + // 4. [dnslink -d-> account -*-> server -a-> device] + + // 1. account -*-> server + let account_to_server = crate::Delegation::try_sign( + &account_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(account.clone()) + .audience(server.clone()) + .command("/".into()) + .build()?, + )?; + + // 2. server -a-> device + let server_to_device = crate::Delegation::try_sign( + &server_signer, + varsig_header.clone(), // FIXME can also put this on a builder + crate::delegation::PayloadBuilder::default() + .subject(None) // FIXME needs a sibject when we figure out powerbox + .issuer(server.clone()) + .audience(device.clone()) + .command("/".into()) + .build()?, // I don't love this is now failable + )?; + + // 3. dnslink -d-> account + let dnslink_to_account = crate::Delegation::try_sign( + &dnslink_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(dnslink.clone())) + .issuer(dnslink.clone()) + .audience(account.clone()) + .command("/".into()) + .build()?, + )?; + + del_store.insert(account_to_server.clone())?; + del_store.insert(server_to_device.clone())?; + del_store.insert(dnslink_to_account.clone())?; + + let chain_for_dnslink: Vec = del_store + .get_chain( + &device, + &dnslink.clone(), + "/".into(), + vec![], + SystemTime::now(), + )? + .ok_or("failed during proof lookup")? + .iter() + .map(|x| x.0.clone()) + .collect(); + + // 4. [dnslink -d-> account -*-> server -a-> device] + let account_invocation = crate::Invocation::try_sign( + &device_signer, + varsig_header.clone(), + crate::invocation::PayloadBuilder::default() + .subject(dnslink.clone()) + .issuer(device.clone()) + .audience(Some(server.clone())) + .ability(AccountManage) + .proofs(chain_for_dnslink.clone()) + .build()?, + )?; + + let dnslink_len = chain_for_dnslink.len(); + + Ok(Ctx { + varsig_header, + dnslink_len, + inv_store, + del_store, + account_invocation, + server, + server_signer, + device, + dnslink, + }) + } + + #[test_log::test] + fn test_chain_ok() -> TestResult { + let ctx = setup_test_chain()?; + + let agent = Agent::new( + ctx.server.clone(), + ctx.server_signer.clone(), + &ctx.inv_store, + &ctx.del_store, + ); + + let observed = agent.receive(ctx.account_invocation.clone()); + assert_matches!(observed, Ok(Recipient::You(_))); + Ok(()) + } + + #[test_log::test] + fn test_chain_wrong_sub() -> TestResult { + let ctx = setup_test_chain()?; + + let agent = Agent::new( + ctx.server.clone(), + ctx.server_signer.clone(), + &ctx.inv_store, + &ctx.del_store, + ); + + let not_account_invocation = crate::Invocation::try_sign( + &ctx.server_signer, + ctx.varsig_header, + crate::invocation::PayloadBuilder::default() + .subject(ctx.dnslink.clone()) + .issuer(ctx.server.clone()) + .audience(Some(ctx.device.clone())) + .ability(AccountManage) + .proofs(vec![]) // FIXME + .build()?, + )?; + + let observed_other = agent.receive(not_account_invocation); + assert_matches!( + observed_other, + Err(ReceiveError::ValidationError( + ValidationError::DidNotTerminateInSubject + )) + ); + + Ok(()) + } + } +} diff --git a/src/invocation/payload.rs b/src/invocation/payload.rs new file mode 100644 index 00000000..4543046f --- /dev/null +++ b/src/invocation/payload.rs @@ -0,0 +1,834 @@ +use super::promise::Resolvable; +use crate::ability::parse::ParseAbilityError; +use crate::time; +use crate::{ + ability::{arguments::Named, command::ToCommand, parse::ParseAbility}, + capsule::Capsule, + crypto::Nonce, + delegation::{ + self, + policy::{selector::SelectorError, Predicate}, + }, + did::{Did, Verifiable}, + time::{Expired, Timestamp}, +}; +use derive_builder::Builder; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{ + de::{self, MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Serialize, Serializer, +}; +use std::collections::BTreeSet; +use std::str::FromStr; +use std::{collections::BTreeMap, fmt}; +use thiserror::Error; +use web_time::SystemTime; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld; + +#[cfg(feature = "test_utils")] +use crate::ipld::cid; + +#[derive(Debug, Clone, PartialEq, Builder)] +pub struct Payload { + /// The subject of the [`Invocation`]. + /// + /// This is typically also the `audience`, hence the [`audence`] + /// field is optional. + /// + /// This role *must* have issued the earlier (root) + /// delegation in the chain. This makes the chains + /// self-certifying. + /// + /// The semantics of the delegation are established + /// by the subject. + /// + /// [`Invocation`]: super::Invocation + pub subject: DID, + + /// The issuer of the [`Invocation`]. + /// + /// This [`Did`] *must* match the signature on + /// the outer layer of [`Invocation`]. + /// + /// [`Invocation`]: super::Invocation + pub issuer: DID, + + /// The agent being delegated to. + /// + /// Note that if this is the same as the [`subject`], + /// this field may be omitted. + #[builder(default)] + pub audience: Option, + + /// The [Ability] being invoked. + /// + /// The specific shape and semantics of this ability + /// are established by the [`subject`] and the `A` type. + /// + /// [Ability]: crate::ability + pub ability: A, + + /// [`Cid`] links to the proofs that authorize this [`Invocation`]. + /// + /// These must be given in order starting from one where the [`issuer`] + /// of this invocation matches the [`audience`] of that [`Delegation`] proof. + /// + /// [`Invocation`]: super::Invocation + /// [`Delegation`]: crate::delegation::Delegation + #[builder(default)] + pub proofs: Vec, + + /// An optional [`Cid`] of the [`Receipt`] that requested this be invoked. + /// + /// This is helpful for provenance of calls. + /// + /// [`Receipt`]: crate::receipt::Receipt + #[builder(default)] + pub cause: Option, + + /// Extensible, free-form fields. + #[builder(default)] + pub metadata: BTreeMap, + + /// A [cryptographic nonce] to ensure that the UCAN's [`Cid`] is unique. + /// + /// [cryptographic nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + #[builder(default = "Nonce::generate_16()")] + pub nonce: Nonce, + + /// An optional [Unix timestamp] (wall-clock time) at which this [`Invocation`] + /// was created. + #[builder(default)] + pub issued_at: Option, + + /// An optional [Unix timestamp] (wall-clock time) at which this [`Invocation`] + /// should no longer be executed. + /// + /// One way of thinking about this is as a `timeout`. It also guards against + /// certain types of denial-of-service attacks. + #[builder(default = "Some(Timestamp::five_minutes_from_now())")] + pub expiration: Option, +} + +impl Payload { + pub fn map_ability(self, f: F) -> Payload + where + F: FnOnce(A) -> Z, + { + Payload { + issuer: self.issuer, + subject: self.subject, + audience: self.audience, + ability: f(self.ability), + proofs: self.proofs, + cause: self.cause, + metadata: self.metadata, + nonce: self.nonce, + issued_at: self.issued_at, + expiration: self.expiration, + } + } + + pub fn check_time(&self, now: SystemTime) -> Result<(), Expired> { + let ts_now = &Timestamp::postel(now); + + if let Some(ref exp) = self.expiration { + if exp < ts_now { + return Err(Expired); + } + } + + Ok(()) + } + + pub fn check( + &self, + proofs: Vec<&delegation::Payload>, + now: SystemTime, + ) -> Result<(), ValidationError> + where + A: ToCommand + Clone, + DID: Clone, + Named: From, + { + let now_ts = Timestamp::postel(now); + + if let Some(exp) = self.expiration { + if exp < now_ts { + return Err(ValidationError::Expired); + } + } + + let args: Named = self.ability.clone().into(); + + let mut cmd = self.ability.to_command(); + if !cmd.ends_with('/') { + cmd.push('/'); + } + + let (final_iss, vias) = proofs.into_iter().try_fold( + (&self.issuer, BTreeSet::new()), + |(iss, mut vias), proof| { + if *iss != proof.audience { + return Err(ValidationError::MisalignedIssAud.into()); + } + + if let Some(proof_subject) = &proof.subject { + if self.subject != *proof_subject { + return Err(ValidationError::InvalidSubject.into()); + } + } + + if let Some(exp) = proof.expiration { + if exp < now_ts { + return Err(ValidationError::Expired.into()); + } + } + + if let Some(nbf) = proof.not_before.clone() { + if nbf > now_ts { + return Err(ValidationError::NotYetValid.into()); + } + } + + vias.remove(&iss); + if let Some(via_did) = &proof.via { + vias.insert(via_did); + } + + if !cmd.starts_with(&proof.command) { + return Err(ValidationError::CommandMismatch(proof.command.clone())); + } + + let ipld_args = Ipld::from(args.clone()); + + for predicate in proof.policy.iter() { + if !predicate + .clone() + .run(&ipld_args) + .map_err(ValidationError::SelectorError)? + { + return Err(ValidationError::FailedPolicy(predicate.clone())); + } + } + + Ok((&proof.issuer, vias)) + }, + )?; + + if self.subject != *final_iss { + return Err(ValidationError::DidNotTerminateInSubject); + } + + if !vias.is_empty() { + return Err(ValidationError::UnfulfilledViaConstraint( + vias.into_iter().cloned().collect(), + )); + } + + Ok(()) + } +} + +/// Delegation validation errors. +#[derive(Debug, Clone, PartialEq, Error)] +pub enum ValidationError { + #[error("The subject of the delegation is invalid")] + InvalidSubject, + + #[error("The issuer and audience of the delegation are misaligned")] + MisalignedIssAud, + + #[error("The delegation has expired")] + Expired, + + #[error("The delegation is not yet valid")] + NotYetValid, + + #[error("The command of the delegation does not match the proof: {0:?}")] + CommandMismatch(String), + + #[error("The delegation failed a policy predicate: {0:?}")] + FailedPolicy(Predicate), + + #[error(transparent)] + SelectorError(#[from] SelectorError), + + #[error("via field constraint was unfulfilled: {0:?}")] + UnfulfilledViaConstraint(BTreeSet), + + #[error("The chain did not terminate in the expected subject")] + DidNotTerminateInSubject, +} + +impl Capsule for Payload { + const TAG: &'static str = "ucan/i@1.0.0-rc.1"; +} + +impl From> for Named +where + Named: From, +{ + fn from(payload: Payload) -> Self { + let mut args = Named::from_iter([ + ("iss".into(), { payload.issuer.to_string().into() }), + ("sub".into(), { payload.subject.to_string().into() }), + ("cmd".into(), { payload.ability.to_command().into() }), + ("args".into(), { + Ipld::Map(Named::::from(payload.ability).0) + }), + ("prf".into(), { + Ipld::List(payload.proofs.iter().map(Into::into).collect()) + }), + ("nonce".into(), payload.nonce.into()), + ]); + + if let Some(aud) = payload.audience { + args.insert("aud".into(), aud.to_string().into()); + } + + if let Some(cause) = payload.cause { + args.insert("cause".into(), cause.into()); + } + + if !payload.metadata.is_empty() { + args.insert("meta".into(), payload.metadata.into()); + } + + if let Some(iat) = payload.issued_at { + args.insert("iat".into(), iat.into()); + } + + if let Some(exp) = payload.expiration { + args.insert("exp".into(), exp.into()); + } + + args + } +} + +impl From> for Ipld +where + Named: From>, +{ + fn from(payload: Payload) -> Self { + Named::from(payload).into() + } +} + +impl Serialize for Payload +where + A: ToCommand + Into + Serialize, + DID: Did + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let field_count = 9 + + self.audience.is_some() as usize + + self.issued_at.is_some() as usize + + self.expiration.is_some() as usize; + + let mut state = serializer.serialize_struct("invocation::Payload", field_count)?; + + state.serialize_field("iss", &self.issuer)?; + state.serialize_field("sub", &self.subject)?; + + state.serialize_field("cmd", &self.ability.to_command())?; + state.serialize_field("args", &self.ability)?; + + state.serialize_field("prf", &self.proofs)?; + state.serialize_field("nonce", &self.nonce)?; + state.serialize_field("cause", &self.cause)?; + state.serialize_field("meta", &self.metadata)?; + + if let Some(aud) = &self.audience { + state.serialize_field("aud", aud)?; + } + + if let Some(iat) = &self.issued_at { + state.serialize_field("iat", iat)?; + } + + if let Some(exp) = &self.expiration { + state.serialize_field("exp", &exp)?; + } + + state.end() + } +} + +impl<'de, A: ParseAbility, DID: Did + Deserialize<'de>> Deserialize<'de> for Payload { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + struct InvocationPayloadVisitor(std::marker::PhantomData<(A, DID)>); + + const FIELDS: &'static [&'static str] = &[ + "iss", "sub", "aud", "cmd", "args", "prf", "nonce", "cause", "meta", "iat", "exp", + ]; + + impl<'de, T: ParseAbility, DID: Did + Deserialize<'de>> Visitor<'de> + for InvocationPayloadVisitor + { + type Value = Payload; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct invocation::Payload") + } + + fn visit_map>(self, mut map: M) -> Result { + let mut issuer = None; + let mut subject = None; + let mut audience = None; + let mut command = None; + let mut arguments = None; + let mut proofs = None; + let mut nonce = None; + let mut cause = None; + let mut metadata = None; + let mut issued_at = None; + let mut expiration = None; + + while let Some(key) = map.next_key()? { + match key { + "iss" => { + if issuer.is_some() { + return Err(de::Error::duplicate_field("iss")); + } + issuer = Some(map.next_value()?); + } + "sub" => { + if subject.is_some() { + return Err(de::Error::duplicate_field("sub")); + } + subject = Some(map.next_value()?); + } + "aud" => { + if audience.is_some() { + return Err(de::Error::duplicate_field("aud")); + } + audience = map.next_value()?; + } + "cmd" => { + if command.is_some() { + return Err(de::Error::duplicate_field("cmd")); + } + command = Some(map.next_value()?); + } + "args" => { + if arguments.is_some() { + return Err(de::Error::duplicate_field("args")); + } + arguments = Some(map.next_value()?); + } + "prf" => { + if proofs.is_some() { + return Err(de::Error::duplicate_field("prf")); + } + proofs = Some(map.next_value()?); + } + "nonce" => { + if nonce.is_some() { + return Err(de::Error::duplicate_field("nonce")); + } + nonce = Some(map.next_value()?); + } + "cause" => { + if cause.is_some() { + return Err(de::Error::duplicate_field("cause")); + } + cause = map.next_value()?; + } + "meta" => { + if metadata.is_some() { + return Err(de::Error::duplicate_field("meta")); + } + metadata = Some(map.next_value()?); + } + "issued_at" => { + if issued_at.is_some() { + return Err(de::Error::duplicate_field("iat")); + } + issued_at = map.next_value()?; + } + "exp" => { + if expiration.is_some() { + return Err(de::Error::duplicate_field("exp")); + } + expiration = map.next_value()?; + } + other => { + return Err(de::Error::unknown_field(other, FIELDS)); + } + } + } + + let cmd: String = command.ok_or(de::Error::missing_field("cmd"))?; + let args = arguments.ok_or(de::Error::missing_field("args"))?; + + let ability = ::try_parse(cmd.as_str(), args).map_err(|e| { + de::Error::custom(format!( + "Unable to parse ability field for {:?} because {:?}", + cmd, e + )) + })?; + + Ok(Payload { + issuer: issuer.ok_or(de::Error::missing_field("iss"))?, + subject: subject.ok_or(de::Error::missing_field("sub"))?, + proofs: proofs.ok_or(de::Error::missing_field("prf"))?, + metadata: metadata.ok_or(de::Error::missing_field("meta"))?, + nonce: nonce.ok_or(de::Error::missing_field("nonce"))?, + audience, + ability, + cause, + issued_at, + expiration, + }) + } + } + + deserializer.deserialize_struct( + "invocation::Payload", + FIELDS, + InvocationPayloadVisitor(Default::default()), + ) + } +} + +impl Verifiable for Payload { + fn verifier(&self) -> &DID { + &self.issuer + } +} + +impl TryFrom> for Payload +where + ::ArgsErr: fmt::Debug, + ::Err: fmt::Debug, +{ + type Error = ParseError; + + fn try_from(named: Named) -> Result { + let mut subject = None; + let mut issuer = None; + let mut audience = None; + let mut command = None; + let mut args = None; + let mut cause = None; + let mut metadata = None; + let mut nonce = None; + let mut expiration = None; + let mut proofs = None; + let mut issued_at = None; + + for (k, v) in named { + match k.as_str() { + "sub" => { + subject = Some(match v { + Ipld::String(s) => { + DID::from_str(s.as_str()).map_err(ParseError::DidParseError)? + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }) + } + "iss" => match v { + Ipld::String(s) => { + issuer = Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "aud" => match v { + Ipld::String(s) => { + audience = + Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "cmd" => match v { + Ipld::String(s) => command = Some(s), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "args" => match v.try_into() { + Ok(a) => args = Some(a), + _ => return Err(ParseError::ArgsNotAMap), + }, + "meta" => match v { + Ipld::Map(m) => metadata = Some(m), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "nonce" => match v { + Ipld::Bytes(b) => nonce = Some(Nonce::from(b)), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "cause" => match v { + Ipld::Link(c) => cause = Some(c), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "exp" => match v { + Ipld::Integer(i) => expiration = Some(i.try_into()?), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "iat" => match v { + Ipld::Integer(i) => issued_at = Some(i.try_into()?), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "prf" => match &v { + Ipld::List(xs) => { + proofs = Some( + xs.iter() + .map(|x| match x { + Ipld::Link(cid) => Ok(*cid), + _ => Err(ParseError::WrongTypeForField(k.clone(), v.clone())), + }) + .collect::, ParseError>>()?, + ) + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + _ => return Err(ParseError::UnknownField(k.to_string())), + } + } + + let cmd = command.ok_or(ParseError::MissingCmd)?; + let some_args = args.ok_or(ParseError::MissingArgs)?; + let ability = ::try_parse(cmd.as_str(), some_args) + .map_err(|e| ParseError::AbilityError(e))?; + + Ok(Payload { + issuer: issuer.ok_or(ParseError::MissingIss)?, + subject: subject.ok_or(ParseError::MissingSub)?, + audience, + ability, + proofs: proofs.ok_or(ParseError::MissingProofsField)?, + cause, + metadata: metadata.unwrap_or_default(), + nonce: nonce.ok_or(ParseError::MissingNonce)?, + issued_at, + expiration, + }) + } +} + +#[derive(Debug, Error)] +pub enum ParseError +where + ::ArgsErr: fmt::Debug, + ::Err: fmt::Debug, +{ + #[error("Unknown field: {0}")] + UnknownField(String), + + #[error("Missing sub field")] + MissingSub, + + #[error("Missing iss field")] + MissingIss, + + #[error("Missing cmd field")] + MissingCmd, + + #[error("Missing args field")] + MissingArgs, + + #[error("Unable to parse ability: {0:?}")] + AbilityError(ParseAbilityError<::ArgsErr>), + + #[error("Missing nonce field")] + MissingNonce, + + #[error("Wrong type for field {0}: {1:?}")] + WrongTypeForField(String, Ipld), + + #[error("Cannot parse DID")] + DidParseError(::Err), + + // FIXME + #[error("Cannot parse timestamp: {0}")] + BadTimestamp(#[from] time::OutOfRangeError), + + #[error("Args are not a map")] + ArgsNotAMap, + + #[error("Misisng proofs field")] + MissingProofsField, +} + +/// A variant that accepts [`Promise`]s. +/// +/// [`Promise`]: crate::invocation::promise::Promise +pub type Promised = Payload<::Promised, DID>; + +#[cfg(feature = "test_utils")] +impl Arbitrary for Payload +where + T::Strategy: 'static, + DID::Parameters: Clone, +{ + type Parameters = (T::Parameters, DID::Parameters); + type Strategy = BoxedStrategy; + + fn arbitrary_with((t_args, did_args): Self::Parameters) -> Self::Strategy { + ( + T::arbitrary_with(t_args), + DID::arbitrary_with(did_args.clone()), + DID::arbitrary_with(did_args.clone()), + Option::::arbitrary_with((0.5.into(), did_args)), + Nonce::arbitrary(), + prop::collection::vec(cid::Newtype::arbitrary().prop_map(|nt| nt.cid), 0..12), + Option::::arbitrary().prop_map(|opt_nt| opt_nt.map(|nt| nt.cid)), + Option::::arbitrary(), + Option::::arbitrary(), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..12).prop_map(|m| { + m.into_iter() + .map(|(k, v)| (k, v.0)) + .collect::>() + }), + ) + .prop_map( + |( + ability, + issuer, + subject, + audience, + nonce, + proofs, + cause, + expiration, + issued_at, + metadata, + )| { + Payload { + issuer, + subject, + audience, + ability, + proofs, + cause, + nonce, + metadata, + issued_at, + expiration, + } + }, + ) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ability::msg::Msg; + use crate::ipld; + use assert_matches::assert_matches; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test_log::test] + fn test_ipld_round_trip(payload in Payload::::arbitrary()) { + let observed: Named = payload.clone().into(); + let parsed = Payload::::try_from(observed.clone()); + + prop_assert!(parsed.is_ok()); + prop_assert_eq!(parsed.unwrap(), payload); + } + + #[test_log::test] + fn test_ipld_only_has_correct_fields(payload in Payload::::arbitrary()) { + let observed: Ipld = payload.clone().into(); + + if let Ipld::Map(named) = observed { + prop_assert!(named.len() >= 6); + prop_assert!(named.len() <= 11); + + for key in named.keys() { + prop_assert!(matches!(key.as_str(), "sub" | "iss" | "aud" | "cmd" | "args" | "prf" | "cause" | "meta" | "nonce" | "exp" | "iat")); + } + } else { + prop_assert!(false, "ipld map"); + } + } + + #[test_log::test] + fn test_ipld_field_types(payload in Payload::::arbitrary()) { + let named: Named = payload.clone().into(); + + let sub = named.get("sub".into()); + let iss = named.get("iss".into()); + let cmd = named.get("cmd".into()); + let args = named.get("args".into()); + let prf = named.get("prf".into()); + let nonce = named.get("nonce".into()); + + // Required Fields + prop_assert_eq!(sub.unwrap(), &Ipld::String(payload.subject.to_string())); + prop_assert_eq!(iss.unwrap(), &Ipld::String(payload.issuer.to_string())); + prop_assert_eq!(cmd.unwrap(), &Ipld::String(payload.ability.to_command())); + + prop_assert_eq!(args.unwrap(), &payload.ability.into()); + prop_assert!(matches!(args, Some(Ipld::Map(_)))); + + prop_assert!(matches!(prf.unwrap(), &Ipld::List(_))); + if let Some(Ipld::List(ipld_proofs)) = prf { + prop_assert_eq!(ipld_proofs.len(), payload.proofs.len()); + + for entry in ipld_proofs { + prop_assert!(matches!(entry, Ipld::Link(_))); + } + } else { + prop_assert!(false); + } + + prop_assert_eq!(nonce.unwrap(), &payload.nonce.into()); + + // Optional Fields + prop_assert_eq!(payload.audience.map(|did| did.into()), named.get("aud").cloned()); + prop_assert_eq!(payload.cause.map(Ipld::Link), named.get("cause").cloned()); + + match (payload.metadata.is_empty(), named.get("meta")) { + (false, Some(Ipld::Map(btree))) => { + prop_assert_eq!(&payload.metadata, btree); + } + (true, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.expiration, named.get("exp")) { + (Some(exp), Some(Ipld::Integer(i))) => { + prop_assert_eq!(i128::from(exp), i.clone()); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.issued_at, named.get("iat")) { + (Some(iat), Some(Ipld::Integer(i))) => { + prop_assert_eq!(i128::from(iat), i.clone()); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + } + + #[test_log::test] + fn test_non_payload(named in Named::::arbitrary()) { + // Just ensuring that a negative test shows up + let parsed = Payload::::try_from(named); + prop_assert!(parsed.is_err()) + } + } +} diff --git a/src/invocation/promise.rs b/src/invocation/promise.rs new file mode 100644 index 00000000..baacb12b --- /dev/null +++ b/src/invocation/promise.rs @@ -0,0 +1,43 @@ +//! [UCAN Promise](https://github.com/ucan-wg/promise)s: selectors, wrappers, and traits. + +mod any; +mod err; +mod ok; +mod pending; +mod resolvable; + +pub mod store; +// FIXME pub mod js; + +pub use any::Any; +pub use err::PromiseErr; +pub use ok::PromiseOk; +pub use pending::Pending; +pub use resolvable::*; +pub use store::Store; + +use enum_as_inner::EnumAsInner; +use libipld_core::cid::Cid; +use serde::{Deserialize, Serialize}; + +/// Top-level union of all UCAN Promise options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner)] +pub enum Promise { + /// The `ucan/await/ok` promise + Ok(T), + + /// The `ucan/await/err` promise + Err(E), + + /// The `ucan/await/ok` promise + PendingOk(Cid), + + /// The `ucan/await/err` promise + PendingErr(Cid), + + /// The `ucan/await/*` promise + PendingAny(Cid), + + /// The `ucan/await` promise + PendingTagged(Cid), +} diff --git a/src/invocation/promise/any.rs b/src/invocation/promise/any.rs new file mode 100644 index 00000000..657c1366 --- /dev/null +++ b/src/invocation/promise/any.rs @@ -0,0 +1,111 @@ +use crate::ipld; +use super::pending::Pending; +use enum_as_inner::EnumAsInner; +use libipld_core::cid::Cid; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +// FIXME +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner)] +pub enum Any { + /// The `ucan/await/ok` promise + Resolved(T), + + /// The `ucan/await/ok` promise + PendingOk(Cid), + + /// The `ucan/await/err` promise + PendingErr(Cid), + + /// The `ucan/await/*` promise + PendingAny(Cid), +} + +impl Any { + pub fn try_resolve(self) -> Result> { + match self { + Any::Resolved(value) => Ok(value), + _ => Err(self), + } + } + + pub fn from_ipld(ipld: Ipld) -> Self + where + T: From, + { + match ipld { + Ipld::Map(ref map) => { + if let Some(Ipld::Link(cid)) = map.get("ucan/await/ok") { + return Any::PendingOk(cid.clone()); + } + + if let Some(Ipld::Link(cid)) = map.get("ucan/await/err") { + return Any::PendingErr(cid.clone()); + } + + if let Some(Ipld::Link(cid)) = map.get("ucan/await/*") { + return Any::PendingAny(cid.clone()); + } + + Any::Resolved(ipld.into()) + } + other => Any::Resolved(other.into()), + } + } + + pub fn to_promised_ipld(self) -> ipld::Promised + where + T: Into, + { + match self { + Any::Resolved(value) => value.into(), + Any::PendingOk(cid) => ipld::Promised::WaitOk(cid), + Any::PendingErr(cid) => ipld::Promised::WaitErr(cid), + Any::PendingAny(cid) => ipld::Promised::WaitAny(cid), + } + } +} + +impl From for Any { + fn from(pending: Pending) -> Any { + match pending { + Pending::Ok(cid) => Any::PendingOk(cid), + Pending::Err(cid) => Any::PendingErr(cid), + Pending::Any(cid) => Any::PendingAny(cid), + } + } +} + +impl> From> for Ipld { + fn from(promise: Any) -> Ipld { + match promise { + Any::Resolved(val) => val.into(), + Any::PendingOk(cid) => Ipld::Map(BTreeMap::from_iter([( + "ucan/await/ok".to_string(), + cid.into(), + )])), + Any::PendingErr(cid) => Ipld::Map(BTreeMap::from_iter([( + "ucan/await/err".to_string(), + cid.into(), + )])), + Any::PendingAny(cid) => Ipld::Map(BTreeMap::from_iter([( + "ucan/await/*".to_string(), + cid.into(), + )])), + } + } +} + +impl> TryFrom for Any { + type Error = >::Error; + + fn try_from(promised: ipld::Promised) -> Result, Self::Error> { + match promised { + ipld::Promised::WaitOk(cid) => Ok(Any::PendingOk(cid)), + ipld::Promised::WaitErr(cid) => Ok(Any::PendingErr(cid)), + ipld::Promised::WaitAny(cid) => Ok(Any::PendingAny(cid)), + other => Ok(Any::Resolved(T::try_from(other)?)), + } + } +} diff --git a/src/invocation/promise/err.rs b/src/invocation/promise/err.rs new file mode 100644 index 00000000..01c47403 --- /dev/null +++ b/src/invocation/promise/err.rs @@ -0,0 +1,113 @@ +use crate::{ability::arguments, ipld::cid}; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A promise that only selects the `{"err": error}` branch of a result. +/// +/// On resolution, the value is unwrapped from the `{"err": error}`, +/// leaving just the `error` (much like [`Result::unwrap_err`]). +/// +/// FIXME exmaple +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PromiseErr { + /// The failure state of a promise. + Rejected(E), + + /// The [`Cid`] that is being waited on to return an `{"err": value}` + Pending(#[serde(rename = "await/err")] Cid), +} + +impl PromiseErr { + pub fn try_resolve(self) -> Result> { + match self { + PromiseErr::Rejected(err) => Ok(err), + PromiseErr::Pending(_cid) => Err(self), + } + } + + pub fn map(self, f: F) -> PromiseErr + where + F: FnOnce(E) -> X, + { + match self { + PromiseErr::Rejected(err) => PromiseErr::Rejected(f(err)), + PromiseErr::Pending(cid) => PromiseErr::Pending(cid), + } + } +} + +impl From> for Option { + fn from(p: PromiseErr) -> Option { + match p { + PromiseErr::Rejected(err) => Some(err), + PromiseErr::Pending(_) => None, + } + } +} + +impl From> for Ipld { + fn from(p: PromiseErr) -> Ipld { + p.into() + } +} + +impl TryFrom for PromiseErr { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result, Self::Error> { + ipld_serde::from_ipld(ipld) + } +} + +impl>> From> for arguments::Named +where + Ipld: From, +{ + fn from(p: PromiseErr) -> arguments::Named { + match p { + PromiseErr::Rejected(err) => err.into(), + PromiseErr::Pending(cid) => { + arguments::Named::from_iter([("await/err".into(), Ipld::Link(cid))]) + } + } + } +} + +impl> TryFrom> for PromiseErr { + type Error = >::Error; + + fn try_from(args: arguments::Named) -> Result, Self::Error> { + if let Some(ipld) = args.get("ucan/err") { + if args.len() == 1 { + if let Ok(cid::Newtype { cid }) = cid::Newtype::try_from(ipld) { + return Ok(PromiseErr::Pending(cid)); + } + } + } + + E::try_from(Ipld::from(args)).map(PromiseErr::Rejected) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for PromiseErr +where + T::Strategy: 'static, + T::Parameters: 'static, +{ + type Parameters = T::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(t_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + T::arbitrary_with(t_args).prop_map(PromiseErr::Rejected), + cid::Newtype::arbitrary().prop_map(|nt| PromiseErr::Pending(nt.cid)), + ] + .boxed() + } +} diff --git a/src/invocation/promise/js.rs b/src/invocation/promise/js.rs new file mode 100644 index 00000000..9f8b9069 --- /dev/null +++ b/src/invocation/promise/js.rs @@ -0,0 +1,123 @@ +use crate::ability::arguments; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde_derive::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Debug}; + +// FIXME +// #[cfg(target_arch = "wasm32")] +// use wasm_bindgen::prelude::*; +// +// #[cfg(target_arch = "wasm32")] +// #[derive(Clone, Debug, PartialEq, Eq)] +// #[wasm_bindgen] +// pub enum UcanPromiseStatus { +// Fulfilled, +// Pending, +// } +// +// // FIXME no way to make this consistent, because of C enums ruining Rust convetions, right? +// // FIXME consider wrapping in a trait +// #[cfg(target_arch = "wasm32")] +// #[derive(Clone, Debug, PartialEq)] +// #[wasm_bindgen] +// pub struct UcanPromise { +// status: UcanPromiseStatus, +// selector: Option, +// value: Option, +// } +// +// #[cfg(target_arch = "wasm32")] +// #[wasm_bindgen(getter_with_clone)] +// pub struct IncoherentPromise(pub UcanPromise); +// +// #[cfg(target_arch = "wasm32")] +// impl TryFrom for Promise { +// type Error = IncoherentPromise; +// +// fn try_from(js: UcanPromise) -> Result { +// match js.status { +// UcanPromiseStatus::Fulfilled => { +// if let Some(val) = &js.value { +// return Ok(Promise::Fulfilled(val.clone())); +// } +// } +// UcanPromiseStatus::Pending => { +// if let Some(selector) = &js.selector { +// return Ok(Promise::Pending(selector.clone())); +// } +// } +// } +// +// Err(IncoherentPromise(js)) +// } +// } +// +// #[cfg(target_arch = "wasm32")] +// impl> From> for UcanPromise { +// fn from(promise: Promise) -> Self { +// match promise { +// Promise::Fulfilled(val) => UcanPromise { +// status: UcanPromiseStatus::Fulfilled, +// selector: None, +// value: Some(val.into()), +// }, +// Promise::Pending(cid) => UcanPromise { +// status: UcanPromiseStatus::Pending, +// selector: Some(cid), +// value: None, +// }, +// } +// } +// } +// +// /// A [`Promise`] is a way to defer the presence of a value to the result of some [`Invocation`]. +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// #[serde(untagged, deny_unknown_fields)] // FIXME check that this is right, also +// pub enum Selector { +// Any { +// #[serde(rename = "ucan/*")] // FIXME test to make sure that this is right? +// any: Cid, +// }, +// Ok { +// #[serde(rename = "await/ok")] +// ok: Cid, +// }, +// Err { +// #[serde(rename = "await/err")] +// err: Cid, +// }, +// } +// +// impl From for Ipld { +// fn from(selector: Selector) -> Self { +// selector.into() +// } +// } +// +// impl TryFrom for Selector { +// type Error = (); +// +// fn try_from(ipld: Ipld) -> Result { +// ipld_serde::from_ipld(ipld).map_err(|_| ()) +// } +// } +// +// impl From for arguments::Named { +// fn from(selector: Selector) -> Self { +// let mut btree = BTreeMap::new(); +// +// match selector { +// Selector::Any { any } => { +// btree.insert("ucan/*".into(), any.into()); +// } +// Selector::Ok { ok } => { +// btree.insert("await/ok".into(), ok.into()); +// } +// Selector::Err { err } => { +// btree.insert("await/err".into(), err.into()); +// } +// } +// +// arguments::Named(btree) +// } +// } diff --git a/src/invocation/promise/ok.rs b/src/invocation/promise/ok.rs new file mode 100644 index 00000000..4cc406a9 --- /dev/null +++ b/src/invocation/promise/ok.rs @@ -0,0 +1,114 @@ +use crate::{ability::arguments, ipld::cid}; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A promise that only selects the `{"ok": value}` branch of a result. +/// +/// On resolution, the value is unwrapped from the `{"ok": value}`, +/// leaving just the `value` (much like [`Result::unwrap`]). +/// +/// FIXME exmaple +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +pub enum PromiseOk { + /// The fulfilled (resolved) value. + Fulfilled(T), + + /// The [`Cid`] that is being waited on to return an `{"ok": value}` + Pending(#[serde(rename = "await/ok")] Cid), +} + +// FIXME move try_resolve to a trait, give a blanket impl for prims, and tag them +impl PromiseOk { + pub fn try_resolve(self) -> Result> { + match self { + PromiseOk::Fulfilled(value) => Ok(value), + PromiseOk::Pending(_cid) => Err(self), + } + } + + pub fn map(self, f: F) -> PromiseOk + where + F: FnOnce(T) -> U, + { + match self { + PromiseOk::Fulfilled(val) => PromiseOk::Fulfilled(f(val)), + PromiseOk::Pending(cid) => PromiseOk::Pending(cid), + } + } +} + +impl From> for Option { + fn from(p: PromiseOk) -> Option { + match p { + PromiseOk::Fulfilled(value) => Some(value), + PromiseOk::Pending(_) => None, + } + } +} + +impl From> for Ipld { + fn from(p: PromiseOk) -> Ipld { + p.into() + } +} + +impl TryFrom for PromiseOk { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result, Self::Error> { + ipld_serde::from_ipld(ipld) + } +} + +impl>> From> for arguments::Named +where + Ipld: From, +{ + fn from(p: PromiseOk) -> arguments::Named { + match p { + PromiseOk::Fulfilled(val) => val.into(), + PromiseOk::Pending(cid) => { + arguments::Named::from_iter([("await/ok".into(), Ipld::Link(cid))]) + } + } + } +} + +impl> TryFrom> for PromiseOk { + type Error = >::Error; + + fn try_from(args: arguments::Named) -> Result, Self::Error> { + if let Some(ipld) = args.get("ucan/ok") { + if args.len() == 1 { + if let Ok(cid::Newtype { cid }) = cid::Newtype::try_from(ipld) { + return Ok(PromiseOk::Pending(cid)); + } + } + } + + T::try_from(Ipld::from(args)).map(PromiseOk::Fulfilled) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for PromiseOk +where + T::Strategy: 'static, + T::Parameters: 'static, +{ + type Parameters = T::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(t_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + T::arbitrary_with(t_args).prop_map(PromiseOk::Fulfilled), + cid::Newtype::arbitrary().prop_map(|nt| PromiseOk::Pending(nt.cid)), + ] + .boxed() + } +} diff --git a/src/invocation/promise/pending.rs b/src/invocation/promise/pending.rs new file mode 100644 index 00000000..1c56e48c --- /dev/null +++ b/src/invocation/promise/pending.rs @@ -0,0 +1,9 @@ +use libipld_core::cid::Cid; + +// AKA Selector +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Pending { + Ok(Cid), + Err(Cid), + Any(Cid), +} diff --git a/src/invocation/promise/resolvable.rs b/src/invocation/promise/resolvable.rs new file mode 100644 index 00000000..c65c70da --- /dev/null +++ b/src/invocation/promise/resolvable.rs @@ -0,0 +1,92 @@ +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParsePromised}, + }, + invocation::promise::Pending, + ipld, +}; +use libipld_core::{cid::Cid, ipld::Ipld}; +use std::{collections::BTreeSet, fmt}; +use thiserror::Error; + +/// A trait for [`Delegable`]s that can be deferred (by promises). +/// +/// FIXME exmaples +pub trait Resolvable: Sized + ParseAbility + ToCommand { + /// The promise type that resolves to `Self`. + /// + /// Note that this may be a more complex type than the promise selector + /// variants. One example is [letting any leaf][PromiseIpld] of an [`Ipld`] graph + /// be a promise. + /// + /// [PromiseIpld]: crate::ipld::Promised + type Promised: ToCommand + + ParsePromised // TryFrom> + + Into>; + + /// Attempt to resolve the [`Self::Promised`]. + fn try_resolve(promised: Self::Promised) -> Result> + where + Self::Promised: Clone, + { + let ipld_promise: arguments::Named = promised.clone().into(); + match arguments::Named::::try_from(ipld_promise) { + Err(pending) => Err(CantResolve { + promised, + reason: ResolveError::StillWaiting(pending), + }), + Ok(named) => { + ParseAbility::try_parse(&promised.to_command(), named).map_err(|_reason| { + CantResolve { + promised, + reason: ResolveError::ConversionError, + } + }) + } + } + } + + fn get_all_pending(promised: Self::Promised) -> BTreeSet { + let promise_map: arguments::Named = promised.into(); + + promise_map + .values() + .fold(BTreeSet::new(), |mut set, promised| { + if let ipld::Promised::Link(cid) = promised { + set.insert(*cid); + } + + set + }) + } +} + +#[derive(Error, Clone)] +pub struct CantResolve { + pub promised: S::Promised, + pub reason: ResolveError, +} + +impl fmt::Debug for CantResolve +where + S::Promised: fmt::Debug, + Pending: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CantResolve") + .field("promised", &self.promised) + .field("reason", &self.reason) + .finish() + } +} + +#[derive(Error, PartialEq, Eq, Clone, Debug)] +pub enum ResolveError { + #[error("The promise is still has arguments waiting to be resolved")] + StillWaiting(Pending), + + #[error("The resolved promise was unable to reify an ability from IPLD")] + ConversionError, +} diff --git a/src/invocation/promise/store.rs b/src/invocation/promise/store.rs new file mode 100644 index 00000000..2cd345c2 --- /dev/null +++ b/src/invocation/promise/store.rs @@ -0,0 +1,7 @@ +//! Storage of resolved and unresolved promises. + +mod memory; +mod traits; + +pub use memory::MemoryStore; +pub use traits::Store; diff --git a/src/invocation/promise/store/memory.rs b/src/invocation/promise/store/memory.rs new file mode 100644 index 00000000..afc5653b --- /dev/null +++ b/src/invocation/promise/store/memory.rs @@ -0,0 +1,48 @@ +use super::Store; +use libipld_core::cid::Cid; +use std::{ + collections::{BTreeMap, BTreeSet}, + convert::Infallible, +}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct MemoryStore { + pub index: BTreeMap>, +} + +impl Store for MemoryStore { + type PromiseStoreError = Infallible; + + fn put_waiting( + &mut self, + invocation: Cid, + waiting_on: Vec, + ) -> Result<(), Self::PromiseStoreError> { + self.index + .insert(invocation, BTreeSet::from_iter(waiting_on)); + + Ok(()) + } + + fn get_waiting( + &self, + waiting_on: &mut Vec, + ) -> Result, Self::PromiseStoreError> { + Ok(match waiting_on.pop() { + None => BTreeSet::new(), + Some(first) => waiting_on + .iter() + .try_fold(BTreeSet::from_iter([first]), |acc, cid| { + let next = self.index.get(cid).ok_or(())?; + + let reduced: BTreeSet = acc.intersection(&next).cloned().collect(); + if reduced.is_empty() { + return Err(()); + } + + Ok(reduced) + }) + .unwrap_or_default(), + }) + } +} diff --git a/src/invocation/promise/store/traits.rs b/src/invocation/promise/store/traits.rs new file mode 100644 index 00000000..35568ab3 --- /dev/null +++ b/src/invocation/promise/store/traits.rs @@ -0,0 +1,17 @@ +use libipld_core::cid::Cid; +use std::collections::BTreeSet; + +pub trait Store { + type PromiseStoreError; + + fn put_waiting( + &mut self, + invocation: Cid, + waiting_on: Vec, + ) -> Result<(), Self::PromiseStoreError>; + + fn get_waiting( + &self, + waiting_on: &mut Vec, + ) -> Result, Self::PromiseStoreError>; +} diff --git a/src/invocation/store.rs b/src/invocation/store.rs new file mode 100644 index 00000000..224e0c77 --- /dev/null +++ b/src/invocation/store.rs @@ -0,0 +1,7 @@ +//! Storage for [`Invocation`]s. + +mod memory; +mod traits; + +pub use memory::{MemoryStore, MemoryStoreInner}; +pub use traits::Store; diff --git a/src/invocation/store/memory.rs b/src/invocation/store/memory.rs new file mode 100644 index 00000000..06af37c6 --- /dev/null +++ b/src/invocation/store/memory.rs @@ -0,0 +1,62 @@ +use super::Store; +use crate::{crypto::varsig, did::Did, invocation::Invocation}; +use libipld_core::{cid::Cid, codec::Codec}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{collections::BTreeMap, convert::Infallible}; + +#[derive(Debug, Clone)] +pub struct MemoryStore< + T = crate::ability::preset::Preset, + DID: crate::did::Did = crate::did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + inner: Arc>>, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MemoryStoreInner< + T = crate::ability::preset::Preset, + DID: crate::did::Did = crate::did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + store: BTreeMap>>, +} + +impl, Enc: Codec> MemoryStore { + fn lock(&self) -> MutexGuard<'_, MemoryStoreInner> { + match self.inner.lock() { + Ok(guard) => guard, + Err(poison) => { + // There's no logic errors through lock poisoning in our case + poison.into_inner() + } + } + } +} + +impl, Enc: Codec> Default for MemoryStore { + fn default() -> Self { + Self { + inner: Arc::new(Mutex::new(MemoryStoreInner { + store: BTreeMap::new(), + })), + } + } +} + +impl, Enc: Codec> Store + for MemoryStore +{ + type Error = Infallible; + + fn get(&self, cid: Cid) -> Result>>, Self::Error> { + Ok(self.lock().store.get(&cid).cloned()) + } + + fn put(&self, cid: Cid, invocation: Invocation) -> Result<(), Self::Error> { + self.lock().store.insert(cid, Arc::new(invocation)); + Ok(()) + } +} diff --git a/src/invocation/store/traits.rs b/src/invocation/store/traits.rs new file mode 100644 index 00000000..5340c925 --- /dev/null +++ b/src/invocation/store/traits.rs @@ -0,0 +1,29 @@ +use crate::{crypto::varsig, did::Did, invocation::Invocation}; +use libipld_core::{cid::Cid, codec::Codec}; +use std::{fmt::Debug, sync::Arc}; + +pub trait Store, C: Codec> { + type Error: Debug; + + fn get(&self, cid: Cid) -> Result>>, Self::Error>; + + fn put(&self, cid: Cid, invocation: Invocation) -> Result<(), Self::Error>; + + fn has(&self, cid: Cid) -> Result { + Ok(self.get(cid).is_ok()) + } +} + +impl, T, DID: Did, V: varsig::Header, C: Codec> Store + for &S +{ + type Error = >::Error; + + fn get(&self, cid: Cid) -> Result>>, Self::Error> { + (**self).get(cid) + } + + fn put(&self, cid: Cid, invocation: Invocation) -> Result<(), Self::Error> { + (**self).put(cid, invocation) + } +} diff --git a/src/ipld.rs b/src/ipld.rs new file mode 100644 index 00000000..61b2e2bb --- /dev/null +++ b/src/ipld.rs @@ -0,0 +1,19 @@ +//! Helpers for working with [`Ipld`][libipld_core::ipld::Ipld]. +//! +//! [`Ipld`] is a fully concrete data type, and only has a few trait implementations. +//! This module provides a few newtype wrappers that allow you to add trait implementations, +//! and generalized forms to embed non-IPLD into IPLD structure. +//! +//! [`Ipld`]: libipld_core::ipld::Ipld + +mod collection; +mod number; +mod promised; + +pub mod cid; +pub mod newtype; + +pub use collection::Collection; +pub use newtype::Newtype; +pub use number::Number; +pub use promised::*; diff --git a/src/ipld/cid.rs b/src/ipld/cid.rs new file mode 100644 index 00000000..09d852a8 --- /dev/null +++ b/src/ipld/cid.rs @@ -0,0 +1,142 @@ +//! Utilities for [`Cid`]s + +use crate::ipld; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_derive::TryFromJsValue; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::test_utils::SomeCodec; + +#[cfg(feature = "test_utils")] +use crate::test_utils::SomeMultihash; + +/// A newtype wrapper around a [`Cid`] +/// +/// This is largely to attach traits to [`Cid`]s, such as [`wasm_bindgen`] conversions. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct Newtype { + pub cid: Cid, +} + +/// A newtype wrapper around a [`Cid`] +/// +/// This is largely to attach traits to [`Cid`]s, such as [`wasm_bindgen`] conversions. +#[cfg(target_arch = "wasm32")] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct Newtype { + #[wasm_bindgen(skip)] + pub cid: Cid, +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + /// This is here because the TryFromJsValue derivation macro + /// doesn't automatically support `Option`. + /// + /// [https://docs.rs/wasm-bindgen-derive/0.2.1/wasm_bindgen_derive/#optional-arguments] + #[wasm_bindgen(typescript_type = "Newtype | undefined")] + pub type OptionNewtype; +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl Newtype { + /// Parse a [`Newtype`] from a string + pub fn from_string(cid_string: String) -> Result { + Newtype::try_from(cid_string).map_err(|e| JsError::new(&format!("{}", e))) + } + + pub fn try_from_js_value(js: &JsValue) -> Result { + match &js.as_string() { + Some(s) => Newtype::from_string(s.clone()), + None => Err(JsError::new("Expected a string")), + } + } +} + +impl Newtype { + /// Convert the [`Cid`] to a string + pub fn to_string(&self) -> String { + self.cid.to_string() + } +} + +impl TryFrom for Newtype { + type Error = >::Error; + + fn try_from(cid_string: String) -> Result { + Cid::try_from(cid_string).map(Into::into) + } +} + +impl From for Cid { + fn from(wrapper: Newtype) -> Self { + wrapper.cid + } +} + +impl From for Newtype { + fn from(cid: Cid) -> Self { + Self { cid } + } +} + +impl TryFrom for Newtype { + type Error = NotACid; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::Link(cid) => Ok(Newtype { cid }), + other => Err(NotACid(other.into())), + } + } +} + +impl TryFrom<&Ipld> for Newtype { + type Error = NotACid; + + fn try_from(ipld: &Ipld) -> Result { + match ipld { + Ipld::Link(cid) => Ok(Newtype { cid: *cid }), + other => Err(NotACid(other.clone().into())), + } + } +} + +// #[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, PartialEq, Clone, Error, Serialize, Deserialize)] +#[error("Not a CID: {0:?}")] +pub struct NotACid(pub ipld::Newtype); + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // Very much faking it + any::<([u8; 32], SomeMultihash, SomeCodec)>() + .prop_map(|(hash_bytes, hasher, codec)| { + let multihash = + multihash::MultihashGeneric::wrap(hasher.0.into(), &hash_bytes.as_slice()) + .expect("Sha2_256 should always successfully encode a hash"); + + let cid = Cid::new_v1(codec.0.into(), multihash); + Newtype { cid } + }) + .boxed() + } +} diff --git a/src/ipld/collection.rs b/src/ipld/collection.rs new file mode 100644 index 00000000..fb7bc4b0 --- /dev/null +++ b/src/ipld/collection.rs @@ -0,0 +1,50 @@ +use crate::ipld; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Collection { + Array(Vec), + Map(BTreeMap), +} + +impl From for Ipld { + fn from(collection: Collection) -> Self { + match collection { + Collection::Array(xs) => Ipld::List(xs.into_iter().map(Into::into).collect()), + Collection::Map(xs) => Ipld::Map( + xs.into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(), + ), + } + } +} + +impl Collection { + pub fn to_vec(self) -> Vec { + match self { + Collection::Array(xs) => xs, + Collection::Map(xs) => xs.into_values().collect(), + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Collection { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + prop::collection::vec(ipld::Newtype::arbitrary(), 0..10).prop_map(Collection::Array), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..10) + .prop_map(Collection::Map), + ] + .boxed() + } +} diff --git a/src/ipld/enriched.rs b/src/ipld/enriched.rs new file mode 100644 index 00000000..bea703fc --- /dev/null +++ b/src/ipld/enriched.rs @@ -0,0 +1,201 @@ +//! A generalized version of [`Ipld`][libipld_core::ipld::Ipld] +//! that can contain non-IPLD leaves. + +use enum_as_inner::EnumAsInner; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A generalized version of [`Ipld`][libipld_core::ipld::Ipld] +/// that can contain non-IPLD leaves. +/// +/// This is helpful especially when building (mutually) recursive +/// data strutcures that are reducable to [`Ipld`], such as +/// [`ipld::Promised`][crate::ipld::Promised]. +#[derive(Clone, Debug, PartialEq, EnumAsInner, Serialize, Deserialize)] +pub enum Enriched { + /// Lifted [`Ipld::Null`] + Null, + + /// Lifted [`Ipld::Bool`] + Bool(bool), + + /// Lifted [`Ipld::Integer`] + Integer(i128), + + /// Lifted [`Ipld::Float`] + Float(f64), + + /// Lifted [`Ipld::String`] + String(String), + + /// Lifted [`Ipld::Bytes`] (byte array) + Bytes(Vec), + + /// Lifted [`Ipld::Link`] + Link(Cid), + + /// [`Ipld::List`], but where the values are the provided [`T`]. + List(Vec), + + /// [`Ipld::Map`], but where the values are the provided [`T`]. + Map(BTreeMap), +} + +impl<'a, T: Clone> IntoIterator for &'a Enriched { + type Item = Item<'a, T>; + type IntoIter = PostOrderIpldIter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + PostOrderIpldIter::new(&self) + } +} + +impl<'a, T: Clone> FromIterator> for Enriched { + fn from_iter>>(it: I) -> Self { + it.into_iter().fold(Enriched::Null, |acc, x| match x { + Item::Node(Enriched::Null) => Enriched::Null, + Item::Node(Enriched::Bool(b)) => Enriched::Bool(*b), + Item::Node(Enriched::Integer(i)) => Enriched::Integer(*i), + Item::Node(Enriched::Float(f)) => Enriched::Float(*f), + Item::Node(Enriched::String(s)) => Enriched::String(s.clone()), + Item::Node(Enriched::Bytes(b)) => Enriched::Bytes(b.clone()), + Item::Node(Enriched::Link(c)) => Enriched::Link(c.clone()), + Item::Node(Enriched::List(vec)) => { + let mut list = vec![]; + for item in vec { + list.push(item); + } + Enriched::List(list.iter().map(|a| (*a).clone()).collect()) + } + Item::Node(Enriched::Map(btree)) => { + let mut map = BTreeMap::new(); + for (k, v) in btree { + map.insert(k.clone(), (*v).clone()); + } + Enriched::Map(map) + } + Item::Inner(_) => acc, + }) + } +} + +impl<'a, T: Clone> From<&'a Enriched> for PostOrderIpldIter<'a, T> { + fn from(enriched: &'a Enriched) -> Self { + PostOrderIpldIter::new(enriched) + } +} +impl> From for Enriched { + fn from(ipld: Ipld) -> Self { + match ipld { + Ipld::Null => Enriched::Null, + Ipld::Bool(b) => Enriched::Bool(b), + Ipld::Integer(i) => Enriched::Integer(i), + Ipld::Float(f) => Enriched::Float(f), + Ipld::String(s) => Enriched::String(s), + Ipld::Bytes(b) => Enriched::Bytes(b), + Ipld::List(l) => Enriched::List(l.into_iter().map(From::from).collect()), + Ipld::Map(m) => Enriched::Map(m.into_iter().map(|(k, v)| (k, From::from(v))).collect()), + Ipld::Link(c) => Enriched::Link(c), + } + } +} + +impl> TryFrom> for Ipld { + type Error = Enriched; + + fn try_from(enriched: Enriched) -> Result { + match enriched { + Enriched::List(ref vec) => { + let result: Result, ()> = vec.iter().try_fold(vec![], |mut acc, x| { + let resolved = x.clone().try_into().map_err(|_| ())?; + acc.push(resolved); + Ok(acc) + }); + + match result { + Ok(vec) => Ok(vec.into()), + Err(()) => Err(enriched), + } + } + Enriched::Map(ref btree) => { + let result: Result, ()> = + btree.iter().try_fold(BTreeMap::new(), |mut acc, (k, v)| { + let resolved = v.clone().try_into().map_err(|_| ())?; + acc.insert(k.clone(), resolved); + Ok(acc) + }); + + match result { + Ok(vec) => Ok(vec.into()), + Err(()) => Err(enriched), + } + } + Enriched::Null => Ok(Ipld::Null), + Enriched::Bool(b) => Ok(b.into()), + Enriched::Integer(i) => Ok(i.into()), + Enriched::Float(f) => Ok(f.into()), + Enriched::String(s) => Ok(s.into()), + Enriched::Bytes(b) => Ok(b.into()), + Enriched::Link(l) => Ok(l.into()), + } + } +} + +/*************************** +| POST ORDER IPLD ITERATOR | +***************************/ + +/// A post-order [`Ipld`] iterator +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde-codec", derive(serde::Serialize))] +#[allow(clippy::module_name_repetitions)] +pub struct PostOrderIpldIter<'a, T> { + inbound: Vec>, + outbound: Vec>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Item<'a, T> { + Node(&'a Enriched), + Inner(&'a T), +} + +impl<'a, T> PostOrderIpldIter<'a, T> { + /// Initialize a new [`PostOrderIpldIter`] + #[must_use] + pub fn new(enriched: &'a Enriched) -> Self { + PostOrderIpldIter { + inbound: vec![Item::Node(enriched)], + outbound: vec![], + } + } +} + +impl<'a, T: Clone> Iterator for PostOrderIpldIter<'a, T> { + type Item = Item<'a, T>; + + fn next(&mut self) -> Option { + loop { + match self.inbound.pop() { + None => return self.outbound.pop(), + Some(ref map @ Item::Node(Enriched::Map(ref btree))) => { + self.outbound.push(map.clone()); + + for node in btree.values() { + self.inbound.push(Item::Inner(node)); + } + } + + Some(ref list @ Item::Node(Enriched::List(ref vector))) => { + self.outbound.push(list.clone()); + + for node in vector { + self.inbound.push(Item::Inner(node)); + } + } + Some(node) => self.outbound.push(node), + } + } + } +} diff --git a/src/ipld/newtype.rs b/src/ipld/newtype.rs new file mode 100644 index 00000000..7e898ed2 --- /dev/null +++ b/src/ipld/newtype.rs @@ -0,0 +1,310 @@ +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use js_sys::{Array, Map, Object, Uint8Array}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use super::cid; + +#[cfg(target_arch = "wasm32")] +use super::cid; + +/// A newtype wrapper around [`Ipld`] that has additional trait implementations. +/// +/// Usage is very simple: wrap a [`Newtype`] to gain access to additional traits and methods. +/// +/// ```rust +/// # use libipld_core::ipld::Ipld; +/// # use ucan::ipld; +/// # +/// let ipld = Ipld::String("hello".into()); +/// let wrapped = ipld::Newtype(ipld.clone()); +/// // wrapped.some_trait_method(); +/// ``` +/// +/// Unwrap a [`Newtype`] to use any interfaces that expect plain [`Ipld`]. +/// +/// ``` +/// # use libipld_core::ipld::Ipld; +/// # use ucan::ipld; +/// # +/// # let ipld = Ipld::String("hello".into()); +/// # let wrapped = ipld::Newtype(ipld.clone()); +/// # +/// assert_eq!(wrapped.0, ipld); +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Newtype(pub Ipld); + +impl From for Newtype { + fn from(ipld: Ipld) -> Self { + Self(ipld) + } +} + +impl From for Newtype { + fn from(i: i128) -> Self { + Newtype(Ipld::Integer(i)) + } +} + +impl From for Newtype { + fn from(f: f64) -> Self { + Newtype(Ipld::Float(f)) + } +} + +impl From<&str> for Newtype { + fn from(s: &str) -> Self { + Newtype(Ipld::String(s.to_string())) + } +} + +impl From for Newtype { + fn from(s: String) -> Self { + Newtype(Ipld::String(s)) + } +} + +impl TryFrom for String { + type Error = (); + + fn try_from(nt: Newtype) -> Result { + match nt.0 { + Ipld::String(s) => Ok(s), + _ => Err(()), + } + } +} + +impl TryFrom for i128 { + type Error = (); + + fn try_from(nt: Newtype) -> Result { + match nt.0 { + Ipld::Integer(i) => Ok(i), + _ => Err(()), + } + } +} + +impl From for Ipld { + fn from(wrapped: Newtype) -> Self { + wrapped.0 + } +} + +impl From for Newtype { + fn from(path: PathBuf) -> Self { + Newtype(Ipld::String(path.to_string_lossy().to_string())) + } +} + +impl TryFrom for PathBuf { + type Error = NotAString; + + fn try_from(wrapped: Newtype) -> Result { + match wrapped.0 { + Ipld::String(s) => Ok(PathBuf::from(s)), + ipld => Err(NotAString(ipld)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +#[error("Ipld variant is not a string")] +pub struct NotAString(pub Ipld); + +#[cfg(target_arch = "wasm32")] +impl Newtype { + pub fn try_from_js>(js_val: JsValue) -> Result + where + JsError: From<>::Error>, + { + match Newtype::try_from(js_val) { + Err(_err) => Err(JsError::new("can't convert")), + Ok(nt) => nt.0.try_into().map_err(JsError::from), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for JsValue { + fn from(wrapped: Newtype) -> Self { + match wrapped.0 { + Ipld::Null => JsValue::NULL, + Ipld::Bool(b) => JsValue::from(b), + Ipld::Integer(i) => JsValue::from(i), + Ipld::Float(f) => JsValue::from_f64(f), + Ipld::String(s) => JsValue::from_str(&s), + Ipld::Bytes(bs) => { + let u8arr = Uint8Array::new(&bs.len().into()); + for (i, b) in bs.iter().enumerate() { + u8arr.set_index(i as u32, *b); + } + JsValue::from(u8arr) + } + Ipld::List(ls) => { + let arr = Array::new(); + for ipld in ls { + arr.push(&JsValue::from(Newtype(ipld))); + } + JsValue::from(arr) + } + Ipld::Map(m) => { + let map = Map::new(); + for (k, v) in m { + map.set(&JsValue::from(k), &JsValue::from(Newtype(v))); + } + JsValue::from(map) + } + Ipld::Link(cid) => cid::Newtype::from(cid).into(), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl TryFrom for Newtype { + type Error = (); // FIXME + + fn try_from(js_val: JsValue) -> Result { + if js_val.is_null() { + return Ok(Newtype(Ipld::Null)); + } + + if let Some(b) = js_val.as_bool() { + return Ok(Newtype(Ipld::Bool(b))); + } + + if let Some(f) = js_val.as_f64() { + return Ok(Newtype(Ipld::Float(f))); + } + + if let Some(s) = js_val.as_string() { + return Ok(Newtype(Ipld::String(s))); + } + + if let Some(arr) = js_val.dyn_ref::() { + let mut list = vec![]; + for x in arr.to_vec().iter() { + let ipld = Newtype::try_from(x.clone())?.into(); + list.push(ipld); + } + + return Ok(Newtype(Ipld::List(list))); + } + + if let Some(arr) = js_val.dyn_ref::() { + let mut v = vec![]; + for item in arr.to_vec().iter() { + v.push(item.clone()); + } + + return Ok(Newtype(Ipld::Bytes(v))); + } + + if let Some(map) = js_val.dyn_ref::() { + let mut m = std::collections::BTreeMap::new(); + let mut acc = Ok(()); + + // Weird order, but correct per the docs + // vvvvvvvvvv + map.for_each(&mut |value, key| { + if acc.is_err() { + return; + } + + match (key.as_string(), Newtype::try_from(value.clone())) { + (Some(k), Ok(v)) => { + m.insert(k, v.0); + } + _ => { + acc = Err(()); + } + } + }); + + return acc.map(|_| Newtype(Ipld::Map(m))); + } + + // NOTE *must* come before `is_object` (which is hopefully below) + if let Ok(nt) = cid::Newtype::try_from_js_value(&js_val) { + return Ok(Newtype(Ipld::Link(nt.into()))); + } + + if js_val.is_object() { + let obj = Object::from(js_val); + let mut m = std::collections::BTreeMap::new(); + let mut acc = Ok(()); + + Object::entries(&obj).for_each(&mut |js_val, _, _| { + if acc.is_err() { + return; + } + + // By definition this must be the array [value, key], in that order + let arr = Array::from(&js_val); + + match (arr.get(0).as_string(), Newtype::try_from(arr.get(1))) { + (Some(k), Ok(v)) => { + m.insert(k, v.0); + } + // FIXME more specific errors + _ => { + acc = Err(()); + } + } + }); + + return acc.map(|_| Newtype(Ipld::Map(m))); + } + + // NOTE fails on `undefined` and `function` + + Err(()) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + let leaf = prop_oneof![ + Just(Ipld::Null), + any::().prop_map(Ipld::Bool), + any::>().prop_map(Ipld::Bytes), + any::().prop_map(move |i| { + Ipld::Integer((i % (2 ^ 53)).into()) // NOTE Because DAG-JSON + }), + any::().prop_map(Ipld::Float), + ".*".prop_map(Ipld::String), + any::().prop_map(|newtype_cid| { Ipld::Link(newtype_cid.cid) }) + ]; + + let coll = leaf.clone().prop_recursive(16, 1024, 128, |inner| { + prop_oneof![ + prop::collection::vec(inner.clone(), 0..128).prop_map(Ipld::List), + prop::collection::btree_map(".*", inner, 0..128).prop_map(Ipld::Map), + ] + }); + + prop_oneof![ + 1 => leaf, + 9 => coll + ] + .prop_map(Newtype) + .boxed() + } +} diff --git a/src/ipld/number.rs b/src/ipld/number.rs new file mode 100644 index 00000000..25cbaae3 --- /dev/null +++ b/src/ipld/number.rs @@ -0,0 +1,87 @@ +//! Helpers for working with [`Ipld`] numerics. + +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// The union of [`Ipld`] numeric types +/// +/// This is helpful when comparing different numeric types, such as +/// bounds checking in [`Predicate`]s. +/// +/// [`Predicate`]: crate::delegation::policy::predicate::Predicate +#[derive(Debug, Clone, PartialEq, EnumAsInner, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Number { + /// Designate a floating point number + Float(f64), + + /// Designate an integer + Integer(i128), +} + +impl PartialOrd for Number { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Number::Float(a), Number::Float(b)) => a.partial_cmp(b), + (Number::Integer(a), Number::Integer(b)) => a.partial_cmp(b), + (Number::Float(a), Number::Integer(b)) => a.partial_cmp(&(*b as f64)), + (Number::Integer(a), Number::Float(b)) => (*a as f64).partial_cmp(b), + } + } +} + +impl From for Ipld { + fn from(number: Number) -> Self { + match number { + Number::Float(f) => Ipld::Float(f), + Number::Integer(i) => Ipld::Integer(i), + } + } +} + +impl TryFrom for Number { + type Error = NotANumber; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::Integer(i) => Ok(Number::Integer(i)), + Ipld::Float(f) => Ok(Number::Float(f)), + _ => Err(NotANumber(ipld)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +#[error("Expected Ipld numeric, got: {0:?}")] +pub struct NotANumber(Ipld); + +impl From for Number { + fn from(i: i128) -> Number { + Number::Integer(i) + } +} + +impl From for Number { + fn from(f: f64) -> Number { + Number::Float(f) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Number { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + any::().prop_map(Number::Float), + any::().prop_map(Number::Integer), + ] + .boxed() + } +} diff --git a/src/ipld/promised.rs b/src/ipld/promised.rs new file mode 100644 index 00000000..5e336148 --- /dev/null +++ b/src/ipld/promised.rs @@ -0,0 +1,405 @@ +use crate::{ + ability::arguments, + invocation::promise::{self, Pending, PromiseErr, PromiseOk}, + url, +}; +use enum_as_inner::EnumAsInner; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt, path::PathBuf}; + +/// A recursive data structure whose leaves may be [`Ipld`] or promises. +/// +/// [`Promised`] resolves to regular [`Ipld`]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner)] +pub enum Promised { + /// Resolved null. + Null, + + /// Resolved Boolean. + Bool(bool), + + /// Resolved integer. + Integer(i128), + + /// Resolved float. + Float(f64), + + /// Resolved string. + String(String), + + /// Resolved bytes. + Bytes(Vec), + + /// Resolved link. + Link(Cid), + + /// Promise pending the `ok` branch. + WaitOk(Cid), + + /// Promise pending the `err` branch. + WaitErr(Cid), + + /// Promise pending either branch. + WaitAny(Cid), + + /// Recursively promised list. + List(Vec), + + /// Recursively promised map. + Map(BTreeMap), +} + +impl Promised { + pub fn try_resolve(self) -> Result { + match self { + Promised::WaitOk(cid) => Err(Pending::Ok(cid)), + Promised::WaitErr(cid) => Err(Pending::Err(cid)), + Promised::WaitAny(cid) => Err(Pending::Any(cid)), + other => other.try_into().map_err(Into::into), + } + } + + pub fn with_resolved(self, f: F) -> Result + where + F: FnOnce(Ipld) -> T, + { + match self.try_into() { + Ok(ipld) => Ok(f(ipld)), + Err(pending) => Err(pending), + } + } + + pub fn with_pending(self, f: F) -> Result + where + F: FnOnce(Pending) -> E, + { + match self.try_into() { + Ok(ipld) => Err(ipld), + Err(promised) => Ok(f(promised)), + } + } + + pub fn to_promise_any>( + self, + ) -> Result, >::Error> { + Ok(match Ipld::try_from(self) { + Ok(ipld) => promise::Any::Resolved(ipld.try_into()?), + Err(pending) => match pending { + Pending::Ok(cid) => promise::Any::PendingOk(cid), + Pending::Err(cid) => promise::Any::PendingErr(cid), + Pending::Any(cid) => promise::Any::PendingAny(cid), + }, + }) + } + + // FIXME return type + pub fn to_promise_any_string(self) -> Result, ()> { + match self { + Promised::String(s) => Ok(promise::Any::Resolved(s)), + Promised::WaitOk(cid) => Ok(promise::Any::PendingOk(cid)), + Promised::WaitErr(cid) => Ok(promise::Any::PendingErr(cid)), + Promised::WaitAny(cid) => Ok(promise::Any::PendingAny(cid)), + _ => Err(()), + } + } +} + +impl fmt::Display for Promised { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Promised::Null => write!(f, "null"), + Promised::Bool(b) => write!(f, "{}", b), + Promised::Integer(i) => write!(f, "{}", i), + Promised::Float(fl) => write!(f, "{}", fl), + Promised::String(s) => write!(f, "{}", s), + Promised::Bytes(b) => write!(f, "{:?}", b), + Promised::Link(cid) => write!(f, "{}", cid), + Promised::WaitOk(cid) => write!(f, "await/ok: {}", cid), + Promised::WaitErr(cid) => write!(f, "await/err: {}", cid), + Promised::WaitAny(cid) => write!(f, "await/*: {}", cid), + Promised::List(list) => { + write!(f, "[")?; + for (i, promised) in list.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", promised)?; + } + write!(f, "]") + } + Promised::Map(map) => { + write!(f, "{{")?; + for (i, (k, v)) in map.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", k, v)?; + } + write!(f, "}}") + } + } + } +} + +impl From for Promised { + fn from(ipld: Ipld) -> Promised { + match ipld { + Ipld::Null => Promised::Null, + Ipld::Bool(b) => Promised::Bool(b), + Ipld::Integer(i) => Promised::Integer(i), + Ipld::Float(f) => Promised::Float(f), + Ipld::String(s) => Promised::String(s), + Ipld::Bytes(b) => Promised::Bytes(b), + Ipld::Link(cid) => Promised::Link(cid), + Ipld::List(list) => Promised::List(list.into_iter().map(Into::into).collect()), + Ipld::Map(map) => { + if map.len() == 1 { + if let Some((k, Ipld::Link(cid))) = map.first_key_value() { + return match k.as_str() { + "await/ok" => Promised::WaitOk(*cid), + "await/err" => Promised::WaitErr(*cid), + "await/*" => Promised::WaitAny(*cid), + _ => Promised::Map(BTreeMap::from_iter([( + k.to_string(), + Promised::Link(*cid), + )])), + }; + } + } + + let map = map.into_iter().fold(BTreeMap::new(), |mut acc, (k, v)| { + acc.insert(k, v.into()); + acc + }); + + Promised::Map(map) + } + } + } +} + +impl TryFrom for Ipld { + type Error = Pending; + + fn try_from(promised: Promised) -> Result { + match promised { + Promised::Null => Ok(Ipld::Null), + Promised::Bool(b) => Ok(Ipld::Bool(b)), + Promised::Integer(i) => Ok(Ipld::Integer(i)), + Promised::Float(f) => Ok(Ipld::Float(f)), + Promised::String(s) => Ok(Ipld::String(s)), + Promised::Bytes(b) => Ok(Ipld::Bytes(b)), + Promised::Link(cid) => Ok(Ipld::Link(cid)), + Promised::List(list) => list + .into_iter() + .try_fold(Vec::new(), |mut acc, promised| { + acc.push(promised.try_into()?); + Ok(acc) + }) + .map(Ipld::List), + Promised::Map(map) => map + .into_iter() + .try_fold(BTreeMap::new(), |mut acc, (k, v)| { + acc.insert(k, v.try_into()?); + Ok(acc) + }) + .map(Ipld::Map), + Promised::WaitOk(cid) => Err(Pending::Ok(cid).into()), + Promised::WaitErr(cid) => Err(Pending::Err(cid).into()), + Promised::WaitAny(cid) => Err(Pending::Any(cid).into()), + } + } +} + +impl From> for Promised { + fn from(p_ok: PromiseOk) -> Promised { + match p_ok { + PromiseOk::Fulfilled(ipld) => ipld.into(), + PromiseOk::Pending(cid) => Promised::WaitOk(cid), + } + } +} + +impl From> for Promised { + fn from(p_err: PromiseErr) -> Promised { + match p_err { + PromiseErr::Rejected(ipld) => ipld.into(), + PromiseErr::Pending(cid) => Promised::WaitErr(cid), + } + } +} + +impl From> for Promised { + fn from(p_any: promise::Any) -> Promised { + match p_any { + promise::Any::Resolved(ipld) => ipld.into(), + promise::Any::PendingOk(cid) => Promised::WaitOk(cid), + promise::Any::PendingErr(cid) => Promised::WaitErr(cid), + promise::Any::PendingAny(cid) => Promised::WaitAny(cid), + } + } +} + +impl From> for Promised +where + Promised: From, +{ + fn from(args: arguments::Named) -> Promised { + Promised::Map( + args.into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(), + ) + } +} + +impl From for Promised { + fn from(path: PathBuf) -> Promised { + Promised::String(path.to_string_lossy().to_string()) + } +} + +impl From for Promised { + fn from(cid: Cid) -> Promised { + Promised::Link(cid) + } +} + +impl From<::url::Url> for Promised { + fn from(url: ::url::Url) -> Promised { + Promised::String(url.to_string()) + } +} + +impl TryFrom for url::Newtype { + type Error = (); + + fn try_from(promised: Promised) -> Result { + match promised { + Promised::String(s) => Ok(url::Newtype(::url::Url::parse(&s).map_err(|_| ())?)), + _ => Err(()), + } + } +} + +impl From for Promised { + fn from(nt: url::Newtype) -> Promised { + nt.0.into() + } +} + +impl From> for Promised +where + Promised: From, +{ + fn from(opt: Option) -> Promised { + match opt { + Some(val) => val.into(), + None => Promised::Null, + } + } +} + +impl From for Promised { + fn from(s: String) -> Promised { + Promised::String(s) + } +} + +impl From for Promised { + fn from(f: f64) -> Promised { + Promised::Float(f) + } +} + +impl From for Promised { + fn from(i: i128) -> Promised { + Promised::Integer(i) + } +} + +impl From for Promised { + fn from(b: bool) -> Promised { + Promised::Bool(b) + } +} + +impl From> for Promised { + fn from(b: Vec) -> Promised { + Promised::Bytes(b) + } +} + +impl From> for Promised +where + Promised: From, +{ + fn from(map: BTreeMap) -> Promised { + Promised::Map( + map.into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(), + ) + } +} +impl From> for Promised +where + Promised: From, +{ + fn from(list: Vec) -> Promised { + Promised::List(list.into_iter().map(Into::into).collect()) + } +} + +/*************************** +| POST ORDER IPLD ITERATOR | +***************************/ + +/// A post-order [`Ipld`] iterator +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde-codec", derive(serde::Serialize))] +#[allow(clippy::module_name_repetitions)] +pub struct PostOrderIpldIter<'a> { + inbound: Vec<&'a Promised>, + outbound: Vec<&'a Promised>, +} + +impl<'a> PostOrderIpldIter<'a> { + /// Initialize a new [`PostOrderIpldIter`] + #[must_use] + pub fn new(promised: &'a Promised) -> Self { + PostOrderIpldIter { + inbound: vec![promised], + outbound: vec![], + } + } +} + +impl<'a> Iterator for PostOrderIpldIter<'a> { + type Item = &'a Promised; + + fn next(&mut self) -> Option { + loop { + match self.inbound.pop() { + None => return self.outbound.pop(), + Some(ref map @ Promised::Map(ref btree)) => { + self.outbound.push(map); + + for node in btree.values() { + self.inbound.push(node); + } + } + + Some(ref list @ Promised::List(ref vector)) => { + self.outbound.push(list); + + for node in vector { + self.inbound.push(node); + } + } + Some(node) => self.outbound.push(node), + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a7a023f4..a793a1ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,75 +1,35 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] +#![warn( + // FIXME missing_debug_implementations, + future_incompatible, + let_underscore, + // FIXME missing_docs, + rust_2021_compatibility, + nonstandard_style +)] #![deny(unreachable_pub)] -//! rs-ucan +//! ucan -use std::str::FromStr; - -use cid::{multihash, Cid}; -use serde::{de, Deserialize, Deserializer, Serialize}; +#[cfg(target_arch = "wasm32")] +extern crate alloc; -pub mod builder; -pub mod capability; +pub mod ability; +pub mod capsule; pub mod crypto; -pub mod did_verifier; -pub mod error; -pub mod plugins; -pub mod semantics; -pub mod store; +pub mod delegation; +pub mod did; +pub mod invocation; +pub mod ipld; +pub mod reader; +pub mod receipt; +pub mod task; pub mod time; -pub mod ucan; - -#[cfg(target_arch = "wasm32")] -mod wasm; - -#[cfg(target_arch = "wasm32")] -pub use wasm::*; - -#[doc(hidden)] -#[cfg(not(target_arch = "wasm32"))] -pub use linkme; - -/// The default multihash algorithm used for UCANs -pub const DEFAULT_MULTIHASH: multihash::Code = multihash::Code::Sha2_256; +pub mod url; -/// A decentralized identifier. -pub type Did = String; - -/// The empty fact -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct EmptyFact {} - -/// The default fact -pub type DefaultFact = EmptyFact; - -/// A newtype around Cid that (de)serializes as a string -#[derive(Debug, Clone)] -pub struct CidString(pub(crate) Cid); - -impl Serialize for CidString { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.0.to_string().as_str()) - } -} - -impl<'de> Deserialize<'de> for CidString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - - Cid::from_str(&s) - .map(CidString) - .map_err(|e| de::Error::custom(format!("invalid CID: {}", e))) - } -} - -/// Test utilities. -#[cfg(any(test, feature = "test_utils"))] -#[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] +#[cfg(feature = "test_utils")] pub mod test_utils; + +pub use delegation::Delegation; +pub use invocation::Invocation; +pub use receipt::Receipt; diff --git a/src/plugins.rs b/src/plugins.rs deleted file mode 100644 index 6f19c827..00000000 --- a/src/plugins.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! Plugins for definining custom semantics - -#[cfg(not(target_arch = "wasm32"))] -use linkme::distributed_slice; - -use core::fmt; -use downcast_rs::{impl_downcast, Downcast}; -use std::sync::RwLock; -use url::Url; - -use crate::{ - error::Error, - semantics::{ - ability::{Ability, TopAbility}, - caveat::{Caveat, EmptyCaveat}, - resource::Resource, - }, -}; - -pub mod ucan; -pub mod wnfs; - -#[cfg(not(target_arch = "wasm32"))] -#[distributed_slice] -#[doc(hidden)] -pub static STATIC_PLUGINS: [&dyn Plugin< - Resource = Box, - Ability = Box, - Caveat = Box, - Error = Error, ->] = [..]; - -type ErasedPlugin = dyn Plugin< - Resource = Box, - Ability = Box, - Caveat = Box, - Error = Error, ->; - -lazy_static::lazy_static! { - static ref RUNTIME_PLUGINS: RwLock> = RwLock::new(Vec::new()); -} - -/// A plugin for handling a specific scheme -pub trait Plugin: Send + Sync + Downcast + 'static { - /// The type of resource this plugin handles - type Resource; - - /// The type of ability this plugin handles - type Ability; - - /// The type of caveat this plugin handles - type Caveat; - - /// The type of error this plugin may return - type Error; - - /// The scheme this plugin handles - fn scheme(&self) -> &'static str; - - /// Handle a resource - fn try_handle_resource( - &self, - resource_uri: &Url, - ) -> Result, Self::Error>; - - /// Handle an ability - fn try_handle_ability( - &self, - resource: &Self::Resource, - ability: &str, - ) -> Result, Self::Error>; - - /// Handle a caveat - fn try_handle_caveat( - &self, - resource: &Self::Resource, - ability: &Self::Ability, - deserializer: &mut dyn erased_serde::Deserializer<'_>, - ) -> Result, Self::Error>; -} - -impl_downcast!(Plugin assoc Resource, Ability, Caveat, Error); - -/// A wrapped plugin that unifies plugin error handling, and handles common semantics, such -/// as top abilities. -pub struct WrappedPlugin -where - R: 'static, - A: 'static, - C: 'static, - E: 'static, -{ - #[doc(hidden)] - pub inner: &'static dyn Plugin, -} - -impl Plugin for WrappedPlugin -where - R: Resource, - A: Ability, - C: Caveat, - E: Into, -{ - type Resource = Box; - type Ability = Box; - type Caveat = Box; - - type Error = Error; - - fn scheme(&self) -> &'static str { - self.inner.scheme() - } - - fn try_handle_resource( - &self, - resource_uri: &Url, - ) -> Result, Self::Error> { - self.inner.try_handle_resource(resource_uri).map_or_else( - |e| Err(Error::PluginError(anyhow::anyhow!(e).into())), - |r| Ok(r.map(|r| Box::new(r) as Box)), - ) - } - - fn try_handle_ability( - &self, - resource: &Self::Resource, - ability: &str, - ) -> Result>, Self::Error> { - if ability == "*" { - return Ok(Some(Box::new(TopAbility))); - } - - let Some(resource) = resource.downcast_ref::() else { - return Ok(None); - }; - - self.inner - .try_handle_ability(resource, ability) - .map_or_else( - |e| Err(Error::PluginError(anyhow::anyhow!(e).into())), - |a| Ok(a.map(|a| Box::new(a) as Box)), - ) - } - - fn try_handle_caveat( - &self, - resource: &Self::Resource, - ability: &Self::Ability, - deserializer: &mut dyn erased_serde::Deserializer<'_>, - ) -> Result, Self::Error> { - let Some(resource) = resource.downcast_ref::() else { - return Ok(None); - }; - - if ability.is::() { - return Ok(Some(Box::new( - erased_serde::deserialize::(deserializer) - .map_err(|e| anyhow::anyhow!(e))?, - ))); - } - - let Some(ability) = ability.downcast_ref::() else { - return Ok(None); - }; - - self.inner - .try_handle_caveat(resource, ability, deserializer) - .map_or_else( - |e| Err(Error::PluginError(anyhow::anyhow!(e).into())), - |c| Ok(c.map(|c| Box::new(c) as Box)), - ) - } -} - -impl fmt::Debug for WrappedPlugin { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("WrappedPlugin") - .field("scheme", &self.inner.scheme()) - .finish() - } -} - -/// Get an iterator over all plugins -pub fn plugins() -> impl Iterator< - Item = &'static dyn Plugin< - Resource = Box, - Ability = Box, - Caveat = Box, - Error = Error, - >, -> { - cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - RUNTIME_PLUGINS - .read() - .expect("plugin lock poisoned") - .clone() - .into_iter() - } else { - let static_plugins = STATIC_PLUGINS.iter().copied(); - let runtime_plugins = RUNTIME_PLUGINS - .read() - .expect("plugin lock poisoned") - .clone() - .into_iter(); - - static_plugins.chain(runtime_plugins) - } - } -} - -/// Register a plugin -pub fn register_plugin( - plugin: &'static dyn Plugin, -) where - R: Resource, - A: Ability, - C: Caveat, - E: Into, -{ - let erased = Box::new(WrappedPlugin { inner: plugin }); - let leaked = Box::leak::<'static>(erased); - - RUNTIME_PLUGINS - .write() - .expect("plugin lock poisoned") - .push(leaked); -} - -/// Register a plugin at compile time -#[cfg(not(target_arch = "wasm32"))] -#[macro_export] -macro_rules! register_plugin { - ($name:ident, $plugin:expr) => { - #[$crate::linkme::distributed_slice($crate::plugins::STATIC_PLUGINS)] - static $name: &'static dyn $crate::plugins::Plugin< - Resource = Box, - Ability = Box, - Caveat = Box, - Error = $crate::error::Error, - > = &$crate::plugins::WrappedPlugin { inner: $plugin }; - }; -} - -/// Register a plugin at compile time -#[cfg(target_arch = "wasm32")] -#[macro_export] -macro_rules! register_plugin { - ($name:ident, $plugin:expr) => {}; -} diff --git a/src/plugins/ucan.rs b/src/plugins/ucan.rs deleted file mode 100644 index 9190d590..00000000 --- a/src/plugins/ucan.rs +++ /dev/null @@ -1,392 +0,0 @@ -//! A plugin for handling the `ucan` scheme. - -use std::fmt::Display; - -use cid::Cid; -use url::Url; - -use crate::{ - semantics::{ability::Ability, caveat::EmptyCaveat}, - Did, -}; - -use super::{Plugin, Resource}; - -/// A plugin for handling the `ucan` scheme. -#[derive(Debug)] -pub struct UcanPlugin; - -crate::register_plugin!(UCAN, &UcanPlugin); - -impl Plugin for UcanPlugin { - type Resource = UcanResource; - type Ability = UcanAbilityDelegation; - type Caveat = EmptyCaveat; - - type Error = anyhow::Error; - - fn scheme(&self) -> &'static str { - "ucan" - } - - fn try_handle_resource( - &self, - resource_uri: &Url, - ) -> Result, Self::Error> { - // TODO: I'm not handling the OwnedBy or OwnedByWithScheme cases yet, - // because the spec probably needs to be modified to treat the DID as - // a literal, by wrapping it in square brackets, to avoid parsing issues - // from treating it as an authority with a port. - match resource_uri.path() { - "*" => Ok(Some(UcanResource::AllProvable)), - "./*" => Ok(Some(UcanResource::LocallyProvable)), - path => { - if let Ok(cid) = Cid::try_from(path) { - return Ok(Some(UcanResource::ByCid(cid))); - } - - Ok(None) - } - } - } - - fn try_handle_ability( - &self, - _resource: &Self::Resource, - ability: &str, - ) -> Result, Self::Error> { - match ability { - "ucan/*" => Ok(Some(UcanAbilityDelegation)), - _ => Ok(None), - } - } - - fn try_handle_caveat( - &self, - _resource: &Self::Resource, - _ability: &Self::Ability, - deserializer: &mut dyn erased_serde::Deserializer<'_>, - ) -> Result, Self::Error> { - erased_serde::deserialize(deserializer).map_err(|e| anyhow::anyhow!(e)) - } -} - -/// A resource for the `ucan` scheme. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum UcanResource { - /// ucan: - ByCid(Cid), - /// ucan:* - AllProvable, - /// ucan:./* - LocallyProvable, - /// ucan:///* - OwnedBy(Did), - /// ucan:/// - OwnedByWithScheme(Did, String), -} - -impl Display for UcanResource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let hier_part = match self { - UcanResource::ByCid(cid) => cid.to_string(), - UcanResource::AllProvable => "*".to_string(), - UcanResource::LocallyProvable => "./*".to_string(), - UcanResource::OwnedBy(did) => format!("//{}/*", did), - UcanResource::OwnedByWithScheme(did, scheme) => format!("//{}/{}", did, scheme), - }; - - f.write_fmt(format_args!("ucan:{}", hier_part)) - } -} - -impl Resource for UcanResource { - fn is_valid_attenuation(&self, other: &dyn Resource) -> bool { - if let Some(resource) = other.downcast_ref::() { - return self == resource; - }; - - false - } -} - -/// The UCAN delegation ability from the `ucan` scheme. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct UcanAbilityDelegation; - -impl Ability for UcanAbilityDelegation { - fn is_valid_attenuation(&self, other: &dyn Ability) -> bool { - if let Some(ability) = other.downcast_ref::() { - return self == ability; - }; - - false - } -} - -impl Display for UcanAbilityDelegation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "ucan/*") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_plugin_scheme() { - assert_eq!(UcanPlugin.scheme(), "ucan"); - } - - #[test] - fn test_plugin_try_handle_resource_by_cid() -> anyhow::Result<()> { - let resource = UcanPlugin.try_handle_resource(&Url::parse( - "ucan:bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - )?)?; - - assert_eq!( - resource, - Some(UcanResource::ByCid(Cid::try_from( - "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" - )?)) - ); - - Ok(()) - } - - #[test] - fn test_plugin_try_handle_resource_all_provable() -> anyhow::Result<()> { - let resource = UcanPlugin.try_handle_resource(&Url::parse("ucan:*")?)?; - - assert_eq!(resource, Some(UcanResource::AllProvable)); - - Ok(()) - } - - #[test] - fn test_plugin_try_handle_resource_locally_provable() -> anyhow::Result<()> { - let resource = UcanPlugin.try_handle_resource(&Url::parse("ucan:./*")?)?; - - assert_eq!(resource, Some(UcanResource::LocallyProvable)); - - Ok(()) - } - - #[test] - #[ignore = "Spec expects DID not to be URL encoded, but this results in invalid URLs"] - fn test_plugin_try_handle_resource_owned_by() -> anyhow::Result<()> { - let resource = UcanPlugin.try_handle_resource(&Url::parse( - "ucan://did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/*", - )?)?; - - assert_eq!( - resource, - Some(UcanResource::OwnedBy( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string() - )) - ); - - Ok(()) - } - - #[test] - #[ignore = "Spec expects DID not to be URL encoded, but this results in invalid URLs"] - fn test_plugin_try_handle_resource_owned_with_scheme() -> anyhow::Result<()> { - let resource = UcanPlugin.try_handle_resource(&Url::parse( - "ucan://did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/wnfs", - )?)?; - - assert_eq!( - resource, - Some(UcanResource::OwnedByWithScheme( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(), - "wnfs".to_string() - )) - ); - - Ok(()) - } - - #[test] - fn test_plugin_try_handle_ability_delegation() -> anyhow::Result<()> { - let ability = UcanPlugin.try_handle_ability(&UcanResource::AllProvable, "ucan/*")?; - - assert_eq!(ability, Some(UcanAbilityDelegation)); - - Ok(()) - } - - #[test] - fn test_resource_by_cid_display() -> anyhow::Result<()> { - let resource = UcanResource::ByCid(Cid::try_from( - "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - )?); - - assert_eq!( - resource.to_string(), - "ucan:bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" - ); - - Ok(()) - } - - #[test] - fn test_resource_all_provable_display() { - let resource = UcanResource::AllProvable; - - assert_eq!(resource.to_string(), "ucan:*"); - } - - #[test] - fn test_resource_locally_provable_display() { - let resource = UcanResource::LocallyProvable; - - assert_eq!(resource.to_string(), "ucan:./*"); - } - - #[test] - fn test_resource_owned_by_display() { - let resource = UcanResource::OwnedBy( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(), - ); - - assert_eq!( - resource.to_string(), - "ucan://did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/*" - ); - } - - #[test] - fn test_resource_owned_by_with_scheme_display() { - let resource = UcanResource::OwnedByWithScheme( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(), - "wnfs".to_string(), - ); - - assert_eq!( - resource.to_string(), - "ucan://did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/wnfs" - ); - } - - #[test] - fn test_ability_delegation_display() { - let ability = UcanAbilityDelegation; - - assert_eq!(ability.to_string(), "ucan/*"); - } - - #[test] - fn test_resource_attenuation() -> anyhow::Result<()> { - let all_provable = UcanResource::AllProvable; - let locally_provable = UcanResource::LocallyProvable; - - let by_cid_1 = UcanResource::ByCid(Cid::try_from( - "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - )?); - - let by_cid_2 = UcanResource::ByCid(Cid::try_from( - "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - )?); - - let owned_by_1 = UcanResource::OwnedBy( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(), - ); - - let owned_by_2 = UcanResource::OwnedBy("did:example:123456789abcdefghi".to_string()); - - let owned_by_with_scheme_1 = UcanResource::OwnedByWithScheme( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(), - "wnfs".to_string(), - ); - - let owned_by_with_scheme_2 = UcanResource::OwnedByWithScheme( - "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(), - "ucan".to_string(), - ); - - assert!(all_provable.is_valid_attenuation(&all_provable)); - assert!(!all_provable.is_valid_attenuation(&locally_provable)); - assert!(!all_provable.is_valid_attenuation(&by_cid_1)); - assert!(!all_provable.is_valid_attenuation(&by_cid_2)); - assert!(!all_provable.is_valid_attenuation(&owned_by_1)); - assert!(!all_provable.is_valid_attenuation(&owned_by_2)); - assert!(!all_provable.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!all_provable.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!locally_provable.is_valid_attenuation(&all_provable)); - assert!(locally_provable.is_valid_attenuation(&locally_provable)); - assert!(!locally_provable.is_valid_attenuation(&by_cid_1)); - assert!(!locally_provable.is_valid_attenuation(&by_cid_2)); - assert!(!locally_provable.is_valid_attenuation(&owned_by_1)); - assert!(!locally_provable.is_valid_attenuation(&owned_by_2)); - assert!(!locally_provable.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!locally_provable.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!by_cid_1.is_valid_attenuation(&all_provable)); - assert!(!by_cid_1.is_valid_attenuation(&locally_provable)); - assert!(by_cid_1.is_valid_attenuation(&by_cid_1)); - assert!(!by_cid_1.is_valid_attenuation(&by_cid_2)); - assert!(!by_cid_1.is_valid_attenuation(&owned_by_1)); - assert!(!by_cid_1.is_valid_attenuation(&owned_by_2)); - assert!(!by_cid_1.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!by_cid_1.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!by_cid_2.is_valid_attenuation(&all_provable)); - assert!(!by_cid_2.is_valid_attenuation(&locally_provable)); - assert!(!by_cid_2.is_valid_attenuation(&by_cid_1)); - assert!(by_cid_2.is_valid_attenuation(&by_cid_2)); - assert!(!by_cid_2.is_valid_attenuation(&owned_by_1)); - assert!(!by_cid_2.is_valid_attenuation(&owned_by_2)); - assert!(!by_cid_2.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!by_cid_2.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!owned_by_1.is_valid_attenuation(&all_provable)); - assert!(!owned_by_1.is_valid_attenuation(&locally_provable)); - assert!(!owned_by_1.is_valid_attenuation(&by_cid_1)); - assert!(!owned_by_1.is_valid_attenuation(&by_cid_2)); - assert!(owned_by_1.is_valid_attenuation(&owned_by_1)); - assert!(!owned_by_1.is_valid_attenuation(&owned_by_2)); - assert!(!owned_by_1.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!owned_by_1.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!owned_by_2.is_valid_attenuation(&all_provable)); - assert!(!owned_by_2.is_valid_attenuation(&locally_provable)); - assert!(!owned_by_2.is_valid_attenuation(&by_cid_1)); - assert!(!owned_by_2.is_valid_attenuation(&by_cid_2)); - assert!(!owned_by_2.is_valid_attenuation(&owned_by_1)); - assert!(owned_by_2.is_valid_attenuation(&owned_by_2)); - assert!(!owned_by_2.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!owned_by_2.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&all_provable)); - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&locally_provable)); - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&by_cid_1)); - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&by_cid_2)); - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&owned_by_1)); - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&owned_by_2)); - assert!(owned_by_with_scheme_1.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(!owned_by_with_scheme_1.is_valid_attenuation(&owned_by_with_scheme_2)); - - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&all_provable)); - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&locally_provable)); - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&by_cid_1)); - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&by_cid_2)); - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&owned_by_1)); - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&owned_by_2)); - assert!(!owned_by_with_scheme_2.is_valid_attenuation(&owned_by_with_scheme_1)); - assert!(owned_by_with_scheme_2.is_valid_attenuation(&owned_by_with_scheme_2)); - - Ok(()) - } - - #[test] - fn test_ability_attenuation() -> anyhow::Result<()> { - let ability = UcanAbilityDelegation; - - assert!(ability.is_valid_attenuation(&ability)); - - Ok(()) - } -} diff --git a/src/plugins/wnfs.rs b/src/plugins/wnfs.rs deleted file mode 100644 index 66e3045b..00000000 --- a/src/plugins/wnfs.rs +++ /dev/null @@ -1,389 +0,0 @@ -//! A plugin for handling the `wnfs` scheme. - -use std::fmt::Display; - -use crate::semantics::{ability::Ability, caveat::EmptyCaveat, resource::Resource}; -use url::Url; - -use super::Plugin; - -/// A plugin for handling the `wnfs` scheme. -#[derive(Debug)] -pub struct WnfsPlugin; - -crate::register_plugin!(WNFS, &WnfsPlugin); - -impl Plugin for WnfsPlugin { - type Resource = WnfsResource; - type Ability = WnfsAbility; - type Caveat = EmptyCaveat; - - type Error = anyhow::Error; - - fn scheme(&self) -> &'static str { - "wnfs" - } - - fn try_handle_resource( - &self, - resource_uri: &Url, - ) -> Result, Self::Error> { - let Some(user) = resource_uri.host_str() else { - return Ok(None); - }; - - let Some(path_segments) = resource_uri.path_segments() else { - return Ok(None); - }; - - match path_segments.collect::>().as_slice() { - ["public", path @ ..] => Ok(Some(WnfsResource::PublicPath { - user: user.to_string(), - path: path.iter().map(|s| s.to_string()).collect(), - })), - ["private", ..] => todo!(), - _ => Ok(None), - } - } - - fn try_handle_ability( - &self, - _resource: &Self::Resource, - ability: &str, - ) -> Result, Self::Error> { - match ability { - "wnfs/create" => Ok(Some(WnfsAbility::Create)), - "wnfs/revise" => Ok(Some(WnfsAbility::Revise)), - "wnfs/soft_delete" => Ok(Some(WnfsAbility::SoftDelete)), - "wnfs/overwrite" => Ok(Some(WnfsAbility::Overwrite)), - "wnfs/super_user" => Ok(Some(WnfsAbility::SuperUser)), - _ => Ok(None), - } - } - - fn try_handle_caveat( - &self, - _resource: &Self::Resource, - _ability: &Self::Ability, - deserializer: &mut dyn erased_serde::Deserializer<'_>, - ) -> Result, Self::Error> { - erased_serde::deserialize(deserializer).map_err(|e| anyhow::anyhow!(e)) - } -} - -/// A resource for the `wnfs` scheme. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum WnfsResource { - /// wnfs:///public/ - PublicPath { - /// The user - user: String, - /// The path - path: Vec, - }, - /// wnfs:///private/ - PrivatePath { - /// The user - user: String, - }, // TODO -} - -impl Resource for WnfsResource { - fn is_valid_attenuation(&self, other: &dyn Resource) -> bool { - let Some(other) = other.downcast_ref::() else { - return false; - }; - - match self { - WnfsResource::PublicPath { user, path } => { - let WnfsResource::PublicPath { - user: other_user, - path: other_path, - } = other - else { - return false; - }; - - if user != other_user { - return false; - } - - path.strip_prefix(other_path.as_slice()).is_some() - } - WnfsResource::PrivatePath { .. } => todo!(), - } - } -} - -impl Display for WnfsResource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WnfsResource::PublicPath { user, path } => { - f.write_fmt(format_args!("wnfs://{}/public/{}", user, path.join("/"))) - } - - WnfsResource::PrivatePath { .. } => todo!(), - } - } -} - -/// An ability for the `wnfs` scheme. -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum WnfsAbility { - /// wnfs/create - Create, - /// wnfs/revise - Revise, - /// wnfs/soft_delete - SoftDelete, - /// wnfs/overwrite - Overwrite, - /// wnfs/super_user - SuperUser, -} - -impl Ability for WnfsAbility { - fn is_valid_attenuation(&self, other: &dyn Ability) -> bool { - let Some(other) = other.downcast_ref::() else { - return false; - }; - - self <= other - } -} - -impl Display for WnfsAbility { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WnfsAbility::Create => f.write_str("wnfs/create"), - WnfsAbility::Revise => f.write_str("wnfs/revise"), - WnfsAbility::SoftDelete => f.write_str("wnfs/soft_delete"), - WnfsAbility::Overwrite => f.write_str("wnfs/overwrite"), - WnfsAbility::SuperUser => f.write_str("wnfs/super_user"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_plugin_scheme() { - assert_eq!(WnfsPlugin.scheme(), "wnfs"); - } - - #[test] - fn test_plugin_try_handle_resource_public() -> anyhow::Result<()> { - let resource = - WnfsPlugin.try_handle_resource(&Url::parse("wnfs://user/public/path/to/file")?)?; - - assert_eq!( - resource, - Some(WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["path".to_string(), "to".to_string(), "file".to_string()], - }) - ); - - Ok(()) - } - - #[test] - fn test_plugin_try_handle_resource_invalid() -> anyhow::Result<()> { - let resource = - WnfsPlugin.try_handle_resource(&Url::parse("wnfs://user/invalid/path/to/file")?)?; - - assert_eq!(resource, None); - - Ok(()) - } - - #[test] - fn test_plugin_try_handle_ability_public() -> anyhow::Result<()> { - let resource = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["path".to_string(), "to".to_string(), "file".to_string()], - }; - - let ability_create = WnfsPlugin.try_handle_ability(&resource, "wnfs/create")?; - let ability_revise = WnfsPlugin.try_handle_ability(&resource, "wnfs/revise")?; - let ability_soft_delete = WnfsPlugin.try_handle_ability(&resource, "wnfs/soft_delete")?; - let ability_overwrite = WnfsPlugin.try_handle_ability(&resource, "wnfs/overwrite")?; - let ability_super_user = WnfsPlugin.try_handle_ability(&resource, "wnfs/super_user")?; - let ability_invalid = WnfsPlugin.try_handle_ability(&resource, "wnfs/not-an-ability")?; - - assert_eq!(ability_create, Some(WnfsAbility::Create)); - assert_eq!(ability_revise, Some(WnfsAbility::Revise)); - assert_eq!(ability_soft_delete, Some(WnfsAbility::SoftDelete)); - assert_eq!(ability_overwrite, Some(WnfsAbility::Overwrite)); - assert_eq!(ability_super_user, Some(WnfsAbility::SuperUser)); - assert_eq!(ability_invalid, None); - - Ok(()) - } - - #[test] - fn test_resource_public_display() { - let resource = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - assert_eq!(resource.to_string(), "wnfs://user/public/foo/bar"); - } - - #[test] - fn test_resource_public_attenuation_identity() { - let resource = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - assert!(resource.is_valid_attenuation(&resource)); - } - - #[test] - fn test_resource_public_attenuation_child() { - let parent = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - let child = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()], - }; - - assert!(child.is_valid_attenuation(&parent)); - } - - #[test] - fn test_resource_public_attenuation_descendent() { - let ancestor = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - let descendent = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec![ - "foo".to_string(), - "bar".to_string(), - "baz".to_string(), - "qux".to_string(), - ], - }; - - assert!(descendent.is_valid_attenuation(&ancestor)); - } - - #[test] - fn test_resource_public_attenuation_parent() { - let parent = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - let child = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()], - }; - - assert!(!parent.is_valid_attenuation(&child)); - } - - #[test] - fn test_resource_public_attenuation_ancestor() { - let ancestor = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - let descendent = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec![ - "foo".to_string(), - "bar".to_string(), - "baz".to_string(), - "qux".to_string(), - ], - }; - - assert!(!ancestor.is_valid_attenuation(&descendent)); - } - - #[test] - fn test_resource_public_attenuation_sibling() { - let sibling_1 = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - let sibling_2 = WnfsResource::PublicPath { - user: "user".to_string(), - path: vec!["foo".to_string(), "baz".to_string()], - }; - - assert!(!sibling_1.is_valid_attenuation(&sibling_2)); - assert!(!sibling_2.is_valid_attenuation(&sibling_1)); - } - - #[test] - fn test_resource_public_attenuation_distinct_users() { - let path_1 = WnfsResource::PublicPath { - user: "user1".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - let path_2 = WnfsResource::PublicPath { - user: "user2".to_string(), - path: vec!["foo".to_string(), "bar".to_string()], - }; - - assert!(!path_1.is_valid_attenuation(&path_2)); - assert!(!path_2.is_valid_attenuation(&path_1)); - } - - #[test] - fn test_ability_attenuation() { - assert!(WnfsAbility::Create.is_valid_attenuation(&WnfsAbility::Create)); - assert!(WnfsAbility::Create.is_valid_attenuation(&WnfsAbility::Revise)); - assert!(WnfsAbility::Create.is_valid_attenuation(&WnfsAbility::SoftDelete)); - assert!(WnfsAbility::Create.is_valid_attenuation(&WnfsAbility::Overwrite)); - assert!(WnfsAbility::Create.is_valid_attenuation(&WnfsAbility::SuperUser)); - - assert!(!WnfsAbility::Revise.is_valid_attenuation(&WnfsAbility::Create)); - assert!(WnfsAbility::Revise.is_valid_attenuation(&WnfsAbility::Revise)); - assert!(WnfsAbility::Revise.is_valid_attenuation(&WnfsAbility::SoftDelete)); - assert!(WnfsAbility::Revise.is_valid_attenuation(&WnfsAbility::Overwrite)); - assert!(WnfsAbility::Revise.is_valid_attenuation(&WnfsAbility::SuperUser)); - - assert!(!WnfsAbility::SoftDelete.is_valid_attenuation(&WnfsAbility::Create)); - assert!(!WnfsAbility::SoftDelete.is_valid_attenuation(&WnfsAbility::Revise)); - assert!(WnfsAbility::SoftDelete.is_valid_attenuation(&WnfsAbility::SoftDelete)); - assert!(WnfsAbility::SoftDelete.is_valid_attenuation(&WnfsAbility::Overwrite)); - assert!(WnfsAbility::SoftDelete.is_valid_attenuation(&WnfsAbility::SuperUser)); - - assert!(!WnfsAbility::Overwrite.is_valid_attenuation(&WnfsAbility::Create)); - assert!(!WnfsAbility::Overwrite.is_valid_attenuation(&WnfsAbility::Revise)); - assert!(!WnfsAbility::Overwrite.is_valid_attenuation(&WnfsAbility::SoftDelete)); - assert!(WnfsAbility::Overwrite.is_valid_attenuation(&WnfsAbility::Overwrite)); - assert!(WnfsAbility::Overwrite.is_valid_attenuation(&WnfsAbility::SuperUser)); - - assert!(!WnfsAbility::SuperUser.is_valid_attenuation(&WnfsAbility::Create)); - assert!(!WnfsAbility::SuperUser.is_valid_attenuation(&WnfsAbility::Revise)); - assert!(!WnfsAbility::SuperUser.is_valid_attenuation(&WnfsAbility::SoftDelete)); - assert!(!WnfsAbility::SuperUser.is_valid_attenuation(&WnfsAbility::Overwrite)); - assert!(WnfsAbility::Overwrite.is_valid_attenuation(&WnfsAbility::SuperUser)); - } - - #[test] - fn test_ability_display() { - assert_eq!(WnfsAbility::Create.to_string(), "wnfs/create"); - assert_eq!(WnfsAbility::Revise.to_string(), "wnfs/revise"); - assert_eq!(WnfsAbility::SoftDelete.to_string(), "wnfs/soft_delete"); - assert_eq!(WnfsAbility::Overwrite.to_string(), "wnfs/overwrite"); - assert_eq!(WnfsAbility::SuperUser.to_string(), "wnfs/super_user"); - } -} diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 00000000..2a4e12c5 --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,9 @@ +//! Configure & attach an ambient environment to a value. +//! +//! See the [`Reader`] struct for more information. + +mod generic; +mod promised; + +pub use generic::Reader; +pub use promised::Promised; diff --git a/src/reader/generic.rs b/src/reader/generic.rs new file mode 100644 index 00000000..680ead6c --- /dev/null +++ b/src/reader/generic.rs @@ -0,0 +1,175 @@ +use crate::ability::{arguments, command::ToCommand, parse::ParseAbilityError}; +use libipld_core::ipld::Ipld; + +/// A struct that attaches an ambient environment to a value. +/// +/// This is a simple way to perform runtime [dependency injection][DI] in a way +/// that plumbs through traits. +/// +/// This is helpful for dependency injection and/or passing around values that +/// would otherwise need to be threaded through next to the value. +/// +/// This is loosely based on the [functional `Reader`][SO] type, +/// but is not implemented with forced purity. Many of the "ambient" features +/// and guarantees of the [functional `Reader`][SO] monad are not present here. +/// +/// # Examples +/// +/// ```rust +/// # use ucan::reader::Reader; +/// # use std::string::ToString; +/// # +/// struct Config { +/// name: String, +/// formatter: Box String>, +/// trimmer: Box String>, +/// } +/// +/// fn run(r: Reader) -> String { +/// let formatted = (r.env.formatter)(r.val.to_string()); +/// (r.env.trimmer)(formatted) +/// } +/// +/// let cfg1 = Config { +/// name: "cfg1".into(), +/// formatter: Box::new(|s| s.to_uppercase()), +/// trimmer: Box::new(|mut s| s.trim().into()) +/// }; +/// +/// let cfg2 = Config { +/// name: "cfg2".into(), +/// formatter: Box::new(|s| s.to_lowercase()), +/// trimmer: Box::new(|mut s| s.split_off(5).into()) +/// }; +/// +/// +/// let reader1 = Reader { +/// env: cfg1, +/// val: " value", +/// }; +/// +/// let reader2 = Reader { +/// env: cfg2, +/// val: " value", +/// }; +/// +/// assert_eq!(run(reader1), "VALUE"); +/// assert_eq!(run(reader2), "e"); +/// ``` +/// +/// [SO]: https://stackoverflow.com/questions/14178889/what-is-the-purpose-of-the-reader-monad +/// [DI]: https://en.wikipedia.org/wiki/Dependency_injection +#[derive(Clone, PartialEq, Debug)] +pub struct Reader { + /// The environment (or configuration) being passed with the value + pub env: Env, + + /// The raw value + pub val: T, +} + +impl Reader { + /// Map a function over the `val` of the [`Reader`] + pub fn map(self, func: F) -> Reader + where + F: FnOnce(T) -> U, + { + Reader { + env: self.env, + val: func(self.val), + } + } + + /// Modify the `env` field of the [`Reader`] + pub fn map_env(self, func: F) -> Reader + where + F: FnOnce(Env) -> NewEnv, + { + Reader { + env: func(self.env), + val: self.val, + } + } + + /// Temporarily modify the environment + /// + /// # Examples + /// + /// ```rust + /// # use ucan::reader::Reader; + /// # use std::string::ToString; + /// # + /// # #[derive(Clone)] + /// struct Config<'a> { + /// name: String, + /// formatter: &'a dyn Fn(String) -> String, + /// trimmer: &'a dyn Fn(String) -> String, + /// } + /// + /// fn run(r: Reader) -> String { + /// let formatted = (r.env.formatter)(r.val.to_string()); + /// (r.env.trimmer)(formatted) + /// } + /// + /// let cfg = Config { + /// name: "cfg1".into(), + /// formatter: &|s| s.to_uppercase(), + /// trimmer: &|mut s| s.trim().into() + /// }; + /// + /// let my_reader = Reader { + /// env: cfg, + /// val: " value", + /// }; + /// + /// assert_eq!(run(my_reader.clone()), "VALUE"); + /// + /// // Modify the env locally + /// let observed = my_reader.clone().local(|mut env| { + /// // Modifying env + /// env.trimmer = &|mut s: String| s.split_off(5).into(); + /// env + /// }, |r| run(r)); // Running + /// assert_eq!(observed, "E"); + /// + /// // Back to normal (the above was in fact "local") + /// assert_eq!(run(my_reader.clone()), "VALUE"); + /// ``` + pub fn local(&self, modify_env: F, closure: G) -> U + where + T: Clone, + Env: Clone, + F: Fn(Env) -> Env, + G: Fn(Reader) -> U, + { + closure(Reader { + val: self.val.clone(), + env: modify_env(self.env.clone()), + }) + } +} + +impl>> From> for arguments::Named { + fn from(reader: Reader) -> Self { + reader.val.into() + } +} + +impl ToCommand for Reader { + fn to_command(&self) -> String { + self.env.to_command() + } +} + +impl>> TryFrom> + for Reader +{ + type Error = ParseAbilityError<>>::Error>; + + fn try_from(args: arguments::Named) -> Result { + Ok(Reader { + env: Default::default(), + val: T::try_from(args).map_err(ParseAbilityError::InvalidArgs)?, + }) + } +} diff --git a/src/reader/promised.rs b/src/reader/promised.rs new file mode 100644 index 00000000..bea7d705 --- /dev/null +++ b/src/reader/promised.rs @@ -0,0 +1,41 @@ +use super::Reader; +use crate::ability::{arguments, command::ToCommand}; +use serde::{Deserialize, Serialize}; + +/// A helper newtype that marks a value as being a [`Resolvable::Promised`][crate::invocation::Resolvable::Promised]. +/// +/// Despite this being the intention, due to constraits, the consuming type needs to +/// implement the [`Resolvable`][crate::invocation::Resolvable] trait. +/// For example, there is a `wasm_bindgen` implementation in this crate if +/// compiled for `wasm32`. +/// +/// The is often used as: +/// +/// ```rust +/// # use ucan::reader::{Reader, Promised}; +/// # type Env = (); +/// # let env = (); +/// let example: Reader> = Reader { +/// env: env, +/// val: Promised(42), +/// }; +/// ``` +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Promised(pub T); + +impl>> From> for arguments::Named { + fn from(promised: Promised) -> Self { + promised.0.into() + } +} +impl From> for Reader> { + fn from(reader: Reader) -> Self { + reader.map(Promised) + } +} + +impl From>> for Reader { + fn from(reader: Reader>) -> Self { + reader.map(|p| p.0) + } +} diff --git a/src/receipt.rs b/src/receipt.rs new file mode 100644 index 00000000..abf1d38c --- /dev/null +++ b/src/receipt.rs @@ -0,0 +1,128 @@ +//! A [`Receipt`] is the (optional) response from an [`Invocation`][`crate::invocation::Invocation`]. +//! +//! - [`Receipt`]s are the result of an [`Invocation`][`crate::invocation::Invocation`]. +//! - [`Payload`] contains the pimary semantic information for a [`Receipt`]. +//! - [`Store`] is the storage interface for [`Receipt`]s. +//! - [`Responds`] associates the response success type to an [Ability][crate::ability]. + +mod payload; +mod responds; + +pub mod store; + +pub use payload::*; +pub use responds::Responds; +pub use store::Store; + +use crate::ability::arguments; +use crate::{ + crypto::{signature::Envelope, varsig}, + did::{self, Did}, +}; +use libipld_core::{codec::Codec, ipld::Ipld}; +use serde::{Deserialize, Serialize}; + +/// The complete, signed receipt of an [`Invocation`][`crate::invocation::Invocation`]. +#[derive(Clone, Debug, PartialEq)] +pub struct Receipt< + T: Responds, + DID: Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec = varsig::encoding::Preset, +> { + pub varsig_header: V, + pub signature: DID::Signature, + pub payload: Payload, + + _marker: std::marker::PhantomData, +} + +impl, C: Codec> did::Verifiable + for Receipt +{ + fn verifier(&self) -> &DID { + &self.payload.verifier() + } +} + +impl + Clone, C: Codec> + From> for Ipld +where + Ipld: From, + Payload: TryFrom>, +{ + fn from(rec: Receipt) -> Self { + rec.to_ipld_envelope() + } +} + +impl + Clone, C: Codec> Envelope + for Receipt +where + Ipld: From, + Payload: TryFrom>, +{ + type DID = DID; + type Payload = Payload; + type VarsigHeader = V; + type Encoder = C; + + fn construct( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Receipt { + Receipt { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + fn varsig_header(&self) -> &V { + &self.varsig_header + } + + fn payload(&self) -> &Payload { + &self.payload + } + + fn signature(&self) -> &DID::Signature { + &self.signature + } + + fn verifier(&self) -> &DID { + &self.payload.issuer + } +} + +impl + Clone, C: Codec> Serialize + for Receipt +where + Ipld: From, + Payload: TryFrom>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_ipld_envelope().serialize(serializer) + } +} + +impl<'de, T: Responds + Clone, DID: Did + Clone, V: varsig::Header + Clone, C: Codec> + Deserialize<'de> for Receipt +where + Ipld: From, + Payload: TryFrom>, + as TryFrom>>::Error: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Self::try_from_ipld_envelope(ipld).map_err(serde::de::Error::custom) + } +} diff --git a/src/receipt/payload.rs b/src/receipt/payload.rs new file mode 100644 index 00000000..6bbca643 --- /dev/null +++ b/src/receipt/payload.rs @@ -0,0 +1,334 @@ +//! The payload (non-signature) portion of a response from an [`Invocation`]. +//! +//! [`Invocation`]: crate::invocation::Invocation + +use super::responds::Responds; +use crate::{ + ability::arguments, + capsule::Capsule, + crypto::Nonce, + did::{Did, Verifiable}, + time::Timestamp, +}; +use derive_builder::Builder; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{ + de::{self, MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Serialize, Serializer, +}; +use std::{collections::BTreeMap, fmt, fmt::Debug}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld; + +#[cfg(feature = "test_utils")] +use crate::ipld::cid; + +impl Verifiable for Payload { + fn verifier(&self) -> &DID { + &self.issuer + } +} + +/// The payload (non-signature) portion of a response from an [`Invocation`]. +/// +/// [`Invocation`]: crate::invocation::Invocation +#[derive(Debug, Clone, PartialEq, Builder)] +pub struct Payload { + /// The issuer of the [`Receipt`]. This [`Did`] *must* match the signature on + /// the outer layer of [`Receipt`]. + /// + /// [`Receipt`]: super::Receipt + pub issuer: DID, + + /// The [`Cid`] of the [`Invocation`] that was run. + /// + /// [`Invocation`]: crate::invocation::Invocation + pub ran: Cid, + + /// The output of the [`Invocation`]. This is always of + /// the form `{"ok": ...}` or `{"err": ...}`. + /// + /// [`Invocation`]: crate::invocation::Invocation + pub out: Result>, + + /// Any further [`Invocation`]s that the `ran` [`Invocation`] + /// requested to be queued next. + /// + /// [`Invocation`]: crate::invocation::Invocation + #[builder(default)] + pub next: Vec, + + /// An optional proof chain authorizing a different [`Did`] to + /// be the receipt `iss` than the audience (or subject) of the + /// [`Invocation`] that was run. + /// + /// [`Invocation`]: crate::invocation::Invocation + #[builder(default)] + pub proofs: Vec, + + /// Extensible, free-form fields. + #[builder(default)] + pub metadata: BTreeMap, + + /// A [cryptographic nonce] to ensure that the UCAN's [`Cid`] is unique. + /// + /// [cryptographic nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + /// [`Cid`]: libipld_core::cid::Cid + #[builder(default = "Nonce::generate_16()")] + pub nonce: Nonce, + + /// An optional [Unix timestamp] (wall-clock time) at which the + /// receipt claims to have been issued at. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + #[builder(default)] + pub issued_at: Option, +} + +impl Capsule for Payload { + const TAG: &'static str = "ucan/r@1.0.0-rc.1"; +} + +impl From> for arguments::Named +where + Ipld: From, +{ + fn from(payload: Payload) -> Self { + let out_ipld = match payload.out { + Ok(ok) => BTreeMap::from_iter([("ok".to_string(), Ipld::from(ok))]).into(), + Err(err) => BTreeMap::from_iter([("err".to_string(), err.0.into())]).into(), + }; + + let mut args = arguments::Named::::from_iter([ + ("iss".to_string(), Ipld::String(payload.issuer.to_string())), + ("ran".to_string(), payload.ran.into()), + ("out".to_string(), out_ipld), + ( + "next".to_string(), + Ipld::List( + payload + .next + .clone() + .into_iter() + .map(|x| Ipld::Link(x)) + .collect(), + ), + ), + ( + "prf".to_string(), + Ipld::List(payload.next.into_iter().map(|x| Ipld::Link(x)).collect()), + ), + ("meta".to_string(), payload.metadata.into()), + ("nonce".to_string(), payload.nonce.into()), + ]); + + if let Some(issued_at) = payload.issued_at { + args.insert("iat".to_string(), issued_at.into()); + } + + args + } +} + +impl Serialize for Payload +where + T::Success: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let field_count = 7 + self.issued_at.is_some() as usize; + + let mut state = serializer.serialize_struct("receipt::Payload", field_count)?; + + state.serialize_field("iss", &self.issuer.to_string().as_str())?; + state.serialize_field("ran", &self.ran)?; + state.serialize_field("out", &self.out)?; + state.serialize_field("next", &self.next)?; + state.serialize_field("prf", &self.proofs)?; + state.serialize_field("meta", &self.metadata)?; + state.serialize_field("nonce", &self.nonce)?; + state.serialize_field("iat", &self.issued_at)?; + + state.end() + } +} + +impl<'de, T: Responds, DID: Did + Deserialize<'de>> Deserialize<'de> for Payload +where + T::Success: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ReceiptPayloadVisitor(std::marker::PhantomData<(T, DID)>); + + const FIELDS: &'static [&'static str] = + &["iss", "ran", "out", "next", "prf", "meta", "nonce", "iat"]; + + impl<'de, T: Responds, DID: Did + Deserialize<'de>> Visitor<'de> for ReceiptPayloadVisitor + where + T::Success: Deserialize<'de>, + { + type Value = Payload; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("struct delegation::Payload") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut issuer = None; + let mut ran = None; + let mut out = None; + let mut next = None; + let mut proofs = None; + let mut metadata = None; + let mut nonce = None; + let mut issued_at = None; + + while let Some(key) = map.next_key()? { + match key { + "iss" => { + if issuer.is_some() { + return Err(de::Error::duplicate_field("iss")); + } + issuer = Some(map.next_value()?); + } + "ran" => { + if ran.is_some() { + return Err(de::Error::duplicate_field("ran")); + } + ran = Some(map.next_value()?); + } + "out" => { + if out.is_some() { + return Err(de::Error::duplicate_field("out")); + } + out = Some(map.next_value()?); + } + "next" => { + if next.is_some() { + return Err(de::Error::duplicate_field("next")); + } + next = Some(map.next_value()?); + } + "prf" => { + if proofs.is_some() { + return Err(de::Error::duplicate_field("prf")); + } + proofs = Some(map.next_value()?); + } + "meta" => { + if metadata.is_some() { + return Err(de::Error::duplicate_field("meta")); + } + metadata = Some(map.next_value()?); + } + "nonce" => { + if nonce.is_some() { + return Err(de::Error::duplicate_field("nonce")); + } + nonce = Some(map.next_value()?); + } + "iat" => { + if issued_at.is_some() { + return Err(de::Error::duplicate_field("iat")); + } + issued_at = map.next_value()?; + } + other => { + return Err(de::Error::unknown_field(other, FIELDS)); + } + } + } + + Ok(Payload { + issuer: issuer.ok_or(de::Error::missing_field("iss"))?, + ran: ran.ok_or(de::Error::missing_field("ran"))?, + out: out.ok_or(de::Error::missing_field("out"))?, + next: next.ok_or(de::Error::missing_field("next"))?, + proofs: proofs.ok_or(de::Error::missing_field("prf"))?, + metadata: metadata.ok_or(de::Error::missing_field("meta"))?, + nonce: nonce.ok_or(de::Error::missing_field("nonce"))?, + issued_at, + }) + } + } + + deserializer.deserialize_struct( + "ReceiptPayload", + FIELDS, + ReceiptPayloadVisitor(Default::default()), + ) + } +} + +impl From> for Ipld { + fn from(payload: Payload) -> Self { + payload.into() + } +} + +impl TryFrom for Payload +where + Payload: for<'de> Deserialize<'de>, +{ + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Payload +where + T::Success: Arbitrary + 'static, + DID::Parameters: Clone, + DID::Strategy: 'static, +{ + type Parameters = (::Parameters, DID::Parameters); + type Strategy = BoxedStrategy; + + fn arbitrary_with((t_params, did_params): Self::Parameters) -> Self::Strategy { + ( + DID::arbitrary_with(did_params), + cid::Newtype::arbitrary(), + prop_oneof![ + T::Success::arbitrary_with(t_params).prop_map(Result::Ok), + arguments::Named::arbitrary().prop_map(Result::Err), + ], + prop::collection::vec(cid::Newtype::arbitrary(), 0..25), + prop::collection::vec(cid::Newtype::arbitrary(), 0..25), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..50), + Nonce::arbitrary(), + prop::option::of(Timestamp::arbitrary()), + ) + .prop_map( + |(issuer, ran, out, next, proofs, newtype_metadata, nonce, issued_at)| Payload { + issuer, + ran: ran.cid, + out, + next: next.into_iter().map(|nt| nt.cid).collect(), + proofs: proofs.into_iter().map(|nt| nt.cid).collect(), + metadata: newtype_metadata + .into_iter() + .map(|(k, v)| (k, v.0)) + .collect(), + nonce, + issued_at, + }, + ) + .boxed() + } +} diff --git a/src/receipt/responds.rs b/src/receipt/responds.rs new file mode 100644 index 00000000..24db9f43 --- /dev/null +++ b/src/receipt/responds.rs @@ -0,0 +1,25 @@ +use crate::{crypto::Nonce, task, task::Task}; +use std::fmt; + +/// Describe the relationship between an ability and the [`Receipt`]s. +/// +/// This is used for constucting [`Receipt`]s, and indexing them for +/// reverse lookup. +/// +/// [`Receipt`]: crate::receipt::Receipt +pub trait Responds { + /// The successful return type for running `Self`. + type Success: Clone + fmt::Debug + PartialEq; + + /// Convert an Ability (`Self`) into a [`Task`]. + /// + /// This is used to index receipts by a minimal [`Id`]. + fn to_task(&self, subject: did_url::DID, nonce: Nonce) -> Task; + + /// Convert an Ability (`Self`) directly into a [`Task`]'s [`Id`]. + fn to_task_id(&self, subject: did_url::DID, nonce: Nonce) -> task::Id { + task::Id { + cid: self.to_task(subject, nonce).into(), + } + } +} diff --git a/src/receipt/store.rs b/src/receipt/store.rs new file mode 100644 index 00000000..ff1ecad8 --- /dev/null +++ b/src/receipt/store.rs @@ -0,0 +1,7 @@ +//! Store trait and MemoryStore implementation. + +mod memory; +mod traits; + +pub use memory::MemoryStore; +pub use traits::Store; diff --git a/src/receipt/store/memory.rs b/src/receipt/store/memory.rs new file mode 100644 index 00000000..955ffada --- /dev/null +++ b/src/receipt/store/memory.rs @@ -0,0 +1,35 @@ +use super::Store; +use crate::{ + crypto::varsig, + did::Did, + receipt::{Receipt, Responds}, + task, +}; +use libipld_core::{codec::Codec, ipld::Ipld}; +use std::{collections::BTreeMap, convert::Infallible, fmt}; + +/// An in-memory [`receipt::Store`][crate::receipt::Store]. +#[derive(Debug, Clone, PartialEq)] +pub struct MemoryStore, Enc: Codec> +where + T::Success: fmt::Debug + Clone + PartialEq, +{ + store: BTreeMap>, +} + +impl, Enc: Codec> Store + for MemoryStore +where + ::Success: TryFrom + Into + Clone + fmt::Debug + PartialEq, +{ + type Error = Infallible; + + fn get(&self, id: &task::Id) -> Result>, Self::Error> { + Ok(self.store.get(id)) + } + + fn put(&mut self, id: task::Id, receipt: Receipt) -> Result<(), Self::Error> { + self.store.insert(id, receipt); + Ok(()) + } +} diff --git a/src/receipt/store/traits.rs b/src/receipt/store/traits.rs new file mode 100644 index 00000000..2bb52329 --- /dev/null +++ b/src/receipt/store/traits.rs @@ -0,0 +1,26 @@ +use crate::{ + crypto::varsig, + did::Did, + receipt::{Receipt, Responds}, + task, +}; +use libipld_core::{codec::Codec, ipld::Ipld}; + +/// A store for [`Receipt`]s indexed by their [`task::Id`]s. +pub trait Store, C: Codec> { + /// The error type representing all the ways a store operation can fail. + type Error; + + /// Retrieve a [`Receipt`] by its [`task::Id`]. + /// + /// If the store itself did not experience an error, but the value + /// was not found, the result will be `Ok(None)`. + fn get<'a>(&self, id: &task::Id) -> Result>, Self::Error> + where + ::Success: TryFrom; + + /// Store a [`Receipt`] by its [`task::Id`]. + fn put(&mut self, id: task::Id, receipt: Receipt) -> Result<(), Self::Error> + where + ::Success: Into; +} diff --git a/src/semantics/ability.rs b/src/semantics/ability.rs deleted file mode 100644 index d4758496..00000000 --- a/src/semantics/ability.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! UCAN Abilities - -use std::fmt::{self, Display}; - -use downcast_rs::{impl_downcast, Downcast}; -use dyn_clone::{clone_trait_object, DynClone}; - -use super::caveat::Caveat; - -/// An ability defined as part of a semantics -pub trait Ability: Send + Sync + Display + DynClone + Downcast + 'static { - /// Returns true if self is a valid attenuation of other - fn is_valid_attenuation(&self, other: &dyn Ability) -> bool; - - /// Returns true if caveat is a valid caveat for self - fn is_valid_caveat(&self, _caveat: &dyn Caveat) -> bool { - false - } -} - -clone_trait_object!(Ability); -impl_downcast!(Ability); - -impl Ability for Box { - fn is_valid_attenuation(&self, other: &dyn Ability) -> bool { - (**self).is_valid_attenuation(other) - } -} - -impl fmt::Debug for dyn Ability { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, r#"Ability("{}")"#, self) - } -} - -/// The top ability -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TopAbility; - -impl Ability for TopAbility { - fn is_valid_attenuation(&self, other: &dyn Ability) -> bool { - if let Some(ability) = other.downcast_ref::() { - return self == ability; - }; - - false - } -} - -impl Display for TopAbility { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "*") - } -} diff --git a/src/semantics/caveat.rs b/src/semantics/caveat.rs deleted file mode 100644 index 836d5023..00000000 --- a/src/semantics/caveat.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! UCAN Caveats - -use std::fmt; - -use downcast_rs::{impl_downcast, Downcast}; -use dyn_clone::{clone_trait_object, DynClone}; -use erased_serde::serialize_trait_object; -use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize}; - -/// A caveat defined as part of a semantics -pub trait Caveat: Send + Sync + DynClone + Downcast + erased_serde::Serialize + 'static { - /// Returns true if the caveat is valid - fn is_valid(&self) -> bool; - - /// Returns true if self is a valid attenuation of other - fn is_valid_attenuation(&self, other: &dyn Caveat) -> bool; -} - -clone_trait_object!(Caveat); -impl_downcast!(Caveat); -serialize_trait_object!(Caveat); - -impl fmt::Debug for dyn Caveat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Caveat({})", std::any::type_name::()) - } -} - -/// A caveat that is always valid -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct EmptyCaveat; - -impl Caveat for EmptyCaveat { - fn is_valid(&self) -> bool { - true - } - - fn is_valid_attenuation(&self, other: &dyn Caveat) -> bool { - if let Some(resource) = other.downcast_ref::() { - return self == resource; - }; - - false - } -} - -impl Caveat for Box { - fn is_valid(&self) -> bool { - (**self).is_valid() - } - - fn is_valid_attenuation(&self, other: &dyn Caveat) -> bool { - (**self).is_valid_attenuation(other) - } -} - -impl<'de> Deserialize<'de> for EmptyCaveat { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct NoFieldsVisitor; - - impl<'de> Visitor<'de> for NoFieldsVisitor { - type Value = EmptyCaveat; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("an empty object") - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - if let Some(field) = map.next_key()? { - return Err(serde::de::Error::unknown_field(field, &[])); - } - - Ok(EmptyCaveat) - } - } - - deserializer.deserialize_map(NoFieldsVisitor) - } -} - -impl Serialize for EmptyCaveat { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_map(Some(0))?.end() - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_serialize_empty_caveat() { - let caveat = EmptyCaveat; - let serialized = serde_json::to_string(&caveat).unwrap(); - - assert_eq!(serialized, "{}"); - } - - #[test] - fn test_deserialize_empty_caveat() { - let deserialized: EmptyCaveat = serde_json::from_value(json!({})).unwrap(); - - assert_eq!(deserialized, EmptyCaveat); - } - - #[test] - fn test_deserialize_empty_caveat_unexpected_fields() { - let deserialized: Result = serde_json::from_value(json!({ - "foo": true - })); - - assert!(deserialized.is_err()); - } -} diff --git a/src/semantics/mod.rs b/src/semantics/mod.rs deleted file mode 100644 index 1d88f3b0..00000000 --- a/src/semantics/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Semantics for UCAN schemes - -pub mod ability; -pub mod caveat; -pub mod resource; diff --git a/src/semantics/resource.rs b/src/semantics/resource.rs deleted file mode 100644 index a7f5360b..00000000 --- a/src/semantics/resource.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! UCAN Resources - -use std::fmt::{self, Display}; - -use downcast_rs::{impl_downcast, Downcast}; -use dyn_clone::{clone_trait_object, DynClone}; - -/// A resource defined as part of a semantics -pub trait Resource: Send + Sync + Display + DynClone + Downcast + 'static { - /// Returns true if self is a valid attenuation of other - fn is_valid_attenuation(&self, other: &dyn Resource) -> bool; -} - -clone_trait_object!(Resource); -impl_downcast!(Resource); - -impl Resource for Box { - fn is_valid_attenuation(&self, other: &dyn Resource) -> bool { - (**self).is_valid_attenuation(other) - } -} - -impl fmt::Debug for dyn Resource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, r#"Resource("{}")"#, self) - } -} diff --git a/src/store.rs b/src/store.rs deleted file mode 100644 index c388aaa5..00000000 --- a/src/store.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! A store for persisting UCAN tokens, to be referencable as proofs by other UCANs - -use std::{collections::HashMap, io::Cursor, marker::PhantomData}; - -use async_trait::async_trait; -use cid::{multihash, Cid}; -use libipld_core::{ - codec::{Codec, Decode, Encode}, - raw::RawCodec, -}; -use multihash::MultihashDigest; - -use crate::DEFAULT_MULTIHASH; - -/// A store for persisting UCAN tokens, to be referencable as proofs by other UCANs -pub trait Store -where - C: Codec, -{ - /// The error type for this store - type Error; - - /// Read a token from the store - fn read(&self, cid: &Cid) -> Result, Self::Error> - where - T: Decode; - - /// Write a token to the store, using the specified hasher - fn write(&mut self, token: T, hasher: Option) -> Result - where - T: Encode; -} - -/// An async store for persisting UCAN tokens, to be referencable as proofs by other UCANs -// TODO: The send / sync bounds need to be conditional based on the target, to support wasm32 -#[async_trait] -pub trait AsyncStore: Send + Sync -where - C: Codec, -{ - /// The error type for this store - type Error; - - /// Read a token from the store - async fn read(&self, cid: &Cid) -> Result, Self::Error> - where - T: Decode; - - /// Write a token to the store, using the specified hasher - async fn write( - &mut self, - token: T, - hasher: Option, - ) -> Result - where - T: Encode + Send; -} - -/// An in-memory store for development and testing -#[derive(Debug, Clone, Default)] -pub struct InMemoryStore { - store: HashMap>, - _phantom: PhantomData, -} - -impl Store for InMemoryStore { - type Error = anyhow::Error; - - fn read(&self, cid: &Cid) -> Result, Self::Error> - where - T: Decode, - { - match self.store.get(cid) { - Some(block) => Ok(Some(T::decode(RawCodec, &mut Cursor::new(block))?)), - None => Ok(None), - } - } - - fn write(&mut self, token: T, hasher: Option) -> Result - where - T: Encode, - { - let hasher = hasher.unwrap_or(DEFAULT_MULTIHASH); - let block = RawCodec.encode(&token)?; - let digest = hasher.digest(&block); - let cid = Cid::new_v1(RawCodec.into(), digest); - - self.store.insert(cid, block); - - Ok(cid) - } -} diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 00000000..6b931c59 --- /dev/null +++ b/src/task.rs @@ -0,0 +1,102 @@ +//! Task indices for [`Receipt`][crate::receipt::Receipt] reverse lookup. + +mod id; +pub use id::Id; + +use crate::{ability::arguments, crypto::Nonce, did}; +use libipld_cbor::DagCborCodec; +use libipld_core::{ + cid::{Cid, CidGeneric}, + codec::Encode, + error::SerdeError, + ipld::Ipld, + multihash::{Code, MultihashGeneric}, + serde as ipld_serde, +}; +use serde_derive::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// The fields required to uniquely identify a [`Task`], potentially across multiple executors. +/// +/// This struct should not be used directly, but rather through a [`From`] instance +/// on the type. In particular, the `nonce` field should be constant for all of the same type. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Task { + /// The `subject`: root issuer, and arbiter of the semantics/namespace. + pub sub: did::Newtype, + + /// A unique identifier for the particular task run. + /// + /// This is an [`Option`] because not all task types require a nonce. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nonce: Option, + + /// The command identifier. + pub cmd: String, + + /// The arguments to the command. + pub args: arguments::Named, +} + +impl TryFrom for Task { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl From for Ipld { + fn from(task: Task) -> Self { + task.into() + } +} + +impl From for Cid { + fn from(task: Task) -> Cid { + let mut buffer = vec![]; + let ipld: Ipld = task.into(); + + ipld.encode(DagCborCodec, &mut buffer) + .expect("DagCborCodec to encode any arbitrary `Ipld`"); + + CidGeneric::new_v1( + DagCborCodec.into(), + MultihashGeneric::wrap(Code::Sha2_256.into(), buffer.as_slice()) + .expect("DagCborCodec + Sha2_256 should always successfully encode Ipld to a Cid"), + ) + } +} + +impl From for Id { + fn from(task: Task) -> Id { + Id { + cid: Cid::from(task), + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Task { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::>(), + any::(), + any::>(), + ) + .prop_map(|(sub, nonce, cmd, args)| Task { + sub, + nonce, + cmd, + args, + }) + .boxed() + } +} diff --git a/src/task/id.rs b/src/task/id.rs new file mode 100644 index 00000000..de799110 --- /dev/null +++ b/src/task/id.rs @@ -0,0 +1,49 @@ +//! A newtype wrapper around [`Cid`]s to tag them as able to identify a particular invocation. + +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde_derive::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld::cid; + +/// The unique identifier for a [`Task`][super::Task]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Id { + /// The CID of the [`Task`][super::Task]. + /// + /// This acts as a unique identifier for the task. + pub cid: Cid, +} + +impl TryFrom for Id { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl From for Ipld { + fn from(id: Id) -> Self { + id.cid.into() + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Id { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + any::() + .prop_map(|cid_newtype| Id { + cid: cid_newtype.cid, + }) + .boxed() + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 00000000..e3817b91 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,95 @@ +use libipld::{ + cid::multihash::{Code, MultihashDigest, MultihashGeneric}, + codec_impl::IpldCodec, +}; +use proptest::prelude::*; + +#[derive(Clone, Debug, PartialEq)] +pub struct SomeCodec(pub IpldCodec); + +impl Arbitrary for SomeCodec { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + Just(IpldCodec::Raw), + Just(IpldCodec::DagCbor), + Just(IpldCodec::DagJson), + Just(IpldCodec::DagPb), + ] + .prop_map(SomeCodec) + .boxed() + } +} + +#[derive(Eq, Copy, Clone, Debug, PartialEq)] +pub struct SomeMultihash(pub Code); + +impl Default for SomeMultihash { + fn default() -> Self { + SomeMultihash(Code::Sha2_256) + } +} + +impl SomeMultihash { + pub fn new(multihash: Code) -> Self { + SomeMultihash(multihash) + } +} + +impl From for SomeMultihash { + fn from(multihash: Code) -> Self { + SomeMultihash(multihash) + } +} + +impl From for Code { + fn from(wrapper: SomeMultihash) -> Self { + wrapper.0 + } +} + +impl From for u64 { + fn from(wrapper: SomeMultihash) -> Self { + wrapper.0.into() + } +} + +impl TryFrom for SomeMultihash { + type Error = >::Error; + + fn try_from(code: u64) -> Result { + let inner = code.try_into()?; + Ok(SomeMultihash(inner)) + } +} + +impl MultihashDigest<64> for SomeMultihash { + fn digest(&self, input: &[u8]) -> MultihashGeneric<64> { + self.0.digest(input) + } + + fn wrap(&self, digest: &[u8]) -> Result, Self::Error> { + self.0.wrap(digest) + } +} + +impl Arbitrary for SomeMultihash { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // Only the 256-bit variants for now + prop_oneof![ + Just(Code::Sha2_256), + Just(Code::Sha3_256), + Just(Code::Keccak256), + Just(Code::Blake2s256), + Just(Code::Blake2b256), + Just(Code::Blake3_256), + ] + .prop_map(SomeMultihash) + .boxed() + } +} diff --git a/src/test_utils/mod.rs b/src/test_utils/mod.rs deleted file mode 100644 index 4a30e2ad..00000000 --- a/src/test_utils/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Random value generator for sampling data. -#[cfg(feature = "test_utils")] -mod rvg; -#[cfg(feature = "test_utils")] -pub use rvg::*; diff --git a/src/test_utils/rvg.rs b/src/test_utils/rvg.rs deleted file mode 100644 index 30e5c3ef..00000000 --- a/src/test_utils/rvg.rs +++ /dev/null @@ -1,63 +0,0 @@ -use proptest::{ - collection::vec, - strategy::{Strategy, ValueTree}, - test_runner::{Config, TestRunner}, -}; - -/// A random value generator (RVG), which, given proptest strategies, will -/// generate random values based on those strategies. -#[derive(Debug, Default)] -pub struct Rvg { - runner: TestRunner, -} - -impl Rvg { - /// Creates a new RVG with the default random number generator. - pub fn new() -> Self { - Rvg { - runner: TestRunner::new(Config::default()), - } - } - - /// Creates a new RVG with a deterministic random number generator, - /// using the same seed across test runs. - pub fn deterministic() -> Self { - Rvg { - runner: TestRunner::deterministic(), - } - } - - /// Samples a value for the given strategy. - /// - /// # Example - /// - /// ``` - /// use rs_ucan::test_utils::Rvg; - /// - /// let mut rvg = Rvg::new(); - /// let int = rvg.sample(&(0..100i32)); - /// ``` - pub fn sample(&mut self, strategy: &S) -> S::Value { - strategy - .new_tree(&mut self.runner) - .expect("No value can be generated") - .current() - } - - /// Samples a vec of some length with a value for the given strategy. - /// - /// # Example - /// - /// ``` - /// use rs_ucan::test_utils::Rvg; - /// - /// let mut rvg = Rvg::new(); - /// let ints = rvg.sample_vec(&(0..100i32), 10); - /// ``` - pub fn sample_vec(&mut self, strategy: &S, len: usize) -> Vec { - vec(strategy, len..=len) - .new_tree(&mut self.runner) - .expect("No value can be generated") - .current() - } -} diff --git a/src/time.rs b/src/time.rs index e57163a0..3c37cb1d 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,11 +1,9 @@ -//! Time utilities +//! Time utilities. +//! +//! The [`Timestamp`] struct is the main type for representing time in a UCAN token. -use web_time::SystemTime; +mod error; +mod timestamp; -/// Get the current time in seconds since UNIX_EPOCH -pub fn now() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() -} +pub use error::*; +pub use timestamp::Timestamp; diff --git a/src/time/error.rs b/src/time/error.rs new file mode 100644 index 00000000..e9c3149e --- /dev/null +++ b/src/time/error.rs @@ -0,0 +1,29 @@ +//! Temporal errors. + +use thiserror::Error; +use web_time::SystemTime; + +/// An error expressing when a time is larger than 2⁵³ seconds past the Unix epoch +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[error("Time out of JsTime (2⁵³) range: {:?}", tried)] +pub struct OutOfRangeError { + /// The [`SystemTime`] that is outside of the [`JsTime`] range (2⁵³). + pub tried: SystemTime, +} + +/// An error expressing when a time is not within the bounds of a UCAN. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Error)] +pub enum TimeBoundError { + /// The UCAN has expired. + #[error("Expired")] + Expired, + + /// The UCAN is not yet valid, but will be in the future. + #[error("Not yet valid")] + NotYetValid, +} + +/// The UCAN has expired. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)] +#[error("Expired")] +pub struct Expired; diff --git a/src/time/timestamp.rs b/src/time/timestamp.rs new file mode 100644 index 00000000..30f62f29 --- /dev/null +++ b/src/time/timestamp.rs @@ -0,0 +1,191 @@ +//! A JavaScript-wrapper for [`Timestamp`][crate::time::Timestamp]. + +use super::OutOfRangeError; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use web_time::{Duration, SystemTime, UNIX_EPOCH}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A [`Timestamp`][super::Timestamp] with safe JavaScript interop. +/// +/// Per the UCAN spec, timestamps MUST respect [IEEE-754] +/// (64-bit double precision = 53-bit truncated integer) for +/// JavaScript interoperability. +/// +/// This range can represent millions of years into the future, +/// and is thus sufficient for "nearly" all auth use cases. +/// +/// This type internally deserializes permissively from any [`SystemTime`], +/// but checks that any time created is in the 53-bit bound when created via +/// the public API. +/// +/// [IEEE-754]: https://en.wikipedia.org/wiki/IEEE_754 +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +pub struct Timestamp { + time: SystemTime, +} + +impl Timestamp { + /// Create a [`Timestamp`] from a [`SystemTime`]. + /// + /// # Arguments + /// + /// * `time` — The time to convert + /// + /// # Errors + /// + /// * [`OutOfRangeError`] — If the time is more than 2⁵³ seconds since the Unix epoch + pub fn new(time: SystemTime) -> Result { + if time + .duration_since(UNIX_EPOCH) + .map_err(|_| OutOfRangeError { tried: time })? + .as_secs() + > 0x1FFFFFFFFFFFFF + { + Err(OutOfRangeError { tried: time }) + } else { + Ok(Timestamp { time }) + } + } + + /// Get the current time in seconds since [`UNIX_EPOCH`] as a [`Timestamp`]. + pub fn now() -> Timestamp { + Self::new(SystemTime::now()) + .expect("the current time to be somtime in the 3rd millenium CE") + } + + pub fn five_minutes_from_now() -> Timestamp { + Self::new(SystemTime::now() + Duration::from_secs(5 * 60)) + .expect("the current time to be somtime in the 3rd millenium CE") + } + + pub fn five_years_from_now() -> Timestamp { + Self::new(SystemTime::now() + Duration::from_secs(5 * 365 * 24 * 60 * 60)) + .expect("the current time to be somtime in the 3rd millenium CE") + } + + /// Convert a [`Timestamp`] to a [Unix timestamp]. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + pub fn to_unix(&self) -> u64 { + self.time + .duration_since(UNIX_EPOCH) + .expect("System time to be after the Unix epoch") + .as_secs() + } + + /// An intentionally permissive variant of `new` for + /// deseriazation. See the note on the struct. + pub(crate) fn postel(time: SystemTime) -> Self { + Timestamp { time } + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl Timestamp { + /// Lift a [`js_sys::Date`] into a Rust [`Timestamp`] + pub fn from_date(date_time: js_sys::Date) -> Result { + let millis = date_time.get_time() as u64; + let secs: u64 = (millis / 1000) as u64; + let duration = Duration::new(secs, 0); // Just round off the nanos + Timestamp::new(UNIX_EPOCH + duration).map_err(Into::into) + } + + /// Lower the [`Timestamp`] to a [`js_sys::Date`] + pub fn to_date(&self) -> js_sys::Date { + js_sys::Date::new(&JsValue::from( + self.time + .duration_since(UNIX_EPOCH) + .expect("time should be in range since it's getting a JS Date") + .as_millis(), + )) + } +} + +impl TryFrom for Timestamp { + type Error = OutOfRangeError; + + fn try_from(sys_time: SystemTime) -> Result { + Timestamp::new(sys_time) + } +} + +impl From for SystemTime { + fn from(js_time: Timestamp) -> Self { + js_time.time + } +} + +impl From for Ipld { + fn from(timestamp: Timestamp) -> Self { + timestamp.to_unix().into() + } +} + +impl TryFrom for Timestamp { + type Error = (); + + fn try_from(ipld: Ipld) -> Result { + match ipld { + // FIXME do bounds checking + Ipld::Integer(secs) => Ok(Timestamp::new( + UNIX_EPOCH + Duration::from_secs(secs as u64), + ) + .map_err(|_| ())?), + _ => Err(()), + } + } +} + +impl From for i128 { + fn from(timestamp: Timestamp) -> i128 { + timestamp.to_unix() as i128 + } +} + +impl TryFrom for Timestamp { + type Error = OutOfRangeError; + + fn try_from(secs: i128) -> Result { + // FIXME do bounds checking + Timestamp::new(UNIX_EPOCH + Duration::from_secs(secs as u64)) + } +} + +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_unix().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seconds = u64::deserialize(deserializer)?; + Ok(Timestamp::postel(UNIX_EPOCH + Duration::from_secs(seconds))) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Timestamp { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (0..(u64::pow(2, 53) - 1)) + .prop_map(|secs| { + Timestamp::new(UNIX_EPOCH + Duration::from_secs(secs)) + .expect("the current time to be somtime in the 3rd millenium CE") + }) + .boxed() + } +} diff --git a/src/ucan.rs b/src/ucan.rs deleted file mode 100644 index ba5e6584..00000000 --- a/src/ucan.rs +++ /dev/null @@ -1,1044 +0,0 @@ -//! JWT embedding of a UCAN - -use std::{collections::vec_deque::VecDeque, str::FromStr}; - -use crate::{ - capability::{Capabilities, Capability, CapabilityParser, DefaultCapabilityParser}, - did_verifier::DidVerifierMap, - error::Error, - semantics::{ability::Ability, resource::Resource}, - store::Store, - CidString, DefaultFact, DEFAULT_MULTIHASH, -}; -use cid::{ - multihash::{self, MultihashDigest}, - Cid, -}; -use libipld_core::{ipld::Ipld, raw::RawCodec}; -use semver::Version; -use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; -use tracing::{span, Level}; - -/// The current UCAN version -pub const UCAN_VERSION: &str = "0.10.0"; - -/// The UCAN header -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UcanHeader { - pub(crate) alg: String, - pub(crate) typ: String, -} - -/// The UCAN payload -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UcanPayload { - pub(crate) ucv: String, - pub(crate) iss: String, - pub(crate) aud: String, - #[serde(deserialize_with = "deserialize_required_nullable")] - pub(crate) exp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) nbf: Option, - // TODO: nonce required in 1.0 - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) nnc: Option, - #[serde(bound = "C: CapabilityParser")] - pub(crate) cap: Capabilities, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) fct: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) prf: Option>, -} - -/// A UCAN -#[derive(Clone, Debug)] -pub struct Ucan { - pub(crate) header: jose_b64::serde::Json, - pub(crate) payload: jose_b64::serde::Json>, - pub(crate) signature: jose_b64::serde::Bytes, -} - -impl Ucan -where - F: Clone + DeserializeOwned, - C: CapabilityParser, -{ - /// Validate the UCAN's signature and timestamps - pub fn validate(&self, at_time: u64, did_verifier_map: &DidVerifierMap) -> Result<(), Error> { - if self.typ() != "JWT" { - return Err(Error::VerifyingError { - msg: format!("expected header typ field to be 'JWT', got {}", self.typ()), - }); - } - - if Version::parse(self.version()).is_err() { - return Err(Error::VerifyingError { - msg: format!( - "expected header ucv field to be a semver, got {}", - self.version() - ), - }); - } - - if self.is_expired(at_time) { - return Err(Error::VerifyingError { - msg: "token is expired".to_string(), - }); - } - - if self.is_too_early(at_time) { - return Err(Error::VerifyingError { - msg: "current time is before token validity period begins".to_string(), - }); - } - - // TODO: parse and validate iss and aud DIDs during deserialization - self.payload - .aud - .strip_prefix("did:") - .and_then(|did| did.split_once(':')) - .ok_or(Error::VerifyingError { - msg: format!( - "expected did::, got {}", - self.payload.aud - ), - })?; - - let (method, identifier) = self - .payload - .iss - .strip_prefix("did:") - .and_then(|did| did.split_once(':')) - .ok_or(Error::VerifyingError { - msg: format!( - "expected did::, got {}", - self.payload.iss - ), - })?; - - let header = serde_json::to_value(&self.header) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let payload = serde_json::to_value(&self.payload) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let signed_data = format!( - "{}.{}", - header.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of header".to_string(), - })?, - payload.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of payload".to_string(), - })?, - ); - - did_verifier_map.verify(method, identifier, signed_data.as_bytes(), &self.signature) - } - - /// Returns true if the UCAN is authorized by the given issuer to - /// perform the ability against the resource - #[tracing::instrument(level = "trace", skip_all, fields(issuer = issuer.as_ref(), %resource, %ability, %at_time, self = %self.to_cid(None)?))] - pub fn capabilities_for( - &self, - issuer: impl AsRef, - resource: R, - ability: A, - at_time: u64, - did_verifier_map: &DidVerifierMap, - store: &S, - ) -> Result, Error> - where - R: Resource, - A: Ability, - S: Store, - { - let issuer = issuer.as_ref(); - - let mut capabilities = vec![]; - let mut proof_queue: VecDeque<(Ucan, Capability, Capability)> = VecDeque::default(); - - self.validate(at_time, did_verifier_map)?; - - for capability in self.capabilities() { - let span = span!(Level::TRACE, "capability", ?capability); - let _enter = span.enter(); - - let attenuated = Capability::clone_box(&resource, &ability, capability.caveat()); - - if !attenuated.is_subsumed_by(capability) { - tracing::trace!("skipping (not subsumed by)"); - - continue; - } - - if self.issuer() == issuer { - tracing::trace!("matched (by parenthood)"); - - capabilities.push(attenuated.clone()) - } - - proof_queue.push_back((self.clone(), capability.clone(), attenuated)); - - tracing::trace!("enqueued"); - } - - while let Some((ucan, attenuated_cap, leaf_cap)) = proof_queue.pop_front() { - let span = - span!(Level::TRACE, "ucan", ucan = %ucan.to_cid(None)?, ?attenuated_cap, ?leaf_cap); - - let _enter = span.enter(); - - for proof_cid in ucan.proofs().unwrap_or(vec![]) { - let span = span!(Level::TRACE, "proof", cid = %proof_cid); - let _enter = span.enter(); - - match store - .read::(proof_cid) - .map_err(|e| Error::InternalUcanError { - msg: format!( - "error while retrieving proof ({}) from store, {}", - proof_cid, e - ), - })? { - Some(Ipld::Bytes(bytes)) => { - let token = - String::from_utf8(bytes).map_err(|e| Error::InternalUcanError { - msg: format!( - "error converting token for proof ({}) into UTF-8 string, {}", - proof_cid, e - ), - })?; - - let proof_ucan = - Ucan::from_str(&token).map_err(|e| Error::InternalUcanError { - msg: format!( - "error decoding token for proof ({}) into UCAN, {}", - proof_cid, e - ), - })?; - - if !proof_ucan.lifetime_encompasses(&ucan) { - tracing::trace!("skipping (lifetime not encompassed)"); - - continue; - } - - if ucan.issuer() != proof_ucan.audience() { - tracing::trace!("skipping (issuer != audience)"); - - continue; - } - - if proof_ucan.validate(at_time, did_verifier_map).is_err() { - tracing::trace!("skipping (validation failed)"); - - continue; - } - - for capability in proof_ucan.capabilities() { - if !attenuated_cap.is_subsumed_by(capability) { - tracing::trace!("skipping (not subsumed by)"); - - continue; - } - - if proof_ucan.issuer() == issuer { - tracing::trace!("matched (by parenthood)"); - - capabilities.push(leaf_cap.clone()); - } - - proof_queue.push_back(( - proof_ucan.clone(), - capability.clone(), - leaf_cap.clone(), - )); - - tracing::trace!("enqueued"); - } - } - Some(ipld) => { - return Err(Error::InternalUcanError { - msg: format!( - "expected proof ({}) to map to bytes, got {:?}", - proof_cid, ipld - ), - }) - } - None => continue, - } - } - } - - Ok(capabilities) - } - - /// Encode the UCAN as a JWT token - pub fn encode(&self) -> Result { - let header = serde_json::to_value(&self.header) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let payload = serde_json::to_value(&self.payload) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - let signature = serde_json::to_value(&self.signature) - .map_err(|e| Error::InternalUcanError { msg: e.to_string() })?; - - Ok(format!( - "{}.{}.{}", - header.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of header".to_string(), - })?, - payload.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of payload".to_string(), - })?, - signature.as_str().ok_or(Error::InternalUcanError { - msg: "Expected base64 encoding of signature".to_string(), - })? - )) - } - - /// Returns true if the UCAN has past its expiration date - pub fn is_expired(&self, at_time: u64) -> bool { - if let Some(exp) = self.payload.exp { - exp < at_time - } else { - false - } - } - - /// Returns the UCAN's signature - pub fn signature(&self) -> &jose_b64::serde::Bytes { - &self.signature - } - - /// Returns true if the not-before ("nbf") time is still in the future - pub fn is_too_early(&self, at_time: u64) -> bool { - match self.payload.nbf { - Some(nbf) => nbf > at_time, - None => false, - } - } - - /// Returns true if this UCAN's lifetime begins no later than the other - pub fn lifetime_begins_before(&self, other: &Ucan) -> bool - where - F2: DeserializeOwned, - C2: CapabilityParser, - { - match (self.payload.nbf, other.payload.nbf) { - (Some(nbf), Some(other_nbf)) => nbf <= other_nbf, - (Some(_), None) => false, - _ => true, - } - } - - /// Returns true if this UCAN expires no earlier than the other - pub fn lifetime_ends_after(&self, other: &Ucan) -> bool - where - F2: DeserializeOwned, - C2: CapabilityParser, - { - match (self.payload.exp, other.payload.exp) { - (Some(exp), Some(other_exp)) => exp >= other_exp, - (Some(_), None) => false, - (None, _) => true, - } - } - - /// Returns true if this UCAN's lifetime fully encompasses the other - pub fn lifetime_encompasses(&self, other: &Ucan) -> bool - where - F2: DeserializeOwned, - C2: CapabilityParser, - { - self.lifetime_begins_before(other) && self.lifetime_ends_after(other) - } - - /// Return the `typ` field of the UCAN header - pub fn typ(&self) -> &str { - &self.header.typ - } - - /// Return the `alg` field of the UCAN header - pub fn algorithm(&self) -> &str { - &self.header.alg - } - - /// Return the `iss` field of the UCAN payload - pub fn issuer(&self) -> &str { - &self.payload.iss - } - - /// Return the `aud` field of the UCAN payload - pub fn audience(&self) -> &str { - &self.payload.aud - } - - /// Return the `prf` field of the UCAN payload - pub fn proofs(&self) -> Option> { - self.payload - .prf - .as_ref() - .map(|f| f.iter().map(|c| &c.0).collect()) - } - - /// Return the `exp` field of the UCAN payload - pub fn expires_at(&self) -> Option { - self.payload.exp - } - - /// Return the `nbf` field of the UCAN payload - pub fn not_before(&self) -> Option { - self.payload.nbf - } - - /// Return the `nnc` field of the UCAN payload - pub fn nonce(&self) -> Option<&String> { - self.payload.nnc.as_ref() - } - - /// Return an iterator over the `cap` field of the UCAN payload - pub fn capabilities(&self) -> impl Iterator { - self.payload.cap.iter() - } - - /// Return the `fct` field of the UCAN payload - pub fn facts(&self) -> Option<&F> { - self.payload.fct.as_ref() - } - - /// Return the `ucv` field of the UCAN payload - pub fn version(&self) -> &str { - &self.payload.ucv - } - - /// Return the CID v1 of the UCAN encoded as a JWT token - pub fn to_cid(&self, hasher: Option) -> Result { - static RAW_CODEC: u64 = 0x55; - - let token = self.encode()?; - let digest = hasher.unwrap_or(DEFAULT_MULTIHASH).digest(token.as_bytes()); - let cid = Cid::new_v1(RAW_CODEC, digest); - - Ok(cid) - } -} - -impl<'a, F, C> TryFrom<&'a str> for Ucan -where - F: DeserializeOwned, - C: CapabilityParser, -{ - type Error = Error; - - fn try_from(ucan_token: &str) -> Result { - Ucan::::from_str(ucan_token) - } -} - -impl TryFrom for Ucan -where - F: DeserializeOwned, - C: CapabilityParser, -{ - type Error = Error; - - fn try_from(ucan_token: String) -> Result { - Ucan::from_str(ucan_token.as_str()) - } -} - -impl FromStr for Ucan -where - F: DeserializeOwned, - C: CapabilityParser, -{ - type Err = Error; - - fn from_str(ucan_token: &str) -> Result { - let &[header, payload, signature] = - ucan_token.splitn(3, '.').collect::>().as_slice() - else { - return Err(Error::TokenParseError { - msg: "malformed token, expected 3 parts separated by dots".to_string(), - }); - }; - - let header = - jose_b64::serde::Json::from_str(header).map_err(|_| Error::TokenParseError { - msg: "malformed header".to_string(), - })?; - - let payload = - jose_b64::serde::Json::from_str(payload).map_err(|_| Error::TokenParseError { - msg: "malformed payload".to_string(), - })?; - - let signature = - jose_b64::serde::Bytes::from_str(signature).map_err(|_| Error::TokenParseError { - msg: "malformed signature".to_string(), - })?; - - Ok(Ucan:: { - header, - payload, - signature, - }) - } -} - -impl<'de, F, C> Deserialize<'de> for Ucan -where - C: CapabilityParser, - F: DeserializeOwned, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ucan::::from_str(&String::deserialize(deserializer)?) - .map_err(|e| serde::de::Error::custom(e.to_string())) - } -} - -impl Serialize for Ucan -where - C: CapabilityParser, - F: Clone + DeserializeOwned, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.encode() - .map_err(|e| serde::ser::Error::custom(e.to_string()))? - .serialize(serializer) - } -} - -fn deserialize_required_nullable<'de, T, D>(deserializer: D) -> Result -where - T: Deserialize<'de>, - D: Deserializer<'de>, -{ - Deserialize::deserialize(deserializer) - .map_err(|_| serde::de::Error::custom("required field is missing or has invalid type")) -} - -#[cfg(test)] -mod tests { - use signature::rand_core; - - use crate::{ - builder::UcanBuilder, - crypto::SignerDid, - did_verifier::DidVerifierMap, - plugins::wnfs::{WnfsAbility, WnfsResource}, - semantics::{ability::TopAbility, caveat::EmptyCaveat}, - store::InMemoryStore, - time, - }; - - use super::*; - - #[test] - fn test_capabilities_for_empty() -> Result<(), anyhow::Error> { - let store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .sign(&iss_key)?; - - let capabilities = ucan.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Create, - 0, - &did_verifier_map, - &store, - )?; - - assert!(capabilities.is_empty()); - - Ok(()) - } - - #[test] - fn test_capabilities_for_root_capability_exact() -> Result<(), anyhow::Error> { - let store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Create, - EmptyCaveat, - )) - .sign(&iss_key)?; - - let capabilities = ucan.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Create, - 0, - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 1); - - assert_eq!( - capabilities[0].resource().downcast_ref::(), - Some(&WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }) - ); - - assert_eq!( - capabilities[0].ability().downcast_ref::(), - Some(&WnfsAbility::Create) - ); - - assert_eq!( - capabilities[0].caveat().downcast_ref::(), - Some(&EmptyCaveat) - ); - - Ok(()) - } - - #[test] - fn test_capabilities_for_root_capability_subsumed_by_semantics() -> Result<(), anyhow::Error> { - let store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Overwrite, - EmptyCaveat, - )) - .sign(&iss_key)?; - - let capabilities = ucan.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string(), "vacation".to_string()], - }, - WnfsAbility::Create, - 0, - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 1); - - assert_eq!( - capabilities[0].resource().downcast_ref::(), - Some(&WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string(), "vacation".to_string()], - }) - ); - - assert_eq!( - capabilities[0].ability().downcast_ref::(), - Some(&WnfsAbility::Create) - ); - - assert_eq!( - capabilities[0].caveat().downcast_ref::(), - Some(&EmptyCaveat) - ); - - Ok(()) - } - - #[test] - fn test_capabilities_for_root_capability_subsumed_by_top() -> Result<(), anyhow::Error> { - let store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - TopAbility, - EmptyCaveat, - )) - .sign(&iss_key)?; - - let capabilities = ucan.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string(), "vacation".to_string()], - }, - WnfsAbility::Overwrite, - 0, - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 1); - - assert_eq!( - capabilities[0].resource().downcast_ref::(), - Some(&WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string(), "vacation".to_string()], - }) - ); - - assert_eq!( - capabilities[0].ability().downcast_ref::(), - Some(&WnfsAbility::Overwrite) - ); - - assert_eq!( - capabilities[0].caveat().downcast_ref::(), - Some(&EmptyCaveat) - ); - - Ok(()) - } - - #[test] - fn test_capabilities_for_invocation_no_lifetime() -> Result<(), anyhow::Error> { - let mut store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let root_ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - TopAbility, - EmptyCaveat, - )) - .sign(&iss_key)?; - - store.write(Ipld::Bytes(root_ucan.encode()?.as_bytes().to_vec()), None)?; - - let invocation: Ucan = UcanBuilder::default() - .for_audience("did:web:fission.codes") - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - EmptyCaveat, - )) - .witnessed_by(&root_ucan, None) - .sign(&aud_key)?; - - let capabilities = invocation.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - time::now(), - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 1); - - assert_eq!( - capabilities[0].resource().downcast_ref::(), - Some(&WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }) - ); - - assert_eq!( - capabilities[0].ability().downcast_ref::(), - Some(&WnfsAbility::Revise) - ); - - assert_eq!( - capabilities[0].caveat().downcast_ref::(), - Some(&EmptyCaveat) - ); - - Ok(()) - } - - #[test] - fn test_capabilities_for_invocation_lifetime_encompassed() -> Result<(), anyhow::Error> { - let mut store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let root_ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - TopAbility, - EmptyCaveat, - )) - .with_lifetime(60) - .sign(&iss_key)?; - - store.write(Ipld::Bytes(root_ucan.encode()?.as_bytes().to_vec()), None)?; - - let invocation: Ucan = UcanBuilder::default() - .for_audience("did:web:fission.codes") - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - EmptyCaveat, - )) - .with_lifetime(30) - .witnessed_by(&root_ucan, None) - .sign(&aud_key)?; - - let capabilities = invocation.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - time::now(), - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 1); - - assert_eq!( - capabilities[0].resource().downcast_ref::(), - Some(&WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }) - ); - - assert_eq!( - capabilities[0].ability().downcast_ref::(), - Some(&WnfsAbility::Revise) - ); - - assert_eq!( - capabilities[0].caveat().downcast_ref::(), - Some(&EmptyCaveat) - ); - - Ok(()) - } - - #[test] - fn test_capabilities_for_invocation_nbf_exposed() -> Result<(), anyhow::Error> { - let mut store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let root_ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - TopAbility, - EmptyCaveat, - )) - .not_before(1) - .sign(&iss_key)?; - - store.write(Ipld::Bytes(root_ucan.encode()?.as_bytes().to_vec()), None)?; - - let invocation: Ucan = UcanBuilder::default() - .for_audience("did:web:fission.codes") - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - EmptyCaveat, - )) - .not_before(0) - .witnessed_by(&root_ucan, None) - .sign(&aud_key)?; - - let capabilities = invocation.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - 0, - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 0); - - Ok(()) - } - - #[test] - fn test_capabilities_for_invocation_exp_exposed() -> Result<(), anyhow::Error> { - let mut store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let root_ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - TopAbility, - EmptyCaveat, - )) - .with_expiration(0) - .sign(&iss_key)?; - - store.write(Ipld::Bytes(root_ucan.encode()?.as_bytes().to_vec()), None)?; - - let invocation: Ucan = UcanBuilder::default() - .for_audience("did:web:fission.codes") - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - EmptyCaveat, - )) - .with_expiration(1) - .witnessed_by(&root_ucan, None) - .sign(&aud_key)?; - - let capabilities = invocation.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - 0, - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 0); - - Ok(()) - } - - #[test] - fn test_capabilities_for_invocation_lifetime_disjoint() -> Result<(), anyhow::Error> { - let mut store = InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - let iss_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - let aud_key = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng); - - let root_ucan: Ucan = UcanBuilder::default() - .for_audience(aud_key.did()?) - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - TopAbility, - EmptyCaveat, - )) - .not_before(0) - .with_expiration(1) - .sign(&iss_key)?; - - store.write(Ipld::Bytes(root_ucan.encode()?.as_bytes().to_vec()), None)?; - - let invocation: Ucan = UcanBuilder::default() - .for_audience("did:web:fission.codes") - .claiming_capability(Capability::new( - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - EmptyCaveat, - )) - .not_before(2) - .with_expiration(3) - .witnessed_by(&root_ucan, None) - .sign(&aud_key)?; - - let capabilities = invocation.capabilities_for( - iss_key.did()?, - WnfsResource::PublicPath { - user: "alice".to_string(), - path: vec!["photos".to_string()], - }, - WnfsAbility::Revise, - 2, - &did_verifier_map, - &store, - )?; - - assert_eq!(capabilities.len(), 0); - - Ok(()) - } -} diff --git a/src/url.rs b/src/url.rs new file mode 100644 index 00000000..dc8bd9c6 --- /dev/null +++ b/src/url.rs @@ -0,0 +1,96 @@ +//! URL utilities. + +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::fmt; +use thiserror::Error; +use url::Url; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A wrapper around [`Url`] that has additional trait implementations. +/// +/// Usage is very simple: wrap a [`Newtype`] to gain access to additional traits and methods. +/// +/// ```rust +/// # use ::url::Url; +/// # use ucan::url; +/// # +/// let url = Url::parse("https://example.com").unwrap(); +/// let wrapped = url::Newtype(url.clone()); +/// // wrapped.some_trait_method(); +/// ``` +/// +/// Unwrap a [`Newtype`] to use any interfaces that expect plain [`Ipld`]. +/// +/// ``` +/// # use ::url::Url; +/// # use ucan::url; +/// # +/// # let url = Url::parse("https://example.com").unwrap(); +/// # let wrapped = url::Newtype(url.clone()); +/// # +/// assert_eq!(wrapped.0, url); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Newtype(pub Url); + +impl Newtype { + pub fn parse(s: &str) -> Result { + Ok(Newtype(Url::parse(s)?)) + } +} + +impl fmt::Display for Newtype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for Ipld { + fn from(newtype: Newtype) -> Self { + Ipld::String(newtype.to_string()) + } +} + +impl TryFrom for Newtype { + type Error = FromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::String(s) => Url::parse(&s) + .map(Newtype) + .map_err(FromIpldError::UrlParseError), + _ => Err(FromIpldError::NotAString), + } + } +} + +/// Possible errors when trying to convert from [`Ipld`]. +#[derive(Debug, Error)] +pub enum FromIpldError { + /// Not an IPLD string. + #[error("Not an IPLD string")] + NotAString, + + /// Failed to parse the URL. + #[error(transparent)] + UrlParseError(#[from] url::ParseError), +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + let url_regex: &str = &r#"[a-zA-Z]+[a-zA-Z0-9]*:(//)?[a-zA-Z0-9._]+(#)?[a-zA-Z0-9_]"#; + url_regex + .prop_map(|s| { + Newtype(Url::parse(&s).expect("the regex generator to create valid URLs")) + }) + .boxed() + } +} diff --git a/src/wasm.rs b/src/wasm.rs deleted file mode 100644 index 0575d3c1..00000000 --- a/src/wasm.rs +++ /dev/null @@ -1,310 +0,0 @@ -use anyhow::{anyhow, bail}; -use async_signature::AsyncSigner; -use async_trait::async_trait; -use js_sys::{Date, Error, Reflect, Uint8Array}; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Crypto, CryptoKey, CryptoKeyPair, SubtleCrypto}; - -use crate::{builder::UcanBuilder, capability::DefaultCapabilityParser, crypto::SignerDid}; - -/// Convenience alias around `Result` -pub type JsResult = Result; - -/// A UCAN whose facts are a JSON value -#[wasm_bindgen] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Ucan { - ucan: crate::ucan::Ucan, -} - -struct JsSigner { - key_pair: CryptoKeyPair, - _marker: std::marker::PhantomData K>, -} - -impl JsSigner { - fn subtle_crypto() -> Result { - let global = js_sys::global(); - - match Reflect::get(&global, &JsValue::from_str("crypto")) { - Ok(value) => { - let crypto = value - .dyn_into::() - .map_err(|_| anyhow!("Failed to cast value to Crypto"))? - .subtle(); - - Ok(crypto) - } - Err(_) => bail!("Failed to get crypto from global object"), - } - } - - fn new(key_pair: CryptoKeyPair) -> Self { - Self { - key_pair, - _marker: std::marker::PhantomData, - } - } - - fn signing_key(&self) -> Result { - match Reflect::get(&self.key_pair, &JsValue::from_str("privateKey")) { - Ok(key) => key - .dyn_into::() - .map_err(|_| anyhow!("Failed to cast value to CryptoKey")), - Err(_) => bail!("Failed to get privateKey from CryptoKeyPair"), - } - } - - fn verifying_key(&self) -> Result { - match Reflect::get(&self.key_pair, &JsValue::from_str("publicKey")) { - Ok(key) => key - .dyn_into::() - .map_err(|_| anyhow!("Failed to cast value to CryptoKey")), - Err(_) => bail!("Failed to get publicKey from CryptoKeyPair"), - } - } -} - -impl SignerDid for JsSigner { - fn did(&self) -> Result { - Ok("test".to_string()) - } -} - -#[async_trait(?Send)] -impl AsyncSigner for JsSigner { - async fn sign_async( - &self, - msg: &[u8], - ) -> Result { - let subtle = Self::subtle_crypto().map_err(|e| async_signature::Error::from_source(e))?; - - // This can be done without copying using the unsafe `Uint8Array::view` method, - // but I've opted to stick to safe APIs for now, until we benchmark signing. - let data = Uint8Array::from(msg).buffer(); - - let key = self - .signing_key() - .map_err(|e| async_signature::Error::from_source(e))?; - - let promise = subtle - .sign_with_str_and_buffer_source("RSASSA-PKCS1-v1_5", &key, &data) - .map_err(|_| async_signature::Error::new())?; - - let result = JsFuture::from(promise) - .await - .map_err(|_| async_signature::Error::new())?; - - let signature = - rsa::pkcs1v15::Signature::try_from(Uint8Array::new(&result).to_vec().as_slice()) - .map_err(|_| async_signature::Error::new())?; - - Ok(signature) - } -} - -#[wasm_bindgen] -impl Ucan { - /// Returns a boolean indicating whether the given UCAN is expired at the given date - #[wasm_bindgen(js_name = "isExpired")] - pub fn is_expired(&self, at_time: &Date) -> bool { - let at_time = f64::floor(at_time.get_time() / 1000.) as u64; - - self.ucan.is_expired(at_time) - } - - /// Returns true if the UCAN is not yet valid at the given date - #[wasm_bindgen(js_name = "isTooEarly")] - pub fn is_too_early(&self, at_time: &Date) -> bool { - let at_time = f64::floor(at_time.get_time() / 1000.) as u64; - - self.ucan.is_too_early(at_time) - } - - /// Returns the UCAN's signature as a `Uint8Array` - #[wasm_bindgen(getter)] - pub fn signature(&self) -> Vec { - self.ucan.signature().to_vec() - } - - /// Returns the `typ` field of the UCAN's JWT header - #[wasm_bindgen(getter)] - pub fn typ(&self) -> String { - self.ucan.typ().to_string() - } - - /// Returns the `alg` field of the UCAN's JWT header - #[wasm_bindgen(getter)] - pub fn algorithm(&self) -> String { - self.ucan.algorithm().to_string() - } - - /// Returns the `iss` field of the UCAN's JWT payload - #[wasm_bindgen(getter)] - pub fn issuer(&self) -> String { - self.ucan.issuer().to_string() - } - - /// Returns the `aud` field of the UCAN's JWT payload - #[wasm_bindgen(getter)] - pub fn audience(&self) -> String { - self.ucan.audience().to_string() - } - - /// Returns the `exp` field of the UCAN's JWT payload - #[wasm_bindgen(getter, js_name = "expiresAt")] - pub fn expires_at(&self) -> Option { - self.ucan - .expires_at() - .map(|expires_at| Date::new(&JsValue::from_f64((expires_at as f64) * 1000.))) - } - - /// Returns the `nbf` field of the UCAN's JWT payload - #[wasm_bindgen(getter, js_name = "notBefore")] - pub fn not_before(&self) -> Option { - self.ucan - .not_before() - .map(|not_before| Date::new(&JsValue::from_f64((not_before as f64) * 1000.))) - } - - /// Returns the `nnc` field of the UCAN's JWT payload - #[wasm_bindgen(getter)] - pub fn nonce(&self) -> Option { - self.ucan.nonce().map(String::to_string) - } - - /// Returns the `fct` field of the UCAN's JWT payload - #[wasm_bindgen(getter)] - pub fn facts(&self) -> JsResult { - self.ucan - .facts() - .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) - .map_err(|e| Error::new(&format!("Failed to serialize facts: {}", e))) - } - - /// Returns the `vsn` field of the UCAN's JWT payload - #[wasm_bindgen(getter)] - pub fn version(&self) -> String { - self.ucan.version().to_string() - } - - /// Returns the CID of the UCAN - #[wasm_bindgen] - pub fn cid(&self) -> JsResult { - match self.ucan.to_cid(None) { - Ok(cid) => Ok(cid.to_string()), - Err(e) => Err(Error::new(&format!("Failed to convert to CID: {}", e))), - } - } -} - -/// Decode a UCAN -#[wasm_bindgen] -pub async fn decode(token: String) -> JsResult { - let ucan = - crate::ucan::Ucan::from_str(&token).map_err(|e| Error::new(e.to_string().as_ref()))?; - - Ok(Ucan { ucan }) -} - -/// Options for building a UCAN -#[derive(Debug, Deserialize)] -pub struct BuildOptions { - /// The lifetime of the UCAN in seconds - #[serde(rename = "lifetimeInSeconds")] - pub lifetime_in_seconds: Option, - /// The expiration time of the UCAN in seconds since epoch - pub expiration: Option, - /// The time before which the UCAN is not valid in seconds since epoch - #[serde(rename = "notBefore")] - pub not_before: Option, - /// The facts included in the UCAN - pub facts: Option, - /// The proof CIDs referenced by the UCAN - pub proofs: Option>, - /// The nonce of the UCAN - pub nonce: Option, - // TODO: capabilities -} - -/// Build a UCAN -#[wasm_bindgen] -pub async fn build(issuer: CryptoKeyPair, audience: &str, options: JsValue) -> JsResult { - let options: BuildOptions = - serde_wasm_bindgen::from_value(options).map_err(|e| Error::new(e.to_string().as_ref()))?; - - let builder = - UcanBuilder::::default().for_audience(audience); - - let builder = match options.lifetime_in_seconds { - Some(lifetime_in_seconds) => builder.with_lifetime(lifetime_in_seconds), - None => builder, - }; - - let builder = match options.expiration { - Some(expiration) => builder.with_expiration(expiration), - None => builder, - }; - - let builder = match options.not_before { - Some(not_before) => builder.not_before(not_before), - None => builder, - }; - - let builder = match options.facts { - Some(facts) => builder.with_fact(facts), - None => builder, - }; - - let builder = match options.nonce { - Some(nonce) => builder.with_nonce(nonce), - None => builder, - }; - - // TODO: proofs (need store) - - let signer = JsSigner::::new(issuer); - - let ucan = builder - .sign_async(&signer) - .await - .map_err(|e| Error::new(e.to_string().as_ref()))?; - - Ok(Ucan { ucan }) -} - -/// Panic hook lets us get better error messages if our Rust code ever panics. -/// -/// For more details see -/// -#[wasm_bindgen(js_name = "setPanicHook")] -pub fn set_panic_hook() { - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} - -#[wasm_bindgen] -extern "C" { - // For alerting - pub(crate) fn alert(s: &str); - // For logging in the console. - #[wasm_bindgen(js_namespace = console)] - pub fn log(s: &str); -} - -/// Return a representation of an object owned by JS. -#[macro_export] -macro_rules! value { - ($value:expr) => { - wasm_bindgen::JsValue::from($value) - }; -} - -/// Calls the wasm_bindgen console.log. -#[macro_export] -macro_rules! console_log { - ($($t:tt)*) => ($crate::log(&format_args!($($t)*).to_string())) -} diff --git a/src/workerd.js b/src/workerd.js index ebd2009d..fad5ae1d 100644 --- a/src/workerd.js +++ b/src/workerd.js @@ -1,6 +1,6 @@ // This entry point is inserted into ./lib/workerd to support Cloudflare workers -import WASM from "./rs_ucan_bg.wasm"; -import { initSync } from "./rs_ucan.js"; +import WASM from "./ucan_bg.wasm"; +import { initSync } from "./ucan.js"; initSync(WASM); -export * from "./rs_ucan.js"; +export * from "./ucan.js"; diff --git a/tests/conformance.rs b/tests/conformance.rs deleted file mode 100644 index a69552fa..00000000 --- a/tests/conformance.rs +++ /dev/null @@ -1,393 +0,0 @@ -use libipld_core::{ipld::Ipld, raw::RawCodec}; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs::File, io::BufReader, str::FromStr}; - -use rs_ucan::{ - capability::DefaultCapabilityParser, - did_verifier::DidVerifierMap, - store::{self, Store}, - ucan::Ucan, - DefaultFact, -}; - -trait TestTask { - fn run(&self, name: &str, report: &mut TestReport); -} - -#[derive(Debug, Default)] -struct TestReport { - num_tests: usize, - successes: Vec, - failures: Vec, -} - -#[derive(Debug)] -struct TestFailure { - name: String, - error: String, -} - -impl TestReport { - fn register_success(&mut self, name: &str) { - self.num_tests += 1; - self.successes.push(name.to_string()); - } - - fn register_failure(&mut self, name: &str, error: String) { - self.num_tests += 1; - self.failures.push(TestFailure { - name: name.to_string(), - error, - }); - } - - fn finish(&self) { - for success in &self.successes { - println!("✅ {}", success); - } - - for failure in &self.failures { - println!("❌ {}: {}", failure.name, failure.error); - } - - println!( - "{} tests, {} successes, {} failures", - self.num_tests, - self.successes.len(), - self.failures.len() - ); - - if !self.failures.is_empty() { - panic!(); - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct TestFixture { - name: String, - #[serde(flatten)] - test_case: TestCase, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "task", rename_all = "camelCase")] -enum TestCase { - Verify(VerifyTest), - Refute(RefuteTest), - Build(BuildTest), - ToCID(ToCidTest), -} - -#[derive(Debug, Serialize, Deserialize)] -struct VerifyTest { - inputs: TestInputsTokenAndProofs, - assertions: TestAssertions, -} - -#[derive(Debug, Serialize, Deserialize)] -struct RefuteTest { - inputs: TestInputsTokenAndProofs, - assertions: TestAssertions, - errors: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -struct BuildTest { - inputs: BuildTestInputs, - outputs: BuildTestOutputs, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ToCidTest { - inputs: ToCidTestInputs, - outputs: ToCidTestOutputs, -} - -#[derive(Debug, Serialize, Deserialize)] -struct TestInputsTokenAndProofs { - token: String, - proofs: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -struct TestAssertions { - header: TestAssertionsHeader, - payload: TestAssertionsPayload, - signature: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct TestAssertionsHeader { - alg: Option, - typ: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct TestAssertionsPayload { - ucv: Option, - iss: Option, - aud: Option, - exp: Option, - // TODO: CAP - // TODO: FCT - prf: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -struct BuildTestInputs { - version: Option, - issuer_base64_key: String, - signature_scheme: String, - audience: Option, - not_before: Option, - expiration: Option, - // TODO CAPABILITIES - // TODO FACTS -} - -#[derive(Debug, Serialize, Deserialize)] -struct BuildTestOutputs { - token: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ToCidTestInputs { - token: String, - hasher: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ToCidTestOutputs { - cid: String, -} - -impl TestTask for VerifyTest { - fn run(&self, name: &str, report: &mut TestReport) { - let mut store = store::InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - for (_cid, token) in self.inputs.proofs.iter() { - store - .write(Ipld::Bytes(token.as_bytes().to_vec()), None) - .unwrap(); - } - - let Ok(ucan) = Ucan::::from_str(&self.inputs.token) - else { - report.register_failure(name, "failed to parse token".to_string()); - - return; - }; - - if let Some(alg) = &self.assertions.header.alg { - if ucan.algorithm() != alg { - report.register_failure( - name, - format!( - "expected algorithm to be {}, but was {}", - alg, - ucan.algorithm() - ), - ); - - return; - } - } - - if let Some(typ) = &self.assertions.header.typ { - if ucan.typ() != typ { - report.register_failure( - name, - format!("expected type to be {}, but was {}", typ, ucan.typ()), - ); - - return; - } - } - - if let Some(ucv) = &self.assertions.payload.ucv { - if ucan.version() != ucv { - report.register_failure( - name, - format!("expected version to be {}, but was {}", ucv, ucan.version()), - ); - - return; - } - } - - if let Some(iss) = &self.assertions.payload.iss { - if ucan.issuer() != iss { - report.register_failure( - name, - format!("expected issuer to be {}, but was {}", iss, ucan.issuer()), - ); - - return; - } - } - - if let Some(aud) = &self.assertions.payload.aud { - if ucan.audience() != aud { - report.register_failure( - name, - format!( - "expected audience to be {}, but was {}", - aud, - ucan.audience() - ), - ); - - return; - } - } - - if ucan.expires_at() != self.assertions.payload.exp { - report.register_failure( - name, - format!( - "expected expiration to be {:?}, but was {:?}", - self.assertions.payload.exp, - ucan.expires_at() - ), - ); - - return; - } - - if ucan - .proofs() - .map(|f| f.iter().map(|c| c.to_string()).collect()) - != self.assertions.payload.prf - { - report.register_failure( - name, - format!( - "expected proofs to be {:?}, but was {:?}", - self.assertions.payload.prf, - ucan.proofs() - ), - ); - - return; - } - - let Ok(signature) = serde_json::to_value(ucan.signature()) else { - report.register_failure(name, "failed to serialize signature".to_string()); - - return; - }; - - let Some(signature) = signature.as_str() else { - report.register_failure(name, "expected signature to be a string".to_string()); - - return; - }; - - if signature != self.assertions.signature { - report.register_failure( - name, - format!( - "expected signature to be {}, but was {}", - self.assertions.signature, signature - ), - ); - - return; - } - - if let Err(err) = ucan.validate(rs_ucan::time::now(), &did_verifier_map) { - report.register_failure(name, err.to_string()); - - return; - } - } -} - -impl TestTask for RefuteTest { - fn run(&self, name: &str, report: &mut TestReport) { - let mut store = store::InMemoryStore::::default(); - let did_verifier_map = DidVerifierMap::default(); - - for (_cid, token) in self.inputs.proofs.iter() { - store - .write(Ipld::Bytes(token.as_bytes().to_vec()), None) - .unwrap(); - } - - if let Ok(ucan) = Ucan::::from_str(&self.inputs.token) - { - if ucan - .validate(rs_ucan::time::now(), &did_verifier_map) - .is_ok() - { - report.register_failure( - &name, - "expected token to fail validation, but it passed".to_string(), - ); - - return; - } - } - } -} - -impl TestTask for BuildTest { - fn run(&self, _: &str, _: &mut TestReport) { - //TODO: can't assert on signature because of canonicalization issues - } -} - -impl TestTask for ToCidTest { - fn run(&self, name: &str, report: &mut TestReport) { - let ucan = - Ucan::::from_str(&self.inputs.token).unwrap(); - let hasher = match self.inputs.hasher.as_str() { - "SHA2-256" => multihash::Code::Sha2_256, - "BLAKE3-256" => multihash::Code::Blake3_256, - _ => panic!(), - }; - - let Ok(cid) = ucan.to_cid(Some(hasher)) else { - report.register_failure(&name, "failed to convert to CID".to_string()); - - return; - }; - - if cid.to_string() != self.outputs.cid { - report.register_failure( - &name, - format!( - "expected CID to be {}, but was {}", - self.outputs.cid, - cid.to_string() - ), - ); - - return; - } - } -} - -#[test] -fn ucan_0_10_0_conformance_tests() { - let fixtures_file = File::open("tests/fixtures/0.10.0/all.json").unwrap(); - let reader = BufReader::new(fixtures_file); - let fixtures: Vec = serde_json::from_reader(reader).unwrap(); - - let mut report = TestReport::default(); - - for fixture in fixtures { - match fixture.test_case { - TestCase::Verify(test) => test.run(&fixture.name, &mut report), - TestCase::Refute(test) => test.run(&fixture.name, &mut report), - TestCase::Build(test) => test.run(&fixture.name, &mut report), - TestCase::ToCID(test) => test.run(&fixture.name, &mut report), - }; - - report.register_success(&fixture.name); - } - - report.finish(); -} diff --git a/tests/rs_ucan.test.js b/tests/rs_ucan.test.js index 3b00bf07..f8385e26 100644 --- a/tests/rs_ucan.test.js +++ b/tests/rs_ucan.test.js @@ -1,5 +1,5 @@ import assert from "assert"; -import { build, decode } from "../dist/bundler/rs_ucan.js"; +import { build, decode } from "../dist/bundler/ucan.js"; describe("decode", async function () { let ucan = await decode( diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b6828dc2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "module": "es2015", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2015" + ] + } +}