From 89a20044a5417c7154a31282499c4b775716fd6d Mon Sep 17 00:00:00 2001 From: Alois Date: Sat, 27 Jun 2026 03:20:25 +0200 Subject: [PATCH] (feat): rename example-usage to just example (feat): add the example's web-client dist folder to a gitignore (fix): format issues (fix): a lot of duplicate code --- .forgejo/workflows/ci.yml | 30 +- client/src/lib.rs | 167 +-- clippy.toml | 2 + codec/src/data_value.rs | 270 ++--- crypto/src/aead.rs | 18 +- example-usage/web-client/package-lock.json | 1041 ----------------- {example-usage => example}/.cargo/config.toml | 0 {example-usage => example}/.gitignore | 0 {example-usage => example}/Cargo.lock | 6 +- {example-usage => example}/Cargo.toml | 0 {example-usage => example}/client/Cargo.toml | 0 {example-usage => example}/client/src/auth.rs | 0 {example-usage => example}/client/src/main.rs | 4 +- .../client/src/messages.rs | 0 {example-usage => example}/server/Cargo.toml | 0 .../server/src/clients.rs | 0 .../server/src/handlers.rs | 0 {example-usage => example}/server/src/keys.rs | 0 {example-usage => example}/server/src/main.rs | 8 +- {example-usage => example}/server/src/tls.rs | 4 +- {example-usage => example}/type-maps.yaml | 0 example/web-client/.gitignore | 1 + .../web-client/bun.lock | 0 .../web-client/index.html | 0 .../web-client/package.json | 0 .../web-client/public/.gitkeep | 0 .../web-client/src/main.ts | 0 .../web-client/src/messages.ts | 0 .../web-client/tsconfig.json | 0 .../web-client/vite.config.ts | 0 flake.nix | 30 +- host/src/lib.rs | 5 +- package.json | 2 +- transport/src/client.rs | 21 +- transport/tests/integration.rs | 197 +--- type-map/build.rs | 42 +- wasm/pkg/mtp_wasm.d.ts | 6 +- wasm/pkg/mtp_wasm.js | 18 +- wasm/pkg/mtp_wasm_bg.wasm | Bin 377146 -> 374427 bytes wasm/pkg/mtp_wasm_bg.wasm.d.ts | 6 +- wasm/src/client.rs | 216 ++-- wasm/src/message.rs | 37 +- wasm/src/transport.rs | 14 +- 43 files changed, 527 insertions(+), 1618 deletions(-) create mode 100644 clippy.toml delete mode 100644 example-usage/web-client/package-lock.json rename {example-usage => example}/.cargo/config.toml (100%) rename {example-usage => example}/.gitignore (100%) rename {example-usage => example}/Cargo.lock (99%) rename {example-usage => example}/Cargo.toml (100%) rename {example-usage => example}/client/Cargo.toml (100%) rename {example-usage => example}/client/src/auth.rs (100%) rename {example-usage => example}/client/src/main.rs (92%) rename {example-usage => example}/client/src/messages.rs (100%) rename {example-usage => example}/server/Cargo.toml (100%) rename {example-usage => example}/server/src/clients.rs (100%) rename {example-usage => example}/server/src/handlers.rs (100%) rename {example-usage => example}/server/src/keys.rs (100%) rename {example-usage => example}/server/src/main.rs (94%) rename {example-usage => example}/server/src/tls.rs (95%) rename {example-usage => example}/type-maps.yaml (100%) create mode 100644 example/web-client/.gitignore rename {example-usage => example}/web-client/bun.lock (100%) rename {example-usage => example}/web-client/index.html (100%) rename {example-usage => example}/web-client/package.json (100%) rename {example-usage => example}/web-client/public/.gitkeep (100%) rename {example-usage => example}/web-client/src/main.ts (100%) rename {example-usage => example}/web-client/src/messages.ts (100%) rename {example-usage => example}/web-client/tsconfig.json (100%) rename {example-usage => example}/web-client/vite.config.ts (100%) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 3f62f86..3399ddb 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -43,8 +43,18 @@ jobs: - name: Run clippy run: | export PATH="$HOME/.cargo/bin:$PATH" - export MTP_TYPE_MAPS="$PWD/example-usage/type-maps.yaml" - cargo clippy --workspace --exclude mtp-wasm --all-targets --all-features -- -D warnings -W unreachable-pub + export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml" + cargo clippy --workspace --exclude mtp-wasm --all-targets --all-features -- -D warnings \ + -W unreachable-pub \ + -W clippy::cognitive_complexity \ + -W clippy::missing_docs_in_private_items \ + -W clippy::missing_errors_doc \ + -W clippy::missing_panics_doc \ + -W clippy::missing_safety_doc \ + -W clippy::undocumented_unsafe_blocks \ + -W clippy::pedantic \ + -W clippy::restriction \ + -A clippy::blanket_clippy_restriction_lints test: name: test @@ -62,7 +72,7 @@ jobs: - name: Run tests run: | export PATH="$HOME/.cargo/bin:$PATH" - export MTP_TYPE_MAPS="$PWD/example-usage/type-maps.yaml" + export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml" cargo test --workspace --exclude mtp-wasm --all-features wasm: @@ -81,13 +91,13 @@ jobs: - name: Build wasm crate run: | export PATH="$HOME/.cargo/bin:$PATH" - export MTP_TYPE_MAPS="$PWD/example-usage/type-maps.yaml" + export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml" cargo build -p mtp-wasm --target wasm32-unknown-unknown env: RUSTFLAGS: --cfg web_sys_unstable_apis - example-usage: - name: example usage + example: + name: example runs-on: docker steps: - uses: https://data.forgejo.org/actions/checkout@v4 @@ -100,7 +110,7 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - name: Check example usage workspace - working-directory: example-usage + working-directory: example run: | export PATH="$HOME/.cargo/bin:$PATH" export MTP_TYPE_MAPS="$PWD/type-maps.yaml" @@ -194,7 +204,7 @@ jobs: - name: Build wasm package run: | export PATH="$HOME/.cargo/bin:$PATH" - export MTP_TYPE_MAPS="$PWD/example-usage/type-maps.yaml" + export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml" wasm-pack build wasm --target web env: RUSTFLAGS: --cfg web_sys_unstable_apis @@ -204,13 +214,13 @@ jobs: curl -fsSL https://bun.sh/install | bash - name: Install dependencies - working-directory: example-usage/web-client + working-directory: example/web-client run: | export PATH="$HOME/.bun/bin:$PATH" bun install --frozen-lockfile - name: Build web client - working-directory: example-usage/web-client + working-directory: example/web-client run: | export PATH="$HOME/.bun/bin:$PATH" bun run build diff --git a/client/src/lib.rs b/client/src/lib.rs index ea42ddb..1627d63 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -185,6 +185,75 @@ fn check_connected( } } +#[cfg(feature = "crypto")] +fn signed_challenge_response( + keys: &mtp_crypto::Keyring, + proof_payload: &[u8], + client_nonce: u128, +) -> Result { + use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme}; + + let signer = Ed25519Signer::new(&keys.sig_cl_secret_key) + .map_err(|e| CommunicationError::Other(e.to_string()))?; + let signature = signer + .sign(proof_payload) + .map_err(|e| CommunicationError::Other(e.to_string()))?; + + let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse) + .add_typed_default( + DataType::ClientNonce, + DataValue::UnsignedNumber(client_nonce), + ) + .add_typed_default(DataType::Signature, DataValue::Bytes(signature)); + + if !keys.sig_pq_secret_key.as_bytes().is_empty() { + let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key) + .map_err(|e| CommunicationError::Other(e.to_string()))?; + let pq_signature = pq_signer + .sign(proof_payload) + .map_err(|e| CommunicationError::Other(e.to_string()))?; + proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); + } + + Ok(proof) +} + +#[cfg(feature = "crypto")] +async fn receive_verified_challenge( + receiver: &Receiver, + tm: &mtp_codec::TypeMap, + host_public_key_bundle: &mtp_crypto::PublicKeyBundle, + bound_id: u64, + context: &str, +) -> Result { + let challenge = receiver.receive().await?; + let expected = mtp_codec::CommunicationType::Challenge.to_id(tm); + if challenge.get_type() != expected { + return Err(unexpected_response_type_error( + context, expected, &challenge, + )); + } + + let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(tm)) { + DataValue::UnsignedNumber(n) => *n, + _ => { + return Err(CommunicationError::AuthenticationFailed( + "Missing server challenge".into(), + )); + } + }; + + verify_host_challenge( + &challenge, + tm, + host_public_key_bundle, + bound_id, + server_challenge, + )?; + + Ok(server_challenge) +} + #[cfg(feature = "crypto")] impl MTPClient { pub async fn auth_connect( @@ -192,7 +261,7 @@ impl MTPClient { keys: &mtp_crypto::Keyring, host_public_key_bundle: &mtp_crypto::PublicKeyBundle, ) -> Result { - use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth}; + use mtp_crypto::auth; let (sender, receiver) = mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; @@ -210,30 +279,14 @@ impl MTPClient { sender.send(&ident).await?; // 2. Receive and verify the host's challenge. - let challenge = receiver.receive().await?; - let expected = mtp_codec::CommunicationType::Challenge.to_id(&tm); - if challenge.get_type() != expected { - return Err(unexpected_response_type_error( - "auth_connect challenge", - expected, - &challenge, - )); - } - let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Missing server challenge".into(), - )); - } - }; - verify_host_challenge( - &challenge, + let server_challenge = receive_verified_challenge( + &receiver, &tm, host_public_key_bundle, config.client_id, - server_challenge, - )?; + "auth_connect challenge", + ) + .await?; // 3. Sign the host's challenge and send the proof. let client_nonce: u128 = rand::random(); @@ -244,26 +297,7 @@ impl MTPClient { client_nonce, ); - let signer = Ed25519Signer::new(&keys.sig_cl_secret_key) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - let signature = signer - .sign(&proof_payload) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - - let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ) - .add_typed_default(DataType::Signature, DataValue::Bytes(signature)); - if !keys.sig_pq_secret_key.as_bytes().is_empty() { - let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - let pq_signature = pq_signer - .sign(&proof_payload) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); - } + let proof = signed_challenge_response(keys, &proof_payload, client_nonce)?; sender.send(&proof).await?; // 4. Receive and verify the host's final confirmation. @@ -300,7 +334,7 @@ impl MTPClient { keys: &mtp_crypto::Keyring, host_public_key_bundle: &mtp_crypto::PublicKeyBundle, ) -> Result { - use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth}; + use mtp_crypto::auth; let (sender, receiver) = mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; @@ -317,50 +351,21 @@ impl MTPClient { sender.send(®ister).await?; // 2. Receive and verify the host's challenge (register binds id = 0). - let challenge = receiver.receive().await?; - let expected = mtp_codec::CommunicationType::Challenge.to_id(&tm); - if challenge.get_type() != expected { - return Err(unexpected_response_type_error( - "auth_register challenge", - expected, - &challenge, - )); - } - let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Missing server challenge".into(), - )); - } - }; - verify_host_challenge(&challenge, &tm, host_public_key_bundle, 0, server_challenge)?; + let server_challenge = receive_verified_challenge( + &receiver, + &tm, + host_public_key_bundle, + 0, + "auth_register challenge", + ) + .await?; // 3. Sign the host's challenge over the bundle and send the proof. let client_nonce: u128 = rand::random(); let proof_payload = auth::register_proof_payload(&version_str, &pk_bytes, server_challenge, client_nonce); - let signer = Ed25519Signer::new(&keys.sig_cl_secret_key) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - let signature = signer - .sign(&proof_payload) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - - let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ) - .add_typed_default(DataType::Signature, DataValue::Bytes(signature)); - if !keys.sig_pq_secret_key.as_bytes().is_empty() { - let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - let pq_signature = pq_signer - .sign(&proof_payload) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); - } + let proof = signed_challenge_response(keys, &proof_payload, client_nonce)?; sender.send(&proof).await?; // 4. Receive the host's final confirmation; extract the assigned id and diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..881b50f --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +cognitive-complexity-threshold = 25 +avoid-breaking-exported-api = false diff --git a/codec/src/data_value.rs b/codec/src/data_value.rs index 886fe71..5f4aced 100644 --- a/codec/src/data_value.rs +++ b/codec/src/data_value.rs @@ -477,8 +477,7 @@ impl DataValue { let kind = Self::kind_marker(value); buf.push(kind); - if kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE || kind == Self::KIND_NULL - { + if Self::kind_has_no_payload(kind) { buf.write_u16::(key.0) .map_err(|_| CodecError::InvalidEncoding)?; return Ok(()); @@ -513,8 +512,7 @@ impl DataValue { let kind = Self::kind_marker(value); buf.push(kind); - if kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE || kind == Self::KIND_NULL - { + if Self::kind_has_no_payload(kind) { return Ok(()); } @@ -625,18 +623,9 @@ impl DataValue { for _ in 0..count { let kind = cursor.read_u8().ok()?; - if kind == Self::KIND_BOOL_TRUE - || kind == Self::KIND_BOOL_FALSE - || kind == Self::KIND_NULL - { + if Self::kind_has_no_payload(kind) { let key = DataTypeId(cursor.read_u16::().ok()?); - let value = if kind == Self::KIND_BOOL_TRUE { - DataValue::BoolTrue - } else if kind == Self::KIND_BOOL_FALSE { - DataValue::BoolFalse - } else { - DataValue::Null - }; + let value = Self::read_payloadless_value(kind)?; entries.push((key, value)); continue; } @@ -644,20 +633,12 @@ impl DataValue { let len = cursor.read_u32::().ok()? as usize; let key = DataTypeId(cursor.read_u16::().ok()?); - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - - let payload = &cursor.get_ref()[start..end]; + let payload = Self::read_payload_slice(cursor, len)?; let mut inner = Cursor::new(payload); let value = Self::read_value_by_kind(&mut inner, kind, Some(len))?; if inner.position() as usize != len { return None; } - - cursor.set_position(end as u64); entries.push((key, value)); } @@ -675,36 +656,19 @@ impl DataValue { for _ in 0..count { let kind = cursor.read_u8().ok()?; - if kind == Self::KIND_BOOL_TRUE - || kind == Self::KIND_BOOL_FALSE - || kind == Self::KIND_NULL - { - let value = if kind == Self::KIND_BOOL_TRUE { - DataValue::BoolTrue - } else if kind == Self::KIND_BOOL_FALSE { - DataValue::BoolFalse - } else { - DataValue::Null - }; + if Self::kind_has_no_payload(kind) { + let value = Self::read_payloadless_value(kind)?; out.push(value); continue; } let len = cursor.read_u32::().ok()? as usize; - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - - let payload = &cursor.get_ref()[start..end]; + let payload = Self::read_payload_slice(cursor, len)?; let mut inner = Cursor::new(payload); let value = Self::read_value_by_kind(&mut inner, kind, Some(len))?; if inner.position() as usize != len { return None; } - - cursor.set_position(end as u64); out.push(value); } @@ -731,95 +695,45 @@ impl DataValue { Some(DataValue::Float(a, b)) } Self::KIND_STR => { - let len = payload_len?; - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let s = std::str::from_utf8(&cursor.get_ref()[start..end]) + let s = std::str::from_utf8(Self::read_payload_slice(cursor, payload_len?)?) .ok()? .to_string(); - cursor.set_position(end as u64); Some(DataValue::Str(s)) } - Self::KIND_BYTES => { - let len = payload_len?; - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let b = cursor.get_ref()[start..end].to_vec(); - cursor.set_position(end as u64); - Some(DataValue::Bytes(b)) - } + Self::KIND_BYTES => Some(DataValue::Bytes(Self::read_blob_payload( + cursor, + payload_len?, + )?)), Self::KIND_ARRAY => { - let start = cursor.position() as usize; let len = payload_len?; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let mut inner = Cursor::new(&cursor.get_ref()[start..end]); + let mut inner = Cursor::new(Self::read_payload_slice(cursor, len)?); let arr = Self::read_array(&mut inner)?; if inner.position() as usize != len { return None; } - cursor.set_position(end as u64); Some(arr) } Self::KIND_CONTAINER => { - let start = cursor.position() as usize; let len = payload_len?; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let mut inner = Cursor::new(&cursor.get_ref()[start..end]); + let mut inner = Cursor::new(Self::read_payload_slice(cursor, len)?); let c = Self::try_read_container(&mut inner)?; if inner.position() as usize != len { return None; } - cursor.set_position(end as u64); Some(c) } #[cfg(feature = "crypto")] - Self::KIND_ENCRYPTED_CONTAINER => { - let len = payload_len?; - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let data = cursor.get_ref()[start..end].to_vec(); - cursor.set_position(end as u64); - Some(DataValue::EncryptedContainer(data)) - } + Self::KIND_ENCRYPTED_CONTAINER => Some(DataValue::EncryptedContainer( + Self::read_blob_payload(cursor, payload_len?)?, + )), #[cfg(feature = "crypto")] - Self::KIND_SIGNED_CONTAINER => { - let len = payload_len?; - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let data = cursor.get_ref()[start..end].to_vec(); - cursor.set_position(end as u64); - Some(DataValue::SignedContainer(data)) - } + Self::KIND_SIGNED_CONTAINER => Some(DataValue::SignedContainer( + Self::read_blob_payload(cursor, payload_len?)?, + )), #[cfg(feature = "crypto")] - Self::KIND_SIGNED_ENCRYPTED_CONTAINER => { - let len = payload_len?; - let start = cursor.position() as usize; - let end = start.checked_add(len)?; - if end > cursor.get_ref().len() { - return None; - } - let data = cursor.get_ref()[start..end].to_vec(); - cursor.set_position(end as u64); - Some(DataValue::SignedEncryptedContainer(data)) - } + Self::KIND_SIGNED_ENCRYPTED_CONTAINER => Some(DataValue::SignedEncryptedContainer( + Self::read_blob_payload(cursor, payload_len?)?, + )), Self::KIND_NULL => Some(DataValue::Null), #[cfg(not(feature = "crypto"))] 0x0A | 0x0B | 0x0C => None, @@ -854,6 +768,33 @@ impl DataValue { DataValue::Null => Self::KIND_NULL, } } + + fn kind_has_no_payload(kind: u8) -> bool { + kind == Self::KIND_BOOL_TRUE || kind == Self::KIND_BOOL_FALSE || kind == Self::KIND_NULL + } + + fn read_payloadless_value(kind: u8) -> Option { + match kind { + Self::KIND_BOOL_TRUE => Some(DataValue::BoolTrue), + Self::KIND_BOOL_FALSE => Some(DataValue::BoolFalse), + Self::KIND_NULL => Some(DataValue::Null), + _ => None, + } + } + + fn read_payload_slice<'a>(cursor: &mut Cursor<&'a [u8]>, len: usize) -> Option<&'a [u8]> { + let start = cursor.position() as usize; + let end = start.checked_add(len)?; + if end > cursor.get_ref().len() { + return None; + } + cursor.set_position(end as u64); + Some(&cursor.get_ref()[start..end]) + } + + fn read_blob_payload(cursor: &mut Cursor<&[u8]>, len: usize) -> Option> { + Some(Self::read_payload_slice(cursor, len)?.to_vec()) + } } impl fmt::Display for DataValue { @@ -1003,9 +944,18 @@ mod tests { container_roundtrip(vec![ (DataType::Version.to_id(&tm), DataValue::SignedNumber(0)), (DataType::Id.to_id(&tm), DataValue::SignedNumber(42)), - (DataType::ClientNonce.to_id(&tm), DataValue::SignedNumber(-42)), - (DataType::ServerNonce.to_id(&tm), DataValue::SignedNumber(i128::MAX)), - (DataType::PublicKeys.to_id(&tm), DataValue::SignedNumber(i128::MIN)), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::SignedNumber(-42), + ), + ( + DataType::ServerNonce.to_id(&tm), + DataValue::SignedNumber(i128::MAX), + ), + ( + DataType::PublicKeys.to_id(&tm), + DataValue::SignedNumber(i128::MIN), + ), ]); } @@ -1015,7 +965,10 @@ mod tests { container_roundtrip(vec![ (DataType::Version.to_id(&tm), DataValue::UnsignedNumber(0)), (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)), - (DataType::ClientNonce.to_id(&tm), DataValue::UnsignedNumber(u128::MAX)), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::UnsignedNumber(u128::MAX), + ), ]); } @@ -1025,7 +978,10 @@ mod tests { container_roundtrip(vec![ (DataType::Version.to_id(&tm), DataValue::Float(0, 0)), (DataType::Id.to_id(&tm), DataValue::Float(2, 12345)), - (DataType::ClientNonce.to_id(&tm), DataValue::Float(255, 4294967295)), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::Float(255, 4294967295), + ), ]); } @@ -1035,7 +991,10 @@ mod tests { container_roundtrip(vec![ (DataType::Version.to_id(&tm), DataValue::Str(String::new())), (DataType::Id.to_id(&tm), DataValue::Str("hello".to_string())), - (DataType::ClientNonce.to_id(&tm), DataValue::Str("a".repeat(1000))), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::Str("a".repeat(1000)), + ), ]); } @@ -1044,8 +1003,14 @@ mod tests { let tm = TypeMap::latest(); container_roundtrip(vec![ (DataType::Version.to_id(&tm), DataValue::Bytes(vec![])), - (DataType::Id.to_id(&tm), DataValue::Bytes(vec![0x00, 0xFF, 0xAB])), - (DataType::ClientNonce.to_id(&tm), DataValue::Bytes(vec![0x42; 100])), + ( + DataType::Id.to_id(&tm), + DataValue::Bytes(vec![0x00, 0xFF, 0xAB]), + ), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::Bytes(vec![0x42; 100]), + ), ]); } @@ -1084,8 +1049,14 @@ mod tests { container_roundtrip(vec![ (DataType::Version.to_id(&tm), DataValue::BoolTrue), (DataType::Id.to_id(&tm), DataValue::SignedNumber(-100)), - (DataType::ClientNonce.to_id(&tm), DataValue::Str("test".to_string())), - (DataType::ServerNonce.to_id(&tm), DataValue::UnsignedNumber(u128::MAX)), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::Str("test".to_string()), + ), + ( + DataType::ServerNonce.to_id(&tm), + DataValue::UnsignedNumber(u128::MAX), + ), (DataType::PublicKeys.to_id(&tm), DataValue::Null), ]); } @@ -1141,27 +1112,40 @@ mod tests { fn test_as_accessors() { let tm = TypeMap::latest(); let dv = DataValue::Container(vec![ - (DataType::Version.to_id(&tm), DataValue::Str("alice".to_string())), + ( + DataType::Version.to_id(&tm), + DataValue::Str("alice".to_string()), + ), (DataType::Id.to_id(&tm), DataValue::SignedNumber(42)), - (DataType::ClientNonce.to_id(&tm), DataValue::Bytes(vec![0x01, 0x02])), - (DataType::ServerNonce.to_id(&tm), DataValue::Array(vec![DataValue::BoolTrue])), + ( + DataType::ClientNonce.to_id(&tm), + DataValue::Bytes(vec![0x01, 0x02]), + ), + ( + DataType::ServerNonce.to_id(&tm), + DataValue::Array(vec![DataValue::BoolTrue]), + ), ]); let map = dv.as_map().expect("should be a container"); assert_eq!( - map.get(&DataType::Version.to_id(&tm)).and_then(|v| v.as_str()), + map.get(&DataType::Version.to_id(&tm)) + .and_then(|v| v.as_str()), Some("alice") ); assert_eq!( - map.get(&DataType::Id.to_id(&tm)).and_then(|v| v.as_signed_number()), + map.get(&DataType::Id.to_id(&tm)) + .and_then(|v| v.as_signed_number()), Some(42) ); assert_eq!( - map.get(&DataType::ClientNonce.to_id(&tm)).and_then(|v| v.as_bytes()), + map.get(&DataType::ClientNonce.to_id(&tm)) + .and_then(|v| v.as_bytes()), Some(vec![0x01, 0x02]) ); assert_eq!( - map.get(&DataType::ServerNonce.to_id(&tm)).and_then(|v| v.as_array()), + map.get(&DataType::ServerNonce.to_id(&tm)) + .and_then(|v| v.as_array()), Some(vec![DataValue::BoolTrue]) ); } @@ -1206,7 +1190,10 @@ mod tests { #[test] fn test_truncated_container_rejected() { let tm = TypeMap::latest(); - let dv = DataValue::Container(vec![(DataType::Version.to_id(&tm), DataValue::Str("hello".to_string()))]); + let dv = DataValue::Container(vec![( + DataType::Version.to_id(&tm), + DataValue::Str("hello".to_string()), + )]); let bytes = dv.to_bytes().expect("encode failed"); // Truncate to fewer than 2 bytes so neither container nor array can be read assert!(DataValue::from_bytes(&bytes[..1]).is_none()); @@ -1264,8 +1251,14 @@ mod tests { fn test_container_display() { let tm = TypeMap::latest(); let dv = DataValue::Container(vec![ - (DataType::ServerNonce.to_id(&tm), DataValue::Str("v2.0".to_string())), - (DataType::PqSignature.to_id(&tm), DataValue::UnsignedNumber(42)), + ( + DataType::ServerNonce.to_id(&tm), + DataValue::Str("v2.0".to_string()), + ), + ( + DataType::PqSignature.to_id(&tm), + DataValue::UnsignedNumber(42), + ), ]); let s = format!("{}", dv); assert!(s.contains("3:")); @@ -1290,7 +1283,10 @@ mod tests { let bundle = keyring.public_key_bundle(); let mut dv = DataValue::Container(vec![ - (DataType::Version.to_id(&tm), DataValue::Str("secret".to_string())), + ( + DataType::Version.to_id(&tm), + DataValue::Str("secret".to_string()), + ), (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)), ]); @@ -1315,8 +1311,10 @@ mod tests { let keyring_a = Keyring::generate(); let keyring_b = Keyring::generate(); - let mut dv = - DataValue::Container(vec![(DataType::Version.to_id(&tm), DataValue::Str("secret".to_string()))]); + let mut dv = DataValue::Container(vec![( + DataType::Version.to_id(&tm), + DataValue::Str("secret".to_string()), + )]); assert!( dv.encrypt_container( @@ -1336,8 +1334,10 @@ mod tests { let tm = TypeMap::latest(); let keyring = Keyring::generate(); - let mut dv = - DataValue::Container(vec![(DataType::Version.to_id(&tm), DataValue::Str("secret".to_string()))]); + let mut dv = DataValue::Container(vec![( + DataType::Version.to_id(&tm), + DataValue::Str("secret".to_string()), + )]); assert!( dv.encrypt_container( diff --git a/crypto/src/aead.rs b/crypto/src/aead.rs index 3729ebd..d708d7f 100644 --- a/crypto/src/aead.rs +++ b/crypto/src/aead.rs @@ -18,6 +18,14 @@ pub trait AeadCipher: AeadEncrypt + AeadDecrypt { fn key_size() -> usize; } +#[cfg(any(feature = "chacha20poly1305", feature = "aes-gcm"))] +fn prepend_nonce(nonce: &[u8], ciphertext: &mut Vec) -> Vec { + let mut out = Vec::with_capacity(nonce.len() + ciphertext.len()); + out.extend_from_slice(nonce); + out.append(ciphertext); + out +} + #[cfg(feature = "chacha20poly1305")] pub struct ChaCha20Poly1305 { key: [u8; 32], @@ -53,10 +61,7 @@ impl AeadEncrypt for ChaCha20Poly1305 { .encrypt(nonce_ref, payload) .map_err(|_| CryptoError::EncryptionFailed)?; - let mut out = Vec::with_capacity(nonce.len() + ciphertext.len()); - out.extend_from_slice(&nonce); - out.append(&mut ciphertext); - Ok(out) + Ok(prepend_nonce(&nonce, &mut ciphertext)) } } @@ -126,10 +131,7 @@ impl AeadEncrypt for Aes256Gcm { .encrypt(nonce_ref, payload) .map_err(|_| CryptoError::EncryptionFailed)?; - let mut out = Vec::with_capacity(nonce.len() + ciphertext.len()); - out.extend_from_slice(&nonce); - out.append(&mut ciphertext); - Ok(out) + Ok(prepend_nonce(&nonce, &mut ciphertext)) } } diff --git a/example-usage/web-client/package-lock.json b/example-usage/web-client/package-lock.json deleted file mode 100644 index f83ec9b..0000000 --- a/example-usage/web-client/package-lock.json +++ /dev/null @@ -1,1041 +0,0 @@ -{ - "name": "mtp-web-client", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mtp-web-client", - "version": "0.1.0", - "devDependencies": { - "typescript": "^5.4.0", - "vite": "^5.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - } - } -} diff --git a/example-usage/.cargo/config.toml b/example/.cargo/config.toml similarity index 100% rename from example-usage/.cargo/config.toml rename to example/.cargo/config.toml diff --git a/example-usage/.gitignore b/example/.gitignore similarity index 100% rename from example-usage/.gitignore rename to example/.gitignore diff --git a/example-usage/Cargo.lock b/example/Cargo.lock similarity index 99% rename from example-usage/Cargo.lock rename to example/Cargo.lock index 48f612f..104fc8b 100644 --- a/example-usage/Cargo.lock +++ b/example/Cargo.lock @@ -908,7 +908,6 @@ dependencies = [ "mtp-crypto", "mtp-transport", "rand 0.8.6", - "tokio", ] [[package]] @@ -953,12 +952,12 @@ dependencies = [ name = "mtp-host" version = "0.1.0" dependencies = [ + "log", "mtp-codec", "mtp-common", "mtp-crypto", "mtp-transport", "rand 0.8.6", - "tokio", ] [[package]] @@ -968,10 +967,8 @@ dependencies = [ "log", "mtp-codec", "mtp-common", - "quinn", "rustls", "rustls-native-certs", - "thiserror", "tokio", "wtransport", ] @@ -1553,7 +1550,6 @@ dependencies = [ "hex", "mtp", "rcgen", - "serde_core", "serde_json", "tokio", ] diff --git a/example-usage/Cargo.toml b/example/Cargo.toml similarity index 100% rename from example-usage/Cargo.toml rename to example/Cargo.toml diff --git a/example-usage/client/Cargo.toml b/example/client/Cargo.toml similarity index 100% rename from example-usage/client/Cargo.toml rename to example/client/Cargo.toml diff --git a/example-usage/client/src/auth.rs b/example/client/src/auth.rs similarity index 100% rename from example-usage/client/src/auth.rs rename to example/client/src/auth.rs diff --git a/example-usage/client/src/main.rs b/example/client/src/main.rs similarity index 92% rename from example-usage/client/src/main.rs rename to example/client/src/main.rs index b822805..a9931b0 100644 --- a/example-usage/client/src/main.rs +++ b/example/client/src/main.rs @@ -9,8 +9,8 @@ use mtp::crypto::{KemPublicKey, PublicKeyBundle, SignaturePqPublicKey, Signature fn dev_cert_path() -> String { std::env::var("MTP_DEV_CERT").unwrap_or_else(|_| { - if Path::new("example-usage/dev-cert/cert.pem").exists() { - "example-usage/dev-cert/cert.pem".to_string() + if Path::new("example/dev-cert/cert.pem").exists() { + "example/dev-cert/cert.pem".to_string() } else { "dev-cert/cert.pem".to_string() } diff --git a/example-usage/client/src/messages.rs b/example/client/src/messages.rs similarity index 100% rename from example-usage/client/src/messages.rs rename to example/client/src/messages.rs diff --git a/example-usage/server/Cargo.toml b/example/server/Cargo.toml similarity index 100% rename from example-usage/server/Cargo.toml rename to example/server/Cargo.toml diff --git a/example-usage/server/src/clients.rs b/example/server/src/clients.rs similarity index 100% rename from example-usage/server/src/clients.rs rename to example/server/src/clients.rs diff --git a/example-usage/server/src/handlers.rs b/example/server/src/handlers.rs similarity index 100% rename from example-usage/server/src/handlers.rs rename to example/server/src/handlers.rs diff --git a/example-usage/server/src/keys.rs b/example/server/src/keys.rs similarity index 100% rename from example-usage/server/src/keys.rs rename to example/server/src/keys.rs diff --git a/example-usage/server/src/main.rs b/example/server/src/main.rs similarity index 94% rename from example-usage/server/src/main.rs rename to example/server/src/main.rs index 28d1ff5..01a748e 100644 --- a/example-usage/server/src/main.rs +++ b/example/server/src/main.rs @@ -9,15 +9,15 @@ use std::path::Path; fn dev_cert_paths() -> (String, String) { let cert = std::env::var("MTP_DEV_CERT").unwrap_or_else(|_| { - if Path::new("example-usage/dev-cert/cert.pem").exists() { - "example-usage/dev-cert/cert.pem".to_string() + if Path::new("example/dev-cert/cert.pem").exists() { + "example/dev-cert/cert.pem".to_string() } else { "dev-cert/cert.pem".to_string() } }); let key = std::env::var("MTP_DEV_KEY").unwrap_or_else(|_| { - if Path::new("example-usage/dev-cert/key.pem").exists() { - "example-usage/dev-cert/key.pem".to_string() + if Path::new("example/dev-cert/key.pem").exists() { + "example/dev-cert/key.pem".to_string() } else { "dev-cert/key.pem".to_string() } diff --git a/example-usage/server/src/tls.rs b/example/server/src/tls.rs similarity index 95% rename from example-usage/server/src/tls.rs rename to example/server/src/tls.rs index b312100..a119b5d 100644 --- a/example-usage/server/src/tls.rs +++ b/example/server/src/tls.rs @@ -52,7 +52,7 @@ pub fn export_webtransport_cert_hash(hash: &str) -> Result<(), Box Result<(), Box v, None => { log::warn!("accept: negotiate failed for client version {client_version:?}"); diff --git a/package.json b/package.json index 80cea8e..06100bd 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "wasm/pkg/mtp_wasm_bg.wasm.d.ts" ], "scripts": { - "build": "MTP_TYPE_MAPS=$PWD/example-usage/type-maps.yaml RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build wasm --target web --out-dir pkg --release", + "build": "MTP_TYPE_MAPS=$PWD/example/type-maps.yaml RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build wasm --target web --out-dir pkg --release", "build:all": "nix run .#build-all", "dup": "jscpd --pattern '**/*.rs' --ignore 'target/**' --ignore 'wasm/pkg/**' --ignore '.git/**' --min-lines 8 --min-tokens 80 --threshold 4 --reporters console --no-tips ." }, diff --git a/transport/src/client.rs b/transport/src/client.rs index 811f5d7..115b37e 100644 --- a/transport/src/client.rs +++ b/transport/src/client.rs @@ -52,19 +52,7 @@ fn configure_client_with_cert( .map_err(|_| CommunicationError::CertificateParseFailed)?; } - let mut tls_config = RustlsClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - tls_config.alpn_protocols = vec![b"h3".to_vec()]; - - Ok(ClientConfig::builder() - .with_bind_default() - .with_custom_tls(tls_config) - .keep_alive_interval(policy.keep_alive_interval) - .max_idle_timeout(policy.max_idle_timeout) - .map_err(|e| CommunicationError::Other(e.to_string()))? - .build()) + client_config_from_roots(root_store, policy) } fn configure_client_system_roots(policy: &Policy) -> Result { @@ -77,6 +65,13 @@ fn configure_client_system_roots(policy: &Policy) -> Result Result { let mut tls_config = RustlsClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(); diff --git a/transport/tests/integration.rs b/transport/tests/integration.rs index db96e8f..73fc824 100644 --- a/transport/tests/integration.rs +++ b/transport/tests/integration.rs @@ -1,7 +1,7 @@ use std::net::{IpAddr, Ipv4Addr}; -use mtp_codec::{CommunicationType, DataType, TypeMap}; -use mtp_transport::{Policy, connect, host}; +use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; +use mtp_transport::{Host, Policy, Receiver, Sender, connect, host}; fn generate_self_signed_cert() -> (Vec, Vec) { let key_pair = rcgen::KeyPair::generate().unwrap(); @@ -13,10 +13,8 @@ fn generate_self_signed_cert() -> (Vec, Vec) { (cert_pem.into_bytes(), key_pem.into_bytes()) } -#[tokio::test] -async fn test_host_start_and_stop() { - let (cert_pem, key_pem) = generate_self_signed_cert(); - let h = host( +async fn start_test_host(cert_pem: Vec, key_pem: Vec) -> Host { + host( IpAddr::V4(Ipv4Addr::LOCALHOST), 0, cert_pem, @@ -24,7 +22,48 @@ async fn test_host_start_and_stop() { Policy::default(), ) .await - .unwrap(); + .unwrap() +} + +async fn connect_to_host(h: &Host, cert_pem: Vec) -> (Sender, Receiver) { + let url = format!("https://127.0.0.1:{}", h.local_addr().port()); + connect(&url, Some(cert_pem), Policy::default()) + .await + .unwrap() +} + +async fn connected_pair() -> (Host, Sender, Receiver, Sender, Receiver) { + let (cert_pem, key_pem) = generate_self_signed_cert(); + let mut h = start_test_host(cert_pem.clone(), key_pem).await; + let (client_tx, client_rx) = connect_to_host(&h, cert_pem).await; + let (host_tx, host_rx) = h.next().await.unwrap(); + (h, client_tx, client_rx, host_tx, host_rx) +} + +fn numbered_message(comm_type: CommunicationType, value: u128, tm: &TypeMap) -> CommunicationValue { + CommunicationValue::new(comm_type).add_data( + DataType::PqSignature.to_id(tm), + DataValue::UnsignedNumber(value), + ) +} + +fn assert_numbered_message( + message: &CommunicationValue, + comm_type: CommunicationType, + value: u128, + tm: &TypeMap, +) { + assert_eq!(message.get_type(), comm_type.to_id(tm)); + assert_eq!( + message.get_data(DataType::PqSignature.to_id(tm)).clone(), + DataValue::UnsignedNumber(value) + ); +} + +#[tokio::test] +async fn test_host_start_and_stop() { + let (cert_pem, key_pem) = generate_self_signed_cert(); + let h = start_test_host(cert_pem, key_pem).await; let addr = h.local_addr(); // Port should be non-zero (OS-assigned) assert!(addr.port() > 0); @@ -32,58 +71,25 @@ async fn test_host_start_and_stop() { #[tokio::test] async fn test_send_receive_roundtrip() { - let (cert_pem, key_pem) = generate_self_signed_cert(); - let mut h = host( - IpAddr::V4(Ipv4Addr::LOCALHOST), - 0, - cert_pem.clone(), - key_pem, - Policy::default(), - ) - .await - .unwrap(); - let addr = h.local_addr(); - - let url = format!("https://127.0.0.1:{}", addr.port()); - let (client_tx, client_rx) = connect(&url, Some(cert_pem), Policy::default()) - .await - .unwrap(); - - // Accept on host side - let (host_tx, host_rx) = h.next().await.unwrap(); + let (_h, client_tx, client_rx, host_tx, host_rx) = connected_pair().await; let tm = TypeMap::latest(); // Client sends a simple message - let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping).add_data( - DataType::PqSignature.to_id(&tm), - mtp_codec::DataValue::UnsignedNumber(42), - ); + let msg = numbered_message(CommunicationType::Ping, 42, &tm); client_tx.send(&msg).await.unwrap(); // Host receives it let received = host_rx.receive().await.unwrap(); - assert_eq!(received.get_type(), CommunicationType::Ping.to_id(&tm)); - let val = received.get_data(DataType::PqSignature.to_id(&tm)).clone(); - assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(42)); + assert_numbered_message(&received, CommunicationType::Ping, 42, &tm); // Host sends a response - let resp = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data( - DataType::PqSignature.to_id(&tm), - mtp_codec::DataValue::UnsignedNumber(99), - ); + let resp = numbered_message(CommunicationType::Pong, 99, &tm); host_tx.send(&resp).await.unwrap(); // Client receives it let client_received = client_rx.receive().await.unwrap(); - assert_eq!( - client_received.get_type(), - CommunicationType::Pong.to_id(&tm) - ); - let client_val = client_received - .get_data(DataType::PqSignature.to_id(&tm)) - .clone(); - assert_eq!(client_val, mtp_codec::DataValue::UnsignedNumber(99)); + assert_numbered_message(&client_received, CommunicationType::Pong, 99, &tm); // Close both sides client_tx.close(); @@ -92,56 +98,31 @@ async fn test_send_receive_roundtrip() { #[tokio::test] async fn test_concurrent_messages() { - let (cert_pem, key_pem) = generate_self_signed_cert(); - let mut h = host( - IpAddr::V4(Ipv4Addr::LOCALHOST), - 0, - cert_pem.clone(), - key_pem, - Policy::default(), - ) - .await - .unwrap(); - let addr = h.local_addr(); - - let url = format!("https://127.0.0.1:{}", addr.port()); - let (client_tx, _client_rx) = connect(&url, Some(cert_pem), Policy::default()) - .await - .unwrap(); - - let (_host_tx, host_rx) = h.next().await.unwrap(); + let (_h, client_tx, _client_rx, _host_tx, host_rx) = connected_pair().await; let tm = TypeMap::latest(); // Send 5 messages in sequence for i in 0..5u128 { - let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping).add_data( - DataType::PqSignature.to_id(&tm), - mtp_codec::DataValue::UnsignedNumber(i), - ); + let msg = numbered_message(CommunicationType::Ping, i, &tm); client_tx.send(&msg).await.unwrap(); } // Receive all 5 in order for i in 0..5u128 { let received = host_rx.receive().await.unwrap(); - let val = received.get_data(DataType::PqSignature.to_id(&tm)).clone(); - assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(i)); + assert_numbered_message(&received, CommunicationType::Ping, i, &tm); } // Send 3 responses back for i in 0..3u128 { - let msg = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data( - DataType::PqSignature.to_id(&tm), - mtp_codec::DataValue::UnsignedNumber(i * 10), - ); + let msg = numbered_message(CommunicationType::Pong, i * 10, &tm); client_tx.send(&msg).await.unwrap(); } for i in 0..3u128 { let received = host_rx.receive().await.unwrap(); - let val = received.get_data(DataType::PqSignature.to_id(&tm)).clone(); - assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(i * 10)); + assert_numbered_message(&received, CommunicationType::Pong, i * 10, &tm); } client_tx.close(); @@ -149,27 +130,10 @@ async fn test_concurrent_messages() { #[tokio::test] async fn test_close_detection() { - let (cert_pem, key_pem) = generate_self_signed_cert(); - let mut h = host( - IpAddr::V4(Ipv4Addr::LOCALHOST), - 0, - cert_pem.clone(), - key_pem, - Policy::default(), - ) - .await - .unwrap(); - let addr = h.local_addr(); - - let url = format!("https://127.0.0.1:{}", addr.port()); - let (client_tx, _client_rx) = connect(&url, Some(cert_pem), Policy::default()) - .await - .unwrap(); - - let (_host_tx, host_rx) = h.next().await.unwrap(); + let (_h, client_tx, _client_rx, _host_tx, host_rx) = connected_pair().await; // Send a message then close - let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping); + let msg = CommunicationValue::new(CommunicationType::Ping); client_tx.send(&msg).await.unwrap(); client_tx.close(); @@ -186,17 +150,8 @@ async fn test_close_detection() { #[tokio::test] async fn test_host_shutdown_stops_accepting() { let (cert_pem, key_pem) = generate_self_signed_cert(); - let mut h = host( - IpAddr::V4(Ipv4Addr::LOCALHOST), - 0, - cert_pem.clone(), - key_pem, - Policy::default(), - ) - .await - .unwrap(); - let addr = h.local_addr(); - let url = format!("https://127.0.0.1:{}", addr.port()); + let mut h = start_test_host(cert_pem.clone(), key_pem).await; + let url = format!("https://127.0.0.1:{}", h.local_addr().port()); // A connection succeeds while the host is accepting. let (_c_tx, _c_rx) = connect(&url, Some(cert_pem.clone()), Policy::default()) @@ -222,27 +177,10 @@ async fn test_host_shutdown_stops_accepting() { #[tokio::test] async fn test_drop_receiver_keeps_sender_alive() { - let (cert_pem, key_pem) = generate_self_signed_cert(); - let mut h = host( - IpAddr::V4(Ipv4Addr::LOCALHOST), - 0, - cert_pem.clone(), - key_pem, - Policy::default(), - ) - .await - .unwrap(); - let addr = h.local_addr(); - - let url = format!("https://127.0.0.1:{}", addr.port()); - let (client_tx, client_rx) = connect(&url, Some(cert_pem), Policy::default()) - .await - .unwrap(); - - let (host_tx, host_rx) = h.next().await.unwrap(); + let (_h, client_tx, client_rx, host_tx, host_rx) = connected_pair().await; // Client sends a message the host receives. - let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping); + let msg = CommunicationValue::new(CommunicationType::Ping); client_tx.send(&msg).await.unwrap(); let _ = host_rx.receive().await.unwrap(); @@ -252,18 +190,11 @@ async fn test_drop_receiver_keeps_sender_alive() { let tm = TypeMap::latest(); - let resp = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data( - DataType::PqSignature.to_id(&tm), - mtp_codec::DataValue::UnsignedNumber(7), - ); + let resp = numbered_message(CommunicationType::Pong, 7, &tm); host_tx.send(&resp).await.unwrap(); let got = client_rx.receive().await.unwrap(); - assert_eq!(got.get_type(), CommunicationType::Pong.to_id(&tm)); - assert_eq!( - got.get_data(DataType::PqSignature.to_id(&tm)).clone(), - mtp_codec::DataValue::UnsignedNumber(7) - ); + assert_numbered_message(&got, CommunicationType::Pong, 7, &tm); client_tx.close(); host_tx.close(); diff --git a/type-map/build.rs b/type-map/build.rs index 5d3d321..9c4c318 100755 --- a/type-map/build.rs +++ b/type-map/build.rs @@ -123,10 +123,7 @@ const RESERVED_DATA_TYPES: &[ReservedEntry] = &[ name: "Version", id: 0, }, - ReservedEntry { - name: "Id", - id: 1, - }, + ReservedEntry { name: "Id", id: 1 }, ReservedEntry { name: "ClientNonce", id: 2, @@ -275,18 +272,7 @@ fn generate(config: &Config, multi_version: bool) -> String { } fn generate_protocol_version(out: &mut String, config: &Config) { - let (major, minor) = if config.protocol_version.is_empty() { - (0, 0) - } else { - let parts: Vec<&str> = config.protocol_version.split('.').collect(); - if parts.len() == 2 { - let maj = parts[0].parse::().unwrap_or(0); - let min = parts[1].parse::().unwrap_or(0); - (maj, min) - } else { - (0, 0) - } - }; + let (major, minor) = parse_protocol_version(&config.protocol_version); writeln!( out, "pub const PROTOCOL_VERSION: Version = Version({}, {});", @@ -297,18 +283,7 @@ fn generate_protocol_version(out: &mut String, config: &Config) { } fn generate_latest_method(out: &mut String, config: &Config) { - let (major, minor) = if config.protocol_version.is_empty() { - (0, 0) - } else { - let parts: Vec<&str> = config.protocol_version.split('.').collect(); - if parts.len() == 2 { - let maj = parts[0].parse::().unwrap_or(0); - let min = parts[1].parse::().unwrap_or(0); - (maj, min) - } else { - (0, 0) - } - }; + let (major, minor) = parse_protocol_version(&config.protocol_version); writeln!(out, "impl TypeMap {{").unwrap(); writeln!(out, " pub fn latest() -> Self {{").unwrap(); writeln!( @@ -322,6 +297,17 @@ fn generate_latest_method(out: &mut String, config: &Config) { writeln!(out).unwrap(); } +fn parse_protocol_version(version: &str) -> (u16, u16) { + let Some((major, minor)) = version.split_once('.') else { + return (0, 0); + }; + + ( + major.parse::().unwrap_or(0), + minor.parse::().unwrap_or(0), + ) +} + fn generate_comm_type_enum(out: &mut String, user_names: &BTreeSet<&str>) { writeln!( out, diff --git a/wasm/pkg/mtp_wasm.d.ts b/wasm/pkg/mtp_wasm.d.ts index 4e3f9d0..c99ca38 100644 --- a/wasm/pkg/mtp_wasm.d.ts +++ b/wasm/pkg/mtp_wasm.d.ts @@ -228,7 +228,6 @@ export interface InitOutput { readonly format_frame: (a: number, b: number) => [number, number, number, number]; readonly parse_auth_response: (a: number, b: number) => [number, number, number]; readonly parse_response_frame: (a: number, b: number) => [number, number, number, number]; - readonly main: () => void; readonly __wbg_connectionconfig_free: (a: number, b: number) => void; readonly __wbg_wasmclient_free: (a: number, b: number) => void; readonly connectionconfig_client_id: (a: number) => bigint; @@ -244,9 +243,10 @@ export interface InitOutput { readonly wasmclient_new: (a: any, b: any, c: any) => number; readonly wasmclient_send: (a: number, b: number, c: number) => any; readonly wasmclient_state: (a: number) => number; + readonly main: () => void; readonly wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584: (a: number, b: number, c: any) => [number, number]; - readonly wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369: (a: number, b: number, c: any) => [number, number]; - readonly wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369_2: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2: (a: number, b: number, c: any) => [number, number]; readonly wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424: (a: number, b: number, c: any, d: any) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; diff --git a/wasm/pkg/mtp_wasm.js b/wasm/pkg/mtp_wasm.js index bbe86a2..946089e 100644 --- a/wasm/pkg/mtp_wasm.js +++ b/wasm/pkg/mtp_wasm.js @@ -977,18 +977,18 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 137, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 135, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WebTransportSendStream")], shim_idx: 63, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369); + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WebTransportSendStream")], shim_idx: 64, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("undefined")], shim_idx: 63, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369_2); + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("undefined")], shim_idx: 64, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2); return ret; }, __wbindgen_cast_0000000000000004: function(arg0) { @@ -1034,15 +1034,15 @@ function wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584(arg0, arg } } -function wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369(arg0, arg1, arg2) { - const ret = wasm.wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369(arg0, arg1, arg2); +function wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f(arg0, arg1, arg2); if (ret[1]) { throw takeFromExternrefTable0(ret[0]); } } -function wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369_2(arg0, arg1, arg2) { - const ret = wasm.wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369_2(arg0, arg1, arg2); +function wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2(arg0, arg1, arg2); if (ret[1]) { throw takeFromExternrefTable0(ret[0]); } diff --git a/wasm/pkg/mtp_wasm_bg.wasm b/wasm/pkg/mtp_wasm_bg.wasm index 379e5b2ae1d4bec30eece01ca38bbbac468bab26..8feff5eb13ee1d2bb60854bdf5986117a01dba37 100644 GIT binary patch delta 73341 zcmdnhBtHAC7#BlgaV{eR0|Vodja)X2tZeo5_4S*57@smuPGB~fyn>TwvMsan z78Y({22KtT1LO!61`bY678V9p6Jb*`7G?%c1|}9(7FIrHW+oPM21Z7ZVa$xIOcqQW z42&EMlQo2_nN$TPrwX+&pJY5a`HheS=aCPG8Mx~i99t&y3R^HXOm-8Nm;THs79U@p zlpbH6Se%=kT3M8tmmZ&1l$y%$esY7bsrGDUzSI;WQ&U6B__EZZ%(O~|c4iTTy8PUD zs5C>v!k?>_LKie z=we938!^n7{7b??Y(29eD8*&wrKG3k#pfpGB}Ms_@yR*)#id254F8xUYpRic-_!OA?cEQsYxn zA;!;Q5gT#{H+!Z3q{vzM8X!I6>s1Yt6GnrR2En=F`+4c{9@=#{Z1VnRhWQ zW8THIi)jt>O6FzEyP1|TA7I+abc|^!^B$&UOe>g|FfC@@!L*TiHS=EPea!or59BcS zFwSLK#k`kk33D&=L8g~XPnga#+B-XKU~Xc$%IxAgi)9K6EWDmG-(`NvbceZ(WfSv9 zrnij!O#LjUnNKlqVLr)po$&>82g@bqL(KP>=P|Z2b+UA^JYt;2c!%i?GY_K-*RP{O zS2u5vKBPUlCOLqyZ}Qn>4aUC72UA2RbEot$_D!CV0^u)Y?3;Mla+J28T%%$&jFbTvh8yY zNL52FOfM_g#3Q*NRRMV*Ri`qfCr`-(sZz`b@xAhEncjSwd@|pNv2XGpu+XhinaPd? z9*ljHy9zY8`j`|L9a}iElsG3FrVCF#50;Q91nG4wOkwPsybR1fU-*HsZ}Nd6koe1@ zc91`dLF`$@AhuEoi0xehVqXTc|CBT^u9;k0YRuR-d1EO^_Ess#H2X3TJEIK5J_}|$ z7RpQ(DhD|yx?GL1Z*o)lW5zX;(<&?&`zFt?P-X0!e6%8(v2U_sC5Y`_xkqHpuLBHO zrc4ZVO2SSIPVEO8SOoeei&rTz_D!~`>Yl7JtygKyrvnUG8cYn?O3aRp2mUkrODiyE zt*uiMMQFG@4djwP(<~WROg5W7k+E;`iRmDbkJH;2`zBY+0I?U&uwz^?`N51SjD3?! zXM#lL&3wn$H@SBfh`nuAC}ZDbp4lL_>1+_Y8_eD^+lq0; zMMlQH$rtB>l>V9vQW`K1#IBqNVxI@Ie}LJ3^FiX}^E(*(CM^IlH5YU+_D$XdX5R+0 z3&A1ZyD*Az&Eyvgs~P(yrz`@g>|L~-v2U{LVh}rT@d|jzJ1haQAt5ic6vT#v{F9}N z82cu-F9U`8x@Dl$4+-y@or#fwTYZJF;Xcv6wOaU{hewV0waV9Ge0g)HqgQ3nb5=z-GqO12ww?OgnO9DY2U|wXiENI5Joy2GLX zcE}A7&7i^bhE0jpjOhx7G4+59 zU^8Rt0MV>wOf4Xq!;GncMS;Qb1Z$QeFApOqK*3BV2M7cO_z6}}fU^s8u`)3-a58f% zFlsO{D6;Xe!&$6w7Be>}WO`T>nH3naHJCbB6v08&!lK9ul4xL25~Rih;ug(z^U$FL0JwkrNbAB94t53Y-FKnH5Sr-=D{wltaQL%0C~yjZcubB8SqdBqtOBzb6__-b6qHyT8yXrKD%@Bc6qpp4 zbQx5v7(Ot9n2sM9v(`F+Bo$iBnKKla9Ct8gDKR?!Z9c%Dz~uOXAxnu_U^}A%vjU^Q zAtsPvIZ9wN3j~fbE3$$F>NAv>SsWA?6__0v1s*bUgT!_)Dlxk9%7K`B7!?@75Yinz~DH6B}?EeqXL8D4Av|KW`P!Pno9HL>y#G$|niY5hfP?_t< zlcmU}z~ac5t;E91$jz<5tjGo`f*7nAet<%X+l=V}h~_b4x&xxw%$RO~Xbv-`DpP6_Wy4sH4aa;%Y#$s)`~DqI~3%)4Q2%f1yJD)qFKzCE--_guEgxfB=D)8 zQGp3m2r02RvJ@#WIQFoD1CPm(4OB!qGJqToDuxu89T^l@1eSx{xBz4sg9k4klY;`t zDUOVgm}1ain!~Ed;>f57@&qF^s`wR{K~cq_z@oqmsVQ zF=;ZbU^Hh|P+(GEg_N!b7@)=L2XOJ)+;xCK0bH=U<0)COlq(+?l^8(@O@rwRBeY!Q z2024v^1&vtdKFNq3(iU!P)Se>GHEbv0EGyX2GbfwSV`#4%LqzAcNnt-`oTK(Fe))Z zvxfqsYZ0%M0+R;Q0hlE`QlR>ZNrPz%qZ!j0P$FbCV_E^Cna!A%fC_{=j9K*vqhRH& z0<&uoD6K%ug4GbXnw)hjS4aG5cEVO3yoVlZb0HGQ0CKfZ7jxU`)peY*_+d8M#5_R3WdJ0)v8p zIr9ThX5cqxegVl00(-!bpy0>|uA?=WR1{fx#JLriKz8seFn|~qpnBm4RNn_sDY}6* zOA%CXOkf31`T?_|Ac%g!tSAJ^V9E@R0@+H; zDh%K>&7#1O<*1mY#O}!8!OH+DH(4CO8H(KzoS`^fd6^WLLG>|*0yC(k#sW%X%#I9Z zO!Yhp%;2_!fH@PWL}kemSPCv(I21WSO%x$31_rQE%#Ms^Ogv!gm^GMKlt2wu1r|>h z2L)zFmMlj`1r~uXjGz*SN0Aj|I*ThPwX%TZH9$pamI99g3nVLp^ecj!usn)3tlSFB z3M>k&;MOXS0=oiBwj;kUNSh-|mI8~T0MuAeGmKe-i3ijQW7c5eP-0bJnVc84$(|Xc zh)CTWphO5Y1Z*d#!sN`5AQp254u#3F;bHX*AjdcgK#T)>7UUoW4wyd`SQI#*4nmj( z@-7G1yBsVIV81BvDR9933U)HL0<$I)3#cmL2APoss$Q5K1+r8aK)P8K*adboGjP{~ zOIZbQU8}(0xCNX76c`+LKpC3Mpfse({DWDb8k&ef6r!=nD+=OkfZDX6mN_`TGDE8( zP;m~*Kd}7EY{qm1#Ah{QIsl?s%$RuhfHRT+BqL3V5}EwAN4mZNDc3tTuw^N-C~)zB zn$J)UgDyh@8>r#U%+0{e$PMb-Xn<-vm@B{)A~PscgNiS3F1^AEs%Js1a{(Waixrd@ z9h+gzT^2`XD+UH0K2TO-Qc)C!R)=gLg(`~73akQOCm)M4tbele0E45Pz(pn=MsCNZ zT?ZH(B?T@raVxTd${3IdyxdTh0;6Li*mPcA1^(rVtO~GVla~h);;ah%P6{2%1p>f! zD6v45gNkiXHa0M4J^*RYF>9DJ?*KC$>vzD~;ozEL1)^5m4QYt3V9Wxws8|HgW(RX-Xmga=!<-r1Ol1ML1wp=505=8o z8Rs!7u{wfEs18tG6zF4CVt4%ipSciRxPhv3B{oM61r~w7OaiYN9S?vSkpi8JuDqNI zY#?Vt#Fbce85kT{+fB>6xb9%h1wHPUsQorlUYECA5@CL5;2GkDpWO@UoZ<) zfE>f&$XEypbWk1zcf%gA2yA5H;Q_Z-9)KF*T$)TAkQ&>TOOuHKTw^;bWC^SS1^)|% zEP)Bk3ZUwi8Csiyb?#vWSHTP#pp*{j+Dtz%i%F~=)c0Y~U|Ir~Uja%RkUkNE2Gb0< z_#71R9=P}fByn&H`7fv>yv?M*;K&H>8FL9tXH?)9ILEBW>d3$&26hhEQ;u)99;jz< zU_nk;AK>C&P{jYRfSPp-3JeQ#fglXZq#&G)JWVF!zQ@-iua>;@&l zDHjefI2sBxvoLTwBC!=1WI^6u!LGpMIDsikg+YOt`=1sQ587y+++-3)^Eknyc|tQl zjUzs0_-Nh>w#gaQvi0C55BjJbWJC=zmg_kj&G(su*Y0$|W!`ojhq zQ+xvD{Q!^XJpl7GnJ=&@@tHG&2Q65jLwFp}Av|XA5MDiakPtkEcLvl51~q&b9KlRb z3IkzK8~hB`F+5gKsR3#)D6;av#_(9djSx-d2`q|ypp@PN>h3duhw{KfcP-$tJq@M? zP}`9~qn_yvbd2u>iy|wiZ^UQL3?5uzHDmg~qQD9osbfSR)8hh(i-5-TxIklitQcc@ zyoj`~z^cIO$mk}m#0nkSV+ALFE(ot)feXw9k8`mrFvCasU>z?|#Ye@FJ{C}giqs#i z2N&H;8cdLpJ{$vGtf1BfXrzzJjHv<4Z_F=;Y+fVy|krWKO|XxI-CWI{+mCJYL)dM40#gNBj_B+OXMn3h1p3_P+Y z0v*{ChK}qBK}Yrkp(A?&(2+fU$f%wIpE(nFpdC8W_W;z;Vl-o#0SaEkNFRKl&+%A2 z)^WZip!k7|^UYy}3??YDD6m0?ivkB^IF22Z z&DlW1arJDDps{rh$aow((s&#zGpO6d0Unb94ac!4uz^RESQOZ^lz2hW1Rjp#1%)4Y zIF1E09LEBhqR3Wag$~DoM(0808hBiQ3vD=##f<4jFvzFupl%3c7$0do4rw@!6)_wK z8o1D8(oy6CyFd|~oKzIqKm`?O=s=f&#=~)JuDp^83<`YE;W!pErYoS41kg|-WCTKq z*^vQrIBpKga2%vrslF9=V<#|l zmJ5m&*w`5}%Geo;BY4z|4b%-{Q(yrNu`+2eAx}qu0~M)|f|XI^51N5ZBw^4@5)`|L zK{H8E#Q+^N+W<;2EYQI-X7Jz{lOuV9W}L8rEKR00*app55rbx+_7`}_N`pxTG|CMc zG-GpQbOQ}Tff6`m(2Nr@XyzyeNzjf9wjd3beL)&5n}Ix7_5nOt1{u5qMfweJe;?c> zzrdQMz=}3-CJY@o;{$al4M5{(piwTCdPwpoJa8ro8BGTD@j%HK_rMtosAmKoIOBs2 zobj78g9py|%)x^mpurGDF37N0J!n=BG;p>AJT3xi=V~&~U{MqRrDe3CGIq3~GDpoU zB{tYN85?+<44mSj1J9swG7j+I3~1UDF;0duOjZvX@`8_(ad0beI0}L~IE;#*Yz7)9 z0}X_MCJS(kkg%0QE}EHK?lY#{yE$JN+Cn!$rqAeShygRJG4 zTo@M4$T6ua+y{Al4W}s_u<<+=ZUrv*P!|_?Xbu#b;F6q0VX|R+5F2Q)jRidJR&UP0 zpnwbX&MKNsr05%8)&hiRMY>?{9jaLMerLhl! zfeKMjt_J5vP>Y1wj0rq{%>o?_V*`zbf%?MGK`_uf4JdPhN1p_gSV1G&Lf{c?5m@GA z1!YbF$cQ$0tPE_p0t0w>j9Gyb+)RaX5MyOr49uy>gJq!lo56|!JXi*HCCCxroCz8% zV^IJ#ez6ReF+&E+c%g%3(7FojS{2Ypn84S`^LufRm4U{5L7j8Z5DPP;cM2aX<5A#S zt_bSZg65hl+(1@9YG*#=u`*^##>$u-QOC-dL1Sg$=={J4n=J$lE2E5+F~i2nm?2|j z;D*~CMo?!7%sc>_*XH4-%2*ksH3dpjps_M02hilrJkXFGbTU+l8QW+Xiz9~uGh|fE z@j(5S0}Rm7GVr(<3wTEL3n+tw5;Uk6f*9KciGYes)L}8uTptr?TnyZ+{{S8r;|32- zfZ2}ypfMis0J;LJ0@7#_3lA5!LOpn0fFmQ!MM})zaXL3%a7PJZ0AehNg@=>S02HeN z2RN@fP5}7~JWho&?gTONGiZ28M&Ke-y&|M2)npa`_0&FqM)Ek}BYB+o2J#T2C*ZLq z*ht9{6!9(a0l6J0;wzxy^%|fVFv!RsILziiB{Y~8AZY* z?oh@g#rSY~bQzNw8)QIh`s6Yub5NO5MvY>nH(JVK-fwwj-c^(=CTqc=0Z@%4vl3Onfzj1D&vvKe(Se0E}6`>p@t0|}8Su{D@Lu?uq54Ui0jBZvZp1{0{!>@97^1fCCeya0_9P-HEcoV!W0{sB~k z!I43ML4na%8Wcw$K?O!LCI)WDCtnUTIR2Lh8NK1tVemj5gX0Piiv=XM1jGWVbUg6s zFatQWK*A?LoIRfoGk`cC8ZL7IBy$8RbL3O~VFpK#JV@OQkklEd6o__&se1sDxdN2| z(T*T#ke(MHnLAJ!5bX$(2FZK?$vlC|fM`dMG)U$LNahVhX7Z0s8X|u{0$(5kjvb#5 zGdTYFG}&>pu=@lMyX7;)8$HOJ86XZw1Z3b8WX=K*2P6WLnFHobZrm&)yadDq@j>Dn zK%6C$w{Etc{C6`i+a3^i%VdEqBJM{(Ob{QW>;{N)25LWub_7X-WFCNIu0Ul#v?E9w zB=Z6!b7ykk77^hOAm$UOA`tC(XL9S7kHVl-pup(JpXFFlsKfwTNXj(1eyiwYt8H3L zcfL$c+h)WLN&$`!zD%CKO^R{JiQU4%>A>dh@qyxq{M! zCKH1q7q|uQxaQMg22eH51ZvT9fC?cl&};zy)U;;uz3nO9pgaRg#f*-i1{EmlvLLc9 zBQ!5DAmt?nNZthtDM6BBmLf>+p2@vCa`-^5*I;4*%|kFdo|(+PQ;BicWUZYZC>F7T z%Lpc9ii7DzK0eBRF+uDS~D<_DtTpQ%mdwI2S6gW-Ed72FPB=3tuMx z-KnPxDnLNJRM2V+Mvw|7@TvxIkp)_J!QgmgvfnNZZcx5gU;@SHnaPd2995xafQxM< z2596lf+G*42xOz=8P(_KmB}70o;~=L%BtVuq z9+{lD+ej6p1{~%JtiHSqjv%fA*qtEbz-B>wqBZ%^?kKh=+dxx_lb!a2NzK^>Vlp#; z*U>5{@`GA0C$@vcHgDP^&B*w9^0~cp(G zIErK`a5ze23A|tem5DrxoFIw=y!ysb5VFvP*_D@Bf!T59nF9>E3=CEb0t(EI6VDuA z&}3i$^*WgyJ7FA9Dis8+hXr+jK)N3?D{z9VeRiJ7(g*nKpMn>uNx;noscVGk2ATWw zG?KX=VH}XT5-@W?x}oO!@^XM?FgO*s1-2ut3|15b&mMqg9zZi`um%N-L%pLY+-lIm zxR)>scomo(pTIaA3e1joU>uOuqA;sLx*swt2tuYF6a>NT5SRx!Kr4G76LgTp$GlKw zykKQ40{59GYaBFXoHaS_picNKCT>Rt9(HcW4WLK`$0MVoAf)xr=qLeWh(Z`=K#D*y z4_()cYBV?0B5r|MlOGExu?glJklTL2y$`YsG?))_A4titUU`kP91GQ!} zm@+_P$}rIcB{pbyu))IvG_?z|H$s8c@di9iutCyQ+#Tn;B$}0*@mTXa%JsgCY;N0;&@j6j>B_ z92r5CfdZ@J9gqW9L5Y$LVyFek<{Jzk5|mC}fC6Chm!qQfpP7}|U3sMy*d0H?f}B-> z-SI7q!=S+K$O{P}c1K=F^5cMdo&)SCkUKni85KZ50cwx3DsY%F#h^M*nZZ#ZTZu)5 zL4n0__L&0=jvP?8iOMLjfXYTz$2$yJN<0cIjxt#SFDB1DrV;$?&Rv@(x4z>apjd)U~&Au3z~fd6j&VJ!#ErYERN4%9FPye zr6(v0K}%0|aH$MlJirR-5x`;@Qb5Z?asi7guYv-L<9(Px;tDK|H$a?vP{|Ln@B)Yj z;(#nX0poxyl!rtRizCP^aOMDc6zqF;1vXG_gm2_fK+ zaqQoX$oO+`FNsfYobia?{$ zpy*>z0HrWh@X82Cc@J9X4Jq##L5p)CNe`@Pvc*Y7eo&G@GInytNn6HQlb4;;@de4C zCR=`(6Di72r-LLJWrN;{{*&F?dtF$pMuSEzG zusR|t0}e=nk^t2O0$EDz@QlCql&sNbX3(lQP|`;#^I4F~d=^I@P*}1nusDJ$iHDQ_ zoRUgGF5OvNc|rP-N_Q6I(w)T-sdQ&?1efk0!=R-*Gt4)Tas?@5CRd%7hnD9etP0HB zpe+k%K>cQnI6OSSX$b}q^jPAV5;6n2=vl165!nlx2SuW&KmJ3qK zvMX?bH9TZiK#CM5&?-=*FoKL%gVzy*hiO2o`avn&3Y=6ybp|M-K;o8BgGoRMw0^^r zml?c#15{9g%jg7T&rX&-@5MNEa>jXi#m~%2yx_IKTnfBqOe_jqpcV#$0=K|a7DZ6& zZ|dX~=jC(-MF)1*6 zf>z*yt@P&QQeaeIT5HbCp}+-h5rBjl6gaaKnLup=1@KNAW=HmHB}PXE(4IhFY0#1f zN9HWYnnDGpY(@SsM+QYkX4pPUMg^uUT?Pgvo&!p}jtqV*4hsAVjM}oEl6FMUD-PO)V9L;KhL)SxSrwe3Rv$s3qPy@9fBw&Bx5Z z$OPJ@3EJh#335J@6N4fP4;wdVJ!F;=k`QS17n&*-&@>NcmJS1h+w?cAXG1qH^nEuf7a+@ReY44~lUbY#p@1h=jj zAuL92P#W;(Wq`&aC{!6dc)|NgLE+Bm&db0JDz6wp(Zs+FGPj-?G|9^i8pvQ#;0BLx za4T?WFd4Y=g0dAjt4VTcqI_~#2^E;pwSdi zV+JG--qpegS{?&Vyo~hz-RgX_zxhfOg4gGAn>9A_Xo@W(iPf&H!Qw zm@|W?B_XCOa5)NO30!6b&G~}&mm+LoQ-Gy&HY7`wxq}qAbQugB`P_KfLDrcOVI3DN zSm4%iPOf;Rzy%Hl3s9XudBH0k9q>XJaP|Y$kW8S~5CdqBC$YOz<4J>yc;my0TAy1QTNoEi zpTor10M@~VV%&5aX0Y@@X2vE=X|d^PEQ}&-6Yd;f5C9wJCH&&HgA%jA7to$;(E2L{ zW`W()@3Jt~GA@`N&B|!Ocxw6-Rz?-ZhtqemGS)E$ZMR@!e8|N3Y`PRD<4oSaOp2g^ z4uvcQCV}Sd2RIqeFfu-y-pkE6nQ`}YMIJ^O#)sSOco;V^GB!;A$jjKu*f71EkI|g5 zVfuPL#xsl!(`)z{_c1nXw-#VjW@Kzw9xuqq#MrPsL71_YF=Wfb0}PIQ3M`K6nX??1 zfH+4Uf*O`g4BS2ZOv3dnpo2a>fKE|SVF2Y}7A4NLieexZgCe&ABe$4NgCEk) z6IT$QZYatqQ_rNptst%-rXZ#u9wxm!OP_&3iN#T(1T=nA;@HsehrO`Kk)=qH3%NZg zx?G6~)E;D05Cs=!ERHOSTnb_eqE7!Ao1IvKcp0U*6~vvGYPgj^cWR{a61P z3zb+E*g!4DdIeT+laUGB2mv)285x+k9R)!9;}s;9XDhKff(FqzeWgL$-yH?Ylz2gl z**&E}6TS*ejs`hOtd0VpHF7Mjyv(3&u?*m)WuVRtXiq69b3^P_;C1C?0PV+jgq6i0 z=hinXiYtgNS7HVEM+oX4&;Z>zhAc%s&~AK3gB(Q`P?G~3h!WtSk_eMtuEY-Br|-z3 z$fUrozzu47I&zeN!V0v}AGF_JfmM+S6t1ukk#J(H;Z_m_m5^Ktp#6RS84FF6*d1Bi zl$fLyxfD1Qgd90a6xfRtn502_+!zZ%8`fA9I2A-efvX@EBE4LRBU_PadY}`dNWBPX z2R*+dlfQz9<9|kX@E9Q^rkNa>{J}>KFez{-aD(HN9TcbRAO|Ai6qh?i;W4Km0COkY znF26pszBVy0ggBpcV5t1YbFJLJ|-3>P)iLQT1?ZqB^XshK8ffJ0gk z<^>jTNb`dv9a-F1!2JjXHc)b4%2HxcVDnU9QQ-6j?>T2wU|9=J%nHn)Rmu_y%%J4} zsv-)^3QP*zS>Qn_Ff&2kpB0Jw6BE|t)Q_8Nu0S#i%VIK-ipsq4#S_X2i5vWi%0BtQsgdhVq z_jEHEMuU1#KLwHjz@8C+1d#w~K_r0g85Yo>>VHOP2ysCS<-%fUJ)VH)LN}F}8*~T| zs2jtA#V9-`fyOF92YA5J2WVndK!H_YH!~x{^mo#XW||D3g}NLH42}jljsoClX9o2v zRT#Lp3NmrmGlD9o4UE}J%+niX7?maqRI^4ggSTbF51?WMEvUT!ZjnM-%?!E>Gazbs zAcs_GF#TXsVgj8o#RQskW(5t!F@VxEXdQ?GXj4CE9V7!U$K=Vc^|U~H=Qy&J5USWf z%YE3OOBERqd(${3zkcne#R2O1GB`e9%u?ilEFpiu2y!3`7b6P;WWWkEwa>!MFgf~- zS^ZgNMJ7;Sf_Gmrf!1$f?!ICJ52u3XO+m{ZvFyHLf(}(Pfkw)q`PPp`jjh`Vub#h%2MS3OaTMG!6|ta)u4uM`Q;rqhV2G1I^s9fcCiX zfU9VB$QBni&@wAlNOi~ps-?i&T-ZU|T$n+H7>fcAbejuXmJ$~zay>vDR`B|12XiJD z(BuRUC`&MbsvsutJ{ML{E(4#%(F0oa1KI_^sKB7WZN~HgL^GK&y#di|W=tp*io8cZ&V?34e$wG-q7on8P*vIRx1WYA!0VNwExCuo2a9G(i`5MWdUtr%5cP~bIZ-T-nIn;Fv@5Y2AJv;x#$ zXEtMUQDAoLnf&6N9LILhN-@x|Cf|E;#^saM-s^BKXH;NOU=wI$oE-ID!vMUUz5%p| zcM0O;6;^@mj0&ufBdwSz^6@gFbW)%aM1yGwBe=9=)?iuzJqKm- zz4sE593bzyfcmFkmIr853ACA>V>15-aYpvZ>K~G$I6xa>7uSM z#lZbq2FEE(pu<}nK@*Oky_SsA1EW|(+z zf(Xdi$%dbmL_y&I+EL8}-UkaB)M6BvHTmvm71&nkHB8`b!mMkUvINdfR{NsH3fdjb zF*)#y0^^S9>aonGjNhim#xmly19^Fhb(F=i{Vg9^Y!jGw2g$TDgPe`Z!<1(iswpc8}`KvPM_rzgrX zN?CJ&3M&p*UM5EtNQUJAEg#{4C!5^wDt6DvW$u*lUYE? zN&r~`RNpy@WC=i|Kr>%0-~*PxvtkO&3LMk9Ldb}K?E@V!CNrMTzHXgJ9RgejMAQq1z3uuywV|t(lqm>Y|6@!T* zlOi*ZD7WK+9iT-y(|^b@8ZkCa*Oq5gs9y}9JZ3gyl2Kp<%`<=(TYzTYKvTy|kX|hh zXqT@9gw3JEX~raiHkHoq_=6z}z7viUI`O zG_MN^5KxN&w0s9V;S3Ul&JThojzFsnK!XKr;DJO1R!0R;OtWh+)j&L30h;p{Pyj8U zV3JT^cigb!0D~iVA{umNAfp01s8&>91iMCo-LZoayo!Wfqn@cmkrfnL1>iglTQtN3 zDyEqn5AB368e($XwG%pJ%b~#JxCzDq9j^ynG{gkf{SY*^0NUHm2Wr=`gHFuq0oe~) zAr6_J?ggDuA^|#JfkmMnv=82wmmk#P1FLNTseQo!8n$R*1f8k&j7fnVwCX~U6XeVs z_@Wnf4W3ju&8yKX?^598beI910wchhZGhAwDi#3LK8$ z6+)o-F;P$n4+?rve1OjB0;Ne#P~r!5g}BU^Kqu2UGCD%m^?=-y0t)`=-HME=;JIQp zXvNAVFl+jLMMlwj4)B?ikSPHcP|e7w$Og*Gpo6SHqY|H)mDoUwVpw4lUEmXlK;1_+ zP;@hcI+F|vpk|^(h2#ImKkS7H91z-(qoT-+sRvZbvV)f2D}jawkAn7UF={aJfcoD8 z0!QmXYnD~Alz2fcIZg#eM~y5cJ`fw!Wnxuka0GSS*;E)5I6!qd_*69o4%iWL3{3S3 zU^lZUGCP8O4%({<%8H+vmB8n$ad0bgF*$&@wSv|XWq~$S3M7DPe$W=#wN9Y+Lc;~N z7DWNbZdh<{mJPI=CJWRtU{eqP?XhHYbjWsmbMXLZ)sO+?co@)eoZyO|MS)F|$pLgm z5OjYlC|fdSDY1e^+@W?Va0n!TPOwwpPz3Lq)nH;!;)m4RQJ@Bl0zWufK~tXKrUPg* zy%H-gXqcW0bf}U7mm^OObZnK4sUFg22YJhp8Fc6pILk>uJjDoFd&j20slmW7{a^)) zgc6gC0*52JHz*3h>J;GW6hNoxX)v&W)hRNmC~!D(!qizaYJme3v;@eSQ46GUx`GO$ zNIi!mmm4pG6i7gqfy;_P-;t5qia}3-4YUNtMu83NW5{;W2W;S@*g!&h&?QM9Ktc&% zRa#J?2KE+n<_NHm0#s-MNGJd-B!MjCp}&YiVBR5Iv@_HPl8yg2VdHu$O>)& zL3$P-1aYVwMIKb{q;Kpe5Cy&;lQG=>jTt6j`UA)nt@+5oiGo^n?13 zO3aSCI}d<50s^4p48cucX3#cj7SOU9R#4vrwq6RfQUP@S7%1)-1s+Z}&|(y|1GNT0 zhX5+O1s-V9g~b7!Mn5ydI&8Z!crKAL4nD! z9-OK{$3ueh!b4Cy5>$PI=S$c@bstjo&CBR`1LPnPNbL<-^#kf12+U&Q0d4vPsRy+u zA)bJo^}+6_0XoK)MF3QlYcOem+^7SIThKDT=@&H_r5OdLztv=v(57;1D&USy6>w}e zYcX0Xuscd*fx7VM4N1^ebI_@&;D+RMF>OXG#-{0k+KeJrpmqVH29pK2=LhOgKze?R zph6JT8AK7%K~^<=zBZ#McxH`JgGmNi2GZG=KoxQXuay8LY>=^@ynH;o+>Xf0P@s#B zk%b_I4nh^EEdX&TXjLW1rKX@-iWR!j2NFqer-OM{y3G47r|L7&lu@$>Yn`i%aJyQixfF#0oonOUqAPEQPfnJ~xVwtXM$S9?GnOTX)jLAR& zw02a9U4w~1l~I9Pfmc9CfmH#tpLG%o=-h4(1@`F$hK#a|v!?eMGOBWb6e@8muutD@ z$SBG5k8yg#6J}8n(83~62Z=)wbjTSusJF#F{l6ijh5-v`Umz&|LpBi#vp6V-C_q-% zaVxMXF)Q$cnmnLg4RdybdokfgjAo1vr}r5#DltBszR`%$S`2!&wjwjQtOd36xE%j{ zn9gL(7|u9rdXzDv7C)%;;{(S$lLDW>tm%`D84V$FQq%7nGYTLnUM2Wx`m%_<8zl6G)em3lxT2pwVdt&}`=M={HOmH5flnXEbFLk(dJQHnM=a zjqK2NGP}U6>AI$jih7e-KutaYB_s(Z(28V8w-I#63Rnu1j6iy7Od0jT9Y!`HJB-&% z8F$Nr25dm5Mt~MzgBnySpzfTb3ber^Fnxm=qhvjJMK=?uJ^`(-(=f!jE&8ca6e`5;ye&?aa{NaV3H;=K zO${amMGi+1Nbj2kbbcHQsM6$6U~vMS*C=4l!~l_PL|=8QMk_*Q~Pz#JEVyH+cw-?L+MXIwd5&7RSb@$vKwd&UOF1=C;JGtOfC zJiWz%QH^oI^wkcGYZ=+6`#LeoO)qz3G+zo-48JVU(aAq_VX9g9!JWAk#7d#`uIe>W zIc9E00R_+mE2k@?88;|HK$T4Qa%J>ke7JqOD`P$*VGsH1dYIC zgWN0Q_yvh4;rIcGC*t@9i6`Lr0*S}t_ymc^;rIZF2TmajjyIq}N-U1@rCE-5VBGC| z@r?Hv88=LSo4}aL_<4G8B4Y;Q=jnSA8FLt$r)wrLW-@M{elUemSQr%D{Gby$c@#K6 z!N$zPIo&ahQF!{9Bt{;_htqE+F?ujIPghE2jAeW{y&;)V5~>rN-T7U4ITWBer|$)+ zX`U{f!kDSN9W+cNC~%iiK@eQJK(Ah);(Xku;LD!dn8iYGw9MC||0T@R>fywa%j3c4Iww4dV8hw^TN-sTZ09_!WS5_K!;vC zUHfph6&?rI$B_lC}b&cVR#S25A}JBqKtjh zr{*!{Gxkkq&1Y0+oHSiGpHYQz()6Hwm{ULtfyCo+TP;vGM*WH~MXPauL;i3;qWZr99M#JGR^#%9KqOpGh1 zN4GJ$GtSt)xQ+2OBV*z8{tm|bj4P)VKVs#6$OGqO!NaF9XZ^z>^}89zeV+s{s86kudrx&7XB#xzF8mD62k zGA?6WIlXTIWA^m$S&Ua13%BdeX4GS3TLRIWIEV3*6od`Rl&p?31V9xZlj9GD9LG1! z({txCN;4Ku@0`o1$n>Up`kJ|nzZq9_0m+AgT zz{SqYBaEQt*qbBZ=9v3YMmNSU)BBDxMlgPv{`@GT7vq=dM#mVVL4wD?a#xRm<@An& zXWYcfcxbxeB}TdF+s`wqvO}VuYkJ@XMvdv;&oa6)ex7c7j&VEV=jmV1 zF+OMfJpJx@#$3kF)4eY+=0a7oLR3G$z$hyLu76=i*Mlxg0oTHxIJOs@VH9D6SY^ui zd3yRK#$pts4^0of!YI!8d3x>@#(2ih)6YUE-K&h=!dMKQ-hGu(T584h0}Kib`iutP z@iQjy!uAhbO1w^MrXRS1&xfJlH&(*lhZusF7Xq(O%=3Cx(@c#Tnmal!Oe*BGVPXE5?IayxFBe)<}t9QT|Kx+>t?U9Oh zZWpkuiV~L@lMO_a0kjAiYCdTEg29TxLxE9&tKN*s0kj;@AzOhFG|+ zaUf3gX9X`n0L=t4Ycjbg3V;;2DDo+=34CP)Pd+q(Pv-}`yu-x6$inR?2N@w|1g%o3XLS4p8{iXDV08Qf;|PF41I7W3L4s}z0G|T@Ivz(3 z)X4=^s35bzeGo@UBvTn3XYGQ{21+O}I!=OZ(*zmX1>=AWZK{XyK&DE5pwhz8~En98qmTD z@WQ(Wn0nBXEoP`Iz#BcmP6MsyW7cGj0G-+omQ4U1FJRPQG7$h@QJ};IKH|)s z7orn%kevn-=nfc0O=bZ_Aq6%NB>J-k0YZZFX*Zj&`=f|s9VeC$OxK+fg~edS6&tcc90ye0-FN+AMllbtd497&lHvV%cqY*XQ3q&SRD_;I06c+j=NzT&=lcj7zZ>M ztpG}cY@kVVP(KWml)%eDSwJ&Ute|r*nH5+;bF&PJkZW2%E(hJd1kwWDR>R{9S_J`G zwgg(yslWxDNqqdMn`3c6gOy*DClfatiGrRZA=EG0noZFMo=jQ4t1WEs;$T(s9y6=qh7#aJv`~GA+2BJlNgWLbx5B*`3W@DT*{T>sOs`n&D1wPPi zJm9OLKr5OZ8624eCNYAlK2Y=8u>m2&=*R$-5y(Q-13K7o(sVy&CP`C>R`AIINLrcT zS{2Z=GC48|OqxE2nMsmy()Ml4OlgcvcV11GV`Z{u`tfRdJS)>Jl^JhAy%JCpUDUCG z$*}{@m2+%hbo}uKBs)EpjmeSi#(NNZ`w}*$eio4zU=HYXH5Nw}M+k4aJ2#Uj1AR}f{YKRua{sln!ZbnDH}F1d|9vq8B1IgOTy!^lMT~C5*SGdrLD_N<+E@pylOkjt9UsmjZ|ZDjufamu8Y+ zd^r7wG?SJ*sHN`6=-9vr%dQ6?^G=S8jt7_oc29SdVd`dlIQ^^)laUs9fdyz68-pVU zbP572B;a^-6R24a>M)*p1sXz`t|`lu$v9#AWLc*DpadQ)&oqItd-@Z3CUwTL={yQd zkDTT+DzG{(*$6&wOv%!mIY5EcaSoJg1zLc~>No|;wFWKgV0G+)a&16O3s%RL?FNcW z#~8J)Y=jMM2{* z7pSubv2gl&6Q<2f4UE&1O_>TM!Idj7XgREi;{)&=VvYwGK()*Cx28;M87EGkZN{X* z4GsDO3yakg! zW)las{I!D@>OXC%Cl)-k1$)EpUSunZa6N z3f!O;3n!>0!|BMV$Oj&R2A#tI+F#8Faxi#WRFTP%4-}H%!8FiGr7vJrF6h9~M=%b9 z0%N@+AE*Kbtsw*Hc*qRe%E1k)M|c%D6}TZSB1T70UkB1iWCA-6(wGD7r3USZa1?`E z20AhB7R-E5ad!pA0reryz&M~Tgc!);dXUW^?GKrGI6zGfPS8{XsG4K&0PUX#)!m?l z@}QNpAYU_qTb`go@H4X#4=6CeN6+$rc6@>z>!=BF3lm5uqoXD`$0{&63WM5hT#lNc zb{iL@LIm+xm>3k8ru(`ug)=fuuc>DWXJS&Atav|YdVnhv3lo#VbO(3FAXak*Nd>0q zYh9UQ>bXGe4L;D;R}Lj61zrVil$IRGCy>P%poJGlVBY0ZU~=3C0BdxJuf z2|UTb3r->rnL$kySjUeE)M96H+Mhq5E1qZpV0+6oM2z(o)(96}CP;08w=xN8hbTc98YmB*k<_!KzJc$rv0 z3!_03>|Blt)1O%~buzA)USP%4!nk7kS1YF5j4P&Jv}USfTroYshG_y@&nIv-^~{DT zoRP6_yPX}=BGACxLwhEp`t@u~OpvW42iUWfK+7fBK&6%fqoYlhz!Ok!olTcv0rade zHU&0ahK0;l3=QnyHFgc`(867z#he*57=MBdbTjq$4$yi>$a*?P&}=gsD7`R(hCe_@ zSt&AsSPbBWa`m9r4I0t-ltO6VOh*JeN#|C!LeRNC;%mO#TRyTl-#Z0z zK>V@+*)QO+aENE#f!1TOfx<+I3FIZvMN@~sK3Kx61U7gDGbj^-g%&V_eMP*d>X|@| zQP6fuP^HV5B~T0YohDNYyCUdlw*|~uN=%Nd0-r#0l&t2=GeC}IF=w6ty2OA55)qCd zhk{nygQfu**t3+_L6>>3fzC1M04Zk#-!{btx;nQ3)D&lS1P$UX2d&g&ss~^8p}?lk z*uW0CC6^7fDz$^%ocRJ9D2_Fm8raR5Pk`9ScYS~Y&2b4kD7GP2emHJm&q5ENEuc%w z*ff}Su)_mr13PK})q|1(=ul)92M}uoJ1p;lmR;&Io?ycmf?xotJHrMVf&ep-?@7a! zQoxFO*g-`e1ITMF?B<}R9h>73<}88!dTJ=Cq%`qe_z#1$t#4W+%mKfqzU~wx9aci)+HHNqiSllK{i7^DURvi%qwqOZc3=MW* zaXSogd$72DmJ)b-I0GmunH&YCt9vkMNyDP`An4c{2FHV-AxNlg({ntSB;g7ULlhiF zRNzxQ%!cmBVqeu#%x#}21<`|O9^moWwPe2r$KvDqJdJ-aY5=mydjVF?`PD2!& zMpjVZiR7%a5CvzE6hO^B2a!34Br|;ncRSRU`#av#&v9t|7@x??cvl1ESytk^-pKn-H0sNHWv!AUo?eM8R!j1p?kk z&bkXxa2H7d)a-i@nR`ew)8o9EBpD}eukdDi$W{+a2M~2g%Af`?fm0!9JrFc~LB&Cv z$QeMVCxK5UL8NlfPIm^-<`pz?R#dB7s3!128#=0h>L*5MKHug!Q!GQ;@~n`3@jmr zA^|F+#lhm@7~&FOaS04@NwByihPV`1Tna;68d63}V@Sw=HOOFy%YwyaF~sG-;&K?` zpyfIYj`Cb;H#re%y@w(CbS6)>{3Tn5ioE{$T!Vf%6k%odMkdcrt=`k!c~ zDU9EzPmE!TV|=*%LkyEF7JFplnC>%vp8hwE$q}>^E1qc!c$pmk%Y&98gDe5}CBUbpJD!-Hm(C>N53^2z5p;+r_=I)vA?zTlK-(Wc zx1l?N&T0o4#^lS(3O-yN#9?sdWd$1r;xIdEdh)V3E}3qS&ZH**(hRW*Y~T@)RRN59 zrteQ@f?qMg3_h(MdR|l5 zJ*12ht|1;zmlfq=JYfaYc)TQorPD@Winr|?bBj*wl_ z)4hwBy%DDYh8Iu81{d-nM4$uIaK(-RA3IlwfUOi}D z9CC6(3TQr@nFT!W&j1>BQ)GuoB!DGY8QDN74|H80Gbk(+K$qlcFv&pH0Dun=NKgRX zPt7C&TF|coVT&lSm@z3RFw}$Y3y@KOnybLTtq5A2zzkX~0N$&h0~v16aAW{wj6bkD z$Q_Tst{w;7Zq3Z(0NMfX3c7^{R5IH@G?{>QMSwcBte~zsgQH275_pD|LBI#(JJ7;& zmU<;-$l?LeZUt7*rB~p?#yLReK?r;W^`_wq1{gG$3_!Q@g3pIwgdLj>-xB~iJ{`2E z9CWlLJNSGEq&vhxr!9gv2rw&vc4mNAC^#~B)boOx5NraIpf?KZfNTKY0K|r211H!n zhz)ECoM0P38$&?bVnF+$KwB{(X$rE<33Spf=u8Aifybc1qyf&hj-bOX*rDqr0zpGn zpsOdqcLW(ACo0JG*E;Azpj(v^KoKnf3Th4z3p8E`u^5z?&(CL)V>~G}%P|Eyp-$9QG>iAp9d z#w*i5S27hbc1};LV)9|UGJSIuSnwW%;;d#`!`L}}TQ!pw(=*oThDl7~Y!{dvncaCO z3%(YfEKtqD)WJ0UV=d!S)l1&H`P&=u(#On>0TNdw4wc>IB`Dg{+r;PV_n1G-?D$%3Cn zr9c%N18D0LYLa7c6qwHF$}9_tk7P)Eq%bMegU(?9A0`1F^kUFpx&tk?K(V30bOvt! z1tf9M=1`wYK%;3W&6wWY%@1zGM%RLBHu>0r_FoW-- zXFtFUzK?$U{c5JQO#0JU8CgJOpaFPD1ynXLIXE_a0B^|wP0zUVGB|cXxQYyp9n*I- zF{w!*$%E?SmJf%)2M{tiHcW49VG^Cr+swqN2^M7p?LlOaVsdbt0Wkz-<^l*;k->4n zbo*u|H7O){kePGfX3hYaDWL@xKp6MsJ&F_mK)8wwj(^@yKiJHqDvhK7WZsYWFgJdA zKb^6KNn8^wf-tZL;vks&ra-ug431N#JGL;XNg>ID%$oprUk}KAD&imkZtw{V%%D*y z9R(&woh*T8)AzJ6NxOq~_c1vdfGQsbes0GXdk!-=US)8cviC4#J+lIXD`;JkTM_sO z2v6`>7;q>Y4OtRo}0-3?qa-V~i4L86U6J9p-45hN6feDsdT2RV{FNiGmgV7u| zmI=xRFW{N-4N9hz!6@1Bl@)j3raVA01=JjXmI+(n3U(kV&|o?M7e9g`4k~1zg&9gI zvjipIFR(?*_;`v74W3h9L8uZ9kP3xvCJ7jAg{;z%39JC5Rf$c3$qlqr#XC!Z8FXtfQ;8Cj`wjb$Xiecn$zTvFIo~6j9z^TBb%g{7kzmLgE_}mSUE0{p-D9|$f zIZTcdrq}c_B?{cWh^+e7#p!SRm@I|2T|!ZQg%P6MW_oEqlR9J9^tt^^(v02HclI;M zu`RfJ7_^f0Za)YL&4&zP@#s0kryAfz9zhLm}vt zNkujVc1NZxutG>wc{3HBp$$jE|4kV6KvJP0*pq`_+7A#-m6C}gIaPGp+J zSg`%VM5YiX#<1;PxV1;R{_1;PxVY2Ovo56ofG=LIhi zW^ojNtPcJ&he@4r`*fALOe%`trN!VIC7D30E}1|Eus{p|AD^~jdgfdvef1fPyrB6D z0npWKi~^G(JOvOBe7*>H3p9h{w(0xlGD$NYo_>2Slgjpa3z*)qGgeOTUBTqYSULUt z3ML&0B|cqvC6g$O?SNo!&t1us#m?9_{l*3+4aUCdj2oFYG4@U0vJuSl+sY&{{p&`i z`JjzJo0!xX`=*11u54nO$JjT$cr%kKOlad~rn!uL)BU#~gnGAtg|xRaDM3^{XY8A9 zwhhdS+{RSM=sx}QHl{E}_vv~&m{g~`Z)cK)vAdAiXOY+fJ78*J7~Q9L?_la-+&Eog zCzx8glc@%@9s_jH2PVHR-7#_QN1 zfGH$_DWs4k@M^l-Nu~y=hs+9mj^Ohb`9MPy;4?#692+zQ+NVD_$rQ}^aJ$JVrhSZz zo2P$1%~ZpapTd5*~j!E-&&v_NJK zczzi)tH$hjWCmlF5+|s}Yv5606L6o-c!9}-(S7=X3rwoh^Di)o!q}_eY%7?M!bK(} zM)&E?7ny%w#qF;zcGo#**nDE;6}7DAnoFmzWe_><3qvc&4wt#3TX}(7ejT zGyOA?fbL}`8^)69S(lj-7#pXbzRVQE*f_oL3X|G&zbi~i5Vi+|Qk(t&E~GYH^D0bQ zZMyDdCZ6eaSDB0$8@F$~%H+V#=rR4%9i}iwkLl;{GO13_yUQdCV{e4BonS)d_Yl%m z_n0ghJ*IED$JEETce?X^rq_&nx9dM(VrOLByWRXDgtmVKq1_)tX#XcnLLm91r%VED zj2ovHzh>%Y+_?S!YbHfVLtRjV>4dZ+I9WZMZt#{##PT6CH06T&B!Z3`_A`N+e60N3 zjz=CGU~trT{PF;_;|;W2tpg-*mBDcfNaWS@+P6%yj3>6we#^9tnd#BH>A@eF;+a0N zOlO?OB+2xH5!8bd(O!Ynh0|aHHHRQ`uWr2H4qFEkcwGkg1i|StF3eKaAZIW-@__Db zWO8K8R$}6Xj8`-;DS}pAf!6keRVy&U4lIIp^p=1-dJ^@Be%=DC{k#uMpsps10)sBY z8yFL`Em+_yq|2wvumLO#8X#+7$^r?4dw&f~Spu-$AA}9=guj6|m_C3TOm5&kvJ4tb z4-iefCtw+v#1*)X8z|xg`sMI`;0d_?Gbs8&-8Rse3aAl>Xi+*kWJ8;LQ{eh$py*ow z7hi%R4jLwdHu*MyBv`>oX#2*GOi@gX6Q=WgVVc7DdHTXHOoohKreFTT0g;d7#Sx{uldI0#n?Fg_&25?#>VMV-;8>f4J2lM8AXOdxToWA=zQ;5P& z&_-J2S{1asT4nLVBd|(#mfaTvH$wNI<0G9ZGDNzEJ_<|%ceaTNIbH)?X z@BCx}orKQ#i%FAl_jK)F;Dvm(znH=qA5Opei|L2ZM9{e*51@zXgGRA-Prv`0DV_22 zboW0@ri{C%*ZyHD2kq$k!xYHaJl*{-Qvmp+0Cv#IKG^bb(8@mWdKDgl-P5=HWeQ{i ztuVX|&EtPYN*)J|}9Kv@KwLqK)f4S1eH9jhTWkAu2f5F1f? zTNfCzpqUwZo+$(3JX4a!bRa`I+#6UJxj;iH7Z|`}DGH3!rT#IAGk)G~^pEKZqY=0j z1KM}M?fBp>>{3=xF3*soU<(!mP!_wBY>oVm9V>#xK)Y&;@W8Kw2HZ#=#Dr`v4O101JshRW&fTfL0`fG|Gw~i$NU10~PwfI6aY*Su_T8 zlN)F>5o)sn!e&Pe&>@nbWOIlKeB_i81BfyZILZv7H6XRG0%);2*rk~6pMHsxS&Q+& z^uL_U=1g~djsP=0P&u{csD@2I|sH;<6#bB1fAK!?s$d?)Ggt5+_L=}FS95! zWA}C*LFO1n_A&)t1vY`Q>G?v;DiX(;75EDk`9b$~W-0MFLblHH3mo6RT8Mc%NUfLx zgFxAIPZ4GfoNA|ciZI7W9RO{?O;%ua1RcG=>Ue=MOQ1}FL4i-8Z2Er@W)*8(HZd!L zHe4-W%5nsm0KTIXbhRePB+w#0u_A%v+v7x;r5Oc4OTQTuSalhGfJXHMj&JW2W8TQf zcznCH1hWI9&~cDST%glfL07*iaSH6;-Ydym&S-WA8c0k53T%$B5Mzo^U~_~8CR2g} zn58(b1hNEj6j-L8lx24CX8~P2#-ae4(N$zq-~u)H zxWI=}GC}HoCIwE=rXw&9yib-1(tHG6POktNCjU30cI2rGibLO z3urQrje&ugnVEr^8#EliqQn5IxtTzh7z9E}9v;vI&5GbV1bG!W!E1~ah;au4#2uhh zb-1AJ*v_NCe1cgTw!o$VbfX&sC|`gk#(5MN1SU>@ugvU-n2z^UVb)85PRBDjKpe>I z2))>Fas*YwpsV7UK^Jm3g2WY=6d-frj7T@Qg9da!{Rxl|{04W>5bd7n$*Rnn z0^mt~@Ps|+3UtR4)90u%OB;gb-!+&Rz!rn9SqCcs34)rXpkZ=X7p0W?QDeOxwNGm_ryDpKYJ7&Me7Nf0qR^p9?yI7Icv>=-67&UAvs%`cDab z%qRmW#ey~fgTfKiAZG-vl4b{$-*71fCa4655(oJFWSAa^1ZWWx=vF^a(}V?dY%O?l zM?k3_bp9P^LK1Rr9r)f=22dshA3q1W>0E(H;4G;71KN^_O&YXs2d07tG>{4!Ap>nj z2gMXKFM|ST^^pK*28&67QImvY?&;q@2l$fy0FX0vrU+f|hGDXfT0K z_h$s1q{)Oaw~Bm@Cg`drMmK3C(Aj_>uYl%jK|9gmQ=AazGbn-gu7g7id|C=~D1qP?4hK2_4;bkvCx0`=vP6Vw^2IVCt z#}^EsG6po=?#Pj4#w4M@;CN*3VFr-B432L$A7B98al!zyg$J~N7-Thrz(de%q$7)? z0_?Q27YtyQ0t@&kSq6UY>D#oKbr@$&$1%;T1)Jst9Y%;Y840=z7)fY)jt;YXFlbtc z0d%q(Blxmg@IW(Yt{yb|&Hy_35lIIm;6YQ;&_fOxVaL1kAWvrCavKZCZ6I4EIYDct z1+usWx~F&RGDk=uP5Ux|qD}!m7%=^>F0---=!`oC4JI3~EuixrK@;63ddv#ogU%Qn z&oE>O^iPl1V^)C8#O8pNf|3u^jgAkdxiZUfK+}H8^n*!E;;f+CtItlqt;ej$_;C7P zJ!Uh;Z`)1una_j9mRSs$J3E}JniIBo!QG?~Ha1Edgq6N=+ru&_QOk2UBPOE5>50df+YB4~h4feB<66Sw1r z=?X^7+KfM^`xr5&yKRA3?Fh00G{v_Av~nGC=pF;;oKi+^xYRTjP<8>$^kK~NO)r?r zY$3Iu33W~nG#1pd?=XWv>vV5pW-Gfn&EUqJ5-WK7zT*@qmko3@1hZogl*gM@4M z)UYCn++YETupx3r@EjpyE#; zOW@&jFKcF54Oq!00a`TzF4^E`1cTR#fX=D}-CHuf-Iz7gYIg9b(^y7BS!G<%y=SMPTE3tvv z2B5;$71X+N`~p_W%m6x}19Y-4Keyu(kWkxnZ+qsKjLW8*Ixv?pE}Oo_fq5e1!|8U8 z%yNvgrpG!m%P^juUggLPS}HQfky(N9;q<+Z%yL$sbsi zU?I@O$qWLsri(f;8}fqIX*pg#dVs;vUZ8nAPDOIFgP9ol?>qOKm~jQ$Mgp-%xofHISr;1XOZLLwv?HS; zngA1th`I0Xn+p2Y<5yqf@;E zi1UZN86=_60oJd;=w#8+($WGdRluf8kZU?#!v#R6?}~u$D`Em)Fu~952s$ouH}m!d zp3GB0qndYonLVf1doxevnh07U4mo*J&WCv~RPr9<#Ob^Jn59L3aWaFhUjpAhB9N`b zqr%|$?67v2B4lkFsJsH5vO9fk2(!K{$d#z) zxI#{bXI5Ylm^J-p2(z>xc*&yz$nmfOd`^Mb~IAq_SrP#eLKNs-B^y`iCj zLy?zT0i}@&8j=JFfJ|T&m^HmIlv&Sg787{gFO!4g4@eCR>K-vMFfnq21;BG!j&C4U zq7pM`2RDP`hY!=AhBB*wLf7#NR0?7+Bew#$Jq*zcy8GMl1E}KV1syvJG6mEs1npu3 zt$+g^uh_HoFoWY?2FEj7p^Iant^s8=n6-S&OpFYS++dSH0mltmQ^?562)+s9!-wg8 zVazs?vzP>=;eiY~T^PJ#$nnpI>5s#h^~Au3<-$#11+AlW{PJPCdN{M02<+Hn6kUbk z%<3FdSrpk7SOlg{Ul7i$tU3XFbv&rJ1YN1YBJdV;r!~kU3P_PR{Y5yl47dhD78H(P zRx>^U2`bPC0*L8Y4{E#IfCwrvI9`FWK&zfXo0b$99M4S8iD1@Ly91K}X$5h>p$O`| zgS&Q2?z}AAj%TLtj$l@-zW`ASDo;S$8bQ}>`~zR>!~#mX%#I*=&0lR;IxOT06{C0=nUh5wwT|tO_)c2p<0jO)e=g`h(7N|8STAszVu6 zZ!?vE7Q2Cb2lgk30CiR%X59HOy)KejjIHM&xEVAnk~vUd$w$yy3KmC31*Q@uX2%25 ze@8N_vq4Jx?HW8ieKV;^fKBtIDYqG#p*6GSI%;I98Jj9^E z)WE330B&A`78nZ5njR3ttjD-xdVLJ|4yLU!%$|(1rvHy&R!6y*$rGj>yh{<<7{~0yHvjBl2FE$mCF7Z` z85d3ujb}DwTs*x!o>`Xh$n>T0%$AG`r{9lfR$*K=oh5-egy{j(^r!@82WimhmI^GO zUN7i2118W;78X!Z-td5F`ql(yb;eiI?V9(Wfs`wdYo1m>IEtWa<*IF4(kD`Yc^DnXUE>KAb($F>iKrFMwbhjL4JFGcd4sw(&sAI?I$mj^lN}#?@mLrz} zQ?@|sbc0;xR>m3Ax8*W>G0vFIn8&Qe_;9*T9r%B;#bYkFBJv%6*ssK96iEwZ_C z>;Qw~K?cV!pexrvg_GkFkchS85wO7YXQj-!De%Lg8$efM!B2;=xadUIF4gfT`R7;=TAceR~=6Va5s5tIL^fVs3!W7zN#SUZKF^cmc*K zP+)O90pnySus9xoaS{|*9CyGt5eh7h8(^FO1s2B@Fph@;i{par!WGO989661gYHxk z_|H83ZzXdGZEajz#cagHIC1)e8fHhnC7`LDGYp`^))@tsY}c=4J`Osx{d*I0D4W1s z&DR2uc*}l1xS(cG!E+fb<%%FvD8>ZjsVxG)c zGrgdjIhXSi6YOl9XWh)riaS7u-$;RPwR2nnVqbXzy}KK9A`$4QoUR^b19*R4j70#{ ze}9KqfDgV1k!gD3EG7vXCdeKL4JHM&0Y~tl7-}mWw1)#()%4vM59*j?> zzv*LEO9CCHaR79l!xm=H`3f^z4uB3FIRHAu9yA#VI@SIVlLC_l(-CGR@FfE$zz0w; zf`kq*gHN*qB|OLk1(OET7RaF#Od3o(Knr6*mj#vdGpmao0PA1?YXD7|D6oL0kEd_! zXMV&uar%@A%;Ahp)89;BR$^?L&Oeb^xgI+HW&!RmgDO?%pc>BcK}gddu1W#6Xj%o- zPJ^wR!B_!}Hb4m(ZWK{s;DTm(P}^tsMCNuaQ2!k?@c}v%5#n3WtV%8I$$;89N4XG8^?1l_VOkX>Rc^>BrMs7y~(75jOkjc!Euo{U4d?llSK>PH) zlbIzXKx22HbAOmX8xhcl03+eD6b}G`n$FO{KPCsq4byw4FzYb>oW5%cb0ln_5!BSi zITFYO8vFqH6lo+7)SH0}?STe|Kv!FVPFZ$5eC_~v_5TOZ>greEQ9*(IOpXnpGtOll zCxFj5pT1)%bBrN)WizPx%$ThRN{uXKkQH2@S_nFE&!E5zT@Jtis)}5vF)IYVc;^gS z{R5hNAWj=2Xy^*mZQ=gI#K?>^Py%XTIo{biea&>{M28u>ok2tC;FYykKy2`KV$dCz z%#L3`oi<(tCeS4Q6Hv$PIHQyTv*Q#{r6>OU_cBhJ?l6m4igD8P=vmChjFYDK&tleKoIHKUEM^(T*6mkkF=vAsF^+SX?=XJe zE;pCCnuT%B^jQm;Eg0u)zr2un3lnd{J@5hbJMMuNk4~SmgxQAi^Yn{LnEk|W+&RGD zXyDiZUZKIv!0mVfvM<6at~f6(D5;#hCRC%FS8>^3N*pO;5cKt`f_Fqz8Mg;3ap?dBy*uC{`iF9cM=FgO}y z2|&~uz|@)`tF?iNIv_+HPcXnVdBCK7Akx$4eq>f;y8@aidN_Uk3T8hM@X>gn4eOvA zVjxRVCQKJx$tI2{itL$J4XdF;{};p}<|Wzl_u0uVdB~eu3zS!K)no^~^dV{@{f{8yK@3 zCx90lI4TI-nx4O&S&2K41(c3JM*}~cK4(3%jl@G{M+RQj1Ox-N0b;QWVVu80a~;Eh)I#paSfCu&rFxCeB} zC!^yAm_h|E&}q;SWxr9pS3G47r&_mo*-de(Jj7g)@p z#Lx6Y*O{$hG9t(^H(l@svoPbV>8dxt^D}NYm~B9v{u|(4^~M|E7U=aG%qGIUpd_aN zKCK>{!9iCn$=zfQWqi23BlmUCPGbd7m-F-V2e+74GVY$Q1&VP{(Q^d4D-?9%7O3kFy009e z3N&~JQndrJ11xU`WCs}cPEc76+2eiy?y~71m*KYFu?59t4Ja<#fv^+ivOQ3j?Lq8K zTZ2#qbJ+%n%a}GWPoHp?ISV=j0&1ay?n4Ld5)-&So$nsAyzGKKhZ!7=V51*Q;I%nR zK>ko*5Xhe{*uf$!52;dkV6_8!`Y5Jky`tV~!Mu#4mWxfkhEC4y^zhtN%Pb~8;ipIKfI!|l^`A27!=i z4>5h-M`i_9(178?>2i;lgBd?hFM7mW#5igC(?{U;65nG`duh7%V`fLj3Da{QGnX;$ zp8n)9vo|y`gBCh|o^JJoIht|z^l49+?IHP2pIM-rX*wd~`I@ize}KVJ2viY*XUBJe zxI7>(sJoy5T2Euf!~;1U9(1l7xaNKF|G@M`PnorqKZ8_*c9L^sIiC1`fI;9mqvLmw z80ffuo-D^3{|`+6@RZq>@xXMwXUx)!tEYQCV^($f3swTM1iUh-x#1v#qoBYUkd5He zwz;n|LXJ-r$W~%d0p-8NOrS#=I20Hh+m9SzaO4ptYdkRla4&IQ_`H2uXh zW@V;7AExs@XVwAF)PRONK}9|E>;c61)%39E%o&Uir|*BxoXB{6y5b9FQO3{H&0jEk za6H*~n88t6;LP;)7t9K_8+g&`{oSAwd%=|&c&ZjOQN!r?AEbvx;0!aU56kG-ya}`- zSm4a`Coh;o^$vi>M8U^@gG(V6$8{hhm<48lTLF+1&jh+589d4~J^dwf@${Hi%(ggM z6zg6w>oBgFe)|=(RS>vg!3$nm0~&H;aD1=@r3t~HzyN7LfeU^P$Dg1^3urNu^8Ka#}0I#0|`NOuPdz5#n?X#|_NHJDsS2fv${V zbo>Lc2-1y#gwgcdADHbJA5NG0$PBt0H1H#{JmW)famn~_y8H@eQO1X$rnM?)l_qyT zsL%z?(<#8$DO|``V|0X0_wX`-S>RbN1qMgZv=yiw%LrOj#Gn8=7YIC^q`|}hnnD6i zOMzOrpuL(|j?hUl&CuSq&7w?>>_X)8`TEaWk$O#cNoeJ(8=QA>b`o;p_<6;!R>waOy_jLL1%;MAi zKQm{6md)?}%q)w;zPF#5Z6*GJSI>c_-$1DuJo2l-1lkp{?hCWR^lzV;jieL=@G0_{ zp8AzpK^ENB1h19^T@R53Zlv~{IKbd2Bhbt;{md6;KQFvW7`VGZF$Nxr2e%eM!ziE; zde9=L`^@~@@R585$2HT>e`OZ)1dZB*+L4Tqk$mu)O^_IPBwv9+0Icf;s6s|P-vDC( ze!AW_W-$&}_)TqK5u5s%MP$1BcV_wNQ@=5rOLT(=8gK;?%XemBxNcDB`%QoNgE^`m zIv_q7Hzm}2|KKr4k~jQ94|0u z5mxOBDv&|Tbn3bB+xwd&dkLzB+sn7Ze;KG0D@8^8y+Qo+b6AS1gFMy4UVQhNISKg?Wa#JG#u5$q&>Zbz8MrW^lZ z4p5v5S~fiav~LqhiiLq2i#_!O?U@2H4BZ}Z&@w{?Oh6$AU#`o5Ww|Z`Yy|_->LgHu z1ho7FG_wv~h$IbOfdy)TfYKmno(i6*r|Ygvsk$2^ zKzDgLz6bT$K%+wnECR2lzxv0l!VL=_76$I=h5wl~*`|S_V9j)o|I9(6)4)Ci9k`Da zT+`S8XLewmIQ_$aW_7sTQ{`DA5u65=P-G4$^V_2cOb=jWK@(Za$l@)q1ymeEya!(0 zGR>Y@YPu2=i!sv|#_0{LEXLEjm{{z=_o;(gAfTx$2GGOPmg0|QGmMLVEQT+77=(bfK}8# z2F>n68pn((42}<=eSc=q`4Hf?3cmuQBY&161E}JKV7Ma2Y(8d2X3&s2NE>*B08|u$ zhLk~re6GBVPz|6_NRZ?I|NsA=y^zHLJn9H8=k>V3qljR(;~dEFp&|pA<=Ai*w33>^ z5jv`<$mBRKi~31UPOL7#*kU zGqI>KwoR{RWeH_$2T>-|IoVj8z&$)hM;=EOfrrz>*jUs-%P8tW^ed+6@7P&9IAGPs z^c8F@+Dx;UzD* zp!Nid1L)981x8Sd5_Fa+BWQmbXg3CEv;edK541plnSnb9w10{bdRyCcUk;WSrUzWp zS97pvGR~NOm4hV=w7ZE3H0#0P2%3+v;ABa`>lGoZ6hVvQz^e*h^RYxRKAi5x&!Q>@Tiu3G0$S%by^)_KlyTkk>-;P;8IMda z6kw5%dd>)1(goio1{&W2tw>^Ud;pebd_H}T0E>ngxbEO^Jk|}q>5d~yfeCa=?{q6c z7G0*NjML8wvZ#TY#d!L9M+I5*2y_QEg;-1(Tc*bgv8clg)56E~g;{hM6{d->NH}f>-3-71N?q)r{Tn=>{X-g{^}B2!mVy#!JuviEZ_qtp zpfyn-A$AQW2?g*i;nO2USmeQb#6d|BREdIeC*&#(aMBZaIDM)Ji#(d{Luz)t4ONz4CFrJwnCd#6~IAeObD2rD8o-?pcIp~-jNP>Zg@hE`OI!_j8CV;{5 z#e4945oorJ!SN1^BLLkk18EI_TL%UpAtb&Ai0>!>-7gD{bOpx^powzO+50S@Sf8#U z#*)PNVS2k5i4M@c_5{K#SDYn~argB7;w&v1dAKv^64oOENYC)r*})REM)vVU00IDL2)@~JlByCe*2pOWLQyw&CwxS zflZ*1aeA*LiviOChUxnxS)|=Sn}!u&w{EmBDuI2|0V;Vx*JXegA%c9y4&E9GDr49* zn4T~yf$q3t(_jMc+y$)=ke6aH0-dlW#iGvGKfOwdMFH$x&~!WKTqH;m0TuFGkX^>p zw@bm32qVaB1$Ke!(^;ii%%EW~JzAPYfU$P^{Ey7?Y-^Z7rO@_?(kz~!`On)jESikX z(^+L%K&MRj%Cdk@nMjgj31e)WZXnMhGX1O^iydR*bRKyYWr$FaTq6s&0-FN=TG0Lz zHU;6epk;WVD>FE2)gFOL4iw`fdRBZ1jKU$@fbn8LJ-#p#ASkW zok3jirWCNO3y2Hekpkwrg1F%AC}6G|hzs6}0_M7dxZsT_V6KNkJ(n&6Xd4PMnC}Ua z=7ihi1>$nSx!xcyH=OGO;_|?`z923yoa+bT^1-?OATB?c>&RFraGV)5DXYZD;^?5j z;b@Viz^uS8;L8nWD={FQ#K8s{yx;(B^8~HsV*uU$%BH~KXahcs8Z16tPmx6&eguaL zXkig(GX7>eHh`eeAB)WTgvntDc#+lQV)mXF` z6{h>Cu}ElxD+_R$#|m;AH|X?Tu-m}r?HYhf0npM+Pzf+mjRiEMwp{#X zSJl9d`L70d40x9}IL~{kqdSIO09p*Lp1w+*C4o_4I=2Rkq|SEGFfJRY0A|%-s!`$s zIi>+xP_b$-RVaZLxpF}lxdv#kh%r8#p02^t$~bHKTMZUHMuq7znk-VTq!_~t_VEo4 z(5+dFAp7ocfDRN8csRXUlf|F$$@J5jEE-;*sJ#JNRJ4EtbPV8kP~@^Y-TSuCZ-5QF!GYpzTP+sQ1$tpxETFZm{aP$y`hS@e*&IP}`k7gY3p7g1<+$en z^b#ro1un-e2cRcqb0}~*t~oIMjV_BM(_g0P@3mO=Gk%`FUYlhD;^0Por70u{dk51Bzb5ja50C_!6dLA!@R-4sZ{&Ecq!C2$#Z7cHd6 zpui8Fs#4$p9WSfGz~lhBNXzj717s|l-IbRYbg!`);|&f+&>pQD99aTjZwP=k6lE|g zaq)r{0ttW(f%%Qil^1kW;Z=m+*c{Iy{Kn>Z6vhG7%z~gC!l(cVnTO1v!=Jbu@4-}n zg5nw?DA@H_3hNg$vN*t<$cfX55+ElsE3t!JC;@RH=$1FoRf?c1z(8j!Km(jjfz9z5 z%u0}>z}Lco90k$Lwigfrmxr*R7Go<850MYg_{TY-ng0hCQ6Ah{HheG|Yr zj8%gv1yVLZL~}rSngOJ^0HN3sY$-RS+W^{g1X>^{1YaNsD$+pHxZq5|;gIvfc;P`++pndvQ6Bc>Kozt(Hu(&YRZ}Sk!d;{Wa0rj`HOrLGVV#@es`$a333?{}a+pTO_8W|bSOy6V2a)NQv^j3QobEZ2N zrth<7DS;W)auIA4o8x@OEXOYw4lp?G0qr~7GCj}%WYqL_2Nuv_{Ig5vu?`Fa}z>gNCIY#oa&y$()Wi7(jj3Pt&J4vMAJlU~DmGj!@uq1f3)4 zcn-8U1FS-c5mGLKJGq=1OfgDeQxhP0np1-*0&G5L;UGAuAy$Hpwc*rYQh^<71C1pP z&}cVkDe7lt$k`VxuAuV<6hsxan>w+yFfu-yzRQ^<9F$;%Tv)U~3D(wyMOqS^U|}ve z!U%D~2}XgV(+gc#L?l5?T}}Ce-X+*xdSjxsBPuKEUv zJegEx9Zg;ikp^n>0kMoj;ir+@Wk zu@Kq-8j5Cs3@3nM^%c`}K_3=_qLU*iKBBbk2I2h!N$P~Zf4f(_(A z4p6re+%x9{u@scJSsWBNAU9J^7w}~f(*`9`P{9iAb%V+^76%1HcMFu{p#Agdfxav{ zj9t^qLRb_T1E$XiVKD<`g!3UR(k|N>L6>@i>}Aqm0`~x!G?*YgK=5+$21alJ3c5iM zk{g*cm`cDxOd3oTkTP|;UMRRUj|*ifWZX0TLMV$XWB+uqFlbv4$(7Sx!r)=ctiTRV z?ANFFg|QeZKG_d0lb9U)8M7R(fH-pwz?bNQ^Tfk27J0_+)9ZX$)EEP%FZBftN-<1- z=nLt`f_8U6R=O`?RAL1!Z(jj!9)f+b0FseGC$++TF$E&Vq`@=;mbsn$!2T%mV~JI{ z%&f#=#?$~R=AMG8Q)bZcEvp&R6wu5!lLnK)4uP}NXZf=zFkYL!+n+@mw1*UVTND%8 zo>iuL(0N2|N(_!--YgD|FAg4HP-J!lcj`e?#YzksOdW?E6u}pAtU2Vs4PFBM9%M5s zvdzp23?MGE8Pfug(?Gi+m_TRnC`@1H&%y(B3wYle>V6nj=;{verdII&Ux6$~1%cPo z0$7ycBWsW&RX~Gp450m{mH{k9jC-c931CqW+rzBHZN}7tu(O9TOW^wSM*%Dv0(+R1 z_{^9lfC7eBfmPt>bg4jauviDO$bt_GL0xFV2XzV`XqPXz(&mL%!rYF{9NxU3K@0_M z@CtrLZUt_zIxeU#E`eFoR|m3aIL=}M4Y+`hv*1?X1gimuga#9XDx-j&q5xID z*3Hmh|A_>#m?pz24hF}?hrn$$a|T`o2FH0Y4rpP`G#Cdo10f2ks6aD`qM)*a8Fgne z@&;P){UIixQ%2SVv8YQ!+Hl}gtdOE^0XP`n1+i$>L#}rQ?Z%jL2)fx`L4n1w2WC0w zWSJHiM?ita@eiyq2Raw(3yi~{z~ZO`aU+YP64;GsNfE0f8K+MRW|0GrT!S(!Xm$pY zRoI#871*Zxgt5p^e-+Fk0xji1t#wdUb$vQ-2#Xnb*PSDyqY|{C2F|Y;o-Fdy4Z>M; zm_d^e|HD{78xLc{!K2H4;Ve3gN2c!%XHn&y0;;x*9Z!Hx6L~%TZ8*zU#{1JBN3ehf ze(fSz)ayZAECmHNN0BTgR>yasEt71ZMg=FR-L-|7A6+P6VvnKSnL^>Y+oP862J)B#KZ(Sf(JB?DX?KWe*#McxW5RF z|0#@FN-U0xKx>Ye92YQVDY1h5!>YgnEsRl%F&0pXwg58p2KCbPjS1lX;;96dOwf26 z=@c@Mg%ST>j z?&)4>EJ}<4({s~Uj6pTz!Zb+T!lJ+ojg%>%^A*5V2FN;a-7TAypW0%l&rv2 zDWnlNohcn$w-}|f6pL04i1ogT#Q$l}K0punuj zEHT-jJ(yil7{nHselDFQq+SrT&YcmY4>TJCYDlhUI6(9>f6nLGuJCuZ+7{GhS8yXs39A{7vbuwwIRS-}RX$g?7QQ&t1 zP4@CPEATp*wkh#SDeyaSceJ!92szcaD)2hlcQn^J*>}|JR}`PVE|W!;1r*lP&u6m8 zhJnueV^m;fsRM0i=1>Hk`3GtYgJ&RFz^9ym8&Et7;N>CQ3M^|`oj_ZR);cM)wJ7p& zD=-OMW(Hk?1#T2<1y#+Q0!OEtXR+v|USk5)sk#gfj*RZS44}N~q6E6=2{d&Nx#$VK z0%mt)0jpFHG-viuU{Vk^X9i7xGJ(!C1}zJm#G=G%#?%6;aQA_%;ubhM{Ztl<4CA%w zce7cf9YEC|$Oh2z34{$G`(RZ(C)5VVlOX+|V_IM)Du@Z3oxV4lMV@%WxM7A}1{uZ) zYJ)(Xry!1M7`P#X$7Kr4kiHxE;6DYvC?Yn!W(n?g~tf z6ToLdwJ>H0z(XC>>0{Dha#7-RY;orR1p=f&fawfSaDf9zff>@wU{zoSS6T|}@Qw}m z3Y`Aw`*K*6!6U1n`ViX8p8g?+C5G|Xbl+SSBgP}s8**7JnVXonr@K|Lm}*XVbbvt< zv|TqsfyuE0#tBeha%_NcJQSE5e>|MNq>9Cnv0?hNDwYR~4cm`bv#>ETHf%pv!=eD1 z;do!mV!`-ux<(y~yeO#q1RAFR4_1K67|@cw>3ecnqU9%mE;M|>2HHW(A`q*<yJkFEcAKnK8X!Q($m> z4cg?$CUA86t$Y?&#?R9g3cwR~VFfInjGw13C}2royf&S&kVO*IPGJNM*D*RC+;f0I zgMq<}0km0aC#*CEjT~--aX{fF0xBy(15P5K!Wh(YfUJ9fBwJ8B6%=F4;3gcXsp!cI zs(NODZ`%OLg5nGzs|?~omMk+kegwH1oEn+UnC5UPFo3Rf7nn4CPa(JkdKcBbD`Bnz zwQd%{I3V}Vf^k6Z1+COj1i4onn|nbnL^uKDK9GAs?gO9G32mlA90-yHyB8!4S~Tzr z*}XGB?&VP67ML`BMiGlb{WT`gDwhqQGnydhK5t=GWOD>L0yMw}8kA#noCI?y$P3*t z4#*45Fb=t1s0TR`yB|RQ1<8U3j(J!?tD)CGmh(C?I*Nevk0Ya_I66ZSRC+<;8ocO9 zfmIRIXa{#?L1T@eJ}iUd0%ph-1vw3-1=J*>wge zBf+&V0P*0tcLj(yVLERG%PPi(?K>(EIn)5;dUi(z(7~kape2N$RtV^r3ef&pa2^9~ zpJfLvb70qCVo_k99$v??jPb?ve|0Qo&{j0K{!w5MxIX<)Gm8T1tPE&VdT~9=VbIF^ z1{QmrnM~ZE!|_1YF~h6_6$U&k+(;V1>3{e1i;XND)3q8|tjX0)wzZ1wSk3Z~Gd zIiM9{AW?83$pQ*#7EnloZo*Mv;8tZ&U;r&kfi2dZUfs%~=!3ox4|0$u12#?yw(GeF^mw6QoKRL*H*kwWnHwXrxevQ1}d zXVGR{GhMHpMMk)rg9H#U3zcE3vCED6oT0hT~9RcVYxJ zu-HM%GjA{}u!GJ|-UFgp%s|`u*%g@0n6`lUOlC|QKs2Kn(;5)XV8*lpM6;PQPXLX} z>^X6OLEtcG7Lr|{4|Gt&1JF`Q1qM|P(7@I#CT^+eiQM2uziJ0dZ9T{tY$^;2Y!GL# zL7c$`a>fPF`4X&PYr$uYZ2<9^!Pc^YQt=89pV5qI35aGeV_E>J9xlMGXA|fGO{u`F zhuSUx>W4RTc!NqwfmuxQ+~B|h@e~-rE07$&fVTT8J9Zoe?c$(&U_30A7B61Ajo&LR>MVxWg zbp9Tea}nSzw+agIWv*bpgyX5k1E8EBkp)`j3Oc`wRRMHUDs-_6=nQ#GJ#RpV)3Z84 z^h`JHWqIp10W>110W#eYcC!2ekPxQko{eZ0vMGR0Wd?OtE^g20WASCI$7`4)3v}5H z)C2d>&0_&Cn*^0wpiZ&^i@-%tsR{BH2eMAE%g_yLZ#uxBzybG~%>`9&Oq0GbhB_nM4-Zq0^QRePhg2;+&bNMB1;wz=m=== zebJ1LXQpqS$fC=*Yx={9EN+a;rfW=M(Pca`J!}$7D>wMaXVAhx$0O69O=9t2Uj~|Q zb38HKbTUhr)15Idqso04zXA#qfs5ONr?ChzvP}VVre{xQ zv9|p154-@DTNspfH9$2AgJTDHZxjQ!7-Z=-Xt4)qknAu6c#8x?Y0vcg(^+JN&oF}m z4ZI})5^Mq!r}NKXk>t8^60`$Xfk|M-bfXz8w&qQsfjmcoz6E6}2FI35aLvnRusGL) zcZ`EhK%Bt{QGj#`C^tWY;~mh^3fc;g5ENo?+;I0WgQKzIo-+p^ms1KbIGzLv9S3ca z18oKdml{XT!jBT=<^iuHRbWzO6j0#i12vmA>^;EX$O>8u%a;b??*UD-f%)8EpE4*g z3gm+IfhrsDB0aFaqo6r7usSIQrg{ZNM@~?S36$ivGJ?9n3*^aV5p z$0~4U`nj1b=}aFuL5nLT8D~rnoW&x`*Z{hA4s>c**DMy<>87(-q#0*SKRk;?ooxXZ zXu13JS+iK=rt{5a;bNRIU2-;ytmXlbICuvybQj&+%?Ch{3)-U3sL0I23BAH?%Jlfz zVAHBW)ZFRQXR|0WE}gz}HjA~y1+Eq)@QJ*jZU%T!BzPtIjOjdcSkxJpO*fgtq9iqg z5p?OUqe2$AZ3jA-6tvP5bh^;=f;lYGj4P-2%we(90GY)JT5QDtTd4m4;zH0l|KP>? zP_sYHVNnx$z*VEj!R!DklR@WlusAN8t~HlM(|iUaFPj2~lR=FlsDWz$u@iLVE7Z%R$tX>2E>Q zjOob>S!OWKnf_-XODW@m=|ziJN*R|-e*vOaOpja4Qp&hy`V|nhVY=56mQuzo(+_~C z9n;O0vXp`b>_OCl=}OC3N*Rw#pRtUkp7F$ViRCOcjAy1#UCvU&bYatU;T0^^OjkBd zp8%q7Y?{uylBJsI&Zg;IAo{_k>Fldms+pc_n%)AUUu>GrxSFM!>CL9;bs+k~rs=;y z^p{Q3E7q`7GyT{!{p%W*YTiGaz)LiMpmV5wo8 zG5r>Znln9oBTEhAg6UU4)RO6en^lT(8-W{7E zPEz2`Qed9`U?Yp_^aX2K6s8MrWzlBb19Ddl;{lM9Y8a1zTvNk%0_2c7#xv7Rw}XOe z^YqQzK|!^7y1@>XN~Rl|r?1`t3aZW1HFmO8GdP7S+RGBa*f2eLFH1FJ%l7+wSr##= zKV{_R1eZzzCJfxX436JHC;mx;cQ5fVI8Fkcmv^2)pn3X=11#3w6Cm3flo%XWFe)%e z3o_4Keb#XnS-zhap##LAVy$ud(c>(BjBV3JPO$hhwoOkt!D7RBefq)^EE3V6ZW(CT zj6bM72p&^lax}u3+T%4Jgu27s>RF+z#kerd2lard4o|>0mqL7@QmzSDclA3~~?=T|+gDVpMHUk4g zI0FO2BX$7>Q3eJET}B25Mj3EeT3ZOi3+5Qu2Cw;WHLBJD8cpB}J);xe9QV=zIkvPs6QE z-+uKOi#U@qD5dZ)Ffgz%Ffa%*Fo08vFoWZr{RbGhd$~aAM`ZfKk1XEXqh7MaFlKI* z6JR(jC%|x1PJrQ|oB+c|IRS=Wasmw8@&XL}@&XLP@&XL9@&XL1@&XJSTt?7jySiUkcOc#93(pWDAbudV!Gy?;J37fFkSEsi>R+A0|Ubi1_lNW1_lN# z21r)XVPIf*zzE5Ix(o~qdeD>z6Ek37U@&BWq}B$ggN+#&7)++itY?*+-t&gVP1%Zp zfdLjH)(i{`HVh05!VHdIP99+3?qUYTh%GoqraQc45n!~N9{83;SQnI3Kv$K3d;r3d zNXbQr5t8v?SwVUF#J4QUvO5_V7(jNz(+!fA!_zOmWl0xU#4W&3%)r3#k(Ge~6n4`+ z-mw_VICh*pz`*?y>=b7P1_sb2DWLof3Y+O0m$9l%4|vbw#m6ovz|g|Lz!1U2z+k{I zz3Lr{=Jd78Saqh~f5)OP&A?z_Xk=_+YG!U>nV6KElA4yAnWA8)kdvA>ee-*k9xK

