├── .cargo └── audit.toml ├── .github ├── dependabot.yml └── workflows │ ├── coverage.yml │ ├── fuzz.yml │ ├── no-std.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── decode.rs ├── clippy.toml ├── codecov.yml ├── deny.toml ├── examples ├── qr.rs └── wasm │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── index.scss │ └── src │ ├── input.rs │ └── main.rs ├── fuzz ├── Cargo.toml └── fuzz_targets │ ├── bytewords_decode.rs │ ├── bytewords_encode.rs │ └── ur_encode.rs └── src ├── bytewords.rs ├── constants.rs ├── fountain.rs ├── lib.rs ├── sampler.rs ├── ur.rs └── xoshiro.rs /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | ignore = ["RUSTSEC-2024-0370"] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: cargo 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | open-pull-requests-limit: 10 12 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | coverage: 11 | runs-on: ubuntu-latest 12 | name: ubuntu / stable / coverage 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: true 17 | - name: Install stable 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | components: llvm-tools-preview 21 | - name: cargo install cargo-llvm-cov 22 | uses: taiki-e/install-action@cargo-llvm-cov 23 | - name: cargo generate-lockfile 24 | run: cargo generate-lockfile 25 | - name: cargo llvm-cov 26 | run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info 27 | - name: Upload to codecov.io 28 | uses: codecov/codecov-action@v5 29 | with: 30 | fail_ci_if_error: true 31 | env: 32 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yml: -------------------------------------------------------------------------------- 1 | name: Fuzz 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | HFUZZ_RUN_ARGS: --run_time 120 --exit_upon_crash -v 11 | 12 | jobs: 13 | fuzz: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | fuzz_target: [bytewords_decode, bytewords_encode, ur_encode] 19 | steps: 20 | - name: Install test dependencies 21 | run: sudo apt-get update -y && sudo apt-get install -y build-essential binutils-dev libunwind-dev libblocksruntime-dev liblzma-dev 22 | - uses: actions/checkout@v4 23 | - uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | override: true 27 | profile: minimal 28 | - run: cargo install honggfuzz 29 | - name: Fuzz 30 | working-directory: ./fuzz 31 | run: | 32 | cargo hfuzz run ${{ matrix.fuzz_target }} 33 | test $(ls hfuzz_workspace/${{ matrix.fuzz_target }} | wc -l) -eq 1 34 | -------------------------------------------------------------------------------- /.github/workflows/no-std.yml: -------------------------------------------------------------------------------- 1 | name: no-std 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | no-std: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | target: thumbv6m-none-eabi 19 | override: true 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | use-cross: true 23 | command: build 24 | args: --target thumbv6m-none-eabi --no-default-features 25 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install crates 20 | run: | 21 | cargo install --debug cargo-quickinstall 22 | cargo quickinstall cargo-sort cargo-rdme cargo-audit cargo-udeps cargo-machete taplo-cli cargo-deny 23 | - name: Lint 24 | run: | 25 | cargo fmt -- --check --config format_code_in_doc_comments=true 26 | git ls-files | grep ".toml$" | taplo fmt --check 27 | cargo rdme --check 28 | cargo sort --workspace --check 29 | cargo audit -D warnings 30 | cargo deny check licenses 31 | cargo machete 32 | cargo clippy --workspace --all --all-targets --all-features -- -Dwarnings -D clippy::pedantic -D clippy::dbg-macro -A clippy::missing-panics-doc 33 | cargo build --workspace 34 | cargo test --workspace 35 | export RUSTC_BOOTSTRAP=1 && cargo udeps && cd fuzz && cargo udeps 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/Cargo.lock 2 | target 3 | 4 | fuzz/hfuzz_target 5 | fuzz/hfuzz_workspace 6 | 7 | **/dist 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## [0.4.1](https://github.com/dspicher/ur-rs/releases/tag/0.4.1) - 2023-10-16 10 | - Take a reference to custom UR type identifiers 11 | 12 | ## [0.4.0](https://github.com/dspicher/ur-rs/releases/tag/0.4.0) - 2023-08-04 13 | - Added support for `no-std` environments. https://github.com/dspicher/ur-rs/pull/183 14 | - Introduced a type-safe `ur::Type` enum and a `ur::Encoder::bytes` shorthand constructor (see the below migration guide). https://github.com/dspicher/ur-rs/pull/186 15 | - Added `wasm` example. https://github.com/dspicher/ur-rs/pull/191 16 | 17 | ### Migration guide 18 | 19 | Replace `Encoder` constructors with `bytes` schemes: 20 | ```rust 21 | ur::Encoder::new(data, max_length, "bytes") 22 | ``` 23 | 24 | with: 25 | ```rust 26 | ur::Encoder::bytes(data, max_length) 27 | ``` 28 | 29 | Leave all other `Encoder` constructors as they are: 30 | ```rust 31 | ur::Encoder::new(data, max_length, "my-scheme") 32 | ``` 33 | 34 | ## [0.3.0](https://github.com/dspicher/ur-rs/releases/tag/0.3.0) - 2023-01-07 35 | - Added `ur::ur::decode` to the public API to decode a single `ur` URI. https://github.com/dspicher/ur-rs/pull/112 36 | - Added `ur::ur::encode` and `ur::ur::decode` to the root library path. https://github.com/dspicher/ur-rs/pull/112 37 | - Bumped the Rust edition to 2021. https://github.com/dspicher/ur-rs/pull/113 38 | - Added an enum indicating whether the UR was single- or multip-part to `ur::ur::decode`. https://github.com/dspicher/ur-rs/pull/121 39 | - Migrated from `anyhow` errors to a custom error enum. https://github.com/dspicher/ur-rs/pull/159 40 | - Remove `std::fmt::Display` implementation of `Part`. https://github.com/dspicher/ur-rs/pull/160 41 | 42 | ## [0.2.0](https://github.com/dspicher/ur-rs/releases/tag/0.2.0) - 2021-12-08 43 | - The public API has been greatly restricted 44 | - All public methods and structs are documented and should be much more stable going forward 45 | - Introduced fuzz testing 46 | 47 | ## 0.1.0 - 2021-08-23 48 | - Initial release 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["examples/wasm", "fuzz"] 3 | 4 | [package] 5 | name = "ur" 6 | description = "A Rust implementation of Uniform Resources" 7 | license = "MIT" 8 | version = "0.4.1" 9 | authors = ["Dominik Spicher "] 10 | edition = "2024" 11 | repository = "https://github.com/dspicher/ur-rs/" 12 | 13 | [dependencies] 14 | bitcoin_hashes = { version = "0.16", default-features = false } 15 | crc = "3" 16 | minicbor = { version = "1.0", features = ["alloc"] } 17 | rand_xoshiro = "0.7" 18 | 19 | [dev-dependencies] 20 | criterion = "0.6" 21 | hex = "0.4" 22 | qrcode = { version = "0.14", default-features = false } 23 | 24 | [features] 25 | default = ["std"] 26 | std = [] 27 | 28 | [[bench]] 29 | name = "decode" 30 | harness = false 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dominik Spicher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rust Uniform Resources 2 | ====================== 3 | [build status](https://github.com/dspicher/ur-rs/actions) 4 | [build status](https://codecov.io/gh/dspicher/ur-rs) 5 | [build status](https://crates.io/crates/ur) 6 | [![dependency status](https://deps.rs/repo/github/dspicher/ur-rs/status.svg)](https://deps.rs/repo/github/dspicher/ur-rs) 7 | 8 | 9 | 10 | `ur` is a crate to interact with [`uniform resource`](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md) encodings of binary data. 11 | The encoding scheme is optimized for transport in URIs and QR codes. 12 | 13 | The [`ur::Encoder`] allows a byte payload to be transmissioned in 14 | multiple stages, respecting maximum size requirements. Under the hood, 15 | a [`fountain`](https://en.wikipedia.org/wiki/Fountain_code) encoder is used to create an unbounded stream of URIs, 16 | subsets of which can be recombined at the receiving side into the payload: 17 | ```rust 18 | let data = String::from("Ten chars!").repeat(10); 19 | let max_length = 5; 20 | let mut encoder = ur::Encoder::bytes(data.as_bytes(), max_length).unwrap(); 21 | let part = encoder.next_part().unwrap(); 22 | assert_eq!( 23 | part, 24 | "ur:bytes/1-20/lpadbbcsiecyvdidatkpfeghihjtcxiabdfevlms" 25 | ); 26 | let mut decoder = ur::Decoder::default(); 27 | while !decoder.complete() { 28 | let part = encoder.next_part().unwrap(); 29 | // Simulate some communication loss 30 | if encoder.current_index() & 1 > 0 { 31 | decoder.receive(&part).unwrap(); 32 | } 33 | } 34 | assert_eq!(decoder.message().unwrap().as_deref(), Some(data.as_bytes())); 35 | ``` 36 | 37 | The following useful building blocks are also part of the public API: 38 | - The [`crate::bytewords`](https://docs.rs/ur/latest/ur/bytewords/) module contains functionality 39 | to encode byte payloads into a suitable alphabet, achieving hexadecimal 40 | byte-per-character efficiency. 41 | - The [`crate::fountain`](https://docs.rs/ur/latest/ur/fountain/) module provides an implementation 42 | of a fountain encoder, which splits up a byte payload into multiple segments 43 | and emits an unbounded stream of parts which can be recombined at the receiving 44 | decoder side. 45 | 46 | 47 | 48 | ## Usage 49 | 50 | Add `ur` to the dependencies of your `Cargo.toml`: 51 | ```shell 52 | cargo add ur 53 | ``` 54 | 55 | ## Examples 56 | 57 | ### Animated QR code 58 | To run this example, execute 59 | ```bash 60 | cargo run --example qr -- "This is my super awesome UR payload" 61 | ``` 62 | which will print out URIs and QR codes transmitting the provided payload. 63 | 64 | ## Background: Uniform Resources 65 | [Uniform Resources](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md) are 66 | > a proposed method of encoding binary data of arbitrary content and length so that it is suitable for transport in either URIs or QR codes. 67 | 68 | The resulting constraints on the permissible encoding alphabet are nicely analyzed [here](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-003-uri-binary-compatibility.md). 69 | 70 | The following building blocks interact to achieve this goal: 71 | - [Bytewords](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-012-bytewords.md) map binary data to case-insensitive characters with a 4 bits/char efficiency (identical to hexadecimal encoding) 72 | - Fragments for transmitting multi-part messages are constructed based on a [Luby transform](https://en.wikipedia.org/wiki/Luby_transform_code) (a particular kind of [fountain encoding](https://en.wikipedia.org/wiki/Fountain_code)), generating a potentially limitless sequence of fragments, small subsets of which can restore the original message 73 | - [CBOR](https://tools.ietf.org/html/rfc7049) allows for self-describing byte payloads 74 | - A properly seeded [Xoshiro](https://en.wikipedia.org/wiki/Xorshift#xoshiro_and_xoroshiro) pseudo-random generator allows the encoding and decoding parties to agree on which message parts were combined into a fountain encoding fragment 75 | 76 | ## Other implementations 77 | This Rust implementation, in particular its test vectors, is based on the following reference implementations: 78 | - C++: [bc-ur](https://github.com/BlockchainCommons/bc-ur/) 79 | - Swift: [URKit](https://github.com/blockchaincommons/URKit) 80 | 81 | ## Contributing 82 | Pull requests are welcome. 83 | 84 | ## License 85 | This project is licensed under the terms of the [MIT](https://choosealicense.com/licenses/mit/) license. 86 | -------------------------------------------------------------------------------- /benches/decode.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, criterion_group, criterion_main}; 2 | use ur::decode; 3 | 4 | fn criterion_benchmark(c: &mut Criterion) { 5 | c.bench_function("decode bytes", |b| b.iter(|| decode(std::hint::black_box("ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch")))); 6 | } 7 | 8 | criterion_group!(benches, criterion_benchmark); 9 | criterion_main!(benches); 10 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | avoid-breaking-exported-api = false 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "src/lib.rs" 3 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | unused-allowed-license = "deny" 3 | allow = ["Apache-2.0", "BlueOak-1.0.0", "CC0-1.0", "MIT"] 4 | -------------------------------------------------------------------------------- /examples/qr.rs: -------------------------------------------------------------------------------- 1 | use qrcode::QrCode; 2 | 3 | use std::io::Write; 4 | 5 | fn main() { 6 | let mut encoder = 7 | ur::Encoder::bytes(std::env::args().next_back().unwrap().as_bytes(), 5).unwrap(); 8 | let mut stdout = std::io::stdout(); 9 | loop { 10 | let ur = encoder.next_part().unwrap(); 11 | let code = QrCode::new(&ur).unwrap(); 12 | let string = code 13 | .render::() 14 | .quiet_zone(false) 15 | .module_dimensions(2, 1) 16 | .build(); 17 | stdout.write_all(format!("{string}\n").as_bytes()).unwrap(); 18 | stdout 19 | .write_all(format!("{ur}\n\n\n\n").as_bytes()) 20 | .unwrap(); 21 | stdout.flush().unwrap(); 22 | std::thread::sleep(std::time::Duration::from_millis(1000)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Dominik Spicher "] 6 | edition = "2021" 7 | 8 | [dependencies] 9 | base64 = "0.22" 10 | gloo = "0.11" 11 | qrcode-generator = "5" 12 | ur = { path = "../.." } 13 | wasm-bindgen = "0.2" 14 | web-sys = { version = "0.3", features = ["Event", "EventTarget", "InputEvent"] } 15 | yew = { version = "0.21", features = ["csr"] } 16 | -------------------------------------------------------------------------------- /examples/wasm/README.md: -------------------------------------------------------------------------------- 1 | WASM example 2 | ====================== 3 | 4 | [Install](https://trunkrs.dev/#install) `trunk` and run from this directory: 5 | ```shell 6 | trunk serve --open 7 | ``` 8 | -------------------------------------------------------------------------------- /examples/wasm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Uniform Resources Demo 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/wasm/index.scss: -------------------------------------------------------------------------------- 1 | $font-stack: Roboto, sans-serif; 2 | $primary-color: #008f53; 3 | 4 | body { 5 | font: 100% $font-stack; 6 | color: white; 7 | background-color: $primary-color; 8 | text-align: center; 9 | } 10 | 11 | #buttons { 12 | text-align: center; 13 | margin-bottom: 10px; 14 | } 15 | 16 | #wrapper { 17 | overflow: hidden; 18 | width: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /examples/wasm/src/input.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{JsCast, UnwrapThrowExt}; 2 | use web_sys::{Event, HtmlInputElement, InputEvent}; 3 | use yew::prelude::*; 4 | 5 | #[derive(Clone, PartialEq, Properties)] 6 | pub struct Props { 7 | pub value: String, 8 | pub on_change: Callback, 9 | } 10 | 11 | fn get_value_from_input_event(e: InputEvent) -> String { 12 | let event: Event = e.dyn_into().unwrap_throw(); 13 | let event_target = event.target().unwrap_throw(); 14 | let target: HtmlInputElement = event_target.dyn_into().unwrap_throw(); 15 | target.value() 16 | } 17 | 18 | #[function_component(TextInput)] 19 | pub fn text_input(props: &Props) -> Html { 20 | let Props { value, on_change } = props.clone(); 21 | 22 | let oninput = Callback::from(move |input_event: InputEvent| { 23 | on_change.emit(get_value_from_input_event(input_event)); 24 | }); 25 | 26 | html! { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/wasm/src/main.rs: -------------------------------------------------------------------------------- 1 | mod input; 2 | 3 | use base64::Engine; 4 | use gloo::console; 5 | use gloo::timers::callback::Interval; 6 | use qrcode_generator::QrCodeEcc; 7 | use yew::prelude::*; 8 | 9 | pub enum Msg { 10 | StartInterval, 11 | Cancel, 12 | Tick, 13 | SetInput(String), 14 | } 15 | 16 | pub struct App { 17 | encoder: ur::Encoder<'static>, 18 | interval: Option, 19 | current_part: Option, 20 | input: String, 21 | } 22 | 23 | impl App { 24 | fn cancel(&mut self) { 25 | self.interval = None; 26 | self.current_part = None; 27 | self.encoder = ur::Encoder::bytes(b"placeholder", MAX_FRAGMENT_SIZE).unwrap(); 28 | self.input = String::new(); 29 | } 30 | } 31 | 32 | const MAX_FRAGMENT_SIZE: usize = 50; 33 | 34 | impl Component for App { 35 | type Message = Msg; 36 | type Properties = (); 37 | 38 | fn create(_ctx: &Context) -> Self { 39 | Self { 40 | encoder: ur::Encoder::bytes(b"placeholder", MAX_FRAGMENT_SIZE).unwrap(), 41 | interval: None, 42 | current_part: None, 43 | input: String::new(), 44 | } 45 | } 46 | 47 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 48 | match msg { 49 | Msg::StartInterval => { 50 | let handle = { 51 | let link = ctx.link().clone(); 52 | Interval::new(1000, move || link.send_message(Msg::Tick)) 53 | }; 54 | self.interval = Some(handle); 55 | true 56 | } 57 | Msg::Cancel => { 58 | self.cancel(); 59 | console::warn!("Canceled!"); 60 | true 61 | } 62 | Msg::Tick => { 63 | self.current_part = Some(self.encoder.next_part().unwrap()); 64 | true 65 | } 66 | Msg::SetInput(s) => { 67 | self.encoder = ur::Encoder::bytes(s.as_bytes(), MAX_FRAGMENT_SIZE).unwrap(); 68 | self.input = s; 69 | true 70 | } 71 | } 72 | } 73 | 74 | fn view(&self, ctx: &Context) -> Html { 75 | let has_job = self.interval.is_some(); 76 | let qrcode_rendered = self.current_part.as_ref().map_or_else( 77 | || html! {}, 78 | |part| { 79 | let qr = base64::prelude::BASE64_STANDARD 80 | .encode(qrcode_generator::to_png_to_vec(part, QrCodeEcc::Low, 1024).unwrap()); 81 | html! { 82 |
83 |
84 | 85 |
86 |
87 | } 88 | }, 89 | ); 90 | let part = self.current_part.as_ref().map_or_else( 91 | || { 92 | html! { 93 | <> 94 | } 95 | }, 96 | |part| { 97 | html! { 98 |
99 | { part.to_string() } 100 |
101 | } 102 | }, 103 | ); 104 | let on_change = ctx.link().callback(Msg::SetInput); 105 | html! { 106 | <> 107 |

{ "Uniform Resources Demo" }

108 |

{ "Enter the text you would like to transmit and click Start" }

109 |
110 | 111 |

112 |
113 |
114 | 117 | 120 |
121 | { qrcode_rendered } 122 |

123 | { part } 124 | 125 | } 126 | } 127 | } 128 | 129 | fn main() { 130 | yew::Renderer::::new().render(); 131 | } 132 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzz" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | honggfuzz = "0.5.56" 8 | ur = { path = ".." } 9 | 10 | [[bin]] 11 | name = "bytewords_decode" 12 | path = "fuzz_targets/bytewords_decode.rs" 13 | 14 | [[bin]] 15 | name = "bytewords_encode" 16 | path = "fuzz_targets/bytewords_encode.rs" 17 | 18 | [[bin]] 19 | name = "ur_encode" 20 | path = "fuzz_targets/ur_encode.rs" 21 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/bytewords_decode.rs: -------------------------------------------------------------------------------- 1 | use honggfuzz::fuzz; 2 | 3 | use ur::bytewords::{decode, Style}; 4 | 5 | fn main() { 6 | loop { 7 | fuzz!(|data: &str| { 8 | decode(data, Style::Minimal).ok(); 9 | decode(data, Style::Standard).ok(); 10 | decode(data, Style::Uri).ok(); 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/bytewords_encode.rs: -------------------------------------------------------------------------------- 1 | use honggfuzz::fuzz; 2 | 3 | fn main() { 4 | loop { 5 | fuzz!(|data: &[u8]| { 6 | for style in [ 7 | ur::bytewords::Style::Standard, 8 | ur::bytewords::Style::Uri, 9 | ur::bytewords::Style::Minimal, 10 | ] { 11 | let encoded = ur::bytewords::encode(data, style); 12 | let decoded = ur::bytewords::decode(&encoded, style).unwrap(); 13 | assert_eq!(data, decoded); 14 | } 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/ur_encode.rs: -------------------------------------------------------------------------------- 1 | use honggfuzz::fuzz; 2 | 3 | fn main() { 4 | loop { 5 | fuzz!(|data: &[u8]| { 6 | let max_length = 1 + *data.first().unwrap() as usize; 7 | let mut encoder = ur::Encoder::bytes(data, max_length).unwrap(); 8 | let mut decoder = ur::Decoder::default(); 9 | for _ in 0..encoder.fragment_count() { 10 | let part = encoder.next_part().unwrap(); 11 | decoder.receive(&part).unwrap(); 12 | } 13 | assert_eq!(decoder.message().unwrap().unwrap(), data); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/bytewords.rs: -------------------------------------------------------------------------------- 1 | //! Encode and decode byte payloads according to the [`bytewords`](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-012-bytewords.md) scheme. 2 | //! 3 | //! The [`bytewords`](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-012-bytewords.md) encoding 4 | //! scheme defines three styles how byte payloads can be encoded. 5 | //! 6 | //! # Standard style 7 | //! ``` 8 | //! use ur::bytewords::{Style, decode, encode}; 9 | //! let data = "Some bytes".as_bytes(); 10 | //! let encoded = encode(data, Style::Standard); 11 | //! assert_eq!( 12 | //! encoded, 13 | //! "guru jowl join inch crux iced kick jury inch junk taxi aqua kite limp" 14 | //! ); 15 | //! assert_eq!(data, decode(&encoded, Style::Standard).unwrap()); 16 | //! ``` 17 | //! 18 | //! # URI style 19 | //! ``` 20 | //! use ur::bytewords::{Style, decode, encode}; 21 | //! let data = "Some bytes".as_bytes(); 22 | //! let encoded = encode(data, Style::Uri); 23 | //! assert_eq!( 24 | //! encoded, 25 | //! "guru-jowl-join-inch-crux-iced-kick-jury-inch-junk-taxi-aqua-kite-limp" 26 | //! ); 27 | //! assert_eq!(data, decode(&encoded, Style::Uri).unwrap()); 28 | //! ``` 29 | //! 30 | //! # Minimal style 31 | //! ``` 32 | //! use ur::bytewords::{Style, decode, encode}; 33 | //! let data = "Some binary data".as_bytes(); 34 | //! let encoded = encode(data, Style::Minimal); 35 | //! assert_eq!(encoded, "gujljnihcxidinjthsjpkkcxiehsjyhsnsgdmkht"); 36 | //! assert_eq!(data, decode(&encoded, Style::Minimal).unwrap()); 37 | //! ``` 38 | 39 | extern crate alloc; 40 | use alloc::vec::Vec; 41 | 42 | /// The three different `bytewords` encoding styles. See the [`encode`] documentation for examples. 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 44 | pub enum Style { 45 | /// Four-letter words, separated by spaces 46 | Standard, 47 | /// Four-letter words, separated by dashes 48 | Uri, 49 | /// Two-letter words, concatenated without separators 50 | Minimal, 51 | } 52 | 53 | /// The two different errors that can be returned when decoding. 54 | #[derive(Debug, PartialEq, Eq)] 55 | pub enum Error { 56 | /// Usually indicates a wrong encoding [`Style`] was passed. 57 | InvalidWord, 58 | /// The CRC32 checksum doesn't validate. 59 | InvalidChecksum, 60 | /// Invalid bytewords string length. 61 | InvalidLength, 62 | /// The bytewords string contains non-ASCII characters. 63 | NonAscii, 64 | } 65 | 66 | impl core::fmt::Display for Error { 67 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 68 | match self { 69 | Self::InvalidWord => write!(f, "invalid word"), 70 | Self::InvalidChecksum => write!(f, "invalid checksum"), 71 | Self::InvalidLength => write!(f, "invalid length"), 72 | Self::NonAscii => write!(f, "bytewords string contains non-ASCII characters"), 73 | } 74 | } 75 | } 76 | 77 | #[cfg(feature = "std")] 78 | impl std::error::Error for Error {} 79 | 80 | /// Decodes a `bytewords`-encoded String back into a byte payload. The encoding 81 | /// must contain a four-byte checksum. 82 | /// 83 | /// # Examples 84 | /// 85 | /// ``` 86 | /// use ur::bytewords::{Style, decode}; 87 | /// assert_eq!( 88 | /// decode("able tied also webs lung", Style::Standard).unwrap(), 89 | /// vec![0] 90 | /// ); 91 | /// assert_eq!( 92 | /// decode("able-tied-also-webs-lung", Style::Uri).unwrap(), 93 | /// vec![0] 94 | /// ); 95 | /// // Notice how the minimal encoding consists of the start and end letters of the bytewords 96 | /// assert_eq!(decode("aetdaowslg", Style::Minimal).unwrap(), vec![0]); 97 | /// ``` 98 | /// 99 | /// # Errors 100 | /// 101 | /// If the encoded string contains unrecognized words, is inconsistent with 102 | /// the provided `style`, or contains an invalid checksum, an error will be 103 | /// returned. 104 | pub fn decode(encoded: &str, style: Style) -> Result, Error> { 105 | if !encoded.is_ascii() { 106 | return Err(Error::NonAscii); 107 | } 108 | 109 | let separator = match style { 110 | Style::Standard => ' ', 111 | Style::Uri => '-', 112 | Style::Minimal => return decode_minimal(encoded), 113 | }; 114 | decode_parts(&mut encoded.split(separator)) 115 | } 116 | 117 | fn decode_minimal(encoded: &str) -> Result, Error> { 118 | if encoded.len() % 2 != 0 { 119 | return Err(Error::InvalidLength); 120 | } 121 | 122 | decode_parts( 123 | &mut (0..encoded.len()) 124 | .step_by(2) 125 | .map(|idx| encoded.get(idx..idx + 2).unwrap()), 126 | ) 127 | } 128 | 129 | fn encoded_byte(str: &str) -> Option { 130 | let mut chars = str.chars(); 131 | let hash = 132 | usize::try_from((25 * (chars.next()? as u32) + 11 * chars.last()? as u32) % 628).ok()?; 133 | crate::constants::BYTES_INDEXED_BY_HASH[hash] 134 | } 135 | 136 | #[allow(clippy::too_many_lines)] 137 | fn decode_parts(parts: &mut dyn Iterator) -> Result, Error> { 138 | strip_checksum( 139 | parts 140 | .map(encoded_byte) 141 | .collect::>>() 142 | .ok_or(Error::InvalidWord)?, 143 | ) 144 | } 145 | 146 | fn strip_checksum(mut data: Vec) -> Result, Error> { 147 | if data.len() < 4 { 148 | return Err(Error::InvalidChecksum); 149 | } 150 | let (payload, checksum) = data.split_at(data.len() - 4); 151 | if crate::crc32().checksum(payload).to_be_bytes() == checksum { 152 | data.truncate(data.len() - 4); 153 | Ok(data) 154 | } else { 155 | Err(Error::InvalidChecksum) 156 | } 157 | } 158 | 159 | /// Encodes a byte payload into a `bytewords` encoded String. 160 | /// 161 | /// # Examples 162 | /// 163 | /// ``` 164 | /// use ur::bytewords::{Style, encode}; 165 | /// assert_eq!(encode(&[0], Style::Standard), "able tied also webs lung"); 166 | /// assert_eq!(encode(&[0], Style::Uri), "able-tied-also-webs-lung"); 167 | /// // Notice how the minimal encoding consists of the start and end letters of the bytewords 168 | /// assert_eq!(encode(&[0], Style::Minimal), "aetdaowslg"); 169 | /// ``` 170 | #[must_use] 171 | pub fn encode(data: &[u8], style: Style) -> alloc::string::String { 172 | let checksum = crate::crc32().checksum(data).to_be_bytes(); 173 | let data = data.iter().chain(checksum.iter()); 174 | let words: Vec<&str> = match style { 175 | Style::Standard | Style::Uri => data 176 | .map(|&b| crate::constants::WORDS.get(b as usize).copied().unwrap()) 177 | .collect(), 178 | Style::Minimal => data 179 | .map(|&b| crate::constants::MINIMALS.get(b as usize).copied().unwrap()) 180 | .collect(), 181 | }; 182 | let separator = match style { 183 | Style::Standard => " ", 184 | Style::Uri => "-", 185 | Style::Minimal => "", 186 | }; 187 | words.join(separator) 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | 194 | #[test] 195 | fn test_crc() { 196 | assert_eq!(crate::crc32().checksum(b"Hello, world!"), 0xebe6_c6e6); 197 | assert_eq!(crate::crc32().checksum(b"Wolf"), 0x598c_84dc); 198 | } 199 | 200 | #[test] 201 | fn test_bytewords() { 202 | let input = vec![0, 1, 2, 128, 255]; 203 | assert_eq!( 204 | encode(&input, Style::Standard), 205 | "able acid also lava zoom jade need echo taxi" 206 | ); 207 | assert_eq!( 208 | encode(&input, Style::Uri), 209 | "able-acid-also-lava-zoom-jade-need-echo-taxi" 210 | ); 211 | assert_eq!(encode(&input, Style::Minimal), "aeadaolazmjendeoti"); 212 | 213 | assert_eq!( 214 | decode( 215 | "able acid also lava zoom jade need echo taxi", 216 | Style::Standard 217 | ) 218 | .unwrap(), 219 | input 220 | ); 221 | assert_eq!( 222 | decode("able-acid-also-lava-zoom-jade-need-echo-taxi", Style::Uri).unwrap(), 223 | input 224 | ); 225 | assert_eq!(decode("aeadaolazmjendeoti", Style::Minimal).unwrap(), input); 226 | 227 | // empty payload is allowed 228 | decode(&encode(&[], Style::Minimal), Style::Minimal).unwrap(); 229 | 230 | // bad checksum 231 | assert_eq!( 232 | decode( 233 | "able acid also lava zero jade need echo wolf", 234 | Style::Standard 235 | ) 236 | .unwrap_err(), 237 | Error::InvalidChecksum 238 | ); 239 | assert_eq!( 240 | decode("able-acid-also-lava-zero-jade-need-echo-wolf", Style::Uri).unwrap_err(), 241 | Error::InvalidChecksum 242 | ); 243 | assert_eq!( 244 | decode("aeadaolazojendeowf", Style::Minimal).unwrap_err(), 245 | Error::InvalidChecksum 246 | ); 247 | 248 | // too short 249 | assert_eq!( 250 | decode("wolf", Style::Standard).unwrap_err(), 251 | Error::InvalidChecksum 252 | ); 253 | assert_eq!(decode("", Style::Standard).unwrap_err(), Error::InvalidWord); 254 | 255 | // invalid length 256 | assert_eq!( 257 | decode("aea", Style::Minimal).unwrap_err(), 258 | Error::InvalidLength 259 | ); 260 | 261 | // non ASCII 262 | assert_eq!(decode("₿", Style::Standard).unwrap_err(), Error::NonAscii); 263 | assert_eq!(decode("₿", Style::Uri).unwrap_err(), Error::NonAscii); 264 | assert_eq!(decode("₿", Style::Minimal).unwrap_err(), Error::NonAscii); 265 | } 266 | 267 | #[test] 268 | fn test_encoding() { 269 | let input: [u8; 100] = [ 270 | 245, 215, 20, 198, 241, 235, 69, 59, 209, 205, 165, 18, 150, 158, 116, 135, 229, 212, 271 | 19, 159, 17, 37, 239, 240, 253, 11, 109, 191, 37, 242, 38, 120, 223, 41, 156, 189, 242, 272 | 254, 147, 204, 66, 163, 216, 175, 191, 72, 169, 54, 32, 60, 144, 230, 210, 137, 184, 273 | 197, 33, 113, 88, 14, 157, 31, 177, 46, 1, 115, 205, 69, 225, 150, 65, 235, 58, 144, 274 | 65, 240, 133, 69, 113, 247, 63, 53, 242, 165, 160, 144, 26, 13, 79, 237, 133, 71, 82, 275 | 69, 254, 165, 138, 41, 85, 24, 276 | ]; 277 | 278 | let encoded = "yank toys bulb skew when warm free fair tent swan \ 279 | open brag mint noon jury list view tiny brew note \ 280 | body data webs what zinc bald join runs data whiz \ 281 | days keys user diet news ruby whiz zone menu surf \ 282 | flew omit trip pose runs fund part even crux fern \ 283 | math visa tied loud redo silk curl jugs hard beta \ 284 | next cost puma drum acid junk swan free very mint \ 285 | flap warm fact math flap what limp free jugs yell \ 286 | fish epic whiz open numb math city belt glow wave \ 287 | limp fuel grim free zone open love diet gyro cats \ 288 | fizz holy city puff"; 289 | 290 | let encoded_minimal = "yktsbbswwnwmfefrttsnonbgmtnnjyltvwtybwne\ 291 | bydawswtzcbdjnrsdawzdsksurdtnsrywzzemusf\ 292 | fwottppersfdptencxfnmhvatdldroskcljshdba\ 293 | ntctpadmadjksnfevymtfpwmftmhfpwtlpfejsyl\ 294 | fhecwzonnbmhcybtgwwelpflgmfezeonledtgocs\ 295 | fzhycypf"; 296 | 297 | assert_eq!(decode(encoded, Style::Standard).unwrap(), input.to_vec()); 298 | assert_eq!( 299 | decode(encoded_minimal, Style::Minimal).unwrap(), 300 | input.to_vec() 301 | ); 302 | assert_eq!(encode(&input, Style::Standard), encoded); 303 | assert_eq!(encode(&input, Style::Minimal), encoded_minimal); 304 | } 305 | 306 | #[test] 307 | fn test_error_formatting() { 308 | assert_eq!(super::Error::InvalidWord.to_string(), "invalid word"); 309 | assert_eq!( 310 | super::Error::InvalidChecksum.to_string(), 311 | "invalid checksum" 312 | ); 313 | assert_eq!(super::Error::InvalidLength.to_string(), "invalid length"); 314 | assert_eq!( 315 | super::Error::NonAscii.to_string(), 316 | "bytewords string contains non-ASCII characters" 317 | ); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | pub const WORDS: [&str; 256] = [ 3 | "able", "acid", "also", "apex", "aqua", "arch", "atom", "aunt", 4 | "away", "axis", "back", "bald", "barn", "belt", "beta", "bias", 5 | "blue", "body", "brag", "brew", "bulb", "buzz", "calm", "cash", 6 | "cats", "chef", "city", "claw", "code", "cola", "cook", "cost", 7 | "crux", "curl", "cusp", "cyan", "dark", "data", "days", "deli", 8 | "dice", "diet", "door", "down", "draw", "drop", "drum", "dull", 9 | "duty", "each", "easy", "echo", "edge", "epic", "even", "exam", 10 | "exit", "eyes", "fact", "fair", "fern", "figs", "film", "fish", 11 | "fizz", "flap", "flew", "flux", "foxy", "free", "frog", "fuel", 12 | "fund", "gala", "game", "gear", "gems", "gift", "girl", "glow", 13 | "good", "gray", "grim", "guru", "gush", "gyro", "half", "hang", 14 | "hard", "hawk", "heat", "help", "high", "hill", "holy", "hope", 15 | "horn", "huts", "iced", "idea", "idle", "inch", "inky", "into", 16 | "iris", "iron", "item", "jade", "jazz", "join", "jolt", "jowl", 17 | "judo", "jugs", "jump", "junk", "jury", "keep", "keno", "kept", 18 | "keys", "kick", "kiln", "king", "kite", "kiwi", "knob", "lamb", 19 | "lava", "lazy", "leaf", "legs", "liar", "limp", "lion", "list", 20 | "logo", "loud", "love", "luau", "luck", "lung", "main", "many", 21 | "math", "maze", "memo", "menu", "meow", "mild", "mint", "miss", 22 | "monk", "nail", "navy", "need", "news", "next", "noon", "note", 23 | "numb", "obey", "oboe", "omit", "onyx", "open", "oval", "owls", 24 | "paid", "part", "peck", "play", "plus", "poem", "pool", "pose", 25 | "puff", "puma", "purr", "quad", "quiz", "race", "ramp", "real", 26 | "redo", "rich", "road", "rock", "roof", "ruby", "ruin", "runs", 27 | "rust", "safe", "saga", "scar", "sets", "silk", "skew", "slot", 28 | "soap", "solo", "song", "stub", "surf", "swan", "taco", "task", 29 | "taxi", "tent", "tied", "time", "tiny", "toil", "tomb", "toys", 30 | "trip", "tuna", "twin", "ugly", "undo", "unit", "urge", "user", 31 | "vast", "very", "veto", "vial", "vibe", "view", "visa", "void", 32 | "vows", "wall", "wand", "warm", "wasp", "wave", "waxy", "webs", 33 | "what", "when", "whiz", "wolf", "work", "yank", "yawn", "yell", 34 | "yoga", "yurt", "zaps", "zero", "zest", "zinc", "zone", "zoom", 35 | ]; 36 | 37 | #[rustfmt::skip] 38 | pub const MINIMALS: [&str; 256] = [ 39 | "ae", "ad", "ao", "ax", "aa", "ah", "am", "at", 40 | "ay", "as", "bk", "bd", "bn", "bt", "ba", "bs", 41 | "be", "by", "bg", "bw", "bb", "bz", "cm", "ch", 42 | "cs", "cf", "cy", "cw", "ce", "ca", "ck", "ct", 43 | "cx", "cl", "cp", "cn", "dk", "da", "ds", "di", 44 | "de", "dt", "dr", "dn", "dw", "dp", "dm", "dl", 45 | "dy", "eh", "ey", "eo", "ee", "ec", "en", "em", 46 | "et", "es", "ft", "fr", "fn", "fs", "fm", "fh", 47 | "fz", "fp", "fw", "fx", "fy", "fe", "fg", "fl", 48 | "fd", "ga", "ge", "gr", "gs", "gt", "gl", "gw", 49 | "gd", "gy", "gm", "gu", "gh", "go", "hf", "hg", 50 | "hd", "hk", "ht", "hp", "hh", "hl", "hy", "he", 51 | "hn", "hs", "id", "ia", "ie", "ih", "iy", "io", 52 | "is", "in", "im", "je", "jz", "jn", "jt", "jl", 53 | "jo", "js", "jp", "jk", "jy", "kp", "ko", "kt", 54 | "ks", "kk", "kn", "kg", "ke", "ki", "kb", "lb", 55 | "la", "ly", "lf", "ls", "lr", "lp", "ln", "lt", 56 | "lo", "ld", "le", "lu", "lk", "lg", "mn", "my", 57 | "mh", "me", "mo", "mu", "mw", "md", "mt", "ms", 58 | "mk", "nl", "ny", "nd", "ns", "nt", "nn", "ne", 59 | "nb", "oy", "oe", "ot", "ox", "on", "ol", "os", 60 | "pd", "pt", "pk", "py", "ps", "pm", "pl", "pe", 61 | "pf", "pa", "pr", "qd", "qz", "re", "rp", "rl", 62 | "ro", "rh", "rd", "rk", "rf", "ry", "rn", "rs", 63 | "rt", "se", "sa", "sr", "ss", "sk", "sw", "st", 64 | "sp", "so", "sg", "sb", "sf", "sn", "to", "tk", 65 | "ti", "tt", "td", "te", "ty", "tl", "tb", "ts", 66 | "tp", "ta", "tn", "uy", "uo", "ut", "ue", "ur", 67 | "vt", "vy", "vo", "vl", "ve", "vw", "va", "vd", 68 | "vs", "wl", "wd", "wm", "wp", "we", "wy", "ws", 69 | "wt", "wn", "wz", "wf", "wk", "yk", "yn", "yl", 70 | "ya", "yt", "zs", "zo", "zt", "zc", "ze", "zm", 71 | ]; 72 | 73 | #[rustfmt::skip] 74 | pub const BYTES_INDEXED_BY_HASH: [Option; 628] = [ 75 | None, Some(101), None, None, None, None, Some(82), None, Some(41), 76 | Some(89), Some(127), None, None, Some(17), Some(65), None, Some(27), 77 | None, Some(124), None, Some(93), None, Some(57), None, Some(21), None, 78 | None, Some(32), Some(85), None, None, None, Some(137), Some(56), None, 79 | None, Some(59), None, Some(26), None, Some(123), Some(44), Some(96), 80 | Some(138), None, None, None, Some(61), None, None, None, None, None, 81 | None, Some(130), None, Some(106), Some(149), Some(58), Some(115), 82 | Some(160), Some(75), Some(125), Some(48), Some(91), Some(141), None, 83 | Some(105), Some(145), None, Some(111), None, Some(76), None, None, 84 | None, None, None, Some(103), None, None, None, Some(155), Some(77), 85 | Some(121), None, None, None, Some(50), None, None, Some(66), 86 | Some(109), Some(159), Some(83), None, None, Some(97), None, 87 | Some(177), None, Some(144), Some(67), Some(112), None, None, None, 88 | None, Some(90), Some(140), None, None, None, Some(68), Some(114), 89 | None, Some(79), Some(122), Some(162), None, None, None, Some(104), 90 | None, Some(64), None, None, None, Some(118), None, None, None, 91 | Some(168), None, Some(152), None, None, None, Some(81), Some(117), 92 | None, None, Some(134), Some(175), None, None, None, Some(113), None, 93 | None, None, None, None, Some(136), Some(176), None, None, Some(179), 94 | Some(110), None, None, None, None, Some(94), Some(133), None, None, 95 | Some(142), None, None, Some(153), None, Some(120), None, Some(194), 96 | None, None, None, Some(146), None, None, None, Some(186), 97 | Some(119), None, Some(203), Some(132), None, Some(102), None, None, 98 | None, Some(158), Some(181), None, Some(166), None, Some(131), None, 99 | Some(217), None, None, None, None, Some(188), None, None, None, 100 | Some(135), Some(170), Some(214), None, None, Some(116), None, None, 101 | None, Some(165), Some(193), Some(139), Some(174), None, Some(151), 102 | None, Some(108), None, Some(185), None, None, Some(204), None, 103 | Some(173), Some(210), Some(150), None, None, None, None, None, None, 104 | Some(202), None, None, Some(211), Some(147), None, None, Some(156), 105 | None, Some(230), None, None, None, None, None, None, None, None, 106 | Some(157), Some(187), None, None, None, Some(129), None, None, 107 | Some(148), None, Some(222), None, Some(183), None, Some(167), None, 108 | None, None, None, None, None, None, None, None, Some(231), 109 | Some(163), Some(197), None, Some(178), Some(208), Some(143), None, 110 | None, None, Some(190), Some(228), None, None, None, Some(172), None, 111 | None, None, None, None, Some(184), None, None, None, Some(234), 112 | Some(169), Some(207), None, None, None, Some(154), Some(182), None, 113 | None, Some(205), Some(237), None, Some(213), None, None, None, 114 | Some(248), None, None, Some(164), Some(201), Some(243), None, None, 115 | None, None, None, None, None, None, Some(161), Some(200), None, 116 | None, Some(218), None, None, None, None, Some(191), None, None, 117 | None, None, Some(4), Some(206), None, None, None, None, Some(192), 118 | None, None, Some(195), None, Some(171), Some(216), None, None, 119 | None, None, None, Some(227), Some(253), Some(196), None, None, 120 | None, None, Some(14), Some(220), None, None, None, None, Some(199), 121 | Some(244), Some(1), None, None, Some(20), None, None, None, None, 122 | Some(254), None, Some(233), Some(0), Some(215), None, Some(180), 123 | None, None, Some(29), Some(226), None, None, Some(235), None, 124 | Some(209), None, Some(11), Some(223), None, Some(189), None, None, 125 | Some(198), Some(241), None, None, None, Some(16), None, None, None, 126 | None, None, Some(37), None, Some(5), None, None, None, Some(221), 127 | Some(245), None, None, None, None, Some(236), None, None, None, 128 | Some(18), None, Some(247), Some(28), Some(232), None, None, None, 129 | None, None, None, None, None, None, Some(25), Some(224), None, 130 | None, None, None, Some(212), None, None, None, Some(246), None, 131 | None, None, Some(40), Some(239), None, Some(53), None, None, None, 132 | None, Some(23), None, Some(255), None, Some(240), Some(6), None, 133 | None, Some(10), Some(219), None, None, Some(229), None, None, None, 134 | None, Some(52), None, None, None, None, None, Some(73), Some(251), 135 | None, None, Some(2), None, None, None, Some(72), None, Some(30), 136 | Some(225), None, Some(39), None, None, None, None, Some(12), 137 | Some(69), None, Some(33), None, None, None, None, None, Some(49), 138 | None, None, None, Some(249), Some(22), Some(80), None, Some(36), 139 | Some(238), None, None, None, None, Some(70), None, Some(35), 140 | Some(74), Some(250), Some(47), Some(242), Some(9), None, Some(99), 141 | None, Some(63), None, None, None, Some(252), Some(46), Some(88), 142 | Some(7), None, None, None, None, None, Some(34), None, None, 143 | Some(43), Some(95), None, None, None, Some(15), None, None, None, 144 | Some(84), None, None, Some(86), None, Some(55), Some(98), Some(13), 145 | None, None, None, None, None, Some(45), Some(87), None, Some(54), 146 | Some(100), None, Some(71), None, Some(24), None, None, None, 147 | Some(92), Some(3), Some(51), None, None, Some(62), None, Some(31), 148 | None, Some(126), Some(42), None, Some(8), None, None, Some(19), 149 | Some(60), Some(107), None, Some(78), None, Some(38), None, 150 | Some(128), 151 | ]; 152 | -------------------------------------------------------------------------------- /src/fountain.rs: -------------------------------------------------------------------------------- 1 | //! Split up big payloads into constantly sized chunks which can be recombined by a decoder. 2 | //! 3 | //! The `fountain` module provides an implementation of a fountain encoder, which splits 4 | //! up a byte payload into multiple segments and emits an unbounded stream of parts which 5 | //! can be recombined at the receiving decoder site. The emitted parts are either original 6 | //! payload segments, or constructed by xor-ing a certain set of payload segments. 7 | //! 8 | //! A seeded `Xoshiro` RNG ensures that the receiver can reconstruct which segments 9 | //! were combined into the part. 10 | //! ``` 11 | //! let xor = 12 | //! |a: &[u8], b: &[u8]| -> Vec<_> { a.iter().zip(b.iter()).map(|(x1, x2)| x1 ^ x2).collect() }; 13 | //! 14 | //! let data = String::from("Ten chars!"); 15 | //! let max_length = 4; 16 | //! // note the padding 17 | //! let (p1, p2, p3) = (b"Ten ", b"char", "s!\u{0}\u{0}".as_bytes()); 18 | //! 19 | //! let mut encoder = ur::fountain::Encoder::new(data.as_bytes(), max_length).unwrap(); 20 | //! let mut decoder = ur::fountain::Decoder::default(); 21 | //! 22 | //! // the fountain encoder first emits all original segments in order 23 | //! let part1 = encoder.next_part(); 24 | //! assert_eq!(part1.data(), p1); 25 | //! // receive the first part into the decoder 26 | //! decoder.receive(part1).unwrap(); 27 | //! 28 | //! let part2 = encoder.next_part(); 29 | //! assert_eq!(part2.data(), p2); 30 | //! // receive the second part into the decoder 31 | //! decoder.receive(part2).unwrap(); 32 | //! 33 | //! // miss the third part 34 | //! assert_eq!(encoder.next_part().data(), p3); 35 | //! 36 | //! // the RNG then first selects the original third segment 37 | //! assert_eq!(encoder.next_part().data(), p3); 38 | //! 39 | //! // the RNG then selects all three segments to be xored 40 | //! let xored = encoder.next_part(); 41 | //! assert_eq!(xored.data(), xor(&xor(p1, p2), p3)); 42 | //! // receive the xored part into the decoder 43 | //! // since it already has p1 and p2, p3 can be computed 44 | //! // from p1 xor p2 xor p3 45 | //! decoder.receive(xored).unwrap(); 46 | //! assert!(decoder.complete()); 47 | //! assert_eq!(decoder.message().unwrap().as_deref(), Some(data.as_bytes())); 48 | //! ``` 49 | //! 50 | //! The index selection is biased towards combining fewer segments. 51 | //! 52 | //! ``` 53 | //! let data = String::from("Fifty chars").repeat(5); 54 | //! let max_length = 5; 55 | //! let mut encoder = ur::fountain::Encoder::new(data.as_bytes(), max_length).unwrap(); 56 | //! // 40% of the emitted parts represent original message segments 57 | //! assert_eq!( 58 | //! (0..100) 59 | //! .map(|_i| if encoder.next_part().is_simple() { 60 | //! 1 61 | //! } else { 62 | //! 0 63 | //! }) 64 | //! .sum::(), 65 | //! 39 66 | //! ); 67 | //! let mut encoder = ur::fountain::Encoder::new(data.as_bytes(), max_length).unwrap(); 68 | //! // On average, 3.33 segments (out of ten total) are combined into a part 69 | //! assert_eq!( 70 | //! (0..100) 71 | //! .map(|_i| encoder.next_part().indexes().len()) 72 | //! .sum::(), 73 | //! 333 74 | //! ); 75 | //! ``` 76 | 77 | extern crate alloc; 78 | use alloc::vec::Vec; 79 | use core::convert::Infallible; 80 | 81 | /// Errors that can happen during fountain encoding and decoding. 82 | #[derive(Debug)] 83 | pub enum Error { 84 | /// CBOR decoding error. 85 | CborDecode(minicbor::decode::Error), 86 | /// CBOR encoding error. 87 | CborEncode(minicbor::encode::Error), 88 | /// Expected non-empty message. 89 | EmptyMessage, 90 | /// Expected non-empty part. 91 | EmptyPart, 92 | /// Fragment length should be a positive integer greater than 0. 93 | InvalidFragmentLen, 94 | /// Received part is inconsistent with previous ones. 95 | InconsistentPart, 96 | /// An item was expected. 97 | ExpectedItem, 98 | /// Invalid padding detected. 99 | InvalidPadding, 100 | } 101 | 102 | impl core::fmt::Display for Error { 103 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 104 | match self { 105 | Self::CborDecode(e) => write!(f, "minicbor decoding error: {e}"), 106 | Self::CborEncode(e) => write!(f, "minicbor encoding error: {e}"), 107 | Self::EmptyMessage => write!(f, "expected non-empty message"), 108 | Self::EmptyPart => write!(f, "expected non-empty part"), 109 | Self::InvalidFragmentLen => write!(f, "expected positive maximum fragment length"), 110 | Self::InconsistentPart => write!(f, "part is inconsistent with previous ones"), 111 | Self::ExpectedItem => write!(f, "expected item"), 112 | Self::InvalidPadding => write!(f, "invalid padding"), 113 | } 114 | } 115 | } 116 | 117 | impl From for Error { 118 | fn from(e: minicbor::decode::Error) -> Self { 119 | Self::CborDecode(e) 120 | } 121 | } 122 | 123 | impl From> for Error { 124 | fn from(e: minicbor::encode::Error) -> Self { 125 | Self::CborEncode(e) 126 | } 127 | } 128 | 129 | /// An encoder capable of emitting fountain-encoded transmissions. 130 | /// 131 | /// # Examples 132 | /// 133 | /// See the [`crate::fountain`] module documentation for an example. 134 | #[derive(Debug)] 135 | pub struct Encoder { 136 | parts: Vec>, 137 | message_length: usize, 138 | checksum: u32, 139 | current_sequence: usize, 140 | } 141 | 142 | impl Encoder { 143 | /// Constructs a new [`Encoder`], given a message and a maximum fragment length. 144 | /// 145 | /// # Examples 146 | /// 147 | /// ``` 148 | /// use ur::fountain::Encoder; 149 | /// let encoder = Encoder::new(b"binary data", 4).unwrap(); 150 | /// ``` 151 | /// 152 | /// Note that the effective fragment size will not always equal the maximum 153 | /// fragment size: 154 | /// 155 | /// ``` 156 | /// use ur::fountain::Encoder; 157 | /// let mut encoder = Encoder::new(b"data", 3).unwrap(); 158 | /// assert_eq!(encoder.next_part().data().len(), 2); 159 | /// ``` 160 | /// 161 | /// # Errors 162 | /// 163 | /// If an empty message or a zero maximum fragment length is passed, an error 164 | /// will be returned. 165 | pub fn new(message: &[u8], max_fragment_length: usize) -> Result { 166 | if message.is_empty() { 167 | return Err(Error::EmptyMessage); 168 | } 169 | if max_fragment_length == 0 { 170 | return Err(Error::InvalidFragmentLen); 171 | } 172 | let fragment_length = fragment_length(message.len(), max_fragment_length); 173 | let fragments = partition(message.to_vec(), fragment_length); 174 | Ok(Self { 175 | parts: fragments, 176 | message_length: message.len(), 177 | checksum: crate::crc32().checksum(message), 178 | current_sequence: 0, 179 | }) 180 | } 181 | 182 | /// Returns the current count of how many parts have been emitted. 183 | /// 184 | /// # Examples 185 | /// 186 | /// ``` 187 | /// use ur::fountain::Encoder; 188 | /// let mut encoder = Encoder::new(b"data", 3).unwrap(); 189 | /// assert_eq!(encoder.current_sequence(), 0); 190 | /// encoder.next_part(); 191 | /// assert_eq!(encoder.current_sequence(), 1); 192 | /// ``` 193 | #[must_use] 194 | pub const fn current_sequence(&self) -> usize { 195 | self.current_sequence 196 | } 197 | 198 | /// Returns the next part to be emitted by the fountain encoder. 199 | /// After all parts of the original message have been emitted once, 200 | /// the fountain encoder will emit the result of xoring together the parts 201 | /// selected by the Xoshiro RNG (which could be a single part). 202 | /// 203 | /// # Examples 204 | /// 205 | /// See the [`crate::fountain`] module documentation for an example. 206 | pub fn next_part(&mut self) -> Part { 207 | self.current_sequence += 1; 208 | let indexes = choose_fragments(self.current_sequence, self.parts.len(), self.checksum); 209 | 210 | let mut mixed = alloc::vec![0; self.parts[0].len()]; 211 | for item in indexes { 212 | xor(&mut mixed, &self.parts[item]); 213 | } 214 | 215 | Part { 216 | sequence: self.current_sequence, 217 | sequence_count: self.parts.len(), 218 | message_length: self.message_length, 219 | checksum: self.checksum, 220 | data: mixed, 221 | } 222 | } 223 | 224 | /// Returns the number of segments the original message has been split up into. 225 | /// 226 | /// # Examples 227 | /// 228 | /// ``` 229 | /// use ur::fountain::Encoder; 230 | /// let mut encoder = Encoder::new(b"data", 3).unwrap(); 231 | /// assert_eq!(encoder.fragment_count(), 2); 232 | /// ``` 233 | #[must_use] 234 | pub fn fragment_count(&self) -> usize { 235 | self.parts.len() 236 | } 237 | 238 | /// Returns whether all original segments have been emitted at least once. 239 | /// The fountain encoding is defined as doing this before combining segments 240 | /// with each other. Thus, this is equivalent to checking whether 241 | /// [`current_sequence`] >= [`fragment_count`]. 242 | /// 243 | /// # Examples 244 | /// 245 | /// ``` 246 | /// use ur::fountain::Encoder; 247 | /// let mut encoder = Encoder::new(&b"data".repeat(10), 3).unwrap(); 248 | /// while !encoder.complete() { 249 | /// assert!(encoder.current_sequence() < encoder.fragment_count()); 250 | /// encoder.next_part(); 251 | /// } 252 | /// assert_eq!(encoder.current_sequence(), encoder.fragment_count()); 253 | /// ``` 254 | /// 255 | /// [`fragment_count`]: Encoder::fragment_count 256 | /// [`current_sequence`]: Encoder::current_sequence 257 | #[must_use] 258 | pub fn complete(&self) -> bool { 259 | self.current_sequence >= self.parts.len() 260 | } 261 | } 262 | 263 | /// A decoder capable of receiving and recombining fountain-encoded transmissions. 264 | /// 265 | /// # Examples 266 | /// 267 | /// See the [`crate::fountain`] module documentation for an example. 268 | #[derive(Default)] 269 | pub struct Decoder { 270 | decoded: alloc::collections::btree_map::BTreeMap, 271 | received: alloc::collections::btree_set::BTreeSet>, 272 | buffer: alloc::collections::btree_map::BTreeMap, Part>, 273 | queue: Vec<(usize, Part)>, 274 | sequence_count: usize, 275 | message_length: usize, 276 | checksum: u32, 277 | fragment_length: usize, 278 | } 279 | 280 | impl Decoder { 281 | /// Receives a fountain-encoded part into the decoder. 282 | /// 283 | /// # Examples 284 | /// 285 | /// See the [`crate::fountain`] module documentation for an example. 286 | /// 287 | /// # Errors 288 | /// 289 | /// If the part would fail [`validate`] because it is inconsistent 290 | /// with previously received parts, an error will be returned. 291 | /// 292 | /// [`validate`]: Decoder::validate 293 | pub fn receive(&mut self, part: Part) -> Result { 294 | if self.complete() { 295 | return Ok(false); 296 | } 297 | 298 | // Only receive parts that will yield data. 299 | if part.sequence_count == 0 || part.data.is_empty() || part.message_length == 0 { 300 | return Err(Error::EmptyPart); 301 | } 302 | 303 | if self.received.is_empty() { 304 | self.sequence_count = part.sequence_count; 305 | self.message_length = part.message_length; 306 | self.checksum = part.checksum; 307 | self.fragment_length = part.data.len(); 308 | } else if !self.validate(&part) { 309 | return Err(Error::InconsistentPart); 310 | } 311 | let indexes = part.indexes(); 312 | if self.received.contains(&indexes) { 313 | return Ok(false); 314 | } 315 | self.received.insert(indexes); 316 | if part.is_simple() { 317 | self.process_simple(part)?; 318 | } else { 319 | self.process_complex(part)?; 320 | } 321 | Ok(true) 322 | } 323 | 324 | fn process_simple(&mut self, part: Part) -> Result<(), Error> { 325 | let index = *part.indexes().first().ok_or(Error::ExpectedItem)?; 326 | self.decoded.insert(index, part.clone()); 327 | self.queue.push((index, part)); 328 | self.process_queue()?; 329 | Ok(()) 330 | } 331 | 332 | fn process_queue(&mut self) -> Result<(), Error> { 333 | while !self.queue.is_empty() { 334 | let (index, simple) = self.queue.pop().ok_or(Error::ExpectedItem)?; 335 | let to_process: Vec> = self 336 | .buffer 337 | .keys() 338 | .filter(|&idxs| idxs.contains(&index)) 339 | .cloned() 340 | .collect(); 341 | for indexes in to_process { 342 | let mut part = self.buffer.remove(&indexes).ok_or(Error::ExpectedItem)?; 343 | let mut new_indexes = indexes.clone(); 344 | let to_remove = indexes 345 | .iter() 346 | .position(|&x| x == index) 347 | .ok_or(Error::ExpectedItem)?; 348 | new_indexes.remove(to_remove); 349 | xor(&mut part.data, &simple.data); 350 | if new_indexes.len() == 1 { 351 | self.decoded 352 | .insert(*new_indexes.first().unwrap(), part.clone()); 353 | self.queue.push((*new_indexes.first().unwrap(), part)); 354 | } else { 355 | self.buffer.insert(new_indexes, part); 356 | } 357 | } 358 | } 359 | Ok(()) 360 | } 361 | 362 | fn process_complex(&mut self, mut part: Part) -> Result<(), Error> { 363 | let mut indexes = part.indexes(); 364 | let to_remove: Vec = indexes 365 | .clone() 366 | .into_iter() 367 | .filter(|idx| self.decoded.keys().any(|k| k == idx)) 368 | .collect(); 369 | if indexes.len() == to_remove.len() { 370 | return Ok(()); 371 | } 372 | for remove in to_remove { 373 | let idx_to_remove = indexes 374 | .iter() 375 | .position(|&x| x == remove) 376 | .ok_or(Error::ExpectedItem)?; 377 | indexes.remove(idx_to_remove); 378 | xor( 379 | &mut part.data, 380 | &self.decoded.get(&remove).ok_or(Error::ExpectedItem)?.data, 381 | ); 382 | } 383 | if indexes.len() == 1 { 384 | self.decoded.insert(*indexes.first().unwrap(), part.clone()); 385 | self.queue.push((*indexes.first().unwrap(), part)); 386 | } else { 387 | self.buffer.insert(indexes, part); 388 | } 389 | Ok(()) 390 | } 391 | 392 | /// Returns whether the decoder is complete and hence the message available. 393 | /// 394 | /// # Examples 395 | /// 396 | /// See the [`crate::fountain`] module documentation for an example. 397 | #[must_use] 398 | pub fn complete(&self) -> bool { 399 | self.message_length != 0 && self.decoded.len() == self.sequence_count 400 | } 401 | 402 | /// Checks whether a [`Part`] is receivable by the decoder. 403 | /// This can fail if other parts were previously received whose 404 | /// metadata (such as number of segments) is inconsistent with the 405 | /// present [`Part`]. Note that a fresh decoder will always return 406 | /// false here. 407 | /// 408 | /// # Examples 409 | /// 410 | /// ``` 411 | /// use ur::fountain::{Decoder, Encoder}; 412 | /// let mut decoder = Decoder::default(); 413 | /// let mut encoder = Encoder::new(b"data", 3).unwrap(); 414 | /// let part = encoder.next_part(); 415 | /// 416 | /// // a fresh decoder always returns false 417 | /// assert!(!decoder.validate(&part)); 418 | /// 419 | /// // parts with the same metadata validate successfully 420 | /// decoder.receive(part).unwrap(); 421 | /// let part = encoder.next_part(); 422 | /// assert!(decoder.validate(&part)); 423 | /// 424 | /// // parts with the different metadata don't validate 425 | /// let mut encoder = Encoder::new(b"more data", 3).unwrap(); 426 | /// let part = encoder.next_part(); 427 | /// assert!(!decoder.validate(&part)); 428 | /// ``` 429 | #[must_use] 430 | pub fn validate(&self, part: &Part) -> bool { 431 | if self.received.is_empty() { 432 | return false; 433 | } 434 | 435 | if part.sequence_count != self.sequence_count 436 | || part.message_length != self.message_length 437 | || part.checksum != self.checksum 438 | || part.data.len() != self.fragment_length 439 | { 440 | return false; 441 | } 442 | 443 | true 444 | } 445 | 446 | /// If [`complete`], returns the decoded message, `None` otherwise. 447 | /// 448 | /// # Errors 449 | /// 450 | /// If an inconsistent internal state is detected, an error will be returned. 451 | /// 452 | /// # Examples 453 | /// 454 | /// See the [`crate::fountain`] module documentation for an example. 455 | /// 456 | /// [`complete`]: Decoder::complete 457 | pub fn message(&self) -> Result>, Error> { 458 | if !self.complete() { 459 | return Ok(None); 460 | } 461 | let combined = (0..self.sequence_count) 462 | .map(|idx| self.decoded.get(&idx).ok_or(Error::ExpectedItem)) 463 | .collect::, Error>>()? 464 | .iter() 465 | .fold(alloc::vec![], |a, b| [a, b.data.clone()].concat()); 466 | if !combined 467 | .get(self.message_length..) 468 | .ok_or(Error::ExpectedItem)? 469 | .iter() 470 | .all(|&x| x == 0) 471 | { 472 | return Err(Error::InvalidPadding); 473 | } 474 | Ok(Some( 475 | combined 476 | .get(..self.message_length) 477 | .ok_or(Error::ExpectedItem)? 478 | .to_vec(), 479 | )) 480 | } 481 | } 482 | 483 | /// A part emitted by a fountain [`Encoder`]. 484 | /// 485 | /// Most commonly, this is obtained by calling [`next_part`] on the encoder. 486 | /// 487 | /// [`next_part`]: Encoder::next_part 488 | #[derive(Clone, Debug, PartialEq, Eq)] 489 | pub struct Part { 490 | sequence: usize, 491 | sequence_count: usize, 492 | message_length: usize, 493 | checksum: u32, 494 | data: Vec, 495 | } 496 | 497 | impl minicbor::Encode for Part { 498 | fn encode( 499 | &self, 500 | e: &mut minicbor::Encoder, 501 | _ctx: &mut C, 502 | ) -> Result<(), minicbor::encode::Error> { 503 | #[allow(clippy::cast_possible_truncation)] 504 | e.array(5)? 505 | .u32(self.sequence as u32)? 506 | .u32(self.sequence_count as u32)? 507 | .u32(self.message_length as u32)? 508 | .u32(self.checksum)? 509 | .bytes(&self.data)?; 510 | 511 | Ok(()) 512 | } 513 | } 514 | 515 | impl<'b, C> minicbor::Decode<'b, C> for Part { 516 | fn decode( 517 | d: &mut minicbor::Decoder<'b>, 518 | _ctx: &mut C, 519 | ) -> Result { 520 | if !matches!(d.array()?, Some(5)) { 521 | return Err(minicbor::decode::Error::message( 522 | "invalid CBOR array length", 523 | )); 524 | } 525 | 526 | Ok(Self { 527 | sequence: d.u32()? as usize, 528 | sequence_count: d.u32()? as usize, 529 | message_length: d.u32()? as usize, 530 | checksum: d.u32()?, 531 | data: d.bytes()?.to_vec(), 532 | }) 533 | } 534 | } 535 | 536 | impl Part { 537 | pub(crate) fn from_cbor(cbor: &[u8]) -> Result { 538 | minicbor::decode(cbor).map_err(Error::from) 539 | } 540 | 541 | /// Returns the indexes of the message segments that were combined into this part. 542 | /// 543 | /// # Examples 544 | /// 545 | /// ``` 546 | /// use ur::fountain::Encoder; 547 | /// let mut encoder = Encoder::new(b"data", 3).unwrap(); 548 | /// assert_eq!(encoder.next_part().indexes(), vec![0]); 549 | /// assert_eq!(encoder.next_part().indexes(), vec![1]); 550 | /// ``` 551 | #[must_use] 552 | pub fn indexes(&self) -> Vec { 553 | choose_fragments(self.sequence, self.sequence_count, self.checksum) 554 | } 555 | 556 | /// Indicates whether this part is an original segment of the message, or was obtained by 557 | /// combining multiple segments via xor. 558 | /// 559 | /// # Examples 560 | /// 561 | /// The encoding setup follows the `fountain` module example. 562 | /// 563 | /// ``` 564 | /// use ur::fountain::Encoder; 565 | /// let mut encoder = Encoder::new(b"Ten chars!", 4).unwrap(); 566 | /// // The encoder always emits the simple parts covering the message first 567 | /// assert!(encoder.next_part().is_simple()); 568 | /// assert!(encoder.next_part().is_simple()); 569 | /// assert!(encoder.next_part().is_simple()); 570 | /// // The encoder then emits segment 3 again 571 | /// assert!(encoder.next_part().is_simple()); 572 | /// // The encoder then emits all 3 segments combined 573 | /// assert!(!encoder.next_part().is_simple()); 574 | /// ``` 575 | #[must_use] 576 | pub fn is_simple(&self) -> bool { 577 | self.indexes().len() == 1 578 | } 579 | 580 | pub(crate) fn cbor(&self) -> Result, Error> { 581 | minicbor::to_vec(self).map_err(Error::from) 582 | } 583 | 584 | #[must_use] 585 | pub(crate) fn sequence_id(&self) -> alloc::string::String { 586 | alloc::format!("{}-{}", self.sequence, self.sequence_count) 587 | } 588 | 589 | /// Returns a slice view onto the underlying data. 590 | /// 591 | /// Note that for non-simple parts this will be the result of 592 | /// xoring multiple message segments together. 593 | /// 594 | /// # Examples 595 | /// 596 | /// ``` 597 | /// let data = String::from("Ten chars!"); 598 | /// let mut encoder = ur::fountain::Encoder::new(data.as_bytes(), 4).unwrap(); 599 | /// assert_eq!(encoder.next_part().data(), "Ten ".as_bytes()); 600 | /// ``` 601 | #[must_use] 602 | pub fn data(&self) -> &[u8] { 603 | &self.data 604 | } 605 | } 606 | 607 | /// Calculates the quotient of `a` and `b`, rounding the results towards 608 | /// positive infinity. 609 | /// 610 | /// Note: there's an implementation on the `usize` type of this function, 611 | /// but it's not stable yet. 612 | #[must_use] 613 | const fn div_ceil(a: usize, b: usize) -> usize { 614 | let d = a / b; 615 | let r = a % b; 616 | if r > 0 { d + 1 } else { d } 617 | } 618 | 619 | #[must_use] 620 | pub(crate) const fn fragment_length(data_length: usize, max_fragment_length: usize) -> usize { 621 | let fragment_count = div_ceil(data_length, max_fragment_length); 622 | div_ceil(data_length, fragment_count) 623 | } 624 | 625 | #[must_use] 626 | pub(crate) fn partition(mut data: Vec, fragment_length: usize) -> Vec> { 627 | let mut padding = 628 | alloc::vec![0; (fragment_length - (data.len() % fragment_length)) % fragment_length]; 629 | data.append(&mut padding); 630 | data.chunks(fragment_length).map(<[u8]>::to_vec).collect() 631 | } 632 | 633 | #[must_use] 634 | fn choose_fragments(sequence: usize, fragment_count: usize, checksum: u32) -> Vec { 635 | if sequence <= fragment_count { 636 | return alloc::vec![sequence - 1]; 637 | } 638 | 639 | #[allow(clippy::cast_possible_truncation)] 640 | let sequence = sequence as u32; 641 | 642 | let mut seed = [0u8; 8]; 643 | seed[0..4].copy_from_slice(&sequence.to_be_bytes()); 644 | seed[4..8].copy_from_slice(&checksum.to_be_bytes()); 645 | 646 | let mut xoshiro = crate::xoshiro::Xoshiro256::from(seed.as_slice()); 647 | let degree = xoshiro.choose_degree(fragment_count); 648 | let indexes = (0..fragment_count).collect(); 649 | let mut shuffled = xoshiro.shuffled(indexes); 650 | shuffled.truncate(degree as usize); 651 | shuffled 652 | } 653 | 654 | fn xor(v1: &mut [u8], v2: &[u8]) { 655 | debug_assert_eq!(v1.len(), v2.len()); 656 | 657 | for (x1, &x2) in v1.iter_mut().zip(v2.iter()) { 658 | *x1 ^= x2; 659 | } 660 | } 661 | 662 | #[cfg(test)] 663 | mod tests { 664 | use super::*; 665 | 666 | #[test] 667 | fn test_fragment_length() { 668 | assert_eq!(fragment_length(12345, 1955), 1764); 669 | assert_eq!(fragment_length(12345, 30000), 12345); 670 | 671 | assert_eq!(fragment_length(10, 4), 4); 672 | assert_eq!(fragment_length(10, 5), 5); 673 | assert_eq!(fragment_length(10, 6), 5); 674 | assert_eq!(fragment_length(10, 10), 10); 675 | } 676 | 677 | #[test] 678 | fn test_partition_and_join() { 679 | let join = |data: Vec>, message_length: usize| { 680 | let mut flattened: Vec = data.into_iter().flatten().collect(); 681 | flattened.truncate(message_length); 682 | flattened 683 | }; 684 | 685 | let message = crate::xoshiro::test_utils::make_message("Wolf", 1024); 686 | let fragment_length = fragment_length(message.len(), 100); 687 | let fragments = partition(message.clone(), fragment_length); 688 | let expected_fragments = vec![ 689 | "916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3ccba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f965e25ee29039f", 690 | "df8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3ec4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f595e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff173f021c0e6f65b05c0a494e50791", 691 | "270a0050a73ae69b6725505a2ec8a5791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d41977fa6f78dc07eecd072aae5bc8a852397e06034dba6a0b570", 692 | "797c3a89b16673c94838d884923b8186ee2db5c98407cab15e13678d072b43e406ad49477c2e45e85e52ca82a94f6df7bbbe7afbed3a3a830029f29090f25217e48d1f42993a640a67916aa7480177354cc7440215ae41e4d02eae9a1912", 693 | "33a6d4922a792c1b7244aa879fefdb4628dc8b0923568869a983b8c661ffab9b2ed2c149e38d41fba090b94155adbed32f8b18142ff0d7de4eeef2b04adf26f2456b46775c6c20b37602df7da179e2332feba8329bbb8d727a138b4ba7a5", 694 | "03215eda2ef1e953d89383a382c11d3f2cad37a4ee59a91236a3e56dcf89f6ac81dd4159989c317bd649d9cbc617f73fe10033bd288c60977481a09b343d3f676070e67da757b86de27bfca74392bac2996f7822a7d8f71a489ec6180390", 695 | "089ea80a8fcd6526413ec6c9a339115f111d78ef21d456660aa85f790910ffa2dc58d6a5b93705caef1091474938bd312427021ad1eeafbd19e0d916ddb111fabd8dcab5ad6a6ec3a9c6973809580cb2c164e26686b5b98cfb017a337968", 696 | "c7daaa14ae5152a067277b1b3902677d979f8e39cc2aafb3bc06fcf69160a853e6869dcc09a11b5009f91e6b89e5b927ab1527a735660faa6012b420dd926d940d742be6a64fb01cdc0cff9faa323f02ba41436871a0eab851e7f5782d10", 697 | "fbefde2a7e9ae9dc1e5c2c48f74f6c824ce9ef3c89f68800d44587bedc4ab417cfb3e7447d90e1e417e6e05d30e87239d3a5d1d45993d4461e60a0192831640aa32dedde185a371ded2ae15f8a93dba8809482ce49225daadfbb0fec629e", 698 | "23880789bdf9ed73be57fa84d555134630e8d0f7df48349f29869a477c13ccca9cd555ac42ad7f568416c3d61959d0ed568b2b81c7771e9088ad7fd55fd4386bafbf5a528c30f107139249357368ffa980de2c76ddd9ce4191376be0e6b5", 699 | "170010067e2e75ebe2d2904aeb1f89d5dc98cd4a6f2faaa8be6d03354c990fd895a97feb54668473e9d942bb99e196d897e8f1b01625cf48a7b78d249bb4985c065aa8cd1402ed2ba1b6f908f63dcd84b66425df00000000000000000000", 700 | ]; 701 | assert_eq!(fragments.len(), expected_fragments.len()); 702 | for (fragment, expected_fragment) in fragments.iter().zip(expected_fragments) { 703 | assert_eq!(hex::encode(fragment), expected_fragment); 704 | } 705 | let rejoined = join(fragments, message.len()); 706 | assert_eq!(rejoined, message); 707 | } 708 | 709 | #[test] 710 | fn test_choose_fragments() { 711 | let message = crate::xoshiro::test_utils::make_message("Wolf", 1024); 712 | let checksum = crate::crc32().checksum(&message); 713 | let fragment_length = crate::fountain::fragment_length(message.len(), 100); 714 | let fragments = crate::fountain::partition(message, fragment_length); 715 | let expected_fragment_indexes = vec![ 716 | vec![0], 717 | vec![1], 718 | vec![2], 719 | vec![3], 720 | vec![4], 721 | vec![5], 722 | vec![6], 723 | vec![7], 724 | vec![8], 725 | vec![9], 726 | vec![10], 727 | vec![9], 728 | vec![2, 5, 6, 8, 9, 10], 729 | vec![8], 730 | vec![1, 5], 731 | vec![1], 732 | vec![0, 2, 4, 5, 8, 10], 733 | vec![5], 734 | vec![2], 735 | vec![2], 736 | vec![0, 1, 3, 4, 5, 7, 9, 10], 737 | vec![0, 1, 2, 3, 5, 6, 8, 9, 10], 738 | vec![0, 2, 4, 5, 7, 8, 9, 10], 739 | vec![3, 5], 740 | vec![4], 741 | vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 742 | vec![0, 1, 3, 4, 5, 6, 7, 9, 10], 743 | vec![6], 744 | vec![5, 6], 745 | vec![7], 746 | ]; 747 | for seq_num in 1..=30 { 748 | let mut indexes = crate::fountain::choose_fragments(seq_num, fragments.len(), checksum); 749 | indexes.sort_unstable(); 750 | assert_eq!(indexes, expected_fragment_indexes[seq_num - 1]); 751 | } 752 | } 753 | 754 | #[test] 755 | fn test_xor() { 756 | let mut rng = crate::xoshiro::Xoshiro256::from("Wolf"); 757 | 758 | let data1 = rng.next_bytes(10); 759 | assert_eq!(hex::encode(&data1), "916ec65cf77cadf55cd7"); 760 | 761 | let data2 = rng.next_bytes(10); 762 | assert_eq!(hex::encode(&data2), "f9cda1a1030026ddd42e"); 763 | 764 | let mut data3 = data1.clone(); 765 | xor(&mut data3, &data2); 766 | assert_eq!(hex::encode(&data3), "68a367fdf47c8b2888f9"); 767 | 768 | xor(&mut data3, &data1); 769 | assert_eq!(hex::encode(data3), hex::encode(data2)); 770 | } 771 | 772 | #[test] 773 | fn test_fountain_encoder() { 774 | let message = crate::xoshiro::test_utils::make_message("Wolf", 256); 775 | let mut encoder = Encoder::new(&message, 30).unwrap(); 776 | let expected_parts = [ 777 | "916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3c", 778 | "cba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a", 779 | "8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f", 780 | "965e25ee29039fdf8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3e", 781 | "c4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f59", 782 | "5e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff1", 783 | "73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5", 784 | "791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22", 785 | "951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d0000000000", 786 | "330f0f33a05eead4f331df229871bee733b50de71afd2e5a79f196de09", 787 | "3b205ce5e52d8c24a52cffa34c564fa1af3fdffcd349dc4258ee4ee828", 788 | "dd7bf725ea6c16d531b5f03254783803048ca08b87148daacd1cd7a006", 789 | "760be7ad1c6187902bbc04f539b9ee5eb8ea6833222edea36031306c01", 790 | "5bf4031217d2c3254b088fa7553778b5003632f46e21db129416f65b55", 791 | "73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5", 792 | "b8546ebfe2048541348910267331c643133f828afec9337c318f71b7df", 793 | "23dedeea74e3a0fb052befabefa13e2f80e4315c9dceed4c8630612e64", 794 | "d01a8daee769ce34b6b35d3ca0005302724abddae405bdb419c0a6b208", 795 | "3171c5dc365766eff25ae47c6f10e7de48cfb8474e050e5fe997a6dc24", 796 | "e055c2433562184fa71b4be94f262e200f01c6f74c284b0dc6fae6673f", 797 | ] 798 | .iter() 799 | .enumerate() 800 | .map(|(i, data)| super::Part { 801 | sequence: i + 1, 802 | sequence_count: 9, 803 | message_length: 256, 804 | checksum: 23_570_951, 805 | data: hex::decode(data).unwrap(), 806 | }); 807 | for (sequence, e) in expected_parts.into_iter().enumerate() { 808 | assert_eq!(encoder.current_sequence(), sequence); 809 | assert_eq!(encoder.next_part(), e); 810 | } 811 | } 812 | 813 | #[test] 814 | fn test_fountain_encoder_cbor() { 815 | let max_fragment_length = 30; 816 | let size = 256; 817 | let message = crate::xoshiro::test_utils::make_message("Wolf", size); 818 | let mut encoder = Encoder::new(&message, max_fragment_length).unwrap(); 819 | let expected_parts = vec![ 820 | "8501091901001a0167aa07581d916ec65cf77cadf55cd7f9cda1a1030026ddd42e905b77adc36e4f2d3c", 821 | "8502091901001a0167aa07581dcba44f7f04f2de44f42d84c374a0e149136f25b01852545961d55f7f7a", 822 | "8503091901001a0167aa07581d8cde6d0e2ec43f3b2dcb644a2209e8c9e34af5c4747984a5e873c9cf5f", 823 | "8504091901001a0167aa07581d965e25ee29039fdf8ca74f1c769fc07eb7ebaec46e0695aea6cbd60b3e", 824 | "8505091901001a0167aa07581dc4bbff1b9ffe8a9e7240129377b9d3711ed38d412fbb4442256f1e6f59", 825 | "8506091901001a0167aa07581d5e0fc57fed451fb0a0101fb76b1fb1e1b88cfdfdaa946294a47de8fff1", 826 | "8507091901001a0167aa07581d73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5", 827 | "8508091901001a0167aa07581d791457c9876dd34aadd192a53aa0dc66b556c0c215c7ceb8248b717c22", 828 | "8509091901001a0167aa07581d951e65305b56a3706e3e86eb01c803bbf915d80edcd64d4d0000000000", 829 | "850a091901001a0167aa07581d330f0f33a05eead4f331df229871bee733b50de71afd2e5a79f196de09", 830 | "850b091901001a0167aa07581d3b205ce5e52d8c24a52cffa34c564fa1af3fdffcd349dc4258ee4ee828", 831 | "850c091901001a0167aa07581ddd7bf725ea6c16d531b5f03254783803048ca08b87148daacd1cd7a006", 832 | "850d091901001a0167aa07581d760be7ad1c6187902bbc04f539b9ee5eb8ea6833222edea36031306c01", 833 | "850e091901001a0167aa07581d5bf4031217d2c3254b088fa7553778b5003632f46e21db129416f65b55", 834 | "850f091901001a0167aa07581d73f021c0e6f65b05c0a494e50791270a0050a73ae69b6725505a2ec8a5", 835 | "8510091901001a0167aa07581db8546ebfe2048541348910267331c643133f828afec9337c318f71b7df", 836 | "8511091901001a0167aa07581d23dedeea74e3a0fb052befabefa13e2f80e4315c9dceed4c8630612e64", 837 | "8512091901001a0167aa07581dd01a8daee769ce34b6b35d3ca0005302724abddae405bdb419c0a6b208", 838 | "8513091901001a0167aa07581d3171c5dc365766eff25ae47c6f10e7de48cfb8474e050e5fe997a6dc24", 839 | "8514091901001a0167aa07581de055c2433562184fa71b4be94f262e200f01c6f74c284b0dc6fae6673f", 840 | ]; 841 | assert_eq!(encoder.fragment_count(), size / max_fragment_length + 1); 842 | for e in expected_parts { 843 | assert_eq!(hex::encode(encoder.next_part().cbor().unwrap()), e); 844 | } 845 | } 846 | 847 | #[test] 848 | fn test_fountain_encoder_zero_max_length() { 849 | assert!(matches!( 850 | Encoder::new(b"foo", 0), 851 | Err(Error::InvalidFragmentLen) 852 | )); 853 | } 854 | 855 | #[test] 856 | fn test_fountain_encoder_is_complete() { 857 | let message = crate::xoshiro::test_utils::make_message("Wolf", 256); 858 | let mut encoder = Encoder::new(&message, 30).unwrap(); 859 | for _ in 0..encoder.parts.len() { 860 | encoder.next_part(); 861 | } 862 | assert!(encoder.complete()); 863 | } 864 | 865 | #[test] 866 | fn test_decoder() { 867 | let seed = "Wolf"; 868 | let message_size = 32767; 869 | let max_fragment_length = 1000; 870 | 871 | let message = crate::xoshiro::test_utils::make_message(seed, message_size); 872 | let mut encoder = Encoder::new(&message, max_fragment_length).unwrap(); 873 | let mut decoder = Decoder::default(); 874 | while !decoder.complete() { 875 | assert_eq!(decoder.message().unwrap(), None); 876 | let part = encoder.next_part(); 877 | let _next = decoder.receive(part); 878 | } 879 | assert_eq!(decoder.message().unwrap(), Some(message)); 880 | } 881 | 882 | #[test] 883 | fn test_empty_encoder() { 884 | assert!(Encoder::new(&[], 1).is_err()); 885 | } 886 | 887 | #[test] 888 | fn test_decoder_skip_some_simple_fragments() { 889 | let seed = "Wolf"; 890 | let message_size = 32767; 891 | let max_fragment_length = 1000; 892 | 893 | let message = crate::xoshiro::test_utils::make_message(seed, message_size); 894 | let mut encoder = Encoder::new(&message, max_fragment_length).unwrap(); 895 | let mut decoder = Decoder::default(); 896 | let mut skip = false; 897 | while !decoder.complete() { 898 | let part = encoder.next_part(); 899 | if !skip { 900 | let _next = decoder.receive(part); 901 | } 902 | skip = !skip; 903 | } 904 | assert_eq!(decoder.message().unwrap(), Some(message)); 905 | } 906 | 907 | #[test] 908 | fn test_decoder_receive_return_value() { 909 | let seed = "Wolf"; 910 | let message_size = 1000; 911 | let max_fragment_length = 10; 912 | 913 | let message = crate::xoshiro::test_utils::make_message(seed, message_size); 914 | let mut encoder = Encoder::new(&message, max_fragment_length).unwrap(); 915 | let mut decoder = Decoder::default(); 916 | let part = encoder.next_part(); 917 | assert_eq!( 918 | part.data(), 919 | vec![0x91, 0x6e, 0xc6, 0x5c, 0xf7, 0x7c, 0xad, 0xf5, 0x5c, 0xd7] 920 | ); 921 | assert!(decoder.receive(part.clone()).unwrap()); 922 | // same indexes 923 | assert!(!decoder.receive(part).unwrap()); 924 | // non-valid 925 | let mut part = encoder.next_part(); 926 | part.checksum += 1; 927 | assert!(matches!( 928 | decoder.receive(part), 929 | Err(Error::InconsistentPart) 930 | )); 931 | // decoder complete 932 | while !decoder.complete() { 933 | let part = encoder.next_part(); 934 | decoder.receive(part).unwrap(); 935 | } 936 | let part = encoder.next_part(); 937 | assert!(!decoder.receive(part).unwrap()); 938 | } 939 | 940 | #[test] 941 | fn test_decoder_part_validation() { 942 | let mut encoder = Encoder::new(b"foo", 2).unwrap(); 943 | let mut decoder = Decoder::default(); 944 | let mut part = encoder.next_part(); 945 | assert!(decoder.receive(part.clone()).unwrap()); 946 | assert!(decoder.validate(&part)); 947 | part.checksum += 1; 948 | assert!(!decoder.validate(&part)); 949 | part.checksum -= 1; 950 | part.message_length += 1; 951 | assert!(!decoder.validate(&part)); 952 | part.message_length -= 1; 953 | part.sequence_count += 1; 954 | assert!(!decoder.validate(&part)); 955 | part.sequence_count -= 1; 956 | part.data.push(1); 957 | assert!(!decoder.validate(&part)); 958 | } 959 | 960 | #[test] 961 | fn test_empty_decoder_empty_part() { 962 | let mut decoder = Decoder::default(); 963 | let mut part = Part { 964 | sequence: 12, 965 | sequence_count: 8, 966 | message_length: 100, 967 | checksum: 0x1234_5678, 968 | data: vec![1, 5, 3, 3, 5], 969 | }; 970 | 971 | // Check sequence_count. 972 | part.sequence_count = 0; 973 | assert!(matches!( 974 | decoder.receive(part.clone()), 975 | Err(Error::EmptyPart) 976 | )); 977 | part.sequence_count = 8; 978 | 979 | // Check message_length. 980 | part.message_length = 0; 981 | assert!(matches!( 982 | decoder.receive(part.clone()), 983 | Err(Error::EmptyPart) 984 | )); 985 | part.message_length = 100; 986 | 987 | // Check data. 988 | part.data = vec![]; 989 | assert!(matches!( 990 | decoder.receive(part.clone()), 991 | Err(Error::EmptyPart) 992 | )); 993 | part.data = vec![1, 5, 3, 3, 5]; 994 | 995 | // Should not validate as there aren't any previous parts received. 996 | assert!(!decoder.validate(&part)); 997 | } 998 | 999 | #[test] 1000 | fn test_fountain_cbor() { 1001 | let part = Part { 1002 | sequence: 12, 1003 | sequence_count: 8, 1004 | message_length: 100, 1005 | checksum: 0x1234_5678, 1006 | data: vec![1, 5, 3, 3, 5], 1007 | }; 1008 | let cbor = part.cbor().unwrap(); 1009 | let part2 = Part::from_cbor(&cbor).unwrap(); 1010 | let cbor2 = part2.cbor().unwrap(); 1011 | assert_eq!(cbor, cbor2); 1012 | } 1013 | 1014 | #[test] 1015 | fn test_part_from_cbor_errors() { 1016 | // 0x18 is the first byte value that doesn't directly encode a u8, 1017 | // but implies a following value 1018 | assert!(matches!( 1019 | Part::from_cbor(&[0x18]), 1020 | Err(Error::CborDecode(e)) if e.to_string() == "unexpected type u8 at position 0: expected array" 1021 | )); 1022 | // the top-level item must be an array 1023 | assert!( 1024 | matches!(Part::from_cbor(&[0x1]), Err(Error::CborDecode(e)) if e.to_string() == "unexpected type u8 at position 0: expected array") 1025 | ); 1026 | // the array must be of length five 1027 | assert!(matches!( 1028 | Part::from_cbor(&[0x84, 0x1, 0x2, 0x3, 0x4]), 1029 | Err(Error::CborDecode(e)) if e.to_string() == "decode error: invalid CBOR array length" 1030 | )); 1031 | assert!(matches!( 1032 | Part::from_cbor(&[0x86, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]), 1033 | Err(Error::CborDecode(e)) if e.to_string() == "decode error: invalid CBOR array length" 1034 | )); 1035 | // items one through four must be an unsigned integer 1036 | let mut cbor = [0x85, 0x1, 0x2, 0x3, 0x4, 0x41, 0x5]; 1037 | for idx in 1..=4 { 1038 | Part::from_cbor(&cbor).unwrap(); 1039 | cbor[idx] = 0x41; 1040 | assert!(matches!( 1041 | Part::from_cbor(&cbor), 1042 | Err(Error::CborDecode(e)) if e.to_string() == format!("unexpected type bytes at position {idx}: expected u32") 1043 | )); 1044 | cbor[idx] = u8::try_from(idx).unwrap(); 1045 | } 1046 | // the fifth item must be byte string 1047 | assert!(matches!( 1048 | Part::from_cbor(&[0x85, 0x1, 0x2, 0x3, 0x4, 0x5]), 1049 | Err(Error::CborDecode(e)) if e.to_string() == "unexpected type u8 at position 5: expected bytes (definite length)" 1050 | )); 1051 | } 1052 | 1053 | #[test] 1054 | fn test_part_from_cbor_unsigned_types() { 1055 | // u8 1056 | Part::from_cbor(&[0x85, 0x1, 0x2, 0x3, 0x4, 0x41, 0x5]).unwrap(); 1057 | // u16 1058 | Part::from_cbor(&[ 1059 | 0x85, 0x19, 0x1, 0x2, 0x19, 0x3, 0x4, 0x19, 0x5, 0x6, 0x19, 0x7, 0x8, 0x41, 0x5, 1060 | ]) 1061 | .unwrap(); 1062 | // u32 1063 | Part::from_cbor(&[ 1064 | 0x85, 0x1a, 0x1, 0x2, 0x3, 0x4, 0x1a, 0x5, 0x6, 0x7, 0x8, 0x1a, 0x9, 0x10, 0x11, 0x12, 1065 | 0x1a, 0x13, 0x14, 0x15, 0x16, 0x41, 0x5, 1066 | ]) 1067 | .unwrap(); 1068 | // u64 1069 | assert!(matches!( 1070 | Part::from_cbor(&[ 1071 | 0x85, 0x1b, 0x1, 0x2, 0x3, 0x4, 0xa, 0xb, 0xc, 0xd, 0x1a, 0x5, 0x6, 0x7, 0x8, 0x1a, 1072 | 0x9, 0x10, 0x11, 0x12, 0x1a, 0x13, 0x14, 0x15, 0x16, 0x41, 0x5, 1073 | ]), 1074 | Err(Error::CborDecode(e)) if e.to_string().contains("converting u64 to u32") 1075 | )); 1076 | assert!(matches!( 1077 | Part::from_cbor(&[ 1078 | 0x85, 0x1a, 0x1, 0x2, 0x3, 0x4, 0x1b, 0x5, 0x6, 0x7, 0x8, 0xa, 0xb, 0xc, 0xd, 0x1a, 1079 | 0x9, 0x10, 0x11, 0x12, 0x1a, 0x13, 0x14, 0x15, 0x16, 0x41, 0x5, 1080 | ]), 1081 | Err(Error::CborDecode(e)) if e.to_string().contains("converting u64 to u32") 1082 | )); 1083 | assert!(matches!( 1084 | Part::from_cbor(&[ 1085 | 0x85, 0x1a, 0x1, 0x2, 0x3, 0x4, 0x1a, 0x5, 0x6, 0x7, 0x8, 0x1b, 0x9, 0x10, 0x11, 1086 | 0x12, 0xa, 0xb, 0xc, 0xd, 0x1a, 0x13, 0x14, 0x15, 0x16, 0x41, 0x5, 1087 | ]), 1088 | Err(Error::CborDecode(e)) if e.to_string().contains("converting u64 to u32") 1089 | )); 1090 | assert!(matches!( 1091 | Part::from_cbor(&[ 1092 | 0x85, 0x1a, 0x1, 0x2, 0x3, 0x4, 0x1a, 0x5, 0x6, 0x7, 0x8, 0x1a, 0x9, 0x10, 0x11, 1093 | 0x12, 0x1b, 0x13, 0x14, 0x15, 0x16, 0xa, 0xb, 0xc, 0xd, 0x41, 0x5, 1094 | ]), 1095 | Err(Error::CborDecode(e)) if e.to_string().contains("converting u64 to u32") 1096 | )); 1097 | } 1098 | 1099 | #[test] 1100 | fn test_error_formatting() { 1101 | assert_eq!( 1102 | super::Error::from(minicbor::decode::Error::end_of_input()).to_string(), 1103 | "minicbor decoding error: end of input bytes" 1104 | ); 1105 | assert_eq!( 1106 | super::Error::from(minicbor::encode::Error::message("error")).to_string(), 1107 | "minicbor encoding error: error" 1108 | ); 1109 | assert_eq!( 1110 | super::Error::EmptyMessage.to_string(), 1111 | "expected non-empty message" 1112 | ); 1113 | assert_eq!( 1114 | super::Error::EmptyPart.to_string(), 1115 | "expected non-empty part" 1116 | ); 1117 | assert_eq!( 1118 | super::Error::InvalidFragmentLen.to_string(), 1119 | "expected positive maximum fragment length" 1120 | ); 1121 | assert_eq!( 1122 | super::Error::InconsistentPart.to_string(), 1123 | "part is inconsistent with previous ones" 1124 | ); 1125 | assert_eq!(super::Error::ExpectedItem.to_string(), "expected item"); 1126 | assert_eq!(super::Error::InvalidPadding.to_string(), "invalid padding"); 1127 | } 1128 | 1129 | #[test] 1130 | fn test_invalid_padding() { 1131 | let mut encoder = Encoder::new(b"Hello world", 20).unwrap(); 1132 | let mut part = encoder.next_part(); 1133 | part.message_length -= 1; 1134 | let mut decoder = Decoder::default(); 1135 | decoder.receive(part).unwrap(); 1136 | assert_eq!( 1137 | decoder.message().unwrap_err().to_string(), 1138 | "invalid padding" 1139 | ); 1140 | } 1141 | } 1142 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `ur` is a crate to interact with [`uniform resource`](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md) encodings of binary data. 2 | //! The encoding scheme is optimized for transport in URIs and QR codes. 3 | //! 4 | //! The [`ur::Encoder`] allows a byte payload to be transmissioned in 5 | //! multiple stages, respecting maximum size requirements. Under the hood, 6 | //! a [`fountain`](https://en.wikipedia.org/wiki/Fountain_code) encoder is used to create an unbounded stream of URIs, 7 | //! subsets of which can be recombined at the receiving side into the payload: 8 | //! ``` 9 | //! let data = String::from("Ten chars!").repeat(10); 10 | //! let max_length = 5; 11 | //! let mut encoder = ur::Encoder::bytes(data.as_bytes(), max_length).unwrap(); 12 | //! let part = encoder.next_part().unwrap(); 13 | //! assert_eq!( 14 | //! part, 15 | //! "ur:bytes/1-20/lpadbbcsiecyvdidatkpfeghihjtcxiabdfevlms" 16 | //! ); 17 | //! let mut decoder = ur::Decoder::default(); 18 | //! while !decoder.complete() { 19 | //! let part = encoder.next_part().unwrap(); 20 | //! // Simulate some communication loss 21 | //! if encoder.current_index() & 1 > 0 { 22 | //! decoder.receive(&part).unwrap(); 23 | //! } 24 | //! } 25 | //! assert_eq!(decoder.message().unwrap().as_deref(), Some(data.as_bytes())); 26 | //! ``` 27 | //! 28 | //! The following useful building blocks are also part of the public API: 29 | //! - The [`crate::bytewords`](crate::bytewords) module contains functionality 30 | //! to encode byte payloads into a suitable alphabet, achieving hexadecimal 31 | //! byte-per-character efficiency. 32 | //! - The [`crate::fountain`](crate::fountain) module provides an implementation 33 | //! of a fountain encoder, which splits up a byte payload into multiple segments 34 | //! and emits an unbounded stream of parts which can be recombined at the receiving 35 | //! decoder side. 36 | 37 | #![forbid(unsafe_code)] 38 | #![deny(missing_docs)] 39 | #![cfg_attr(not(feature = "std"), no_std)] 40 | 41 | extern crate alloc; 42 | 43 | pub mod bytewords; 44 | pub mod fountain; 45 | pub mod ur; 46 | 47 | mod constants; 48 | mod sampler; 49 | mod xoshiro; 50 | 51 | pub use self::ur::Decoder; 52 | pub use self::ur::Encoder; 53 | pub use self::ur::Type; 54 | pub use self::ur::decode; 55 | pub use self::ur::encode; 56 | 57 | #[must_use] 58 | pub(crate) const fn crc32() -> crc::Crc { 59 | crc::Crc::::new(&crc::CRC_32_ISO_HDLC) 60 | } 61 | -------------------------------------------------------------------------------- /src/sampler.rs: -------------------------------------------------------------------------------- 1 | extern crate alloc; 2 | use alloc::vec::Vec; 3 | 4 | #[derive(Debug)] 5 | pub struct Weighted { 6 | aliases: Vec, 7 | probs: Vec, 8 | } 9 | 10 | #[allow(clippy::cast_possible_truncation)] 11 | #[allow(clippy::cast_precision_loss)] 12 | impl Weighted { 13 | pub fn new(mut weights: Vec) -> Self { 14 | assert!( 15 | !weights.iter().any(|&p| p < 0.0), 16 | "negative probability encountered" 17 | ); 18 | let summed = weights.iter().sum::(); 19 | assert!(summed > 0.0, "probabilities don't sum to a positive value"); 20 | let count = weights.len(); 21 | for w in &mut weights { 22 | *w *= count as f64 / summed; 23 | } 24 | let (mut s, mut l): (Vec, Vec) = (1..=count) 25 | .map(|j| count - j) 26 | .partition(|&j| weights[j] < 1.0); 27 | 28 | let mut probs: Vec = alloc::vec![0.0; count]; 29 | let mut aliases: Vec = alloc::vec![0; count]; 30 | 31 | while !s.is_empty() && !l.is_empty() { 32 | let a = s.remove(s.len() - 1); 33 | let g = l.remove(l.len() - 1); 34 | probs[a] = weights[a]; 35 | aliases[a] = g as u32; 36 | weights[g] += weights[a] - 1.0; 37 | if weights[g] < 1.0 { 38 | s.push(g); 39 | } else { 40 | l.push(g); 41 | } 42 | } 43 | 44 | while !l.is_empty() { 45 | let g = l.remove(l.len() - 1); 46 | probs[g] = 1.0; 47 | } 48 | 49 | while !s.is_empty() { 50 | let a = s.remove(s.len() - 1); 51 | probs[a] = 1.0; 52 | } 53 | 54 | Self { aliases, probs } 55 | } 56 | 57 | #[allow(clippy::cast_sign_loss)] 58 | pub fn next(&self, xoshiro: &mut crate::xoshiro::Xoshiro256) -> u32 { 59 | let r1 = xoshiro.next_double(); 60 | let r2 = xoshiro.next_double(); 61 | let n = self.probs.len(); 62 | let i = (n as f64 * r1) as usize; 63 | if r2 < self.probs[i] { 64 | i as u32 65 | } else { 66 | self.aliases[i] 67 | } 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | 75 | #[test] 76 | fn test_sampler() { 77 | let weights = vec![1.0, 2.0, 4.0, 8.0]; 78 | let mut xoshiro = crate::xoshiro::Xoshiro256::from("Wolf"); 79 | let sampler = Weighted::new(weights); 80 | 81 | let expected_samples = vec![ 82 | 3, 3, 3, 3, 3, 3, 3, 0, 2, 3, 3, 3, 3, 1, 2, 2, 1, 3, 3, 2, 3, 3, 1, 1, 2, 1, 1, 3, 1, 83 | 3, 1, 2, 0, 2, 1, 0, 3, 3, 3, 1, 3, 3, 3, 3, 1, 3, 2, 3, 2, 2, 3, 3, 3, 3, 2, 3, 3, 0, 84 | 3, 3, 3, 3, 1, 2, 3, 3, 2, 2, 2, 1, 2, 2, 1, 2, 3, 1, 3, 0, 3, 2, 3, 3, 3, 3, 3, 3, 3, 85 | 3, 2, 3, 1, 3, 3, 2, 0, 2, 2, 3, 1, 1, 2, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 2, 3, 1, 86 | 2, 1, 1, 3, 1, 3, 2, 2, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 1, 2, 3, 3, 87 | 1, 3, 2, 3, 3, 3, 2, 3, 1, 3, 0, 3, 2, 1, 1, 3, 1, 3, 2, 3, 3, 3, 3, 2, 0, 3, 3, 1, 3, 88 | 0, 2, 1, 3, 3, 1, 1, 3, 1, 2, 3, 3, 3, 0, 2, 3, 2, 0, 1, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 89 | 3, 2, 3, 3, 3, 3, 2, 3, 3, 2, 0, 2, 3, 3, 3, 3, 2, 1, 1, 1, 2, 1, 3, 3, 3, 2, 2, 3, 3, 90 | 1, 2, 3, 0, 3, 2, 3, 3, 3, 3, 0, 2, 2, 3, 2, 2, 3, 3, 3, 3, 1, 3, 2, 3, 3, 3, 3, 3, 2, 91 | 2, 3, 1, 3, 0, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 2, 2, 2, 3, 1, 1, 3, 2, 2, 92 | 0, 3, 2, 1, 2, 1, 0, 3, 3, 3, 2, 2, 3, 2, 1, 2, 0, 0, 3, 3, 2, 3, 3, 2, 3, 3, 3, 3, 3, 93 | 2, 2, 2, 3, 3, 3, 3, 3, 1, 1, 3, 2, 2, 3, 1, 1, 0, 1, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 94 | 2, 2, 2, 3, 2, 2, 2, 2, 2, 1, 2, 3, 3, 2, 2, 2, 2, 3, 3, 2, 0, 2, 1, 3, 3, 3, 3, 0, 3, 95 | 3, 3, 3, 2, 2, 3, 1, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 2, 1, 3, 3, 3, 3, 2, 2, 96 | 0, 1, 2, 3, 2, 0, 3, 3, 3, 3, 3, 3, 1, 3, 3, 2, 3, 2, 2, 3, 3, 3, 3, 3, 2, 2, 3, 3, 2, 97 | 2, 2, 1, 3, 3, 3, 3, 1, 2, 3, 2, 3, 3, 2, 3, 2, 3, 3, 3, 2, 3, 1, 2, 3, 2, 1, 1, 3, 3, 98 | 2, 3, 3, 2, 3, 3, 0, 0, 1, 3, 3, 2, 3, 3, 3, 3, 1, 3, 3, 0, 3, 2, 3, 3, 1, 3, 3, 3, 3, 99 | 3, 3, 3, 0, 3, 3, 2, 100 | ]; 101 | for e in expected_samples { 102 | assert_eq!(sampler.next(&mut xoshiro), e); 103 | } 104 | } 105 | 106 | #[test] 107 | fn test_choose_degree() { 108 | let message = crate::xoshiro::test_utils::make_message("Wolf", 1024); 109 | let fragment_length = crate::fountain::fragment_length(message.len(), 100); 110 | let fragments = crate::fountain::partition(message, fragment_length); 111 | let expected_degrees = vec![ 112 | 11, 3, 6, 5, 2, 1, 2, 11, 1, 3, 9, 10, 10, 4, 2, 1, 1, 2, 1, 1, 5, 2, 4, 10, 3, 2, 1, 113 | 1, 3, 11, 2, 6, 2, 9, 9, 2, 6, 7, 2, 5, 2, 4, 3, 1, 6, 11, 2, 11, 3, 1, 6, 3, 1, 4, 5, 114 | 3, 6, 1, 1, 3, 1, 2, 2, 1, 4, 5, 1, 1, 9, 1, 1, 6, 4, 1, 5, 1, 2, 2, 3, 1, 1, 5, 2, 6, 115 | 1, 7, 11, 1, 8, 1, 5, 1, 1, 2, 2, 6, 4, 10, 1, 2, 5, 5, 5, 1, 1, 4, 1, 1, 1, 3, 5, 5, 116 | 5, 1, 4, 3, 3, 5, 1, 11, 3, 2, 8, 1, 2, 1, 1, 4, 5, 2, 1, 1, 1, 5, 6, 11, 10, 7, 4, 7, 117 | 1, 5, 3, 1, 1, 9, 1, 2, 5, 5, 2, 2, 3, 10, 1, 3, 2, 3, 3, 1, 1, 2, 1, 3, 2, 2, 1, 3, 8, 118 | 4, 1, 11, 6, 3, 1, 1, 1, 1, 1, 3, 1, 2, 1, 10, 1, 1, 8, 2, 7, 1, 2, 1, 9, 2, 10, 2, 1, 119 | 3, 4, 10, 120 | ]; 121 | for nonce in 1..=200 { 122 | let mut xoshiro = crate::xoshiro::Xoshiro256::from(format!("Wolf-{nonce}").as_str()); 123 | assert_eq!( 124 | xoshiro.choose_degree(fragments.len()), 125 | expected_degrees[nonce - 1] 126 | ); 127 | } 128 | } 129 | 130 | #[test] 131 | #[should_panic(expected = "negative probability encountered")] 132 | fn test_negative_weights() { 133 | Weighted::new(vec![2.0, -1.0]); 134 | } 135 | 136 | #[test] 137 | #[should_panic(expected = "probabilities don't sum to a positive value")] 138 | fn test_zero_weights() { 139 | Weighted::new(vec![0.0]); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/ur.rs: -------------------------------------------------------------------------------- 1 | //! Split up big payloads into constantly sized URIs which can be recombined by a decoder. 2 | //! 3 | //! The `ur` module provides thin wrappers around fountain en- and decoders 4 | //! which turn these fountain parts into URIs. To this end the fountain part 5 | //! attributes (data, checksum, indexes being used, etc.) are combined with 6 | //! CBOR into a self-describing byte payload and encoded with the `bytewords` 7 | //! encoding into URIs suitable for web transport and QR codes. 8 | //! ``` 9 | //! let data = String::from("Ten chars!").repeat(10); 10 | //! let max_length = 5; 11 | //! let mut encoder = ur::Encoder::bytes(data.as_bytes(), max_length).unwrap(); 12 | //! let part = encoder.next_part().unwrap(); 13 | //! assert_eq!( 14 | //! part, 15 | //! "ur:bytes/1-20/lpadbbcsiecyvdidatkpfeghihjtcxiabdfevlms" 16 | //! ); 17 | //! let mut decoder = ur::Decoder::default(); 18 | //! while !decoder.complete() { 19 | //! let part = encoder.next_part().unwrap(); 20 | //! // Simulate some communication loss 21 | //! if encoder.current_index() & 1 > 0 { 22 | //! decoder.receive(&part).unwrap(); 23 | //! } 24 | //! } 25 | //! assert_eq!(decoder.message().unwrap().as_deref(), Some(data.as_bytes())); 26 | //! ``` 27 | 28 | extern crate alloc; 29 | use alloc::{string::String, vec::Vec}; 30 | 31 | /// Errors that can happen during encoding and decoding of URs. 32 | #[derive(Debug)] 33 | pub enum Error { 34 | /// A bytewords error. 35 | Bytewords(crate::bytewords::Error), 36 | /// A fountain error. 37 | Fountain(crate::fountain::Error), 38 | /// Invalid scheme. 39 | InvalidScheme, 40 | /// No type specified. 41 | TypeUnspecified, 42 | /// Invalid characters. 43 | InvalidCharacters, 44 | /// Invalid indices in multi-part UR. 45 | InvalidIndices, 46 | /// Tried to decode a single-part UR as multi-part. 47 | NotMultiPart, 48 | } 49 | 50 | impl core::fmt::Display for Error { 51 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 52 | match self { 53 | Self::Bytewords(e) => write!(f, "bytewords: {e}"), 54 | Self::Fountain(e) => write!(f, "fountain: {e}"), 55 | Self::InvalidScheme => write!(f, "invalid scheme"), 56 | Self::TypeUnspecified => write!(f, "no type specified"), 57 | Self::InvalidCharacters => write!(f, "type contains invalid characters"), 58 | Self::InvalidIndices => write!(f, "invalid indices"), 59 | Self::NotMultiPart => write!(f, "can't decode single-part UR as multi-part"), 60 | } 61 | } 62 | } 63 | 64 | impl From for Error { 65 | fn from(e: crate::bytewords::Error) -> Self { 66 | Self::Bytewords(e) 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(e: crate::fountain::Error) -> Self { 72 | Self::Fountain(e) 73 | } 74 | } 75 | 76 | /// Encodes a data payload into a single URI 77 | /// 78 | /// # Examples 79 | /// 80 | /// ``` 81 | /// assert_eq!( 82 | /// ur::ur::encode(b"data", &ur::Type::Bytes), 83 | /// "ur:bytes/iehsjyhspmwfwfia" 84 | /// ); 85 | /// ``` 86 | #[must_use] 87 | pub fn encode(data: &[u8], ur_type: &Type) -> String { 88 | let body = crate::bytewords::encode(data, crate::bytewords::Style::Minimal); 89 | alloc::format!("ur:{}/{body}", ur_type.encoding()) 90 | } 91 | 92 | /// The type of uniform resource. 93 | pub enum Type<'a> { 94 | /// A `bytes` uniform resource. 95 | Bytes, 96 | /// A custom uniform resource. 97 | Custom(&'a str), 98 | } 99 | 100 | impl<'a> Type<'a> { 101 | const fn encoding(&self) -> &'a str { 102 | match self { 103 | Self::Bytes => "bytes", 104 | Self::Custom(s) => s, 105 | } 106 | } 107 | } 108 | 109 | /// A uniform resource encoder with an underlying fountain encoding. 110 | /// 111 | /// # Examples 112 | /// 113 | /// See the [`crate::ur`] module documentation for an example. 114 | pub struct Encoder<'a> { 115 | fountain: crate::fountain::Encoder, 116 | ur_type: Type<'a>, 117 | } 118 | 119 | impl<'a> Encoder<'a> { 120 | /// Creates a new [`bytes`] [`Encoder`] for given a message payload. 121 | /// 122 | /// The emitted fountain parts will respect the maximum fragment length argument. 123 | /// 124 | /// # Examples 125 | /// 126 | /// See the [`crate::ur`] module documentation for an example. 127 | /// 128 | /// # Errors 129 | /// 130 | /// If an empty message or a zero maximum fragment length is passed, an error 131 | /// will be returned. 132 | /// 133 | /// [`bytes`]: Type::Bytes 134 | pub fn bytes(message: &[u8], max_fragment_length: usize) -> Result { 135 | Ok(Self { 136 | fountain: crate::fountain::Encoder::new(message, max_fragment_length)?, 137 | ur_type: Type::Bytes, 138 | }) 139 | } 140 | 141 | /// Creates a new [`custom`] [`Encoder`] for given a message payload. 142 | /// 143 | /// The emitted fountain parts will respect the maximum fragment length argument. 144 | /// 145 | /// # Errors 146 | /// 147 | /// If an empty message or a zero maximum fragment length is passed, an error 148 | /// will be returned. 149 | /// 150 | /// [`custom`]: Type::Custom 151 | pub fn new(message: &[u8], max_fragment_length: usize, s: &'a str) -> Result { 152 | Ok(Self { 153 | fountain: crate::fountain::Encoder::new(message, max_fragment_length)?, 154 | ur_type: Type::Custom(s), 155 | }) 156 | } 157 | 158 | /// Returns the URI corresponding to next fountain part. 159 | /// 160 | /// # Examples 161 | /// 162 | /// See the [`crate::ur`] module documentation for an example. 163 | /// 164 | /// # Errors 165 | /// 166 | /// If serialization fails an error will be returned. 167 | pub fn next_part(&mut self) -> Result { 168 | let part = self.fountain.next_part(); 169 | let body = crate::bytewords::encode(&part.cbor()?, crate::bytewords::Style::Minimal); 170 | Ok(alloc::format!( 171 | "ur:{}/{}/{body}", 172 | self.ur_type.encoding(), 173 | part.sequence_id() 174 | )) 175 | } 176 | 177 | /// Returns the current count of already emitted parts. 178 | /// 179 | /// # Examples 180 | /// 181 | /// ``` 182 | /// let mut encoder = ur::Encoder::bytes(b"data", 5).unwrap(); 183 | /// assert_eq!(encoder.current_index(), 0); 184 | /// encoder.next_part().unwrap(); 185 | /// assert_eq!(encoder.current_index(), 1); 186 | /// ``` 187 | #[must_use] 188 | pub const fn current_index(&self) -> usize { 189 | self.fountain.current_sequence() 190 | } 191 | 192 | /// Returns the number of segments the original message has been split up into. 193 | /// 194 | /// # Examples 195 | /// 196 | /// ``` 197 | /// let mut encoder = ur::Encoder::bytes(b"data", 3).unwrap(); 198 | /// assert_eq!(encoder.fragment_count(), 2); 199 | /// ``` 200 | #[must_use] 201 | pub fn fragment_count(&self) -> usize { 202 | self.fountain.fragment_count() 203 | } 204 | } 205 | 206 | /// An enum used to indicate whether a UR is single- or 207 | /// multip-part. See e.g. [`decode`] where it is returned. 208 | #[derive(Debug, PartialEq, Eq)] 209 | pub enum Kind { 210 | /// This UR contains the full data payload. 211 | SinglePart, 212 | /// This UR contains part of the data payload. 213 | MultiPart, 214 | } 215 | 216 | /// Decodes a single URI (either single- or multi-part) 217 | /// into a tuple consisting of the [`Kind`] and the data 218 | /// payload. 219 | /// 220 | /// # Examples 221 | /// 222 | /// ``` 223 | /// assert_eq!( 224 | /// ur::ur::decode("ur:bytes/iehsjyhspmwfwfia").unwrap(), 225 | /// (ur::ur::Kind::SinglePart, b"data".to_vec()) 226 | /// ); 227 | /// assert_eq!( 228 | /// ur::ur::decode("ur:bytes/1-2/iehsjyhspmwfwfia").unwrap(), 229 | /// (ur::ur::Kind::MultiPart, b"data".to_vec()) 230 | /// ); 231 | /// ``` 232 | /// 233 | /// # Errors 234 | /// 235 | /// This function errors for invalid inputs, for example 236 | /// an invalid scheme different from "ur" or an invalid number 237 | /// of "/" separators. 238 | pub fn decode(value: &str) -> Result<(Kind, Vec), Error> { 239 | let strip_scheme = value.strip_prefix("ur:").ok_or(Error::InvalidScheme)?; 240 | let (r#type, strip_type) = strip_scheme.split_once('/').ok_or(Error::TypeUnspecified)?; 241 | 242 | if !r#type 243 | .trim_start_matches(|c: char| c.is_ascii_alphanumeric() || c == '-') 244 | .is_empty() 245 | { 246 | return Err(Error::InvalidCharacters); 247 | } 248 | 249 | match strip_type.rsplit_once('/') { 250 | None => Ok(( 251 | Kind::SinglePart, 252 | crate::bytewords::decode(strip_type, crate::bytewords::Style::Minimal)?, 253 | )), 254 | Some((indices, payload)) => { 255 | let (idx, idx_total) = indices.split_once('-').ok_or(Error::InvalidIndices)?; 256 | if idx.parse::().is_err() || idx_total.parse::().is_err() { 257 | return Err(Error::InvalidIndices); 258 | } 259 | 260 | Ok(( 261 | Kind::MultiPart, 262 | crate::bytewords::decode(payload, crate::bytewords::Style::Minimal)?, 263 | )) 264 | } 265 | } 266 | } 267 | 268 | /// A uniform resource decoder able to receive URIs that encode a fountain part. 269 | /// 270 | /// # Examples 271 | /// 272 | /// See the [`crate::ur`] module documentation for an example. 273 | #[derive(Default)] 274 | pub struct Decoder { 275 | fountain: crate::fountain::Decoder, 276 | } 277 | 278 | impl Decoder { 279 | /// Receives a URI representing a CBOR and `bytewords`-encoded fountain part 280 | /// into the decoder. 281 | /// 282 | /// # Examples 283 | /// 284 | /// See the [`crate::ur`] module documentation for an example. 285 | /// 286 | /// # Errors 287 | /// 288 | /// This function may error along all the necessary decoding steps: 289 | /// - The string may not be a well-formed URI according to the uniform resource scheme 290 | /// - The URI payload may not be a well-formed `bytewords` string 291 | /// - The decoded byte payload may not be valid CBOR 292 | /// - The CBOR-encoded fountain part may be inconsistent with previously received ones 293 | /// 294 | /// In all these cases, an error will be returned. 295 | pub fn receive(&mut self, value: &str) -> Result<(), Error> { 296 | let (kind, decoded) = decode(value)?; 297 | if kind != Kind::MultiPart { 298 | return Err(Error::NotMultiPart); 299 | } 300 | 301 | self.fountain 302 | .receive(crate::fountain::Part::from_cbor(decoded.as_slice())?)?; 303 | Ok(()) 304 | } 305 | 306 | /// Returns whether the decoder is complete and hence the message available. 307 | /// 308 | /// # Examples 309 | /// 310 | /// See the [`crate::ur`] module documentation for an example. 311 | #[must_use] 312 | pub fn complete(&self) -> bool { 313 | self.fountain.complete() 314 | } 315 | 316 | /// If [`complete`], returns the decoded message, `None` otherwise. 317 | /// 318 | /// # Errors 319 | /// 320 | /// If an inconsistent internal state detected, an error will be returned. 321 | /// 322 | /// # Examples 323 | /// 324 | /// See the [`crate::ur`] module documentation for an example. 325 | /// 326 | /// [`complete`]: Decoder::complete 327 | pub fn message(&self) -> Result>, Error> { 328 | self.fountain.message().map_err(Error::from) 329 | } 330 | } 331 | 332 | #[cfg(test)] 333 | mod tests { 334 | use super::*; 335 | use minicbor::{bytes::ByteVec, data::Tag}; 336 | 337 | fn make_message_ur(length: usize, seed: &str) -> Vec { 338 | let message = crate::xoshiro::test_utils::make_message(seed, length); 339 | minicbor::to_vec(ByteVec::from(message)).unwrap() 340 | } 341 | 342 | #[test] 343 | fn test_single_part_ur() { 344 | let ur = make_message_ur(50, "Wolf"); 345 | let encoded = encode(&ur, &Type::Bytes); 346 | let expected = "ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch"; 347 | assert_eq!(encoded, expected); 348 | let decoded = decode(&encoded).unwrap(); 349 | assert_eq!((Kind::SinglePart, ur), decoded); 350 | } 351 | 352 | #[test] 353 | fn test_ur_encoder() { 354 | let ur = make_message_ur(256, "Wolf"); 355 | let mut encoder = Encoder::bytes(&ur, 30).unwrap(); 356 | let expected = vec![ 357 | "ur:bytes/1-9/lpadascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtdkgslpgh", 358 | "ur:bytes/2-9/lpaoascfadaxcywenbpljkhdcagwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsgmghhkhstlrdcxaefz", 359 | "ur:bytes/3-9/lpaxascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjksopdzmol", 360 | "ur:bytes/4-9/lpaaascfadaxcywenbpljkhdcasotkhemthydawydtaxneurlkosgwcekonertkbrlwmplssjtammdplolsbrdzcrtas", 361 | "ur:bytes/5-9/lpahascfadaxcywenbpljkhdcatbbdfmssrkzmcwnezelennjpfzbgmuktrhtejscktelgfpdlrkfyfwdajldejokbwf", 362 | "ur:bytes/6-9/lpamascfadaxcywenbpljkhdcackjlhkhybssklbwefectpfnbbectrljectpavyrolkzczcpkmwidmwoxkilghdsowp", 363 | "ur:bytes/7-9/lpatascfadaxcywenbpljkhdcavszmwnjkwtclrtvaynhpahrtoxmwvwatmedibkaegdosftvandiodagdhthtrlnnhy", 364 | "ur:bytes/8-9/lpayascfadaxcywenbpljkhdcadmsponkkbbhgsoltjntegepmttmoonftnbuoiyrehfrtsabzsttorodklubbuyaetk", 365 | "ur:bytes/9-9/lpasascfadaxcywenbpljkhdcajskecpmdckihdyhphfotjojtfmlnwmadspaxrkytbztpbauotbgtgtaeaevtgavtny", 366 | "ur:bytes/10-9/lpbkascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtwdkiplzs", 367 | "ur:bytes/11-9/lpbdascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjkvetiiapk", 368 | "ur:bytes/12-9/lpbnascfadaxcywenbpljkhdcarllaluzmdmgstospeyiefmwejlwtpedamktksrvlcygmzemovovllarodtmtbnptrs", 369 | "ur:bytes/13-9/lpbtascfadaxcywenbpljkhdcamtkgtpknghchchyketwsvwgwfdhpgmgtylctotzopdrpayoschcmhplffziachrfgd", 370 | "ur:bytes/14-9/lpbaascfadaxcywenbpljkhdcapazewnvonnvdnsbyleynwtnsjkjndeoldydkbkdslgjkbbkortbelomueekgvstegt", 371 | "ur:bytes/15-9/lpbsascfadaxcywenbpljkhdcaynmhpddpzmversbdqdfyrehnqzlugmjzmnmtwmrouohtstgsbsahpawkditkckynwt", 372 | "ur:bytes/16-9/lpbeascfadaxcywenbpljkhdcawygekobamwtlihsnpalnsghenskkiynthdzotsimtojetprsttmukirlrsbtamjtpd", 373 | "ur:bytes/17-9/lpbyascfadaxcywenbpljkhdcamklgftaxykpewyrtqzhydntpnytyisincxmhtbceaykolduortotiaiaiafhiaoyce", 374 | "ur:bytes/18-9/lpbgascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtntwkbkwy", 375 | "ur:bytes/19-9/lpbwascfadaxcywenbpljkhdcadekicpaajootjzpsdrbalpeywllbdsnbinaerkurspbncxgslgftvtsrjtksplcpeo", 376 | "ur:bytes/20-9/lpbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltataztksmhkbot", 377 | ]; 378 | assert_eq!(encoder.fragment_count(), 9); 379 | for (index, e) in expected.into_iter().enumerate() { 380 | assert_eq!(encoder.current_index(), index); 381 | assert_eq!(encoder.next_part().unwrap(), e); 382 | } 383 | } 384 | 385 | #[test] 386 | fn test_ur_encoder_decoder_bc_crypto_request() { 387 | // https://github.com/BlockchainCommons/crypto-commons/blob/67ea252f4a7f295bb347cb046796d5b445b3ad3c/Docs/ur-99-request-response.md#the-seed-request 388 | 389 | fn crypto_seed() -> Result, minicbor::encode::Error> { 390 | let mut e = minicbor::Encoder::new(Vec::new()); 391 | 392 | let uuid = hex::decode("020C223A86F7464693FC650EF3CAC047").unwrap(); 393 | let seed_digest = 394 | hex::decode("E824467CAFFEAF3BBC3E0CA095E660A9BAD80DDB6A919433A37161908B9A3986") 395 | .unwrap(); 396 | 397 | #[rustfmt::skip] 398 | e.map(2)? 399 | // 2.1 UUID: tag 37 type bytes(16) 400 | .u8(1)?.tag(Tag::new(37))?.bytes(&uuid)? 401 | // 2.2 crypto-seed: tag 500 type map 402 | .u8(2)?.tag(Tag::new(500))?.map(1)? 403 | // 2.2.1 crypto-seed-digest: tag 600 type bytes(32) 404 | .u8(1)?.tag(Tag::new(600))?.bytes(&seed_digest)?; 405 | 406 | Ok(e.into_writer()) 407 | } 408 | 409 | let data = crypto_seed().unwrap(); 410 | 411 | let e = encode(&data, &Type::Custom("crypto-request")); 412 | let expected = "ur:crypto-request/oeadtpdagdaobncpftlnylfgfgmuztihbawfsgrtflaotaadwkoyadtaaohdhdcxvsdkfgkepezepefrrffmbnnbmdvahnptrdtpbtuyimmemweootjshsmhlunyeslnameyhsdi"; 413 | assert_eq!(expected, e); 414 | 415 | // Decoding should yield the same data 416 | let decoded = decode(e.as_str()).unwrap(); 417 | assert_eq!((Kind::SinglePart, data), decoded); 418 | } 419 | 420 | #[test] 421 | fn test_multipart_ur() { 422 | let ur = make_message_ur(32767, "Wolf"); 423 | let mut encoder = Encoder::bytes(&ur, 1000).unwrap(); 424 | let mut decoder = Decoder::default(); 425 | while !decoder.complete() { 426 | assert_eq!(decoder.message().unwrap(), None); 427 | decoder.receive(&encoder.next_part().unwrap()).unwrap(); 428 | } 429 | assert_eq!(decoder.message().unwrap(), Some(ur)); 430 | } 431 | 432 | #[test] 433 | fn test_decoder() { 434 | assert!(matches!( 435 | decode("uhr:bytes/aeadaolazmjendeoti"), 436 | Err(Error::InvalidScheme) 437 | )); 438 | assert!(matches!( 439 | decode("ur:aeadaolazmjendeoti"), 440 | Err(Error::TypeUnspecified) 441 | )); 442 | assert!(matches!( 443 | decode("ur:bytes#4/aeadaolazmjendeoti"), 444 | Err(Error::InvalidCharacters) 445 | )); 446 | assert!(matches!( 447 | decode("ur:bytes/1-1a/aeadaolazmjendeoti"), 448 | Err(Error::InvalidIndices) 449 | )); 450 | assert!(matches!( 451 | decode("ur:bytes/1-1/toomuch/aeadaolazmjendeoti"), 452 | Err(Error::InvalidIndices) 453 | )); 454 | decode("ur:bytes/aeadaolazmjendeoti").unwrap(); 455 | decode("ur:whatever-12/aeadaolazmjendeoti").unwrap(); 456 | } 457 | 458 | #[test] 459 | fn test_custom_encoder() { 460 | let data = String::from("Ten chars!"); 461 | let max_length = 5; 462 | let mut encoder = Encoder::new(data.as_bytes(), max_length, "my-scheme").unwrap(); 463 | assert_eq!( 464 | encoder.next_part().unwrap(), 465 | "ur:my-scheme/1-2/lpadaobkcywkwmhfwnfeghihjtcxiansvomopr" 466 | ); 467 | } 468 | 469 | #[test] 470 | fn test_error_formatting() { 471 | assert_eq!( 472 | super::Error::from(crate::bytewords::Error::InvalidChecksum).to_string(), 473 | "bytewords: invalid checksum" 474 | ); 475 | assert_eq!( 476 | super::Error::from(crate::fountain::Error::EmptyPart).to_string(), 477 | "fountain: expected non-empty part" 478 | ); 479 | assert_eq!(super::Error::InvalidScheme.to_string(), "invalid scheme"); 480 | assert_eq!( 481 | super::Error::TypeUnspecified.to_string(), 482 | "no type specified" 483 | ); 484 | assert_eq!( 485 | super::Error::InvalidCharacters.to_string(), 486 | "type contains invalid characters" 487 | ); 488 | assert_eq!(super::Error::InvalidIndices.to_string(), "invalid indices"); 489 | assert_eq!( 490 | super::Error::NotMultiPart.to_string(), 491 | "can't decode single-part UR as multi-part" 492 | ); 493 | } 494 | 495 | #[test] 496 | fn test_not_multipart() { 497 | let mut decoder = Decoder::default(); 498 | assert_eq!( 499 | decoder 500 | .receive("ur:bytes/iehsjyhspmwfwfia") 501 | .unwrap_err() 502 | .to_string(), 503 | "can't decode single-part UR as multi-part" 504 | ); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /src/xoshiro.rs: -------------------------------------------------------------------------------- 1 | extern crate alloc; 2 | use alloc::vec::Vec; 3 | use rand_xoshiro::Xoshiro256StarStar; 4 | use rand_xoshiro::rand_core::RngCore; 5 | use rand_xoshiro::rand_core::SeedableRng; 6 | 7 | #[allow(clippy::module_name_repetitions)] 8 | pub struct Xoshiro256 { 9 | inner: Xoshiro256StarStar, 10 | } 11 | 12 | impl From for Xoshiro256 { 13 | fn from(from: Xoshiro256StarStar) -> Self { 14 | Self { inner: from } 15 | } 16 | } 17 | 18 | impl From<&[u8]> for Xoshiro256 { 19 | fn from(from: &[u8]) -> Self { 20 | let hash = bitcoin_hashes::sha256::Hash::hash(from); 21 | Self::from(hash.to_byte_array()) 22 | } 23 | } 24 | 25 | #[allow(clippy::cast_precision_loss)] 26 | #[allow(clippy::cast_possible_truncation)] 27 | impl Xoshiro256 { 28 | #[allow(clippy::should_implement_trait)] 29 | pub fn next(&mut self) -> u64 { 30 | self.inner.next_u64() 31 | } 32 | 33 | pub fn next_double(&mut self) -> f64 { 34 | self.next() as f64 / (u64::MAX as f64 + 1.0) 35 | } 36 | 37 | #[allow(clippy::cast_sign_loss)] 38 | pub fn next_int(&mut self, low: u64, high: u64) -> u64 { 39 | (self.next_double() * ((high - low + 1) as f64)) as u64 + low 40 | } 41 | 42 | pub fn shuffled(&mut self, mut items: Vec) -> Vec { 43 | let mut shuffled = Vec::::with_capacity(items.len()); 44 | while !items.is_empty() { 45 | let index = self.next_int(0, (items.len() - 1) as u64) as usize; 46 | let item = items.remove(index); 47 | shuffled.push(item); 48 | } 49 | shuffled 50 | } 51 | 52 | pub fn choose_degree(&mut self, length: usize) -> u32 { 53 | let degree_weights: Vec = (1..=length).map(|x| 1.0 / x as f64).collect(); 54 | let sampler = crate::sampler::Weighted::new(degree_weights); 55 | sampler.next(self) + 1 56 | } 57 | } 58 | 59 | impl From<&str> for Xoshiro256 { 60 | fn from(value: &str) -> Self { 61 | let hash = bitcoin_hashes::sha256::Hash::hash(value.as_bytes()); 62 | Self::from(hash.to_byte_array()) 63 | } 64 | } 65 | 66 | impl From<[u8; 32]> for Xoshiro256 { 67 | fn from(value: [u8; 32]) -> Self { 68 | let mut s = [0_u8; 32]; 69 | for i in 0..4 { 70 | let mut v: u64 = 0; 71 | for n in 0..8 { 72 | v <<= 8; 73 | v |= u64::from(value[8 * i + n]); 74 | } 75 | let bytes = v.to_le_bytes(); 76 | for n in 0..8 { 77 | s[8 * i + n] = bytes[n]; 78 | } 79 | } 80 | Xoshiro256StarStar::from_seed(s).into() 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | pub mod test_utils { 86 | use super::*; 87 | 88 | impl super::Xoshiro256 { 89 | #[allow(clippy::cast_possible_truncation)] 90 | fn next_byte(&mut self) -> u8 { 91 | self.next_int(0, 255) as u8 92 | } 93 | 94 | pub fn next_bytes(&mut self, n: usize) -> Vec { 95 | (0..n).map(|_| self.next_byte()).collect() 96 | } 97 | 98 | #[must_use] 99 | pub fn from_crc(bytes: &[u8]) -> Self { 100 | Self::from(&crate::crc32().checksum(bytes).to_be_bytes()[..]) 101 | } 102 | } 103 | 104 | #[must_use] 105 | pub fn make_message(seed: &str, size: usize) -> Vec { 106 | let mut xoshiro = Xoshiro256::from(seed); 107 | xoshiro.next_bytes(size) 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | 115 | #[test] 116 | fn test_rng_1() { 117 | let mut rng = Xoshiro256::from("Wolf"); 118 | let expected = vec![ 119 | 42, 81, 85, 8, 82, 84, 76, 73, 70, 88, 2, 74, 40, 48, 77, 54, 88, 7, 5, 88, 37, 25, 82, 120 | 13, 69, 59, 30, 39, 11, 82, 19, 99, 45, 87, 30, 15, 32, 22, 89, 44, 92, 77, 29, 78, 4, 121 | 92, 44, 68, 92, 69, 1, 42, 89, 50, 37, 84, 63, 34, 32, 3, 17, 62, 40, 98, 82, 89, 24, 122 | 43, 85, 39, 15, 3, 99, 29, 20, 42, 27, 10, 85, 66, 50, 35, 69, 70, 70, 74, 30, 13, 72, 123 | 54, 11, 5, 70, 55, 91, 52, 10, 43, 43, 52, 124 | ]; 125 | for e in expected { 126 | assert_eq!(rng.next() % 100, e); 127 | } 128 | } 129 | 130 | #[test] 131 | fn test_rng_2() { 132 | let mut rng = Xoshiro256::from_crc(b"Wolf"); 133 | let expected = vec![ 134 | 88, 44, 94, 74, 0, 99, 7, 77, 68, 35, 47, 78, 19, 21, 50, 15, 42, 36, 91, 11, 85, 39, 135 | 64, 22, 57, 11, 25, 12, 1, 91, 17, 75, 29, 47, 88, 11, 68, 58, 27, 65, 21, 54, 47, 54, 136 | 73, 83, 23, 58, 75, 27, 26, 15, 60, 36, 30, 21, 55, 57, 77, 76, 75, 47, 53, 76, 9, 91, 137 | 14, 69, 3, 95, 11, 73, 20, 99, 68, 61, 3, 98, 36, 98, 56, 65, 14, 80, 74, 57, 63, 68, 138 | 51, 56, 24, 39, 53, 80, 57, 51, 81, 3, 1, 30, 139 | ]; 140 | for e in expected { 141 | assert_eq!(rng.next() % 100, e); 142 | } 143 | } 144 | 145 | #[test] 146 | fn test_rng_3() { 147 | let mut rng = Xoshiro256::from("Wolf"); 148 | let expected = vec![ 149 | 6, 5, 8, 4, 10, 5, 7, 10, 4, 9, 10, 9, 7, 7, 1, 1, 2, 9, 9, 2, 6, 4, 5, 7, 8, 5, 4, 2, 150 | 3, 8, 7, 4, 5, 1, 10, 9, 3, 10, 2, 6, 8, 5, 7, 9, 3, 1, 5, 2, 7, 1, 4, 4, 4, 4, 9, 4, 151 | 5, 5, 6, 9, 5, 1, 2, 8, 3, 3, 2, 8, 4, 3, 2, 1, 10, 8, 9, 3, 10, 8, 5, 5, 6, 7, 10, 5, 152 | 8, 9, 4, 6, 4, 2, 10, 2, 1, 7, 9, 6, 7, 4, 2, 5, 153 | ]; 154 | for e in expected { 155 | assert_eq!(rng.next_int(1, 10), e); 156 | } 157 | } 158 | 159 | #[test] 160 | fn test_shuffle() { 161 | let mut rng = Xoshiro256::from("Wolf"); 162 | let values = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 163 | let expected = vec![ 164 | vec![6, 4, 9, 3, 10, 5, 7, 8, 1, 2], 165 | vec![10, 8, 6, 5, 1, 2, 3, 9, 7, 4], 166 | vec![6, 4, 5, 8, 9, 3, 2, 1, 7, 10], 167 | vec![7, 3, 5, 1, 10, 9, 4, 8, 2, 6], 168 | vec![8, 5, 7, 10, 2, 1, 4, 3, 9, 6], 169 | vec![4, 3, 5, 6, 10, 2, 7, 8, 9, 1], 170 | vec![5, 1, 3, 9, 4, 6, 2, 10, 7, 8], 171 | vec![2, 1, 10, 8, 9, 4, 7, 6, 3, 5], 172 | vec![6, 7, 10, 4, 8, 9, 2, 3, 1, 5], 173 | vec![10, 2, 1, 7, 9, 5, 6, 3, 4, 8], 174 | ]; 175 | for e in expected { 176 | let shuffled = rng.shuffled(values.clone()); 177 | assert_eq!(shuffled, e); 178 | } 179 | } 180 | } 181 | --------------------------------------------------------------------------------