5TuXrAf|dZoLoES@r&XHJ^vMpd;M8W z0fsA@0u0wQ1sEP`3NXCU6kzzKDZubwQ-Fa>OMpRCOMpQR$w~_?0fsHwRKs!%E?NQ% z9$EqnzFGndL0SR~F zN(KgojOhj(tg8hxp;|x&gK*aLHyo^nj0>jAak6?ac1%y_WPQc+tLY#EH?sf(BZK3T z?Xg^}fpUyfrmxpv4Pb29rpd}x!MkSTVFqqyr`wDSj#oBr7hlNQ%cz@+W-_SC0Eq?M z5@5(@U|=XU1wg3ay9RUW*I|2+*cLW&9?+7qBHtadf zz`d4p$sGZPt9Jw#nC=QN7)=ja#_BFO9U4R+KhI!bV3;|5<2#mEeg=kF3=9mj85kJm zFfcI8-7dDA^){pLDx?gunt_2~4FdziTB!FxWg5tkbqov)>p^V_1_p+W3=9mLkm6x8 z0|Ubr1_p+$3=9n0w%e^_m1k7k4hn+Ayv*e6)D(rp5(O(R1_oq%c1*8Z#Tu^+Dtcf= z+%5(NhTRMd40{+HcN{&;z`ckY6#09%t!C|JW;{B*XB}%h=P?EbhT{wj3@5hBtY>Xz zWSld7!3I_KNnyC zrBe`QXK?)T{xAc#E{Fs2?nDL#uq22*3DiFNzn@i17JOg>H{(8HC7#SHSHw!A&GlA^qWMJfAWCUq5VqjuoVqsulVq#!m5M~hO1nFR8 zWMN_CW?*GxG-hC8VPpbJGJrhE$jQjSz{0{U%)rUV0b+sN$im3P!O6+O!oX@GY-+~B z%)rUO#KOYL%FN8f0Hpp%#_XjHem%85%fP znHd=b+2mR3866uM4lr=nGdO}z8q?}ZSg_8|L6y%8Ak|PS zi!;;nQj6k26hr6a{UXwgQzzdRkz|}P`KyQp^I~SX$&JEdT=}{2NtGq3#SF712Z*XN zE}1-ERBG}BQ9j0{ljn~v7*$%($wM-sBi-_Pg;IaZX%4|&n#S!SX7)EpIBOw z5nq&AT#%nvoXRj~a)6jPS06J=ZenI0!y2Z^nPO&)TP80MlV!X%d7qd#a7AyffO?$+dfaf#3kjL4cZ zi{pz+3kvd!N>Wo8dYJf-%*{(JXL!cMk0er@nwP?Gl^Gm^P_v6m5=&AU)=p-T$YPv3 zIbT9w$TZo)Ff}dF#MmI!*u>b(l40KD4HEhy=)&$HWfDRXEJ}rlwlYsnkQ8HlIJsQXmGS)Ky^^v_ zEzFbeNJ`hwXA(eCUtE${RKhTig>xb^BZDI&_bJ8?jHehUFx_BU$K-j0v6b;CqyGxV z1C09^_cN|%a=*j8i}@(i6~<@GUzq1JZ)94=1R*yuEk|O3cx{ZEnYJ;lWZc8Fm#LTO z6w_9w2F49cTbSy^WLnJhlBt=cgZV8}KT`|K9p+bz z-Aq$i&M@C+p2{+xZebG4Epf&)CGY zmiZvl8s>vc2bnf8uV-Gve28fc^KqsFOlO%^GaqJJ&9shrCDTgg{Y=}LH!>e)KEiyI z`51FI<07UF%tx43F;8Yb!StT#71Mb}duOL@%pEMZnO$5Lu*_nC1=m~V$IP#p9x?Z@ z>@a2i&h(CPGSgI+i_908cQK!5y3Y8HxsT-r^9klB%ySq!nEF{JusmX%#`uWo6EhE^ zEZ2=&Lbo?Bl|G~`++o4QThCsn%;5NdJzI%Mg@MUo`i0YsqSMb!W@ev$<20kx^bKuH ztl|!qjM;q5OuUQ?+zJei4eVK9@(&jSjIYSWR1YQ*G)T-rfkBty125PF z9tp4s8cYXxCp#92I`L9ghNo0-G7r7j6Zx`5!r))c$L`Am^MIpS9p~; z%$W8-c^7zU@(q_F2Z(;brN|7TpKvL%g6Ica zitHfz3%4R0i2lH>$il6_P!9^TAKVHo8cYn1j8+UBE}%M#f#HL_z-4AdPLQ~WV>#$|0rsr5 zP9Rx<7IS6`1t!N1_ADhv$G^=77!;TsEwYrD1-3FOFe@+$oMZwSXQKo*)j{Aivmz@< zz(R?c#X*5lf!UE!;5jolNT`EdiP4o;1;p%OSEy$M%kZdx<(`1N3^u8O1r)sjAXjKI z{a{w&H)r;Ma6T|A379j3-Kxp-f>}ur(K*E7pfumj^2IP_l%u3882gC96ma$6O!Wnn z2bj&6K7eRZbEY34TEv{G0ThzL=1d(RTF9Jf0*DqgXPN<`1fJLVt3``b7W9tQ(#tPVRlepQ{ZvqWrQX!HU;i1P_|%G zV9!zlX9kcTITTnGpk8DETd2sUz~ac5t;E91zzqsI1}lacprWdt!;Gl`#Naez`U46W zW;3QAAe!Bb=?jPkMHde@)G`Lg4?I~)%-}*viA^BXkr@=I514r+r9jrbVOHb^3A})2 z%O}i=EQq3Q1}`6|$<4^b4KC0jL8A@w3A4asCa@T&;AR9BTVHsr*R zc&BfaVic`saGb)MCD6(QE(Ry?Dlt2@aCq`EC@_Od9u`+#1qB8Lesksyb_E6nRx_p+ z5Y1x7)W8n$gc7qOlfV~H@Eu^!0)=pq0)yic9&jKsIkG_tB}a=a1!hME1r~v&j7$z7 z7wzCtVg|)kJ);PY1y%(X1!hR?#{{aO-Y_dL zf!y%|L^GR#5)+dnyA{I&W@tgj0xjblK@3pJWffQoa-K$(0*e5+(o|s5XVh?HcL$}$ zdQd%a;+cV3XV;~MrXfnKnN9(E;AM+Q_C zC@{Jf@hX5)%mkPG*w`BEpmmp zcmg{(PM(A6Nl?wc1eChD%$PoaOkvPuKEPwn^aY%v6+kT&NP=b)2muAZ29tskizAa8 zuY?rHRVs?C3d{oc7(rRbjaL@ZQdq#NP|u(MHPCSZZG?+f{aDyspkb_{^4B`k-Makg!g$I;*L3Ob?D+rx|NrdS3e1l3rG=n2 z1rq}!Gq>Z6T?ZH(71-3JBE3zuEgW3fQJks1ymLsbIvtuK~P*Eub z{^g*+fMnWwH(n`lCSy7S3RGq@rW2s30eOX0f!|4?W4S;e$PZ6I)g8nLP~0dmX)=Fc zhSd$sFPK3!0~0v)BN8vDNdT??Z?%KV?*r^vpz5Dd;0Y5XZFSVclPA1N11C=lB}PY9 zUq=Qp?0y<_2)B z2dU_oe=vh;J&$^D9mxEFSxFelH%cNP|A1Sidq9bp#f)hOh!!ztJ^-SH&6!VtXd!dv z3m{t1ocRWb7BFXi0HXQLnO}g4W)=ko#|1oDN-P3Xn3Y(q8Q<_IFgWhu&QfFsB`wEy z+*$QX?D~uh3apO*|1%eY8dS`V%nB?5&CCLC7?oIc85kT{+z^nkW=?{076N3W3f`C95ID1MsG8VdmY8MSA8Ay}z2e%`Gzz!xJ zDR7Ak;>8K>VIyGiWleU{{(n zIjA1w_zfTfuJB|jf(!izMS;bUB}-rx6R1Vap~wkp7z=@NF32brM@BOy z9&q+#(O_ax;&5clR$%r7wV@qZvK$!|m<7Hwf;93#su*ThP}vD;*|KObaVUhaW-0K% zsuqv|iX2eI+@LB)fi(-%0pU^LP+-n>5;X&JuVEszxoclvos)1>S>Gw+*BOv2bK`1NAEvm>eCl9p`|n zYfyGEV>$pzUq{$MwLerCWY__AP*rgQoSGCE9PfY`0yh~!;V$qM$>e$_3ng|)A?d~| z1}Xy`6gd=_9a$kQ4i-r30+bAQu$wdAU{_!Qu{W@rGhbi_8RiHo4nYMTs09E@Whku& zb}X$2b_EttNeyZ;Fe|Xv3)CwxIck6!v5&#d(t&tb16Cb_N(**@Y0Nz0pdyD!gUP^* z=>@0|XEkGb0-~ACm>z%%^A7ebfoUMWGgvX8RiR8z;8myqC|nR#C>yw+ApogEMZiU_ z2&meC*P%l2Iux}EWkIe&SwQ6+yb9&vC!q?p12;|KH7FyZ24w-)pge-0`V&%pGHEcq zU{+)YMbZ;yMK%Rifgh8^ z9ZmqH0wzsxeb~XSP!CE19k3oTs6D5lz~l&O&oMjhhI9orz!f4hyh1#{4(pK|K{S4t z^cg$Ym6%}_A~U2y1h>z7*g*|#FcaQd;^F6p)N1t;Sj?Fr{XejiAYEW^#SQ5iGyhZE& zb%Clc#}9#94loFGFuL-BLm4Ixuhu|mlOqe1R}>f=|8Qq1u`95GYdJ_p0+FCy}U>B&G{IN;29#n6EYB7*ELG23#E`jMx3QP)InoJzf)-;zU69c$4&7i^bgBw(i zPhtX9XFs@=m_coq7tH2Np#CD0CNrq-$O4gbWD?i`Ri?qjpu`OB$T%{B2c#GjSU@9D z3>r-JKe#oRBm|yKt2ymz22dLA)V#f3Z)LCG# zVmJb7uYkH_ApQg9EJapODeI^J=_i7!3T99ml~G`J0<{MP%$XP*Uobeb2-N>%R$>9w z>nsYajxQKMrK6$E-&7(k2{3}65fOkfKL}=>%NC86*W7Onczs2T;T}z{R(qh%e!(hdE^hk_5Ql-@v56F7S{^ z0X#1Djz@t@U?QUezrYn{MOH@!9vQGtpozzEC#cQC3-02vDS(<$7Fm#X4llRknXLyH z1U@n|a67Kq3}p#)u`qB8bc2Gco<)HLTt_+nfU5&l_@HXbv11oh11L~nb{*JqfWc7$ z(m3Mg#;#^Q$URaJHINQ5O!ov((@Gd3!^q9R9iYJEc!vSj2eW}!2cV7s#V1HFg9Z~ygx=u>Tg;%rw1*qD^Tz=4 z@d0i`}G5KZi)+PAhnl3fP-}xM3<9pn(DEYiclcz*Aojiue>BPzMC;)fqer>5Sl^ zNNxpQP)=n4r#DAN@Nli;9$wJM5X|%)yohjd+=3#t0ZB@O=>@M6Be>#oWXuMSkZUl# z;RSnKr)=e2xtq3ap@EV@B{GF>KfvJXj2BW`V{B;Bmvm zt-$Ko%;C)oDoj9e-vHBJ&#Xwa0iXdNMs5XG@Q4qC22%r{5+ii{nGqxg^KA=6+>@6{ z0VED8zh;~}z~E>o&cvmoX?ZD1RF6=m*A$lUIj<(;Ok(rzy1>)}j^B+cmT;dk3#yD|uZuDcu zJCg%rEEp$F?ut=moH%(yOf=)f$vm+jwo&XJkvqQ*Fl3oBG1MsuJ25!5A823^m^hg! zP6@13bh1@knebgRCLRK_RtDKjY#}S4Q&v3S2`9%1;MV;FXqSTl#8>11rE}awV8ze` z8UbTBgG@}Ym@!QNweuM?n7)ALw!kxJ3>r)yctP`FpeZzvi~jH`aYAOtK)fHkpswWu zkU0#R%oli-_{^EXgZ7|NW6&TzXw;YsG%m(!#snVaXM>FLJ2vrVDe{2&Xbcc0I9S26 zeof#JGIk*el$kDe_)Hf&52#etWNzSA1c%WdE=5kTTc9&uU!XHyAGjbhT~l}z>%miB z6L`Q=UJ88X%-{(H7Bi+9JfJ=+xQ0ia`QieJgJ-_DKt&kl%oi`XPy-ic3M`-^8)N2+ z3tIAU3Cyo&f(?{|dK1v8FFMYA)gw=RDTA8e@bNJo_>>P9sKu$lgg*1dhdhD94;$73 z&3p+!hPAlBlP%zRoH?KbBmka00S&c7W>omhnd-NIBv_#%wV>fZW(7_<&V1ElnE_Nn z4F)Cfz#nL00u&6IOsF$oe6XGm6Y_u{6Y_u{DBuKP{U6Yjp%5bA>X8NnK_Len5M(i9 zngZ%=Gn+wXwnU&aTf)$pEg|U4mLOzWOF_UKGH}Rm&IBGf!w%$GZ;_F~RLp7=tV_X3aALMMfe@IVGB6qyy+p%YV_3hdB{DXz)><*(;H?*K2SQqJ~6d}go&vs zyciQxTi_E@TX^9UQyZ{MOvx)SfC{AhSbH08*o!GtsuZwwh}!ZH<)dp2pZeU%thCzIqs zv5c5Zk_TlwCh%m^4QSDH1w=EOF;bcM&9X$m+Mf+v$8 ziI5#_GD#df5QaLLBm)@;I{=xv4Mxe99elpL4#r7$s|6|=ohGcU$4NooC=dk zY~a2PXvzS>0u52Z#>zyb6!=gllO!P-;{h~k?tr2OlDsUMznRh^@nCe092aor^;K@=1Ez3Fq9@hs?EuDcdnL#FWa4QOc=mpRj zq!o}EBsR1eBuC9GC05w<5i5B52we7pDoRe!=m~uGh!s40R1X^9Bs_g2h&p`)YOsP< za^RXc0?!4p)GL6-0>RTptXT@&NV7&ftO~5$3ZV5d>Y(90&^!`ozKGqIr5-$YrNHdS z0iB}ZVO0Rl6ixm;DcX?*WG@e>W5Ep52C9c}O*A3qh(K;oVuMUHc_YmZDX@a9#WqdF z0a~j9o%i8_&--wJCbGcuKFkViSxU^4-%JT*;ZR^!n4GT?RBz6}fHWTjYJGzS)*#Ix z@O01_g401_3ZNPjZ93=zXgcT$`gG6*aEI{?IEjE(5AoE4hHbFT2QgVFu|Zl|ZoHzP z;=@6a6YF%)38d+u130FGIL(+)rh`Ds7eQ`jx2i|1CIPMU0j*kC3R*D7M$B{&o4_%|~ke2Cv;PCwc@xbsLlX6GY384HW)QKr=q@g%f;}e>Z3mp77xZ zi$>lNnS}Yl6#g&>{g)dAx#Mpb8YH9Ec?gJdh;| z&?<$84?JEAdoc0+7{X3LKDW zEDppp76;Na)(jrRG}a6r?9*8FP%pyfv1agSV4cT;4qzisV}X={r?J=|y=kn|SZroY zpdJcn1v1uYta{Kq78___h=_SC#55Kg{%Nc~n?Z{~iJ8W#$1;xvTaE@!KVRVE`ah7y z^&Ka`rmk|B9R62OB#wY3K=E<{N!)P)ca}f{lL9Mz zx~hp+fkR+2BW$vYpBuCWg0y)a_}XZ=S3yk`&=e)iX3(%XJGUaE0*?Zy40L2tWOrm% zWZ?l%{(u7ktQ8ar(CHbJVE6-{{JDc|^5+3}0AiLyg9#K=P*X6&3MzpbR&zk>36Uol zI3^pmN^o<4=6gUPI0Z5V#5)Bt1q2eB!48@7n8BVU@EkH5gg3;&{+k1jlLfGl0gJDI zi?2Zu--4L+*@2MY0e6hRvE~@e$PHSo@Aw2|YUcsc)Q;mF6sa3XQW{JX_%LRDrtm># zeWviC%=&=lLBRb%M@Gn)u?ABIA1DaK>-mr-eP-~1;+4rkfgLvK!y)h&)G37Q)L@5C z`YeG>+K@13gFNZ80It8D{7Ih$kjWj;76j1b4t$Dh2}GQzNgoA%7AuAo;JO<+>9anr zkMYiAyLbi0iId~wL9-Se@s^A?CLfBQ$T)FwNCHTtHldAi;$)UY5L-9Vj`7CioWv=N z6DKn!fkd>D-Z4&`ESU^qJ0*uQPMkag%svQai>H9Z9a5|qZ%nREnZ`JAvQVl9-$k-wI~ifv0EEGeP2z zsT#?wD8@UJOR}m#lQ&r)m6F-p87EG@m^$q{p{89z*(GS`{$&Ey+% zt+}6kI?Rxz1X?WfX0qHoRpl>Vz?(~adD#>gd6^tPK)6Z_uDomt3>l70C5~4nr_DRd zcxAH5{N;>qCf}JK3^jvEfzk2JWUB?rjBh5#EwGS;nxq6;OZW$3j3OhGgX5dY>lcU{ zzWH*P0kn#t2sF{j;kVzt|(Muf{1~ZfjYjK{C$DGEXW+tMj!@9km(BS zW=sq`EZksMI)0e!ywFJsBo3+}8C`i99YM-L%la9>*3MrDwU$MJ4QwqV!dezZM$o_x zNJNPRYAvGzi{qQg&lmb}gUkV2=J;i@!y-+_E0ePp?PR<%S!Ho5tO~34h<#_(0-q<;E2moVlZQ3aO?odKq#;) zKpQN)rOlWaz%_XTL@hXKu1sFJOw)e?RD{8iL4iSm(N`MeR*;|qqZtzex8szrhZ!9I z%Y%%5@aZrEBrtA(SS%p1DEJ{wk2Tk z$-OJA8TU-yvqCcQ2uNTLRN(~>2P6VA;tVq928aU^0m)oJ<~#s#Kq4TSJII_DAkLl1 z0V_q=K7g1{CdaP)$a&z)VFt%7Uk)=&PFy8g@3`j+#FHQzEDSQ{0!Ymfs0@gPE4u-b zIRlje(T*T#ke&x1nJZ8k5bX$(2Fbht$=rd;fM`dMG)Sg_@$2M(Rib=PkX8JcT)aw` zspsqDMXQY1LGkN2;p^nvtE3pOO#ZgYhVjZ|G?a}`E-~8)Ust_01Z)q3KTBT>L&bY<<4ZOH7Sg5CfBbK zHwNbmMrgiZK*|>kkemq?QeuE-O$Ns|lTWV6;RCr*s1 zd^36dTDY-H;OxeVWGpLWM-Ny?i4kfnXf?!}$v@X>iTwZ-PYepI*-GG44YI|t;p=3} zb$ZI6!T?&RC@_LlFoDfybtO64#;QmbByUtM+Y6du$DltGq zmk|`YU_~Gs9e+$#UazOn1Ila-4Gkcxd6>B!K{^${*&3>-aJ`HG$QckpkVTGPCKs-k zn7nnplqyIKD9-u!NZG$Z5B$y}S}$WJ(VfI*;}nSmQr@HT)rJ)mV(3r-$ja1;^PKKbY- z9sUic4loFuU}WHSWCSh#owRutiN z(iTibLGT1LXh8sc3nthf^^T%&t3f+8-oh*ZO;EjnaX=n<0ONqH7KK?2(*2xSK@d87 zEeKjS19pfvFL(|fbwejFR2eT=8H>PE=E*5rO&RA;p0!mcd_EJmBLfdRx8oLgTroNd zLRPymI!eG8q7cRfkRnj*DnRy%q8iN&wTN3_{$$y0_R2rD9{??1Jk1PR43VP1>Ue@7 zOM%f*1T@*l>iC2qOW@t)`fajCKbe)-TzMrG*c|8XIKZI6z+lF}p}^)ibq6#Kz`Ni; zerE=4hXbv^ntX1XRLW~+UM5FjNI*jM?}h2-S73A82Gb8RY#odPGE5j|7)bYXX3+c? zhc9ToK4d>EJJcWS0`n*PYzOUy2Q|PYSQS_xyWc>O%naLStH_Qck{t!$PLohzb36%i z0LV25VH^hVawj%N0ho(Gnx2Cub|JkYc7gd!lb>yW&e%Hn+zvTLgUN4qNY!s;RA5tJ z2F>EJYA}^3v4Pf{R6q=1)nF=6VqpR8Gh_h;6f-wSGDV3E)GW|o$^fnY0`IbA)nH0c zVuN~=4U{rKE&(-iSV8+FA{1C1dv?I0kIjq;6nzfa3apMd7&x;OV6oS~Q%P?=xM9NL z$XE#8K*wyxRG`4>_<uh- zyI2KIGlSN#fb?5{EV;n|B0)yIo2;`-60{DM-IZ5bf!%Q#EC4}!pBBP63<~UyypVuk zcjN{8Ux5SaM-Gr56+k}r=I#oH~Ho+6~_6K|Lu~ud&3Mi>LrL`R%8P$dI62UfYt`HDM0o%f&&Lx5L8+) za)Wj}PLAGf&G7)VjSE!d&D$*z{f-F~TRMvDpk^3|sR1r796?15Wbi~@fyMFoZfL#` zP+)P~595ILRBVTFKm+RFf(n$jLAsw$7ThBRE%xLg=@Pu#lErZ{Opmw%i(?Or12Vb= z#sL}qXBX5`kkRsxFlKQC83j(QAYXyK%C5i$${z5=IEvhe>{0*mFla*&XcxOXFT0}* ziaR#%hGu;U1s2EEFsniCSPbKU+%X%*0l7m4<_?fi&zTjF3LqxXRydHW7`YWdnTT6~ z$I%;-3VD!H0s}ZDaD!6`Xd@~(m4I>rX!8$fsUickq+`@zk^tuykSaET`IAfbD)NIi z&+|aj7mvXF$xHUyGR~j;Xs?a|NCq|Kp_a*TL(5$>;Y;GQOMqa-S?HGqAYw$|$fPmzpfdr6!9b4=8{@i!^v3nSmLx zev=inzZI0zK-mkqD1{n|RFtwH7o{wYkfPL(0Wxm`E=xgHK+95)72vWIvZog*5GHTi zFJC{O2~?JffM=)JHJBJci3L%XvLF%*BCWWwIKawMW>D^DMlMU49T8!f zaLEpdb4bC?s4zL?zzfFK$+ibUWogVoDebL{3hYQ_DKjY7fy+{M5DQY4f-(`K!1Kuq z4~qJLRxN|ePEhs+MIP9jkm+$n@U}8=z{a4IpB#|#Qvg(&-2sJYB13LQxy0l+`-I0$Y3%rcV@xY$L3<5tU8yrz~#>~ecFN2~3&C4i40^m@93QeAN zq?>X6WTm6YjNd1>9d%-SKKbO)Y4ry|&0^rXzwO8+=aLmxEu|#6nQ}b#h}2HrN{|(68PK$W=GIT zL5>UxObX1t(ux8KoQ}*{jx~h}oY{(kjtq)S%-~^UCU4M&GR`br1_mXb14?|341O#Q z3W5qu*^bPGpnb;*0{*;A;Dc8XPl8k<@w3VA_u-W*vSE|rVtCb=ar?yz~H#Q-iw!+ z2O*%yJ=x*9oFBJiLZRb=dN+t1x8r=UGA3>XK1ca1a4hqHGAo~BU7=&1J1?jq;o!jw zE~>a49Xuc#9yk@aL3Su|PhN9f!jliwL;ufM=*Z;F%c1~Q#RRgL8`Saz8)@Ll%Lpp$ zxE=fJy?8+re+o>FOj%0YzAO%4-+@aX?#aA2Wa^n5nZX|S=Vb(yR3Ljm_A-IDCQo<( z3JXSVkS3@R3e5hz3~-hE>wS0`AxaN0fQn^a1r`OSwJo46&Ag!HrVOBvkr+ z0_|?&5rC&?UQk+L_T+^$i5Wqunh6wF%%F-wM1h%?5mY^Z9EnKhpraTRc^nzRXR*L` zfD1uR_~2CFQQ*yjt*HTT_~%h@0iO-T0a>yIT6M(^UN4})wAP%N1IaO*Fvo!M5Q_p6 zQuY$yo~&|Lmyu&~&|Qmq0dr>1I4PGVvxGS_$XG5-W(9Derog4itYOY9p}>eR9O7^V zE=PeZfy<1by`11fS~!$o)^T83$Iq?61zJMIp}+--5>Q`2%A5u)$V?+q_$Q7&+f*v1S$nn8ToMQw~ z98(XeefS`*!Eg*8p45Tv7;a?8@DS%1{(2PGKpex1!!f)hIEDw=G2qjIz|{p8$XQ$( zOcvm#fC7W#}UJ(rD9LJ%UT#0J|g!!~^y8>5>RC#bW| z;P`+MR&6opGCW`e8P3AV#mvA64M|2G=ILzgjAr$hnH9muIDof@FoCv*u)+pxU|kGF zHZTjaJ%klBFybby#0u@rLAIwcL3vD|9wTHa7-Wsg9Y!T4=+b)*1t#dydv?(384V^8 z$A*S_&KKS$@CeUhpE(KQ5F;Uz~OpxurY_7Z@1uP1zil7RV zMSJaDfjvVgaq4;{okX@&Gk$c@&s6nH)gJDzG^6WPxYF7_*g_z-L5h zgV%cXFo7CVW=vl|8wuFWm_C4L&|!KYn$3*q1&C&yUdO?xti%a!ZaFe3vVa!UFl#dT zDDr}>0-ao?!Q`UIG5s(Hqn#k=mG$M}4@BPXM(8}wwS7A7T7 z(1Cge;Gk3B00jY~A`1@>Xq_vsIr9dP)7Z?I)_`aZGo}?x3e1iyW=t*$%#LfOui<2r zQ`ibR00O+Io*8tqQv+y!2nXmq2qs4c1y+Hj(_e8i>ToOto#VhJP(NLsi&3KtFcMN3UcjsZug}n6+QI}HN&-u*U{YdJU;_;$gM0>>6=ie2G1ElJK7O4pxyi{n6i`@!OM<3Kxgx5)`ND>JAg8+2Gb2j zB~ZBz%Bc*Xng?_w1adDLeE1#%=s+^i%!C3Hs3*z@TB-!vw+m^8f^#;y3J&l#UXY1A zO6X?P^MdA>dEiSrKyFZA)@1eo?b>Gqu^d3H8PMirMkOXtFfbtwLS%3N9fSxzG6S67 z8A17-2_wHl25KSsoe8uD&rKSfOhL_w2uM0b*@+5TZE^!tt17W8fQBkiLlNv0=z1#f zS`v^_@Omm9*itA)S6;U1j8B<`>%rUje=>uD6m(h~=&&#bD~1#WCIueQ7JeoLZqWXH zCeXa80O(u;P}$4`>LhbO&L0Dv9KZ&Sbogd}M#neMRZ@(gX(Z4p6$VhT07^oTg8>)? z9)izfd%`FMPRk4$pd1g|!OY+|g(*v*ce()&qf+p5W^NuX?3>?N6qvw8zvGW>ps65c zZt(d5=m!ZPcF-#z8!Q9ruYgWlV?y2JtH9v6f@ykv6O%0CrRhC9jLM9crf=Y3RAU9L z=;54xn}<<>aoh9_Va%qCKc_znW7e4-pv}xSJ%yK1n}0Lv!j4aG%wW!ZgGqtaaR!Wc0mPdC-=H-MG0!3_|dobm=F^YTp-hD`TcarOgu z4loEn&DitSOnAl}2PI~KU(8C7%1MD);NWD|ceRWcCO5yc0F6exQ(=5Q`Qf`d#^B8f z?;kQTKA-ILX(n$ovm%cIi=#r80+T@7<`Z1rn|%HI8OFBBOMdKQY}=gtQ<;&mZE^cAP#ksqsbw^K^6&tIBcB3` z<9g;S$15PtmxrKn6($DmZ+uKb^(>&Hd_OQ`D>18p%VQQL&b5l7ldbS963rH8yfzw7Zy3P6e)s^x^sk$K8P(>Vh0U} zuqcRu$00z&AzTWg3Sv(G8JnG0f_NFFxD`a4m}9_ZXHUuh-K(Io<9N}!%8ucx#UD`-B- z(I7{O#ZdsX$AZa~7gX;IOb(zi2uFh)$V>{e19XT85kyLC3Y_lX(>qxd*c7-yBO0Kf zVkuH&0}UCmfQ}SWWC4c;JR}%v>LEd)z@-360-zBi7DpC0B@SstE(JCPK}U`f1(qTO z4$%3!prHX4cU~4yK)@qLi6a}dKf;ltOi2h7WJ;jTAdXD_3M`KQ8QsBCVUV#bHb+n= z*Ae6l4h3#-d~!HSlrSl9OiyHFl&oh%br~#<6!;Z*6xhHfvw<8YB(2D=zyWa>Takj0 zG^kz8SO_{{4t!=OivknqCH`*NDpOzryPnCB1=Rdya%2X%4>WxSTEgnc0@|vf1lnN_Yp@A|4ytAa z9mvK6K13KifyM!z;pBmo49t+uY&`=5s6hq}9Ehp>pj8@-3jCy*%8z0y0|Tg~28lCH zQ0@ca5b5PgY}v3FWCEuz{dxulB{oo&WCNu$M3!WSBsX?YyB{@3;E^hz0NPUrN^PKE zQDOs)&#^-ym8nR94IZhWpkh%FS+2wm8m3?YjRG<$aC-ByD=<4T=z#_um_Y;PBA|oZ z6u7g%vrM4=1+$|-mV%H1i-4a3vo3=JXvlzLx-b)?rZ5YrJuBhP%L!^5uLTza(_NVu z^*|@ZX)v*XEJRH^ERF^_Aj_FG86C_SK}R&PIYJT=lLE6QqlY;o7ifr4lQF=YQ3%3{ zFlUs4uoBD}l_0D%b4Cr&>OW9&0;ezJgvHIxT@Oj*ka!e;#G?Rd@hE^Ej~t+VBA{Fg zPI6ojL%FaUS`VI8XMrU`&`c2|&2gcd$`0OM$e;-7%OIs*&K+eNu1a0vGH8@!v z{~SHQAmE@VC~$~TiB&;R4KzFmI@BE$IiMAmphGU091*7-GlM47Kw4Q8K$G)VK-y0+ zE3pe~VOBz#1f9N`pHb0dCOC-+AWMMCAxDuc0f>|cSPImWgseDWR$!kdz^JJWYSc0F zuyQ-jIC228;K-51Q3HIa71%AkERcpOi$L%65CKMASvJt-6ejTap#~G^fOJ7722h6z zbPh1v^mzh|o@`fkfLgfI-wH4qF}6-u5M)$PTn?ZAXEtM!QD6lb0$#zwuE6RjlO^zO zdZHksDCi(&5%7dRs4oGT8Dew%!2lhs0G&t)+U5eT^CUngqJtL>FbjYe4zMUPgNBJg zJq%E94>Z{bns^44mEdj%Ob|Mm4VlU0gpB%ef+|b~g?d&;1<<)SY#K~8iYy>sRe+Xb z2q-XuGCXMY$rI4}3Qo{E22L}k4n_qw&@ifw7i$e@%T>odMhT!{X3xx0ysb?H*SaVK!?6V7hExcwLb^7Y(Wdi`9LF^Y@j;5 z2V{Q_W0nA98P<1@+rWEhK#c^DH~B%Ef51vxKuTXQWPw&_FoI4-e8Z%`=J|# z*8w;xWGOO(&M$>bR|zPwgHMuyOrt<&JUMxoK+EAl4J^peHE3NDXwyDu!Vq+>HKb|8 z2^xS0m*)%$Yzpj-5*3dB8~?Bug4$;w+OeLaqR5P?2b5dc9D5kElt2yb(@db=z6KKy zs246Ea2m8ITO~`07gXADDKI)}WGV51*es3=;A71>l^GmCEl&;=&~gi&97Sf(*q#E5 z}&4Gjin&`GTvpiC$M@e?DcJI|rOrNJs1t3964ditQoZ&K}k!2!;#UNQ46F{ zpMgV(8PvXHcH}54vS!o)H^sQzcp0QX(z*;>Rt)-%jNDcXdI}t%OmCyW0ros3h&Qmc zfKEpN3F$$vYd8QBN&u_U%BqJby8sf40E;O=l|29n1%QPlkcB+JLN-vL4Kvki3Jp27NFvX*$h+~YA_jqy8xgJ54!cC9<N#GhhZQI>ilC&=3W{DxnG%6grhwLzLC*ANRA6^}4=Xu9 z_1r5M2ULuJ1ke|v;V4ExH>L2@gHCya&d7s`kq}TZqQMjZD#byI{aC;`nGLjnQjr5v zfPkE+z$5UQ*^#jr#f>76l@ic$Z~o2$kQJ2z3ha*4VQvJKd;KsDgCk=C_zZCo(1<9g z-~$-}T|9%O-~+oD>|haix#t6Mum{A!;A+PJ`CgIf=Oh><{5LZzVp;2>$O;~^feg&C zIXYx3fsfM#9ev`a#4HUe=-5GpF1We|7xXURQj~rA0cl40dV?0w%o=D+T8Y_lcjp06 zNhOe_1ReGTFOy`^U=jg!SwX{frIAVuQyS>b z71T)qM@B_MIVX6bBNOOcFlZE_POz|n4z*!dU=aXS z>l#cNptP?8iCoYs%;`s@8KoHwrazEolmM-I1?`PL37OZSc62I$&hTdjr8QR2C@d%u z!J1Smps3QAUM|CE!N@j!gAAhxH=72N3|N#&V){Y>Myu(ZvW!;spxF;_T!AWY@Vu z1i_ir5m^Yjzz8BVT~LluxE`{a7ZgjNoDZ1>V}!XCBt))TLF20+y`ZUjh_R4#kFvm$ z39?d*L4iqN{`9qSjN;0mRum&>m5>Q&<_NUWL;-w^8H)m&;{?Vm6$b9P)1S*Rb}-)D zULemX#KgFBdb1*<3**n}Clnd|8Fx;XP-66F{60NHiBX?%=kys$jGp!%Aj_m6=Z*`2 zc%XiliDOg45e85Lk`;6|66gSTkTC3MRZztZmSCSQtIQ~+d6QX*$BfB9fz{CkDX@Z0aTl1u0y=ijLxE*_f-<8lF)JH$p~X zz-?_%tAqnYKvr*oSR9I=;WBP;*)sjTGNXn8J7^azsPKlAS;8z13L>CU7w~`wn-a4E zKd99M+7xnOH@Gk3uEJ=>_kp|g%hW-QAW*_XT15-$M`(gJ z;BYvKK&FmaK%2f5NzTAYdlT(S=kp+~= zn5Vm&FojM^~v6gWEtCKP9bkUng~D9P9} z{jLpTF5~y<{C12s(vaR9C{=+{1^C7&i01`Lrr)z+w3>d$meB~J*oE==_7FS9i>!>Z zx2w4@O0Y6^PPg}Dlws_g9s{8&JsD+uJ3%8;9iS`tKs`f&WCbS24WK|*U~x1O088Fr z$pX!oE3kki8$dTzC@?xcVF1apK{bJvPZ$YwPJiUdn8>(jyNeg2FBN&!;a1X=t0y9LAWb zypKtNRY4GBpCGtj3Eq$fx*>zbK|w%)8&oimVDgzTMmNT`>B8ZRv3j5c&g>|WC9s=O zfg52Ocus`}EXwc7i*VGuaK;YigUk%m7ltqjPq&F+}u7zcFP$qyJuLV?M#;XhQ50_YqR7zcDy z!UPz{K!M3|#{ccQk&M#JtkYN&7!;;AS}+D}pC7}R$H+KwI$s>4nEpi2qLB*>S)l7_ zoUVO1+wptp%}bU7VBrSFECog<24vys!EuZwj1#Ayh+{P3Idsn1ia}WcR7fA5&KA$8 zC;}Ey1r5imgEALrZ}s8n*71yrjEAPj#xqJW9-3Ye(BX!>jjZ(BU0mcXAghZzJe zg3dl-Qee?%I5ho3Jfi~Rq3MDNj5-crmndX`mg+gZz0&!4;pf+_!C?2HyA9+M1BgN- zk4&#kV9aNnIQ?w`qdMcE>7t2@DvXDwTP1>B5u3;;Wq#qNo!Jh*Rjv9_9{v2Qs zI6gfxpHY_a`tu%V7vynyUqKtc{tCcbqG49zuvy^cq6XWvf z&J~RAj3>5tS1`V2WGt9oUd4EyaryLDOpY^nvXlfBcpOhmUr^21%~&vbL~TH!x@30OGHK@=eT{ zcYyd?pnM~9<^v%9p6$GCjH?;hjvP41AaHT|zIMisQ1tsx0WL!R7 zx0`Vpivu&W>66Z&{6d>1V9xR zlj9GD9LG5=2N|*)e?aceV0GLf2%4p0b-W>zrNAgqG`+B&QITm*%k(MzjK3L|Z=W!M z(E()f#fgkd7?*D^o5U!@!VO7dpv6-grca*6sKo`nN<%=Pcl+^ajGH+4wmt(V<_Dl% z5yzRgKb*xV#W=kzn^APTz#PVO#_h6;7@3&RlDy6mMk&Sz+ufHiRgtKVQWt%Xn}*>uN>^CbmB>4}ikWdo80P^K9;I*ROQ1f>&@=hz$-93i~v zo3}DHGk%|Lz71UHG;U)AwYsis1Gl>Lw==piexF{poiT#(`}FhM8NC?4PnX)k7!4BK z0hZgl11u-D6D*gw6U5uTYbWC-R>qUlbN4ekGya^uaX(`* z2r*i2y7>`CamJt1{g0rTe`vbxF|bVNF~)etpVPNODAD7L-tkxrWoF<$%E81{&jeZ} zVgSDH8FVp~qfwUQmFou>Ku2;KfXC3Fn+dp;c%5!sKfutU!~>cVW>Nq(mLbEx3#RWs z&Zx^cefrzujH<#LxWNN!ppC3E8V)chunJ6{u6lw|gYm$0zZ0O8Jw5XTqlWhZh%(SR zInbm6i(@}XG3Y2bf$5CAjNFb7t{-4fWCjhJf!t!I#O5dl8Z~EgWGYc&)?oT`!$FZ9 z)Vx@5WBQ#FjHcYv89{3SK?}GZPM0~!c$DqP^#cq7@25XM$(YBs1;UO##W;oW@$?U; z7!9OA=7TD1W{`nw8caWKIVduN29j<}w>r(J4{`-~k*OnV2`IBqnqGODQJ(S9^qHp_ zRi-;%VKil&K7H*qM%n4pt}q^CoIX9`Dx(!lXv0;oP|-D*&^^ZK+k37v2Cy(L-+uQt zV*(@N^67SW8NV?u-)?%3QJ+!gz!PYNGXs?HcffcPK)ekw9yrUacrty$eMTk5w(Xnm zGx{S-oNwvG=Vh1-P~nfh5oW`H@M6K_}?SsWp}>04QuJQ?Rt|If-~z&L-p0UMJEf1mlD4iTq4c7#W{Wmlb9*nyw+p6b%t;W}MD0%p?kt=!J{viGsykM43w1 zA22C_ZjPG1QJ6`5`ZZCe7->kU#iRh5SUtcFjv+S31K^&|bYC$h3C8Es)5Msxw}wK$WJ7I^wcLV-bn!I1+xPy!Yba6GyR zR0M$*5&d}uYW7W^F3yz6IAuGd1k-*{+CC`BG=Z^mdXN;8I%DbdDk-K%PIDO*SRJoy z1RofqWC=R0pVjdUlxqb#ubA)gT)zFG64QK84s29qQUf(a zR;V#eWMsUv-AaS0m5K4}_5)f>6G08WAU&|_AL}tGi7(mlC(_;*n)-q0?&Sl7?!3_=h0}KKOr`sAb zCGk%K?X?tiTn?fH1U^k)XUNpX@$mroWP%yfZH$-#8GlaiGh)(bTs3{45tAk3_vt^3 zm_VbR9>z=wjNhlPG-mSEc+SkN0GiGO?Q92)AaE8&+lpNPTMjq0<~oF?3s+}XRt9c)k98RW>R5LU=XN-oTRP*uCWxdK&R>O zC@>042kpUPuwqaIiz{M?gO&*~I4WU?gZgs}j>;I~DqwR|>MpIvC=*U~ydxaXqlO9;$df zgB625SVA9Fg29Tx04#2RA#Mm3H^dM(0*f1Ah#P~&jWNVc6qpq185~V8Buv2?OfkgG zz~W{Y;^ts+a}03{u($<=xFuNJ5+q&^TK51?TvlKSE06?=xHVYZ8bjO$EN+7#ZVMK- z#Spgxi`yZIM?g}$Jy^m%3v?74j{*b8L7>e z1x^JvP;-q_ffY2{3E4ZMz$wtdsK}+jIenrDlX$%X7ibBjBdDnf)}zD?ItQ2C9lS-7 z*O5_?kI4bF8J9FS`P0?+mX3lLBN@m;$3B7q_D(sH6jR)IrCq zJ8FXK6$M5|VbEeyPDf49x)x3aM$i--n8%>NI9=BaREkXh6T=wFBB;QqFggBm&~yWH zCKe_}$bb;5IfJAEUXsIYhBapPrj6oc6ga-HL% z^U&2xpoYpE7>7fF(Qyil1KN-x1{%``-Eare{hXPH6SOpk$&n9k7HG)f7(_?CIRoh4 zy#o*)=x_{1M?RQ-kPd`?kZF*57<8oA4ww#*TQK!FFcwTy{J3A7|ZkqO}y1qRTz8qnAw6KD#J z(@|mi3s)vBX;|%YkP(~_4>BUM=yY8-CP}!0!w?0BkrgDmF-bB`pI(iu?ifVfF=Tb? z-IydzVK$wBC^&(n09wPHgvgvkl9~P&+3eF01*ee}7`lVq;O&m&)^iYb=aALayCb>b z0z|L7I0#y2d%JXaOA=e2b~zt;8@R%A;AMShzCO)RDLr!@?wbd zfz|V2i1UNR`7y*nJK7i=L4$FiEB=t0cc5`=21h{*4WO0b430t=;=*7HgfYZLz~Ukp z;-JBA21ij8@p?xFD+V#J1~C)~khnNlTpUAO0xT|pAub6Pm&6bUEof(Ol)?~~R$!`E zU~rVikdOfzB!eL?3l^8f5SIgs%VCJigT>{sG%X%UG6~i**C}ro31)I&6u1G}4D}u~ z75oCsnw}EE^nmgEbk|U(62?{2cZD*UF@B%^F_Z~3ap@Vxw2bll_77o91&nMPE`vv7 z(ju60*cL$8HzJsxFfN(CHnIafhP3Mbaa%cQLJuHq1G-~;!vWgWs>#d%x^;k~C4iSffkBs{ zfqi;m8IyF(KUUDTB~}JzMiwSeg$h!u%dmotkA;DQgOLL?L)-w$s-WR0@X z{smaR9=tS!!SM}CmOw9fJLnk}lxuuJyLTBhm@cqDw=IKhxxotZ8u-$HJ78lVrf4vo zfg67TMO+5#GtgQlkk3HD2FmXmObpX^moteoUYefR#;nMAY5M*qCIzPFOp_YOd!Jxpv%LYDz7t|VL;AL|BFArMQ0J0m@-ev$F0_*r=dO{+Tq#x7~ z3ZP4Z!N+KWRyQ*sj;{s9E$9SVkXcN=ysY3OV?i7SS6)`INgxihqoyY>i{q8)#}k?K zAcxU{X67IUegWAeF-c8+f+QwQHjr-}?@T|R#AM6V^L0C4GE)r`-wDuID-(k`6KGKC!t@!b zO#O^Mr)#7!IWc~pzA=qSWO{uXlOco+nlxZZX9{NgK0P9x$&c~-^yTSHfsEg$|4e7{ zWjr|DJ%cHTF@O8445m6p&^DPYCNIXH(^ImTIv9U$f1Aa$fDv@U8fdpS=)y$>M#sC` zpiN;=dc6VTfZE021(l%2IAlR3=-|~CkbTt*Snnip1f636J__p(=xiuvD~2DS18+d( zg#hA$az+-=+TrN}x!~bD&0MA=&{A6>uViLIoC(OF!Bpee02-x*?4SpoS_?`?(4Exa zEe#+wsF@wKk=hZo#R+tqGb42E5)N#))3!n-n@1H5o^qhHe z!z^)gNNN29rAU4NiU(Mj+(D7L0hOA3ceWYRPsYiTbIiB<=P?;DO)n^BI?H%-x?KsA z7URw7nI%j`j2+YOlrZ@)-kh#k3Kk53Q1zuuYZyDG>y$BRZMUmrTEkTTn;kO9A^;v_ z0WG6P#tMu`11Rj^(GJl0lZ=X>o<9$hg91BfM*;_^t-%f&_kgrNelml`RG1aO;-JF^ z!GpV??ltI87)GQ46vle6neLD|E*{W{5a3PS9N;a2;Gr09&`=C$xga-a+a-7;MuAC@ z9jRXq9zcP1kQp5XAOk2YSxQ`vlAuu(E>Nk?q5$SGfDZR|vS?Fab!zy>-0Y+=ohz2n z9d$5;5ix+mp~MC1a`PyGZj9kT8bD!mR6rRO=)3?e;y|MUjW7;qRNyD_sK7@U2UPee zfCdFXg&)YM=ggqOk5LhBFmpX$PkXK-Id5?@}YEnq@pxm!STZM1t2q#YxR| z+>Td3N@X3NfK^VfuLCz~ZogvGnf|Vhsg`lo^qhL88I0ejvor;KK*P9lRD>fX2%Vn878BT?V z9Qg}%8NedKU=a=^5zyj$Mn@hb5kasT0jS9Ic^yn)jCZzw?O=*wJ>ofLsCIeG6LNHHXRZ!1T3UOo;;BmylI=U79Y}&15P3>=KIdD~u53 zHq)1NGpRG~ntraENt$u@^jFmw9tJIB;Ok+sbewhDS&0p@@sn8*bO?X~v*Qvb z(86eT$Jw_JGk_M)fOfev6oSs#Py}u9WXb|7bc7ny(Zgg12^mSSACW^w7A%4sGBRKh zxlp4|Jdu=!`LD(6IFM^WT}ol|Qg5g3fsZO)r2oC@{jdWkS2$Q=sj)DTwyl1T5`0 zU4{j0pavyq0PhQHmcS2CYZ^2t01*Zqxe65qx9UEyW(mMrbr3eVRrdhi(s=?(m*6Tx zfx)qXDXU(f8q`U=0#|SYS-~B6t8Ne0R^0(my#h5&gXsjKRd)u-6b+_5aMKT3eVkH6ai8f5r&re~}WBf4PegccA zFtZ~^mcVpIMJCXie9)RA$EVZPrZUMhzMbwil}Uqf()9ePOj3-Gr*}qNzHr$cZ(t$XuJ(HAH<^1$e_gFsO}AFIw>-M7bSrf4KaWg z4KZne7Y%{uv~Endn#QEhJDrgi(lb$D6qq!OZu_0$1^2e%l88*N(P751S#(@ZfG7eN2 zoN+*-S+I-)VS_Uc`Un-Ip90D_agdAy8hm1L0A(TMu_qEous{PSK0RYTlOtpKbi)Nq;?ob# zXA*_6-yyN37l6dK8!ljKWM`Z>ee-fA4aSMn?<{B9#5i$!{|YcKU^SD(^b0GP<}*&5 zUa*o$jd9|1u+aLIO!F8gPWM~IqzV)2S;aJ$apH8X)i9y0j1#A`tzlAvDR5lFbe?hI zbiuV?A(ORCg^V84m#t+AWAvEru#QP}y3#r(SqR$+La9z)0T)u8{u+s`x1PzI(PMhr zdZr%6b<;mXsD=$pHH<%}|J}eO&iHe>=tia}9_aGf0}Gf0dbd|^WU^vp{5^f!CZ>ms zzo*aG%=C=$_w@QLU@C1Z(;dd&)19|5Jr!?xaDc&4$#KO4(1LMLlej?xvAPwb!-sG!Vr?c z6jI0%cs*V508@k1b7loTMAl4>IjzWZXFY z+##kK#*Ndx4>KKQ+&EqE2vZB=#_7|JF!?cVoWAfVQyt^R=?ce~#HLprWl}>CS|D=< zyoLtk4Q9t9Ge8U9LAjxUM~O|qWBQ$AOdgCL(-V#}sZRGg&Lj$9TR|w*=?CCKs?&cT zXHsJHm@apMDVVW%`tnmuR@2v%6HE#awkw2EonCViCZsz35E9$> z3=_|EkyA_tjK$MkPcbDhwoG4kiYbPvg$ZsH&vet%Okz+G!|8Ph4%k?p>Bo^pSk5ph zLDi~G_dUa8$JnyH=M0krJEPb1vzM8|7`>(&TxC+7?s-Gb0sx0!@M^0s%F1lSnYP4|1m z)XlhV`>jVzij1+K!a`7k>4dZ+IF&)UOZXK8HJBFgJ2n`A4~1|%umE(3D_GWX!+s{v zXcj9!x8oO3Qq^}{0y@m?BWTUw4v@fA2FE9$#QR}-#1kf2#vj|uo-l1=W}5VVy50+> z`0b3ZnIf4Or%aE2%QS`Y=kyP6!Gn}a@0h$9o2KWzV{&Jlv3=V+CJ{!)>C;cYXYyif zpDy-+DTuLsdgccv1IG60vmm^;ADCnq+oyAVWC~H(4%*|z07|p00*gVbFd594EEG5u zSV4mYpk=*vADP4@r?V)4kEn1^-~^wz0qU@DfM)BaulvX(AqA86085`>0M9)^yLk@?VhUKYwB}XFNAu`!kaYGsijCZCN{AUuI-uI7*Q*_$*Gxg9jv&cv+3xO4g$CT7q;ygW1WQO2FqpE5IhGJc<~&%*3f z&(UJeY@oodzz(_{8M0Izyu4IHf!$Fi%W*kq!8yC52KZzcb`7QgB?i#MpaxTj0%&n1 zC|`o~%0O49UH};fnmA;4gr8^v67m4sAO=5ID^YqBS6`I`~$9kazfD z#z6fuy^fVxi}Azsm8{I>Og+!HKVoH$WmN3}Z{`5i8BCxB&1+y>5T}93aSoV2J%ycl zzWrR#;U#Mhg7>qOhCRT$4sci>|dV?4V(oR4`rNKQsR$qKBFpzV9Cpp(S~N);Fs_ykI)uNHutq;i&7kpZ;amMO~- zqyu~_S(Xx;07xh3;C- zqY&66F3_o7?2a5+N}K|Fwp)ramou96Kt0VApupw`3jwAG1vW=m;4md9usOoQjR|zK z+M0vYpNTP>f@ZB5L93ELXIu78*AQoxXWTH|L!8-_@%i*Vapp&iPp213Fh`4l4rt>B z9r6e|{pJE_?XUtH_rvLrB$##TK_}@cg3kO{b#;}fH!l60Q?WLG?6IioAhc~W+L{kK-xAWr2A#VH4lf2~ z2L*_7&{3A45|=Ssi3Q|YM@B^k(BcA6vylsavK|Y!0vo6-hZzMf%Rvhe89?V7fR;o; z^E{}R$*aH#8m+bDRv^Yc28ey2`;TGvZ9gE*e1aLiFs%VRDGbUap!r?!8q(>D<(VDv z%t8N^XVzofF=!|gWNoVjlF=Q%OgNXq$>D;ftY|Hp&`dI~L=xwS@ z3e4a+TSlbYR6+fD(9jJ?2!5L?Xd>Xvba6#yO#$#sG|~;G(*qQlr46Ao(O`Q(m(+q4 zfCNDeZ_tP-hz*_zhfOO7C^Ab;-=@gy#?;Kb{huOp2qWY3?ZL{-k}T6tYcgxGf<{~! zu=Mm4)uHoSBIt8spj+RNdWzHQwV2&OcV>dx<0?vw;JZJ;V}+ol%>qBb3oHyU4bg!r z1qA}=E&-5s9&}~c9LD$@WToWv30=%AtO8lw0u!ds(`Js4LRx?VKEzr9J{UG#K!;gb z1ayi8g9eig*bdN15{v@#r@QMgE4YEGLk7n)46x-OAg4hF78Dp=!DGw}8cYVri$QY0 zh9He7f%XB^H-XRYKCQ#7#CU1?I~`_4#^=)obeYW zJ|mAc1B2sxFh`dGbbNyX==uWCwKgETn7ADuOgA=Q)@J-OJ>Gyh-R%j)YDbV2pfUM< z%t{R4LrxeK7(f@3F>=GD=75Hjz{BGh1M|~)44EyYjxnJQ!h@RkTlO7h5a^yBXUJ@2 zccvL!yDG7Q3R7msBTz0IXbm^B;~prN9kiC4*>MY$%K^G9hS_mV^Ym+m%-xmJ4J(=vq&Kv(uLuF}pGDnf}&@*_p9syP+|2By;^yW=3|H@l%gPDnenUR|TbZ!sm90vyw%Dv0lIjw8@V}iP^CkbSEAt@i0T~ zH)7LZ;(;7v$pN|rMBoP_6X+D5Ca5w{<^$go0KON55#yc!;|yALFeH> zS7m|nkBb86OfnTvCjt_1(>u+WlNoPLe{069$vA(yxH+>T^L!@m>5T80MY&i7E`bK~ zG?;k6%h_0XvIH(ciX?}hOe~%P&>I^-MKTA-N*2&MN)8tWD1cndz@Wh-0SXTA#SHKh z;J|?ZI)8`_bpFt7b7m>T4YN|_lz$KQeu^Wx`roB|T+nLgc``6c6$>5Vqb zWsFOv|FB`62-@;!%PhwNsv@Qf+A)jrII1PYCf-OM6v zOpZKR0?(&&+A+&nOH!eVtiar7{_q86CW=*r9J_~Gbb z2FGilrZIn(;{y=q#IeH+0=q%I0`NhJ(-|F@8yTNZ2hFEUpXIpR5?ak~tJNWuV9t&S36L6+m4ta>rx)@tO4KueQ!N9iEedW*quta3TB{CI!GUoP3#9DjfaPj|EYKEh z1|(%Hpn4f}a|R19g95VvDA9u#a-}FRa)SeS07DoYz%ihZAU=doz*gUlhAKf9S)k#Q17Di!ZY# z%!wimWpd)rUK#dM|SffLc z6JF1PPW+p0=Etn>333hUnR1{82dFv%H8?dkJC2< zFq?_M&R|5*^(%l`ontom2yKDc(=`K`l~o}(5re941_jVDsh^n@I2}RDw7?gZgVGX* z!2Icjfy^@CrV~<>PM;IVtY-WJ(&YgS9D$gQ^`O$b1L7%ARS#u>)_Q|x$`u$K|4jcH z$gHW>1Cs%11#!Tk2pY=(FF$8;=Vjq`{4?Dph*`0|0isrcL4hR;be)|ei$Eh2=tK_| zM@BbZW=D`bh|2-8{12$R2HKD3*aFeUt;DFn0lRt8$}!RW|T0&3zsuRe>58 z;OPc%UxLw}m(daA2dEBv&agA0lC4-E{qv;LPSRxvpUMPRvf~!1@S%vZNbn^&iCB~NNQ4!2eZ0FA&W^g<+ zeNhCnHRHwU_ac}L881&4h-8*!{4!lHlG&2+;`EG2W);S3(|aSCLzq6WO@AE8>>&LC zl!jP9gC!iGyJ10F)mXqc#(rR%ZWqO@4pYDgni66GMK+HDTtPz=v%4tBgdHaay z5{w6?&yQi2Vw^sGR}8bX)dh4dJ?%(Zm_eI{|AOog5V!Mkd)o_fQHvpMEcvS(owobe=e7Wya^z4dR&fgnxoI1A!O) z!+QzS3*(rVgLWjuGdnU)njRg`3_57GGM+h|sgY^=qj=_I#)H${5|}~D{uU%Kn~H!( zoI&SiI)WzUcyg2&1@2CNnZT?f2Hu%F@!(+wM-72a7SJ7U3XB2=rzF*PnEkOG#l9)k9@8&|NO-al#kaLhgbqB}>1<>a7 zgWDC8nJqy(DWX%DHDj(o$|&$5xQ;g<97XVQ)jKebBZvj+VlpT&g1UH&g4_&>jE+#9 zyv$&2;KTZbIpuzB!dS3Rixflg2E=*fM=*8gnb- z-0Ak|%wCLhr+1|@YcW2bzBQd$6|`Jr`nwF~xarmz%o@zknfa$HW-tp+@5*H6n%G}E0xhe1yCL2Ikqri`t>;UoLXG~52@!*F{&H(WmU}s4#0P#9t zycHlG{G7=RAl{7s(;W(!4>L}i&Qi#16Vm}Y+YEHGUxfmTV*`v+pupnzqv0Ur&c6%= z7RL`TPJ#l9;|mxkLV?Bc0gMx%z~XoV#_>>KalEj7P9gI{M$YNX3ZS`?R+i~)#mphJ zwb87E*@%g8`t-~)W=Fn7;7L1%EG1?IMuA1!ca$+72OSXGP{SO`CNLXxcO>}AUj;S= zW`VWSpEfaj^DF|1o?ysQ;1*c4-KLpYmXT*RqXG-)up0$Nfwj|9TbL&^R!;xf!ko)_ zoe6fJO@1qLv*L><2N)cs6hOD;INkuUTR`h`K*JHBQ-nb0+X%NY8^8yf*g#uQ1W@)0 zf-i(*0*#E@F+n<;I6K%a9H4XvX7DJ&DvV#IuWM(vV7xZ{c{{U~^j%PW3m(>I(O}{cWM%+uM`ZxD zE$&R$>0tI?d^)|fgISGHVEUX6W@*W-pc8t*SIshMFdbo5Vg?NfonRI?J^exlvxp>U z@SRD6X$v!WjVqG|(+>Bh;jOK z*)HaA#@6YjUCc_1tloM!+XE)OctKzR*1;sQFb9V7$_5x5X&q#qJUa3KZo z4hRO&;qu@D1Twmau`>v576jD8235igu+!Q>V^}WT%ij&y)lGc2Hb z$0*7%I*fm&JNGe1!e%c(tz?|j3rwIEA;_ah(+i-E2xPthG(5=&n`+o| z`T%$jz>Cx1!?Jh`nJom4F**JK3ClWmoB@kE_A|#Y-8nP;Lq9Woi3NiKGjweS188l> zxqfB^&?*d&l6vT}4pb$Kpg~bkW0?C3BO^0t93QsfLxEABk-?GO9X#d?xg!`f)61v{ zx+0iCfk}Z0vIGNk*D~k;U(mjL$7ax_FQ6l{m6$=-zJN-XlT4trucA~BPShHpjV|Ea zpa4$Zpb`W!1qCTBQ%_X!9yCftH?3*>;#g z;5eg{0<+^0kTA$_ku1k8(;rM=7B@T(5(3@bCz9m|Qwi!qF@f6QBH(+&9p8Y|fR0NR z$#T3iU1K7stqv-3z_WVLA_t@gT;$xI-aL_c4_g)^y!j z%odEZwx`Wv-onJY;U4&C-xuH&Mbp*iGTVSQyv}9z6YsctfWgtgamU>Q3&BZChw6U0LB8aZZO z@Eylz4j*Q4lyQ7<_%MUON6_@%ntu5!t637Fw zd0=HKXy+F!aY|$f>=gx9BN8xa8Aw{6zTi2tD0n{(gQG!~z~1Q>7ch%hfC^H`{4Hc9 zv;#yjsC)*UqAIW#Y?cSiEFYLOsG5^!yTSmj+?*CN`_+S2N`VfrK%Dcrfd_oDCn&{( zNB z3|;Rf01i1&P%1Gxe(wM~5j04STIh*@#x&rU4KZo>G6`T7dXU`|pxGr@y@K zaFoJ&w#21D0jUBIp1UTUWjt@Y52?YNK8h^r*=`PEd zE5S*f3Di$$0L^=eOuxO1SxWdNqH_yx2yiTC))5H+FTvZunB_PDe3qD_g20369?O}P zxP!o_oq@NKPA^@~Y$NfU*^z-8w91v;@c?KME#bn6 z)`G5;kU*cd0@X{PK^@SwY@lwI!2IcxS1>EGPhnQ{a-8vT`tB9XHjMYD|5(9nC3WNe z0S18=Op1Jtcc3gG#~VzJ90EV5`>bT15Htf)w1K9Ym>5_Xxg9|Qkde+Qkb+7{PC*Wo zg&4Cz4HBrJ1``7>gCmF|Fo_we2|UC(<>Pd{Rm}QU7r0x@nI#;XLBkcG+Z-KFfUF01 zr8pdKFo9c2jz2)9`CkUd9WXg=$0;ATPhZ9SLxA!A^a;C}HAKMX2srKmRpX3~55Nki zpV`f> z{|>$7fS!r4I8=Pe>0N?7ta)yR!hb-` zHON>CJUPBN%dE|KaQd6u%=)s9pw@CDe2|mH0X)75B0x)d6V5TGz|tE^s+xY|9J93s zG`(S2Yb1a=h-iJDS%LBS^qBL^HjK}w&pZzvgE@YlS(Wkm^tb1kO@wBO~Yz zPX=9v58xd}ptC_If?T!-#brB?UA6^9Y6Fs#2Ga|KouKRls%zdrHU}!aVS~Hu4nh@Z ztQn;00mNlY57?$JyTY7R4;e564OxP=0zKCF-dw2mG&!NAY$ zcwx_B2GHFp;LZwYWjHef_Y#l?6&N551ju=Ipb1)}aOTN^*6AqC2WCcr=hFolSzrwW z6;K0V<5gx6Q)mOh0M-I9K{Cq*W|j?17TSE6{_84pq&Or|fTyWg6hSi@p#3lcKc{D1 zW1i}b-NT?OhLAiAngj+ntW-eD@UVG!DI+7}bnk9vt?4D#nL!(f!QH)yppj6tZ1w0m zvmN8a>54a)LA#M$ZZH=y&Y!;h2D7HYOvpMK^kNx049oy3o+sx672dA&O&1?@T1Pmnl@Z^7m=^=NRb;WP|KfvH9 z1R4PbFDQEj;_?Vwnm*$Wvn=zBhJ(|0++o&MUfghy!4b3@l_SgX$A3`6n$dANNDOrL zI!~5kN5jGCY3jvb3pqB33r)Q#n*!Mfoue=HFVqz;tC2}nm*?)v$W6( zCeWQ^pbMhkpF62FDH4|J`L)W?J)cy7E0{9a+%OI%vEd zG?)OI9t8CqA>#<3k-eOI%o&W&r$;ujm`>-q&#Z%#yc`)7!37j(;nL6PzW13uIHqhm z%-|?3aB2G7`^*ZA7pCvK&n%?`Uc3TYK+EX39wg5qaETc-%);on8N_80xHSFOedbWc z2h#%{FpG;#2X`nT`Gg5{Ehwlkem=eS0duiR10y47B^oFzgHPuIA7O>IPy)QL3G0qn z=I2b@)89W}w!_i9bb83F!+2+U{zGP~AjtWhpoO@g*(?PH#|c|e+L#Oq43K^@xN7Ba z{0VB$g3j@C{ICVufdC)50?tC961sQ#?}yCVppN}%b{2V2c(0uaxnm#mh}oO*&Gr?K znD;Y^S242GgIdfypev$TK`aI(76q2+ed^2-Uf|w01E_xlUMs{5S{cp=+6v1ER){JA zZWnVvmfL|&gaBu7aKXmI&pkc$3G+0@pVOW(ORDfe7MnAH7db01IBwtrwM`X3Lxdbz zpe{3`+W_djM}RKxcG;XE`;P7(g?dpxIJTj|Q|eISV|` zslfzZ`3#!!1PvT8KuiM}32JMBr#QL!LARZs0ncYLGjJz?kJ+An^98dq^PMx!(=W2K zNa9FPr(Q4{G0%99SB>sVW*H7xDL*~%C3B<6jQ7r; z!TlmeD^OG@FgkLBZ_;K2-E0jVB?XO@!j&^-^D#3rgW4(}102CSTS4`S()9m7nMFN7 z!!w{j`~Uy{fA&Ha2k>wVs8_B1;7fLV?+&Kv*-5p)Pfk;(DI^q61F zCZPQnpam2^Ktn4p864Lf0T+azqzks38FT|5Gq>Z0Gmufx7Vs!2XzXPQ_)rqiaMLZ2 z+(!mSxEv_KO#zLMii6g^fP49%UNh(_Y0&C!P^AS~h{@*WIVlSRcNw_V z2?`ESbn7y#;NxRqU}k0njj{;*Wdv0s3;58R9~w+c_`uC{Z(eZa#^5-CJ&UkvUr>bx za!5TlhP@rg_WlFg+knO17BqVs*s}x(s)pOk&7BP{6__0-@J$!|z%1|d2b7Tv!Lz*z zkh2OL8z2`#FoV_@cJP6w>dly1Ks2KnQv;s@v%nnCU^e&$pb30gD$_0AGrJ;ZQqWc! zaD@iXq~`CL)u#V`$E*P9_JA{&0){dc2B_;CH$WWci^XvhkQ}!H2i6M}t3?pb$6lmg`#laDDTgqurk;SM8IyoGC zTo@C$W(SYfGr%@Jfkv`GI|@O2Y(PVh{M?|81fV@g+?kA^l|G;vhS7=vG=Bb{v2X`` znKpP`Jb0Nl18iv%%JQ+p3{IfFGiam=ylNdii$U6ji~@}Frwe>yZbB)yq^58A#GIni zAORY(0A272X&)^B@dX^;gN7bJgS84Q0w1Q^d}dbRfQ7~MjL*zEY_mbJyK?%h&&)xh zvq6!t0kmTdDafWXeqnZCoIc&@3$r@H;=(V?kqDmtS7x#4FTOC_qX>nf@WiGs`iiDN zZ2JGN%-#Z51KrJ*8qVpnZf0|;Pl$>%${tk!DE5b3v8K1K}+xFPJjBH zSs!$Q!VhL6#)H#6elW`-f+_b0vmrY)eNO-PgIN{oDud~vKbb{1E}S|5?wC$5{K;%5 z9RYU)3x*>=3Z}PxV^(DBoZj=B*<^awZ)QctE)cs6JXpdAxl46A?;mD$&>AB%FwHc5 z{U2s+@N_P?DFnKK_W5+izsw@j)BZAxD&9G90MyR`Et+>cbK(Glz-_Sa7#&Z5B|yED zzs%CpMbXthz@^qe6WPcQ*pz`5<%3!d)8qdzdvL(Y{^>UVn6sH0xTmlD$E?XXefs5p z%weE$Vb_Dk~ zrfy>K0N?Y%06q@|)Z9Vzra*%ZjL)a5GqR|I&L#+8WYL*9Z7-sv37ECz_yxCD42 zB%YbYn&|=WbcYEnBGWf8v#>HwpT3Qm#fEY2^v}#JilCSWx5HQ*IRtvAE3&YNSnffY zG2r$DP4s}KML@HT3<7(Z6%k8xAu$VTK!O+8=CQCuF+QJul7&STekC?S324RVbQV^Y zP{!5MqgYvHN__!MWPrj2)GgysV$fya0BtGY6nHV6myJb2YQiDV77x&307zIO$=kEB zXo!J}PY%ap-3LHn%8{kO1Ufu!x&Q}@DfdDq(73e%qrd{D>9uSux=ha)rwg#NsDY-2 z@pLjd*je-l^h0ylSxgz*r!Qh>QHL3(inY-XDltJ340XuV>HZunDxiy_Iapj61E%lf zV9{j!HT^LMiyC9lbS_R7(152lCyN&2!RcX~EILu32^R$gc1MvcB@V}_pgIDy_DqQl zG~ohTLUWP{v~6BRi3dc1Cz;tmECnT$lN&)oB48nQ4JHZ535e70aI(mQ4`l!)NKl0W zO^~3B07}~e&!uyxml7JKTYT2VbMweg&t^VQYA}?-LU~Q zT?Gv|c2MSI1La0;&@MU-(Cj=rh{d3UdPfu+=$3L$PosW-YA>+^K8~9ip6qbUT+m4J1tl(x0XrV5cp}^+okgdQbP(NLepT!^vv?`$i zbhp_P#w^gLTTqH&1Mie(2PGMH1@Ph=HVvj0MkTO^I~YNJX9W4Zff4L^cBn7GA@hV$ z33Mtbn+6m3WCTWm=hK()vw)V>UFT;}XY8H+pPxkm90IH?4hk&rbuXavTe-k~dOqDo z0G^5%L1rtk3*4RFCBR|^2^M|EYS7T73gh$bVuCE5pt<=3Ar?)>rs-WmETHpVE(x)K z&wF_;%o4`fKHW})1$5raL=hHc#`fu3Av_LI7SMSws-i6Z_1hU0*mM~j6u5L57(iP) zK|Dtgj}gQx1aX}}TqZcz8N>x|Ck4y8fVkkjq+qTqhzs6G3g)_jxZqu+V6Hoe%MLfn zL!q8amjSealo>4T36kc7OM8L1TyU;8h|3M<`hd7RaIPJ-ItzyZoUuVyIe~5PQJBsq&cY8rG$#P85OgRGOyP8WaTdg(IuT%b z&=ELr`9yIR31iryItgI;2beBM0ZV|6z=3OAgKTgPSRQl)4qW~ovU~|x9&`i_TwXwe z#Z3fM%do!X_)U@m#U-l-(+v*L8P$v+Tkdc`u6vp$&*IPcbb5e1iw2{>^h$XaX?GMy-2oj3 z&8orl09-$Swcg-R0#c#1V#H#W+g6=MlQz>2cVZs2`F$mzBm9qTbe_G%kjbV0%aB{re@~p zB?>J28GlYUQDoV`_-p!8MHUCfvgv9{EGeLaR9HX z*&I6$!kx|L*mw}`Y&OTAFh78*UqMiY098sLPlJ!AR^W1+bnpNJwT?eNZ zDziv}{Mn<-Qdqx`k;TFB0Rv>XofD@MB_K{@2Rl&$;zZD0W1yC_D=&)zo8tnQtJoCS z9B08eAXkAeCj+?(qyc|I8jw=pMpQyqj4RYc-6_$3! zh11PcSwORFU8*b&j0dNmR%KCPESvs8l_ez;(q>`@4NZWKu7zYTZt$)FCIc^kjE=rqvH*jn?T1p zUY@>Jjm3p=>2xM_78UuWjE>L)V|YNG=W%=iiyu&ee=yxuoyClC>GT?PmN3Tp=~vWQ z93nvN1px(4&;?FxkX4MJ4OU8wpvD5D0yuYaf@T`Q$D&Js`Y@atOakC}RrtZ-ppp)< z5CL@e9JJ|}tHGkr_uqfA8gDy5@$P#$Sq`(T=INrd-!^O?R1zM&e z2=O(eBb>np69FF`1KI)pfE^SFAl3meOM%sK2k6=tZtx9mtQt%akX#MPlnLN`%BsPX z0?Vq-psb$*Dg+ontOA5;h-0`R3nvtLA**(U;H!2)B_C)C98~hH*JQDU7SN2KVqJkp z;O_L_nk;6JYfA+jA20~?PPfrwk!RdKJw}Vgg|T+~LM;|gCZ-d|x4+S031MWsx!qBZ zMTL=Z+VnJi77eBw$EQ!wXDODy2AZPcc5DER1(++aJN7eXIes{PfWdJGs9(Q(x{?8l zDdYF;5e6(7OpG_TA2ecVWMup?-PwfY1mm>nJfOdS`eyPC3;z>L~(@c@IPxdNNx ze8w!t1t87~&|dQ0({Gr9jGE48#sa$YBgu@#7Br1x#uBJg3u;3sg6l2?@KIdQ5+5`q z>?jVtGM3Zv21AwtgTR;RvgRxbOcR)<+ncjUhJXuBSVb741U4c8lJ7Y+m?FT2fwmMu zvH~Y)89V5hGEUIiYv?g$(D3H~O@)G1e*fIQ#+(Iw@voyLOE_cw^jVfHT3(yam5K}3zt)a=>Q|LJErqnv512@`Bp4O;9(t3NAN!X z=_yt$pgktNRxFB)z0=oQLHkLdW$B{uQ~~NWi9rsfnEu)dDOGSdf{qWkJKfxx#f%HI z>&KB%fkoip^m1z!A1*|fboyCq7JXg>pNZRax}PtLl!PN=i9X|z{m%T{jvb&KR9cQ( z_JbB-PPevUF=l)|J>P~!mGSQMsWvP&QU~`QU;ypyQE=mBbey;M00Xxoo1-$QP@MkS zhQ(IrG&A`8Dv-!?&{hxd$|UgaHw88`CIJOz@E|_4GZSLVVhma|V9PR7vVjS7Um9rU z8dQCPmM4QY1%Yyal^shMJO?m?`yqFy&$nYSlY#gZerP}Rh<>n&gVR6Sv6%7Q0ZqFL zLstHtIXB(Zo<)WC$io8+j{Fd@Ez{HNS++6?OqX$hw5?baSV0X0W>DjW6STev+<0LD zu?&P0-Bke>cFDO_-Fcl2YCAgwBHNVce*>B(-FOW!YXiZ zx|bu15mPJ6^g2ft3!w|3Az22OqY# z1qC)xSDn-GDx~A3kfp={DoQyNI6?jZ_a-?&eJyZTofE`TP~v8BP~cGD2JLlV26wtd zl)x8hu)+K4pzU;sZaJucL+NaRk|Gmmu>>f7ZaJ~&Fn*gp*_B0&F>v}WR~Bn0OcQ$;}{f}L5^e6U|Pbc#0nbWSpm*6;N|KI7{SNyu|kgXn(p8R z&N2~h;L^RrjU`s)CbJTU8B+tu{F#RiFgP-UCZbr)n5KY+;FvU+40Z@yp1#4IMS=15 z^o#B+(%GQlVbpz7psmcHnJVy3W~O@3k!fy9431);4aF>Awdj<`r2ZtQE6+y+yVvxHRb>=V16BA;3S5p10*9EDxXqY) zK%oNKJjCSK!Zr}+Hoj^|(*%-i3P%SQ($o|;+=lai-lVn(v)HXtuh0rVMu5$ z0EZ@vH;X37l`NpN z&b(8CxSz#Q3G9AQTLUuP0Zy3MJurQZH;Wv&KL;u$`Jv69o8IuEhZ)oWQD70cJ6+j_ z#SFY5)RECq3EI8^R}CNSSmYTyr?We=C^804*K}qvV^o-)=*%MR0^U#sZiX>I$84B1 zm>^>|;OyDJ2yW*<_sxPv`!$$Kz(PzKOcfxT1)fho;0(^KADmeV89z)C zg+(4b{0(w8v~UNNEKKzZ?9(55v&c{X>%t-eE$NvR*uk~f-RTOhEJpS-K$owZD=<0s zGiEt=z<5SyL?zc`+Q&cuoy5t zpDyXkqQm%Qx|=VHD({hfhZ!7=9e;ohEc!UT!k6VM1(jdm!j%@AH6MtWcH+FHj8uPQ##bn8k4sXh{^4Bd8o^1(n0B3M?RhV3flwpuDjF zG7!fK@yc|YFct~M=hH*OSTY%(PcMvQ5rgb{_&L2Nl0|1adpJuQWAF67a2C)hylfFH zsSuvj^tK2VCB~W4S4OaC$Aa7wpuhxb^(e78?t(Z6G)jZ!oDfK@5CO^^V8;YN`n3?Z zKoXq`C|JQO(>x%-I^8gm#UA1|DMqlfI$>@wh+@%)W?RrL1Jm|G3l~`h7RQM&4k+7p z!Z@I8+X&-;vhB}3P(7e*s|s=$n*xiYDmdGMHc5ley#(_>Jzgfzlme3#1Nh!hPzk{( zGyPr^iy6mrX6Q7EOf<_xNMnt~5j2g`7tSKZ@|2gEYx>n_7A3~O=|7@bj2RWC>&CEv zcR8}a>&z*j6V;hCm}Y>6E)ru{;JX}K7{Qw!!1)!@YMkB|1I~#1Vp#H6Ko^QnH;82s z5d*Opm6&xI7?ju@S=>NV=bFqClkdp|vnvXLjwBVB+%F$ouP6Xovdw752&$(zvOw)B zM~7_4{pb+goN(O&yA(xHRdXsZp{eFZs8$pPStFsy4_Qyctiqtc556v)zfwV1iKo)Z zp;n=u--#heT1jNN<9~)iB^Jj644{h&6<9z682kz>Yn24s6nGpN6j-v?cC;w+Dex!= zwkh&}LZ3&0Wo?@xpA&;34`?2q$DA2-wkIFx!~teaW)4LjkmU@De9(0f3Or8S9ZG^u z4B!JA8X6j29A{7vaWXY&t5x7v5N-*Ou2JA~05?`W=dvhS$buP8R%DxO6a64uf2EV8mJb>Pj};0^KMFMXGLu{2^z_6878%Cd)9Vsgq@6)E zKO3~xW&>B(AR9o_p5W?-$pO?)2ixHI7o?vH+S~z|s30nEnOTW#`h$2<%;Sc-*s&ed zeBwklPYlC6ZvyU9V1`!Syi5vA3cSme*cEu46grk8`+EY^p6Ry|S>zdSPydz3BJF_P zTzGpB)=6PhU+3<_)t zi~=*JXJ)e~)ZYe|m=n0br&53xf=%I4WOEb;bu5`dSL1?K-`s^c6x8Oq2IGLda1O=+ zc>%O8O%db;knZQ8#t@`Q1xeu`M&Fim2_QDL17dRBS z1!hcl&4GJi12fzUTR_9EAV+}4+FW@-8yb$m918Nnei#Sjh3zm7xn8ITIg)@Ezyq?Z zpydc_Agg#C868EyY21;~Q5>BCz9A73c93NdtcpzB3ZUsq4#r%XFgfmkaUv9$95=u?pccW3N7H5USX>#~rYGgG zJZEZSnSLjQMQ?g#KFdwUw(Y?MEH;eVD_|OCfN~RD`vMRTZt@Bc@4)n3g)FN$+gLzn z?`SY>nZB`zMQZxrR2KH>eMKza8Glaylg47f_EcT4^ zrt_7tNF%8MXQYGEEz5{f(=q)+Ig2$R!=_iT^fEOwPR}i2QD>Yt-LI0xj7X;*u4L(B zI>`jl`Ez=86^k)cCulwbRH)vazPAe9lc4*?-&V262EtZmv49Vs0^g#b0A6b^Ajr?a ztpr}%0$%HdzBo*Q0le6eTbV(D!Ii}UzAtEcbTtdoa_ahO7MTESmVk~x5_Dp~(2wj8 zuo^*3HPc^Lvp8sBnhI9W%m5lPKy_1S4U01)>-3p5EZPQ}n7L)Z`zAoUnpr?Sdj*gO z!Ak}OCbDqLqkHH}4T~12cfsAj$i!IBn6J#>xPX1SUM-7c{S0;mc2Ljt51Rrz=oBe!&9I( z3pjW{90f-3#stSDpmSK29d{gM0j+kPe!Y&RM+3A$QxcMNK+Ar>8=b&G&LS|Kkz1UR zF(0(6Pk?QDcRh6edCg^Z6Rs~i^7U;qhuo_G~Z$KAyusTBYOh41a^44twXhcy1 zWV$2lmW=}-AxzCZ8__IegItWRz#?#c`~PMZU&ea8hM_u6!toxuc`O3&L1%G5jAaHH z3*v&j#ev~1bi>-44lpQiz`b_4l_k~b060iNsh3e;C8*8&fDtA23PK_r)VSbKU<73> zhzL}eQDEZqsy3EL#?8~Ow6SFIfX>JV-xSa2_-DFTJBu#kj_DQcEN+ZTrtb&wzD$4H z&eF;aJ`*3b?$7bd^x6&<5B4RXX*0(k)6aCUggNwpR^ID5?g1TY06x9p%%Q^!j(U!N z4uQ_G0IjdbuVQ*qCyNHpk1wDlX`pS&4PU2E>tqRJY~22$lO>3ev2VI{H_J-4E$N00S3_E-Hqc1 zK)RU(rcXcF%VKNZ2wql1FdVqS`Fz7AxMtfv7Uu-;DFmR~d8dPnS71OskeQpG!LjGu zVFpKS1;|kuLJW=%?t%w(*1$G42rxMQ1PL7nZFmJ03Se8loCSri0?3IE`dCEjpX@!r z;K(X)iJ6-Z)EYYgniKGI+r-qXrZ7Boswv9uBD75s)0XB1Xuu zL*-6RSDefu%+JORIyoLxz%nSYI9{9{Fp0@aaX4+Phzpt02#*#I>w(tffc$?540s8 zbjl(dWKBEN_|8czYC;X%HHsXdn?^wAP{=5-I9{55a1sk>1@h}jEb`*lAoj6C>;r8X z1?|1L4)TpO<9!e%JUw7C3kRr(p3EZ6_;q^eWEOqZ6`=c_SOxmQvC8PU7Q|%{=x5~S zMkJ6M(@#!jv0-eP&NhW5gRy0L{uCAk`F3c$PkDNP!I2511DwE?JOdpKA#iE>x+yHt zj60@tO=an2+%tW~RF+c41Jn7Yv6M0%ncf1TPE7wbjb#Sondy_Kvy?Jkn9ezarIhi? z^m-6=WBOMRb!U3^OqNo{2h$&ds3+6IXR(wrzL{W&c4 zj6bHYpTkna_-DG#T$UQ9hRxGg&1I=(YS}zpeI83SQ^)4%OF(qb=IM&_S*n>PY@R+3 zL{Hf~U3vjaHPejE(`SI_Ih&`8E@Y`@TCjQgBoMu1^K|}2EY(aaHc#&X(Q7tO=UmKE z$+%(qwN)&E)2}RM(PrE-Z3#;a0c|_%R-sUcQv2 zhVjVsFCgl~^ulEGbzXMShre`l_sbRb_{RN1+F+Ft!OAYUx&EWZjECuc?1-5C+ zSX8ICtY%S|ZoHC3oAJT)_?0X*j8CTD0Z}ieN3LS2VSEE}OAX_PX-i-(`T}xG4dV}x zGwK-sOh2;*6eL@wd#wcp$(HHI*RoVHb!?gLw2q~ksb|ad{UCb6mgzR@S*n?)Y?;0j zM9H_Y1zV;YY-FisTC!#OS`fWr%XF!}MR9 zSgIMfY%kl)vWQ9j86!6*=u#y{0TTvpUIxeIpkpkh!P|!T7#t6Qc;^`eI;Y!jW3l!= z0NDwq#NfD!QGr2Pkcoi-QncJSb{M$~`vOX%67Upv1;kYVak)V!Oo2+Y{_RJ%vD{~3 ze6fA$PL?!Az84=MX9cbS-HmvBy5w#accvL%rzh=Zac0~wec5gnBaSnm6M8_~s~_xU z(YAp%LO^SAAz2eFBmr)1!0Pl3qTsTM-EljpKw@_4N-Ec39EZ@>g@G)2h$Fu3+ zT%4Jgu27s>RF+z#kerd2lard4o;v-^4HgNu30n^^aDSct>IRFe?qW0(@);Qzv>6x} zit^GG(h@UsQd1D7w@i1q$>Pt!%mh&~eg91siFzI;1_oCoCmdj4UZt0=i!w`6 z6;Q>JbMlK*r#C)fk@61*Nf*IF8Z42XS^^P8wg#pGY#iK1Uq%Lo1cWyjoTg{oW^rdc zIDNxy7HP)E)6d*yQJcr6;SG8U|?{|^Uq68RnP=Qb!v)2Zhld! zLPS>U<1~xCI!B85kHovNA9zP3NA&8Y}9!;LHIA?!RDXIWaIWI5RLX zxJ;LR%2F3K>rZ1_m!k`de`k6f%sUknskE zjQ8|_=PVY=K1iy485kJ+7#JA*85}QMI>5laoE4-x0IYiZx92PoO!bZpHx4jxGx9Px zE&wsMvTxTCU^uEJz;IhjfZ?&00K;c30fs+X0t~#`0t|xM0t}+s0u1un0u1Wf0t~@g z0t^va0u1`v0t|NA0t_D70t_MA0t~U*0t~6z0t_YE0u1fi0t^$i1sE1<3ow9+Suq9% z1`cQu%gHeP)GHRR`k$Ht3=CQV3``(g>R+OLXnO6ddPRBbB4=`~5WCjI% z{C4U0Eai;63D6(_C4j`~^FOc@@jQ5XfPwpo6i8LlcG-_C?mW6FAX}gT1yTu0@E|b- zdjWejEgF|H@;;Fp58ixRfMr@`m7nO@{Hxvcgp^B5Qy=5H^b!+M+1cMVb|Sz63 z7&b95Fl=UEVAz5b7h4$^7`8DmFl=XFVA!#J{ybKBM#Y^B3=9Q{d6~)CsVNGHB??wt z3=GKj?3#XmK5M-4ZX~PsFfcIeWnf^~$Kd$l=wSx#MckkSuz!2v0@iM3#^cl9E@o}# zJi)-gaFT(6;S{Kxl-xdT32PQ3#1S5W4?00YPYMV}#wW6^e# zEv)>E8RpLg7%ZO)FjzkqU;w395N2m^T=L;C1Gg@S1M>7_1_rPsh&=^~Jr#*P4RSmL eH{)~$@Hw;GoFLr`7#J8jwadW6^K` diff --git a/wasm/pkg/mtp_wasm_bg.wasm.d.ts b/wasm/pkg/mtp_wasm_bg.wasm.d.ts index 82b833e..f7dd026 100644 --- a/wasm/pkg/mtp_wasm_bg.wasm.d.ts +++ b/wasm/pkg/mtp_wasm_bg.wasm.d.ts @@ -32,7 +32,6 @@ export const build_request_frame: (a: number, b: number, c: number, d: number, e export const format_frame: (a: number, b: number) => [number, number, number, number]; export const parse_auth_response: (a: number, b: number) => [number, number, number]; export const parse_response_frame: (a: number, b: number) => [number, number, number, number]; -export const main: () => void; export const __wbg_connectionconfig_free: (a: number, b: number) => void; export const __wbg_wasmclient_free: (a: number, b: number) => void; export const connectionconfig_client_id: (a: number) => bigint; @@ -48,9 +47,10 @@ export const wasmclient_is_supported: () => number; export const wasmclient_new: (a: any, b: any, c: any) => number; export const wasmclient_send: (a: number, b: number, c: number) => any; export const wasmclient_state: (a: number) => number; +export const main: () => void; export const wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584: (a: number, b: number, c: any) => [number, number]; -export const wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369: (a: number, b: number, c: any) => [number, number]; -export const wasm_bindgen__convert__closures_____invoke__h5c81efa430e34369_2: (a: number, b: number, c: any) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f: (a: number, b: number, c: any) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2: (a: number, b: number, c: any) => [number, number]; export const wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424: (a: number, b: number, c: any, d: any) => void; export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; diff --git a/wasm/src/client.rs b/wasm/src/client.rs index 36604d6..0df8f24 100644 --- a/wasm/src/client.rs +++ b/wasm/src/client.rs @@ -103,6 +103,33 @@ fn verify_host_final( Ok(()) } +fn random_nonce() -> Result { + let mut nonce_bytes = [0u8; 16]; + getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; + Ok(u128::from_be_bytes(nonce_bytes)) +} + +fn signed_challenge_response_bytes( + keyring: &mtp_crypto::Keyring, + proof_payload: &[u8], + client_nonce: u128, +) -> Result, JsValue> { + let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) + .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; + let signature = signer + .sign(proof_payload) + .map_err(|e| js_error(&format!("signature failed: {}", e)))?; + + CommunicationValue::new(CommunicationType::ChallengeResponse) + .add_typed_default( + DataType::ClientNonce, + DataValue::UnsignedNumber(client_nonce), + ) + .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) + .to_bytes() + .map_err(|e| js_error(&format!("encode failed: {}", e))) +} + #[wasm_bindgen] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConnectionState { @@ -206,17 +233,7 @@ impl WasmClient { .map_err(|e| js_error(&format!("encode failed: {}", e)))?; transport.send_frame(&ident_bytes).await?; - let loop_transport = transport.clone(); - self.transport = Some(transport); - self.set_state(ConnectionState::Connected); - - let state = self.state.clone(); - let on_msg = self.on_message.clone(); - let on_err = self.on_error.clone(); - wasm_bindgen_futures::spawn_local(async move { - loop_transport.receive_loop(on_msg, on_err).await; - state.set(ConnectionState::Disconnected); - }); + self.start_receive_loop(transport); Ok(()) } @@ -258,38 +275,18 @@ impl WasmClient { transport.send_frame(&hello).await?; // 2. Receive and verify the host's challenge. - let challenge_bytes = transport.read_one_frame().await?; - let challenge = CommunicationValue::from_bytes(&challenge_bytes) - .map_err(|e| js_error(&format!("parse challenge: {}", e)))?; - let expected = CommunicationType::Challenge.to_id(&tm); - if challenge.get_type() != expected { - self.set_state(ConnectionState::Disconnected); - return Err(unexpected_response_type_error( + let server_challenge = self + .read_verified_challenge( + &transport, + &tm, + &host_pk, + client_id, "auth_connect challenge", - expected, - challenge.get_type(), - &challenge_bytes, - &challenge, - )); - } - let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => { - self.set_state(ConnectionState::Disconnected); - return Err(js_error("missing server challenge")); - } - }; - if let Err(e) = - verify_host_challenge(&challenge, &tm, &host_pk, client_id, server_challenge) - { - self.set_state(ConnectionState::Disconnected); - return Err(e); - } + ) + .await?; // 3. Sign the host's challenge and send the proof. - let mut nonce_bytes = [0u8; 16]; - getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; - let client_nonce = u128::from_be_bytes(nonce_bytes); + let client_nonce = random_nonce()?; let proof_payload = mtp_crypto::auth::login_proof_payload( &version_str, @@ -297,20 +294,7 @@ impl WasmClient { server_challenge, client_nonce, ); - let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) - .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; - let signature = signer - .sign(&proof_payload) - .map_err(|e| js_error(&format!("signature failed: {}", e)))?; - - let proof = CommunicationValue::new(CommunicationType::ChallengeResponse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ) - .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) - .to_bytes() - .map_err(|e| js_error(&format!("encode failed: {}", e)))?; + let proof = signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce)?; transport.send_frame(&proof).await?; // 4. Receive and verify the host's final confirmation. @@ -357,17 +341,7 @@ impl WasmClient { } }; - let loop_transport = transport.clone(); - self.transport = Some(transport); - self.set_state(ConnectionState::Connected); - - let state = self.state.clone(); - let on_msg = self.on_message.clone(); - let on_err = self.on_error.clone(); - wasm_bindgen_futures::spawn_local(async move { - loop_transport.receive_loop(on_msg, on_err).await; - state.set(ConnectionState::Disconnected); - }); + self.start_receive_loop(transport); Ok(assigned_id) } @@ -409,36 +383,12 @@ impl WasmClient { transport.send_frame(&hello).await?; // 2. Receive and verify the host's challenge (register binds id = 0). - let challenge_bytes = transport.read_one_frame().await?; - let challenge = CommunicationValue::from_bytes(&challenge_bytes) - .map_err(|e| js_error(&format!("parse challenge: {}", e)))?; - let expected = CommunicationType::Challenge.to_id(&tm); - if challenge.get_type() != expected { - self.set_state(ConnectionState::Disconnected); - return Err(unexpected_response_type_error( - "auth_register challenge", - expected, - challenge.get_type(), - &challenge_bytes, - &challenge, - )); - } - let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => { - self.set_state(ConnectionState::Disconnected); - return Err(js_error("missing server challenge")); - } - }; - if let Err(e) = verify_host_challenge(&challenge, &tm, &host_pk, 0, server_challenge) { - self.set_state(ConnectionState::Disconnected); - return Err(e); - } + let server_challenge = self + .read_verified_challenge(&transport, &tm, &host_pk, 0, "auth_register challenge") + .await?; // 3. Sign the host's challenge over the bundle and send the proof. - let mut nonce_bytes = [0u8; 16]; - getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; - let client_nonce = u128::from_be_bytes(nonce_bytes); + let client_nonce = random_nonce()?; let proof_payload = mtp_crypto::auth::register_proof_payload( &version_str, @@ -446,20 +396,7 @@ impl WasmClient { server_challenge, client_nonce, ); - let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) - .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; - let signature = signer - .sign(&proof_payload) - .map_err(|e| js_error(&format!("signature failed: {}", e)))?; - - let proof = CommunicationValue::new(CommunicationType::ChallengeResponse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ) - .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) - .to_bytes() - .map_err(|e| js_error(&format!("encode failed: {}", e)))?; + let proof = signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce)?; transport.send_frame(&proof).await?; // 4. Receive the host's final confirmation; extract + verify assigned id. @@ -505,17 +442,7 @@ impl WasmClient { return Err(e); } - let loop_transport = transport.clone(); - self.transport = Some(transport); - self.set_state(ConnectionState::Connected); - - let state = self.state.clone(); - let on_msg = self.on_message.clone(); - let on_err = self.on_error.clone(); - wasm_bindgen_futures::spawn_local(async move { - loop_transport.receive_loop(on_msg, on_err).await; - state.set(ConnectionState::Disconnected); - }); + self.start_receive_loop(transport); Ok(assigned_id) } @@ -543,4 +470,57 @@ impl WasmClient { .on_state_change .call1(&JsValue::NULL, &JsValue::from(new_state as u8)); } + + fn start_receive_loop(&mut self, transport: WasmTransport) { + let loop_transport = transport.clone(); + self.transport = Some(transport); + self.set_state(ConnectionState::Connected); + + let state = self.state.clone(); + let on_msg = self.on_message.clone(); + let on_err = self.on_error.clone(); + wasm_bindgen_futures::spawn_local(async move { + loop_transport.receive_loop(on_msg, on_err).await; + state.set(ConnectionState::Disconnected); + }); + } + + async fn read_verified_challenge( + &self, + transport: &WasmTransport, + tm: &mtp_codec::TypeMap, + host_pk: &mtp_crypto::PublicKeyBundle, + bound_id: u64, + context: &str, + ) -> Result { + let challenge_bytes = transport.read_one_frame().await?; + let challenge = CommunicationValue::from_bytes(&challenge_bytes) + .map_err(|e| js_error(&format!("parse challenge: {}", e)))?; + let expected = CommunicationType::Challenge.to_id(tm); + if challenge.get_type() != expected { + self.set_state(ConnectionState::Disconnected); + return Err(unexpected_response_type_error( + context, + expected, + challenge.get_type(), + &challenge_bytes, + &challenge, + )); + } + + let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(tm)) { + DataValue::UnsignedNumber(n) => *n, + _ => { + self.set_state(ConnectionState::Disconnected); + return Err(js_error("missing server challenge")); + } + }; + + if let Err(e) = verify_host_challenge(&challenge, tm, host_pk, bound_id, server_challenge) { + self.set_state(ConnectionState::Disconnected); + return Err(e); + } + + Ok(server_challenge) + } } diff --git a/wasm/src/message.rs b/wasm/src/message.rs index bad844f..2d94873 100644 --- a/wasm/src/message.rs +++ b/wasm/src/message.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue}; use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm}; -use mtp_type_map::{communication_type_name, TypeMap}; +use mtp_type_map::{TypeMap, communication_type_name}; use crate::error::js_error; @@ -54,8 +54,14 @@ pub fn build_demo_message( // Encrypted container let inner_enc = DataValue::Container(vec![ - (DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("secret inner data".into())), - (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(42)), + ( + DataType::Version.to_id(&TypeMap::latest()), + DataValue::Str("secret inner data".into()), + ), + ( + DataType::Id.to_id(&TypeMap::latest()), + DataValue::UnsignedNumber(42), + ), ]); let mut dv_enc = inner_enc; dv_enc @@ -64,8 +70,14 @@ pub fn build_demo_message( // Signed container let inner_sig = DataValue::Container(vec![ - (DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("signed by client".into())), - (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(99)), + ( + DataType::Version.to_id(&TypeMap::latest()), + DataValue::Str("signed by client".into()), + ), + ( + DataType::Id.to_id(&TypeMap::latest()), + DataValue::UnsignedNumber(99), + ), ]); let mut dv_sig = inner_sig; dv_sig @@ -78,7 +90,10 @@ pub fn build_demo_message( DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("signed+encrypted payload".into()), ), - (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(7)), + ( + DataType::Id.to_id(&TypeMap::latest()), + DataValue::UnsignedNumber(7), + ), ]); let mut dv_sec = inner_sec; dv_sec @@ -115,7 +130,10 @@ pub fn parse_auth_response(response: &[u8]) -> Result { let comm = CommunicationValue::from_bytes(response) .map_err(|e| js_error(&format!("parse failed: {}", e)))?; - let connected = matches!(comm.get_data(DataType::Connected.to_id(&TypeMap::latest())), DataValue::BoolTrue); + let connected = matches!( + comm.get_data(DataType::Connected.to_id(&TypeMap::latest())), + DataValue::BoolTrue + ); let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) { DataValue::UnsignedNumber(n) => Some(*n), @@ -279,7 +297,10 @@ mod tests { cv.get_data(DataType::Description.to_id(&tm)), &DataValue::Str("with-data".into()) ); - assert_eq!(cv.get_data(DataType::Timestamp.to_id(&tm)), &DataValue::UnsignedNumber(555)); + assert_eq!( + cv.get_data(DataType::Timestamp.to_id(&tm)), + &DataValue::UnsignedNumber(555) + ); assert_eq!( cv.get_data(DataType::Id.to_id(&tm)), &DataValue::Bytes(payload.to_vec()) diff --git a/wasm/src/transport.rs b/wasm/src/transport.rs index 85b2010..795a6f4 100644 --- a/wasm/src/transport.rs +++ b/wasm/src/transport.rs @@ -51,10 +51,18 @@ enum FrameOutcome { * [11..] id/sender/receiver/signature/data, gated by `flags` */ fn log_frame_bytes(wire: &[u8]) { - let hex: String = wire.iter().map(|b| format!("{b:02x}")).collect::>().join(" "); + let hex: String = wire + .iter() + .map(|b| format!("{b:02x}")) + .collect::>() + .join(" "); - let outer_len = wire.get(0..4).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])); - let inner_len = wire.get(4..8).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])); + let outer_len = wire + .get(0..4) + .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])); + let inner_len = wire + .get(4..8) + .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])); let comm_type = wire.get(8..10).map(|b| u16::from_be_bytes([b[0], b[1]])); let flags = wire.get(10).copied();